에이전트의 버그를 재현할 수 없다면, 해결할 수도 없습니다
요약
에이전트 운영 시 발생하는 가장 치명적인 문제는 버그의 재현 불가능성입니다. 모델의 비결정론적 특성, 컨텍스트 누락, 외부 도구의 상태 변화 등으로 인해 발생하는 재현 불가 문제를 해결하기 위한 엔지니어링적 접근의 중요성을 강조합니다.
핵심 포인트
- 재현 가능성은 모든 품질 관리(Evals, CI)의 전제 조건임
- 모델의 비결정론적 특성(Temperature)이 재현을 방해함
- 보간된 컨텍스트(검색 결과, 사용자 상태 등) 캡처가 필수적임
- 외부 도구(API, DB)의 데이터 변화가 실행 결과에 영향을 미침
- 모델 버전 업데이트 및 캐시 상태 등 숨겨진 상태 관리 필요
제가 프로덕션(production) 환경에서 에이전트(agent)를 운영하는 모든 회사로부터 다양한 형태로 받아온 버그 보고서가 하나 있습니다:
"어제 오후 2시경에 에이전트가 고객에게 잘못된 환불 금액을 안내했습니다. 확인해 주실 수 있나요?"
만약 당신의 스택(stack)이 이를 위해 구축되지 않았다면, 조사는 다음과 같이 진행됩니다. 타임스탬프(timestamp)를 찾고, 동일한 프롬프트(prompt)를 다시 실행해 봅니다. 그러면 완벽하게 작동합니다. 매번 정확한 환불 금액이 나옵니다. 아무것도 바꾸지 않았음에도 계속해서 정답을 내놓습니다. 결국 당신은 "재현 불가 — 모니터링 예정"이라고 적게 되는데, 이는 사실상 포기했다는 말을 전문적으로 표현한 것입니다.
이것이 저는 에이전트 분야 전체에서 가장 과소평가되고 있다고 생각하는 실패 요인입니다. 환각 (hallucination), 드리프트 (drift), 비용 (cost)이 아닙니다. 바로 재현 불가능성 (Irreproducibility) 입니다. 재현할 수 없는 버그는 수정할 수도, 테스트할 수도, 수정했다는 것을 증명할 수도 없는 버그입니다. 에이전트는 본질적으로 우리 대부분이 출시한 소프트웨어 중 가장 재현하기 어려운 소프트웨어입니다.
제가 옹호하고자 하는 의견은 다음과 같습니다: 재현 가능성 (reproducibility)은 당신이 주장하는 다른 모든 품질 관리 관행의 전제 조건입니다. 당신의 평가 (evals), 회귀 테스트 (regression tests), 그리고 CI 게이트 (CI gates)는 모두 실제 실패 사례를 가져와서 원할 때 다시 실행할 수 있다는 것을 가정합니다. 만약 그것이 불가능하다면, 그 장치들은 모래 위에 지어진 것과 같습니다.
"다시 실행해 보세요"라는 말이 당신에게 은밀하게 거짓말을 하는 이유
일반적인 백엔드 (backend) 버그의 경우, 재현은 거의 비용이 들지 않습니다. 동일한 요청 (request), 동일한 행 (row), 동일한 코드 (code), 동일한 버그가 발생하기 때문입니다. 우리는 동일한 입력값으로 다시 실행하면 동일한 결과를 볼 것이다 라고 말하는 본능을 길러왔습니다. 하지만 에이전트의 경우, 그 본능은 네 가지 독립적인 방식으로 틀렸으며, 각각의 이유는 재현을 불가능하게 만들기에 충분합니다:
- 모델이 비결정론적(nondeterministic)입니다. Temperature가 0보다 크면 동일한 프롬프트라도 서로 다른 완료 문구(completions)를 생성합니다. 오후 2시의 실행은 당신의 리플레이(replay)가 결코 따르지 않을 추론 경로를 택했으며, 버그는 바로 그 경로에 존재했습니다.
- 동일한 입력을 리플레이하고 있지 않습니다. 당신은 동일한 _템플릿(template)_을 다시 실행했습니다. 하지만 모델이 실제로 본 프롬프트에는 보간된 컨텍스트(interpolated context) — 검색된 문서, 사용자의 계정 상태, 날짜, 도구 결과 등 — 가 포함되어 있었으며, 그렇게 해결된(resolved) 입력값은 이제 사라졌습니다.
- 세상이 변했습니다. 에이전트가 데이터베이스나 API를 호출하는 도구(tool)를 사용했습니다. 오늘 그 도구들은 어제와 다른 값을 반환합니다. 결정론적(deterministic) 모델이라 할지라도 _입력값(inputs)_이 변했기 때문에 다르게 동작할 것입니다.
- 숨겨진 상태(Hidden state). 고정된 이름 뒤에서 모델 버전이 조용히 업데이트되었거나, 캐시(cache)가 당시에는 따뜻(warm)했다가 지금은 차가워(cold)졌거나, 플래그(flag)가 바뀌었을 수 있습니다. 이 중 어느 것도 당신의 코드에는 들어있지 않지만, 이 모든 것이 실행(run)을 변화시켰습니다.
오직 _첫 번째_만이 모델의 무작위성에 관한 것입니다. 나머지 세 가지는 당신이 캡처하는 데 실패한 입력값에 관한 것입니다. 즉, 해결책은 모델의 문제가 아니라 대부분 엔지니어링 규율(engineering discipline)의 문제입니다. 프로덕션 환경을 결정론적으로 만들 수는 없지만, 실행을 _리플레이 가능(replayable)_하게 만들 수는 있으며, 이 둘은 매우 다른 목표입니다.
리플레이 가능성이 결정론적보다 낫다
본능적으로 결정론(determinism)을 쫓게 됩니다: temperature: 0으로 설정하고, 모든 버전을 고정하며, 세상을 동결하는 것 말입니다. 그것은 함정입니다. Temperature가 0인 에이전트는 종종 더 나쁜 에이전트가 되며, 여전히 도구 출력값(tool outputs)을 캡처하지 못했기 때문에 여전히 과거의 실패를 재현할 수 없습니다. 결정론은 프로덕션 전체에 영원히 부과해야 하는 무언가입니다. 반면 리플레이 가능성은 실행이 일어나는 시점에 각 실행에 부여하는 속성이며, 이는 엄격하게 더 강력합니다. 프로덕션이 아무리 비결정론적이었더라도 _그 특정 실패_를 재구성해내기 때문입니다.
실행을 리플레이하려면, 실행이 발생했을 때 다음 사항들을 반드시 캡처했어야 합니다: 해결된 입력값(resolved input) (템플릿 적용 후 모델이 본 정확한 바이트), 모든 도구 호출의 원시 출력(tool call's raw output) (당시 API가 반환한 값), 그리고 실행 파라미터(execution parameters) (모델 ID 및 버전, temperature, seed, 시스템 프롬프트).
이 지점은 제가 의지하는 두 도구가 하나의 단위로 작동하는 이음새입니다. 재현(reproduction)에는 기록(record)과 판결(verdict)이 모두 필요하기 때문입니다. AgentLens는 트레이스(trace)를 캡처합니다 — 모든 모델 및 도구 단계, 해결된 입력값(resolved inputs), 가공되지 않은 출력값(raw outputs), 파라미터(parameters) 등 리플레이(replay)를 재구성하는 데 필요한 원재료를 캡처합니다. agent-eval은 나머지 절반입니다. 이 도구는 캡처된 실행(run)을 가져와 고정된 조건(pinned conditions) 하에서 재실행하고, 버그가 존재하는지 여부를 점수화(scores) 합니다. AgentLens는 실패를 _리플레이 가능(replayable)_하게 만들고, agent-eval은 그 리플레이를 _게이트(gate)로 사용할 수 있는 합격/불합격(pass/fail) 테스트_로 만듭니다. 점수화 도구(scorer)가 없는 트레이스는 직접 읽어야 하는 아카이브일 뿐이며, 트레이스가 없는 점수화 도구는 이미 잃어버린 프롬프트를 채점하는 것과 같습니다.
import { getTrace } from "agentlens";
import { evaluate, assert } from "agent-eval";
...
두 가지 결정이 모든 작업을 수행합니다. 도구 출력값(tool outputs)은 다시 가져오는 것이 아니라 리플레이됩니다: toolResolver는 라이브 API를 호출하는 대신 에이전트에게 어제의 기록된 응답을 전달합니다. 만약 리플레이 과정에서 데이터베이스를 다시 쿼리한다면, 당신은 이미 변해버린 세상을 테스트하고 있는 것이며, 관찰되는 어떤 "수정(fix)"도 단순히 데이터가 다시 움직인 결과일 수 있습니다. 도구 출력값을 고정(pinning)하는 것은 당신이 연구하고자 하는 단 하나의 변수를 격리해 줍니다. 그리고 해결된 프롬프트(resolved prompt)는 템플릿이 아니라 리플레이됩니다: 미묘하게 잘못된 검색된 문서나 특이한 계정 상태는 오직 해결된 입력값(resolved input) 안에만 존재했기 때문입니다.
한 번의 리플레이는 디버깅이지만, 여러 번의 리플레이는 해결책입니다
간과해서는 안 될 미묘한 차이가 있습니다: 만약 실패가 높은 온도의 추론 경로(high-temperature reasoning path)에서 발생했다면, 한 번의 리플레이로는 재현될 수도 있고 되지 않을 수도 있습니다. 다른 경로를 선택할 수 있기 때문입니다. 비결정론적 실패(nondeterministic failures)의 경우, 단 한 번의 리플레이는 재현이 아니라 하나의 샘플(sample)에 불과합니다.
정직한 기술은 번들(bundle)을 _N_번 리플레이하고 실패 _율(rate)_을 측정하는 것입니다. 50번의 리플레이 중 8번에서 버그가 발생한다면, 비록 단일 실행에서 버그가 나타난다는 보장은 없더라도 버그가 실재함을 증명하고 수치화한 것입니다. 당신의 수정 사항은 "리플레이가 한 번 통과했다"가 아니라, "50번의 리플레이에 걸친 실패율이 16%에서 0%로 떨어졌다"가 되어야 합니다.
async function reproductionRate(traceId: string, isBug: (o: string) => boolean, n = 50) {
const b = await bundleFromTrace(traceId);
const runs = await Promise.all(Array.from({ length: n }, () => replay(b)));
...
이것은 버그 수정 루프(bug-fixing loop)를 재정의합니다. 실패를 한 번 수정하고 눈대중으로 확인하는 것이 아니라, 실패를 포착하고, 그 발생률을 설정하며, 에이전트(agent)를 변경하고, 발생률이 떨어졌음을 증명하며, 해당 리플레이를 테스트 스위트(suite)에 유지하여 다시 조용히 발생하지 않도록 하는 것입니다. agent-eval은 번들(bundle)을 실행하며, 그 뒤에 있는 AgentLens 트레이스(trace)는 리플레이가 실패했을 때 어떤 단계가 기록된 경로에서 벗어났는지 알려줍니다.
월요일에 해야 할 일
프로덕션(production) 환경이 결정론적(deterministic)일 필요는 없습니다. 대신 모든 실행(run)이 '재구성 가능(reconstructable)'해야 합니다.
- 템플릿이 아닌, 해결된 입력값(resolved input)을 캡처하세요. 템플릿만 저장한다면 이미 미래의 버그 리포트 대부분을 놓친 것입니다.
- 모든 도구 호출(tool call)의 원시 출력(raw output)을 실행과 함께 인라인으로 기록하세요. 이미 변해버린 세상에서 다시 데이터를 가져오는 방식의 재현은 진정한 재현이 아닙니다.
- 실행 파라미터(execution parameters)를 트레이스에 찍으세요. 모델 버전(model version), 온도(temperature), 시드(seed), 시스템 프롬프트(system prompt) 등이 포함되어야 합니다. "모델 버전이 달랐다"는 것은 그렇지 않으면 절대 발견할 수 없는 실제 근본 원인(root cause)입니다.
- 단일 리플레이가 아닌, 실패율(failure rate)을 측정하세요. 비결정론적(nondeterministic) 버그의 경우, 재현은 통계적입니다. N번 리플레이하고, "발생률이 0이 되었다"를 수정 완료의 기준으로 삼으세요.
에이전트들은 증상만으로는 설명할 수 없는 실패를 계속해서 만들어낼 것입니다. 이를 해결하는 팀과 "재현할 수 없음(could not reproduce)"이라며 티켓을 닫아버리는 팀의 차이는 모델의 품질이나 프롬프트 기술이 아닙니다. 바로 실행(run)이 다시 실행될 수 있을 만큼 충분한 흔적을 남겼느냐의 차이입니다. AgentLens로 트레이스를 캡처하고, agent-eval로 리플레이 및 점수 측정(replay-and-score)을 수행하십시오. 그러면 "재현할 수 없음"은 더 이상 당신이 쓸 수 있는 문장이 아니게 될 것입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기