LLM은 맞았지만 AI 에이전트는 여전히 잘못된 답변을 내놓았다
요약
AI 에이전트의 실패를 재현하고 테스트하는 어려움을 해결하기 위해, 오픈 소스 Python 도구 'Stepfork'가 개발되었습니다. 이 도구는 LLM 및 도구 상호작용을 기록하여 트레이스로 저장하고, 이를 pytest 회귀 테스트로 변환할 수 있게 합니다. 실제 Gemini 모델 실험에서도 에이전트의 결정론적 동작 검증에 활용되었음을 보여줍니다.
핵심 포인트
- AI 에이전트는 LLM 호출, 도구 실행 등 복잡한 과정으로 인해 재현성이 낮다.
- Stepfork는 에이전트 실행을 기록하고 트레이스로 저장하여 실패 지점을 오프라인에서 재현 가능하게 한다.
- 기록된 상호작용을 pytest 회귀 테스트로 변환하여 안정적인 검증 환경을 구축할 수 있다.
저는 AI 에이전트 실행 기록(run)을 남기고, 실패한 부분을 오프라인으로 재현하며, 이를 pytest 회귀 테스트로 변환하는 오픈 소스 Python 도구를 만들었습니다. 실제 Gemini 모델로 이 도구를 테스트했을 때 어떤 일이 벌어졌는지 알려드리겠습니다.
GitHub: https://github.com/utsab345/stepfork
실제 LLM 사례 연구: https://github.com/utsab345/stepfork/tree/main/examples/real-llm-case-study
AI 에이전트 디버깅의 문제점
전통적인 Python 함수가 실패할 경우, 문제를 재현하는 것은 보통 간단합니다. 동일한 입력을 제공하고 함수를 다시 실행하여 무엇이 잘못되었는지 조사하면 됩니다.
하지만 AI 에이전트는 상황이 더 복잡해집니다.
에이전트는 LLM을 호출하고, 문서를 검색하며, 여러 도구(tool)를 실행하고, 중간 결과에 기반하여 결정을 내립니다.
무언가 잘못되었을 때, 에이전트를 다시 실행하면 다른 결과를 생성할 수 있습니다. 모델은 다르게 응답할 수 있고, 외부 데이터는 변경될 수 있으며, 도구에는 부작용(side effects)이 있을 수 있습니다.
저는 계속해서 간단한 질문을 생각했습니다:
만약 에이전트의 실패를 한 번 기록하고 일반적인 회귀 테스트로 만들 수 있다면 어떨까?
그것이 바로 제가 AI 에이전트 실행을 기록하고 재현하기 위한 오픈 소스 Python 라이브러리인 Stepfork를 만들기 시작한 이유입니다.
아이디어는 간단합니다:
- LLM 및 도구 상호작용이 계측(instrumented)된 에이전트 실행을 기록합니다.
- 이러한 상호작용을 이식 가능한 트레이스(trace)로 저장합니다.
- 모델을 다시 호출하지 않고 기록된 상호작용을 재현합니다.
- pytest 회귀 테스트를 생성합니다.
- 애플리케이션을 수정하고 동일한 트레이스를 기준으로 동작을 검증합니다.
하지만 저는 단순히 목업(mocked) 응답이 아닌, 실제 모델로 이를 테스트하고 싶었습니다.
실제 Gemini 실험
저는 작은 IT 인시던트 분류 에이전트를 만들었습니다.
이 에이전트는 인시던트 보고서를 받고, 서비스 상태 및 최근 배포를 확인하며, 런북(runbook)을 읽고, 해당 인시던트가 얼마나 심각한지 판단합니다.
실험을 위해 다음과 같은 합성 인시던트를 사용했습니다:
A production API가 대부분의 고객에게 HTTP 500 오류를 반환하고 있습니다. 배포 이후 오류율이 급격히 증가했으며, 결제 체크아웃 엔드포인트에서 실패가 발생하고 있습니다.
에이전트는 세 가지 유형의 로컬 도구를 사용했습니다:
lookup_service_healthget_recent_deploymentsfetch_incident_runbook
운영 데이터는 합성(synthetic)이었지만, LLM 요청은 실제였습니다.
저는 Google의 OpenAI 호환 API 엔드포인트를 통해 Gemini 2.5 Flash를 사용했습니다.
로컬 테스트 환경(fixtures)에 따르면 체크아웃 API의 오류율이 72%였고, 고객들이 영향을 받았으며, 최근 배포가 발생했음을 나타냈습니다.
독립적으로 정의된 인시던트 심각도 정책에 따라, 이는 즉각적인 에스컬레이션(escalation)이 필요한 P0 등급의 인시던트였습니다.
그리고 Gemini는 이를 정확하게 파악했습니다.
모델은 다음을 반환했습니다:
{
"severity": "P0"
}
그런 다음 제 Python 코드가 이를 P1로 변경했습니다.
버그는 모델에 있지 않았다
이 에이전트는 인시던트를 최근 배포와 연관시키는 결정론적(deterministic) 후처리 함수를 가지고 있었습니다.
저는 이 함수에 의도적으로 버그를 주입했습니다:
def _correlate_recent_deployment(decision, evidence):
recent = evidence["deployments"][INCIDENT["primary_service"]]["recent_deployment"]
...
이 코드는 최근 배포가 인시던트 심각도를 낮추는 정당한 이유라고 잘못 가정했습니다.
그래서 최종 출력은 다음과 같았습니다:
{
"severity": "P1",
"escalation_required": false
...
모델은 치명적인 인시던트를 올바르게 식별했지만, 애플리케이션이 그 결정을 조용히 덮어썼습니다.
예외(exception)는 발생하지 않았습니다. 에이전트는 성공적으로 완료되었습니다.
에이전트가 충돌 없이 실행되는지만 확인하는 테스트만 통과했을 것입니다.
실패는 실행 상태가 아니라 비즈니스 결과에 있었습니다.
Stepfork를 사용한 실행 기록
저는 에이전트의 계측된 의존성(instrumented dependencies)을 기록하기 위해 Stepfork를 사용했습니다.
결과로 나온 .sftrace 번들에는 다음이 포함되어 있었습니다:
| 기록된 정보 | 결과 |
|---|---|
| 모델 | Gemini 2.5 Flash |
| ... | |
| 트레이스(trace)는 도구 호출 시퀀스, 기록된 입력 및 출력, 그리고 LLM 상호작용을 포착했습니다. |
기록된 모델 응답은 P0이었지만, 애플리케이션은 P1을 반환했습니다.
이러한 차이점은 행동이 정확히 어디서 잘못되었는지 알려주기 때문에 중요합니다.
Gemini를 다시 호출하지 않고 실패 재현하기
다음으로 Stepfork의 '프리징 리플레이 모드(frozen replay mode)'를 사용했습니다.
도구를 실행하거나 Gemini에 다른 요청을 보내는 대신, Stepfork는 기록된 응답들로 대체했습니다.
애플리케이션 로직은 여전히 실행되었기 때문에, 저는 그 로직을 변경하고 결과를 테스트할 수 있었습니다.
검증 결과는 다음과 같았습니다:
Instrumented tool bodies executed: 0
Live LLM attempts: 0
Recorded dependency calls: 6
...
이는 추가적인 모델 API 호출 없이도 관련 실행을 재현할 수 있었다는 것을 의미합니다.
또한, 회귀 테스트(regression test)를 API 키 없이 오프라인으로 실행할 수 있다는 것을 의미했습니다.
중요한 제한 사항이 있습니다: Stepfork는 이미 '계측된(instrumented)' 의존성만 대체합니다. 임의로 계측되지 않은 코드가 실행되는 것을 막지는 못하며, 리플레이가 샌드박스(sandbox)는 아닙니다.
실패를 pytest 회귀 테스트로 전환하기
저는 기대하는 비즈니스 결과(expected business outcome)를 독립적으로 정의했습니다:
{
"severity": "P0",
"escalation_required": true
...
그런 다음 Stepfork를 사용하여 회귀 테스트를 내보냈습니다:
stepfork export traces/incident-triage.sftrace \
--pytest \
--entrypoint agent:run_incident_agent \
...
버그가 있는 애플리케이션에 대해 생성된 테스트를 실행한 결과는 다음과 같았습니다:
1 failed
AssertionError: behavior mismatch at 'escalation_required':
...
이것은 제가 정확히 원했던 것이었습니다.
테스트는 기록된 모델 상호작용을 사용하여 실제 애플리케이션 레벨의 실수를 감지했습니다.
버그 수정하기
수정은 간단했습니다: 배포 상관관계(deployment correlation)가 인시던트 심각도 정책을 무효화하도록 두는 것을 중단하는 것이었습니다.
수정된 함수는 다음과 같아졌습니다:
def _correlate_recent_deployment(decision, evidence):
return decision
동일하게 생성된 pytest 테스트를 다시 실행해 보았습니다.
1 passed
트레이스를 재생성하지는 않았습니다.
예상 결과를 변경하지도 않았습니다.
또 다른 Gemini 요청을 보내지도 않았습니다.
유일하게 의미 있는 변화는 애플리케이션 로직이었습니다.
결과는 다음과 같습니다:
| 수정 전 | 수정 후 | |
|---|---|---|
| 기록된 모델 결정 | P0 | P0 |
| ... | ||
| 한 가지 미묘한 점은, 고정된 애플리케이션을 독립형 CLI로 재생하면 원래 기록된 버그 결과와 출력 불일치를 보고할 수 있다는 것입니다. 이는 예상되는 동작입니다. 생성된 pytest 테스트는 독립적으로 정의된 예상 결과와 수정된 결과를 별도로 검증합니다. |
에이전트의 도구 호출(tool calls)이 변경되면 어떻게 되나요?
에이전트의 의존성 궤적(dependency trajectory) 변경도 테스트해 보았습니다.
도구 호출, 인자, 순서 및 모델 프롬프트를 유지한 무해한 리팩터링은 통과했습니다.
하지만 도구 호출 순서를 변경하자, 고정 재생(frozen replay)이 실행을 거부했습니다:
ReplayMismatchError:
call #1: expected tool 'lookup_service_health'
but the agent called 'get_recent_deployments'
도구 인자(window_minutes=60)를 window_minutes=120으로 변경하는 것도 불일치를 유발했습니다.
이것은 에이전트의 회귀(regression)가 항상 최종 답변에 관한 것만은 아니라는 점에서 유용합니다. 때로는 에이전트가 다른 도구를 호출하거나 다른 인자를 전송하기 시작할 수 있습니다.
실험 비용은 얼마나 들었나요?
실제 Gemini 요청을 세 번 보냈습니다:
- 원래 실행을 기록하기 위한 요청 1회.
- 기준(baseline) 프롬프트를 사용한 하이브리드 재생 1회.
- 수정된 프롬프트를 사용한 하이브리드 재생 1회.
보고된 토큰 사용량과 가격 책정 가정을 기반으로 추정 API 비용은 약 $0.009였습니다.
두 하이브리드 평가 모두 P0를 반환했습니다. 프롬프트당 샘플이 하나뿐이기 때문에, 어느 프롬프트가 더 나은지 확립할 수는 없습니다.
중요한 결과는 기록된 실행을 추가적인 모델 요청 없이 반복적으로 재생할 수 있었다는 것입니다.
직접 시도해 보세요
전체 케이스 스터디는 main Stepfork 저장소에 포함되어 있습니다:
[https://github.com/utsab345/stepfork/tree/main/examples/real-llm-case-study]
Gemini API 키 없이도 오프라인 테스트를 재현할 수 있습니다.
git clone https://github.com/utsab345/stepfork.git
cd stepfork/examples/real-llm-case-study
...
기록된 트레이스(trace)를 확인하려면:
uv run stepfork validate \
traces/incident-triage.sftrace \
--verify-integrity
프리징된 리플레이(frozen replay)를 확인하려면:
uv run python scripts/verify_frozen.py
이 케이스 스터디에는 원래 트레이스, 합성 픽스처(synthetic fixtures), 버그가 있는 애플리케이션 스냅샷 및 수정된 애플리케이션 스냅샷, 생성된 pytest 회귀 테스트(regression test), 그리고 기록된 검증 증거가 포함되어 있습니다.
Stepfork가 아직 하지 못하는 것들
Stepfork는 여전히 초기 알파 프로젝트입니다.
에이전트의 답변이 올바른지 자동으로 알지는 못합니다. 의미 있는 기대치(expectations)를 직접 정의해야 합니다.
또한 모든 가능한 부작용(side effect)을 가로채지도 않습니다. 프리징 리플레이는 지원되는, 계측된 종속성(instrumented dependencies)에만 적용됩니다.
현재 OpenAI 어댑터에는 미지원 스트리밍 및 일부 최신 API 표면(API surfaces)을 포함한 제한 사항이 있습니다. 이 프로젝트는 LangGraph 통합도 지원하며, 추가적인 통합이 계획되어 있습니다.
저는 간단한 개발자 워크플로우를 향해 작업하고 있습니다:
에이전트가 한 번 실패합니다. 사용자가 이를 기록합니다. 코드를 수정합니다. 그 실패는 다시 실행할 수 있는 테스트 케이스가 됩니다.
피드백을 받고 싶습니다
저는 Stepfork를 오픈 소스 프로젝트로 구축하고 있으며, 특히 LLM 에이전트, LangGraph, 그리고 Python 테스트 도구를 사용하는 개발자분들의 의견을 듣고 싶습니다.
어떤 종류의 에이전트 실패가 재현하기 가장 어렵습니까?
도구 호출(tool calls)을 기록하고 이를 오프라인으로 리플레이하는 것이 디버깅 워크플로우에 도움이 될까요?
만약 사용해 보거나 기여하고 싶으시다면:
GitHub: [https://github.com/utsab345/stepfork]
문서(Documentation): [https://utsab345.github.io/stepfork/]
Real-Gemini 사례 연구: https://github.com/utsab345/stepfork/tree/main/examples/real-llm-case-study
에이전트가 실패했다. 그 실패를 테스트로 만들어라.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기