검증은 단언(Assertion)이 아니라 루프(Loop)입니다: 왜 당신의 AI 에이전트는 달성하지 못한 성공을 보고하는가
요약
AI 에이전트의 성공 보고와 실제 정확성 사이의 간극을 해결하기 위한 검증 전략을 다룹니다. 결정론적 시스템의 '단언(Assertion)' 방식이 아닌, 결과를 관찰하고 재시도 여부를 결정하는 '루프(Loop)' 방식의 검증이 필요함을 강조합니다.
핵심 포인트
- 에이전트의 확신(Confidence)과 정확성(Correctness)은 별개의 개념임
- 비결정론적 모델 출력에는 단언이 아닌 루프 형태의 검증이 필수적임
- 실제 결과를 관찰하고 요구사항과 비교하는 피드백 루프를 구축해야 함
- 검증 프로세스 자체가 모델의 확률적 특성을 반영해야 함
제가 검토하던 한 에이전트에게 저장소(repository) 전체에서 특정 함수의 이름을 변경하라는 요청이 내려졌습니다. 에이전트는 한동안 실행을 지속하며 수정을 마친 뒤, 깔끔한 요약을 반환했습니다: 완료되었습니다. 모든 곳에서 parseUser를 parseAccount로 변경했습니다. 이 요약은 자신감 넘치고 형식이 잘 갖춰져 있었지만, 틀렸습니다. 8개의 호출 지점(call sites) 중 6개만 업데이트되었습니다. 나머지 2개는 업데이트되지 않았는데, 하나는 이름을 동적으로 생성하는 문자열 안에 숨겨져 있었고, 다른 하나는 검색 과정에서 누락된 테스트 헬퍼(test helper) 안에 있었습니다.
에이전트는 거짓말을 한 것이 아닙니다. 에이전트는 결과물을 반환하는 그 순간, 자신의 모델이 진실이라고 믿었던 것을 반환했을 뿐입니다. 문제는 '내가 성공했다고 믿는다'와 '성공했다고 보고한다' 사이에 아무것도 없었다는 점입니다. 다시 세상 밖으로 나가 실제로 무엇이 바뀌었는지 살펴보고, 그것을 작업 요구 사항과 비교하는 단계가 없었습니다. 에이전트의 확신(confidence)과 에이전트의 정확성(correctness)은 완전히 무관한 두 수치였으며, 그중 하나만이 요약에 포함되었습니다.
이것은 제가 프로덕션(production) 에이전트가 실패하는 것을 보는 가장 흔한 방식이며, 거의 모델의 문제가 아닙니다. 이것은 검증(validation)의 문제이며, 구체적으로는 잘못된 형태로 구축된 검증의 문제입니다.
결정론적 시스템(deterministic system)에서 검증은 단언(assertion)입니다: 한 번 확인하고, 통과하거나 실패합니다. 비결정론적(non-deterministic) 모델 출력에 의해 구동되는 시스템에서 검증은 루프(loop)여야 합니다: 실제 결과를 관찰하고, 그것이 얼마나 근접했는지 점수를 매기며, 수락할지, 재시도할지, 경로를 변경할지, 아니면 중단할지를 결정해야 합니다. 가장 흔한 프로덕션 에이전트의 실패 사례는 두 번째 케이스를 첫 번째 케이스처럼 취급하는 것입니다.
이 글은 두 편의 동반 게시물과 함께 구성되었습니다. 할 수 있는 곳에는 결정론을, 반드시 필요한 곳에는 판단을은 시스템의 각 부분이 어떤 기술을 소유해야 하는지 결정하는 것에 관한 것입니다. 잘못된 자는 자가 없는 것보다 나쁩니다는 검증 항목에 권한을 부여하기 전에 당신이 신뢰하는 그 검증을 확인하는 것에 관한 것입니다. 이 글은 출력이 검증을 필요로 할 만큼 충분히 비결정론적이라고 결정했을 때, 검증 그 자체의 형태에 관한 것입니다.
단언(Assertion)이 가정하는 것
assert(result == expected)는 결정론적(deterministic) 소프트웨어에서 가장 유용한 코드 중 하나이며, 이 코드가 유용한 이유는 그 세계에서 거의 항상 참인 일련의 가정들 덕분입니다. 방금 실행한 작업은 결정론적(deterministic)이므로, 작업이 수행되었거나 수행되지 않았거나 둘 중 하나입니다. 검증(check)은 실행한 작업보다 훨씬 비용이 저렴하므로, 마지막에 한 번 실행하는 것은 무시해도 될 정도의 비용만 듭니다. 그리고 결과는 이진적(binary)입니다. 결정론적 시스템에서는 성공과 실패 사이에 의미 있는 상태가 존재하지 않기 때문입니다. 정수를 파싱했거나, 아니면 예외를 던졌거나(threw). 행을 작성했거나, 아니면 에러가 발생했거나 둘 중 하나입니다.
하지만 작업이 모델 호출(model call)이 되는 순간, 이 모든 가정은 깨집니다.
작업은 더 이상 결정론적(deterministic)이지 않으므로, '작업을 수행했다'는 사실이 아니라 확률이 됩니다. 검증(check)은 더 이상 작업보다 사소하게 저렴하지 않습니다. 모델이 모호한 목표(fuzzy goal)를 실제로 달성했는지 확인하는 것은 목표 그 자체만큼이나 어려울 수 있기 때문입니다. 그리고 결과는 더 이상 이진적(binary)이지 않습니다. 모델 실패의 전체적인 질감은 부분적(partial)이기 때문입니다. 8개 중 6개를 수행한 이름 변경, 5개의 핵심 포인트 중 4개를 포착한 요약, 문자로 쓰인 날짜를 제외한 모든 날짜를 찾아낸 추출 등이 그 예입니다. 이러한 시스템에서 부분적 성공(partial success)은 예외적인 사례(edge case)가 아닙니다. 그것이 주요 사례(main case)이며, 단언(assertion)은 이를 표현할 어휘가 없습니다. 8개 중 6개와 8개 중 8개는 모두 동일한 false로 수렴하며, 이는 당신에게 꼭 필요했던 정보를 정확히 버려버리는 결과를 초래합니다.
루프(Loop)의 형태
루프(loop)는 단언(assertion)이 버리는 정보를 유지합니다. 루프는 '통과했는가'를 묻지 않습니다. 대신 '얼마나 근접했는가, 그리고 그 결과로 무엇이 일어나야 하는가'를 묻습니다. 구체적으로 루프는 네 가지 단계와 하나의 중단 규칙(stopping rule)으로 구성됩니다.
- 관찰을 통한 검증 (Validate by observation). 액션(action)을 수행한 후에는 모델이 보고하는 내용이 아니라, 실제로 관찰 가능한 결과를 확인해야 합니다. 이름 변경(rename) 작업의 경우, 코드베이스에서 이전 이름과 새 이름을 검색하여 어떤 호출 지점(call sites)이 실제로 변경되었는지 확인하는 것을 의미합니다. 검증기(validator)의 입력값은 대화 기록(transcript)이 아니라 세상(world) 그 자체여야 합니다.
- 부분 점수 부여 (Score with partial credit). 관찰된 결과를 '거의 다 왔음'을 표현할 수 있는 숫자나 구조화된 판결(verdict)로 변환합니다. 8개 중 6개를 성공했다면
0.75가 되며, 이 숫자는 불리언(boolean) 방식이었다면 지워버렸을 두 번의 실패를 그대로 담고 있습니다. - 임계값(thresholds)에 따른 결정. 점수가 작업을 완료했다고 부를 만큼 충분히 높은가? 아니면 작업을 포기하고 정직하게 실패를 보고할 만큼 충분히 낮은가? 혹은 더 많은 작업이 격차를 줄일 수 있는 중간 단계인가?
- 중간 사례의 라우팅 (Route the in-between case). 점수가 명확하게 완료된 상태도, 명확하게 가망이 없는 상태도 아닐 때, 다음 액션을 의도적으로 선택합니다. 누락된 두 곳의 호출 지점만 재시도(retry)합니다. 동적 문자열(dynamic-string) 케이스를 건드려야 하는지 사용자에게 묻습니다. 더 강력한 모델로 에스컬레이션(escalate)합니다. 라우터(router)는 루프가 제 역할을 다하는 지점입니다. 왜냐하면 단언(assertion)은 결코 할 수 없는 부분이기 때문입니다.
- 종료 조건까지 반복 (Iterate to a terminal condition). 다음 세 가지 중 하나가 참이 될 때까지 반복합니다: 점수가 수락 임계값(accept threshold)을 통과하거나, 재시도 예산(retry budget)이 소진되거나, 인간이 개입하는 경우입니다. 예산은 점수만큼이나 중요합니다. 예산이 없는 루프는 수렴(converge)하지 않고, 멈춰버립니다(hang).
길게 늘어뜨려 보면 제어 흐름(control flow)은 작지만 이는 하나의 사이클(cycle)이며, 그 사이클이 핵심입니다.
+---------------- route: retry / clarify / escalate ----------------+
| |
v |
...
제어권(control)이 어디에 있는지 주목하십시오. 모델은 이 루프(loop) 내부에서 추론하며, 판단이 주관적인 경우에는 모델이 직접 점수 산정(scoring)을 수행할 수도 있습니다. 하지만 모델이 루프 자체를 소유하는 것은 아닙니다. 무엇이 완료된 것으로 간주될지, 그리고 작업이 완료되지 않았을 때 어떤 일이 일어날지에 대한 결정은 오케스트레이터(orchestrator)의 몫으로 남습니다. 이는 technique piece가 그어놓은 경계와 동일합니다. 즉, 모델은 제어(control) 하에에서 추론할 수는 있지만, 제어 그 자체를 직접 지시할 수는 없습니다. 자신의 작업이 완료되었다고 스스로 선언하고 자신의 권한으로 종료해 버리는 에이전트에게는 루프가 없습니다. 그것은 단지 스스로를 위해 작성하고 스스로 채점한 단언(assertion)을 가지고 있을 뿐입니다.
점수 산정 함수(scoring function)가 어려운 절반입니다
만약 여러분이 불안정한 네트워크 호출을 위해 지수 백오프(retry-with-backoff)를 설계해 본 적이 있다면, 이미 이 루프의 대부분을 구축한 셈입니다. 관찰하고, 결정하고, 예산(budget) 내에서 재시도하고, 우아하게 포기하는 과정 말입니다. 그러한 메커니즘은 거의 변하지 않고 그대로 적용됩니다. 하지만 이 문제를 이미 해결된 분산 시스템(distributed-systems) 문제가 아니라, 진정한 프로덕션 AI 문제로 만드는 부분, 즉 그대로 적용되지 않는 부분은 바로 점수 산정 함수(scoring function)입니다.
이름 변경(rename) 작업의 경우, 결과가 결정론적인(deterministic) 수단으로 관찰 가능하기 때문에 점수 산정이 쉽습니다. 호출 지점(call sites)을 열거하고 개수를 셀 수 있기 때문입니다. 이와 같이 관찰을 통해 점수를 매길 수 있을 때는 언제나 그렇게 하십시오. 숫자는 환각(hallucination)을 일으키지 않기 때문입니다. 어려운 사례는 목표가 주관적인 경우입니다. 이 요약이 충실한가? 이 설명이 일관적인가? 이 답변이 요구 사항(brief)을 충족하는가?와 같은 경우입니다. 이 경우 점수는 판단(judgement)으로부터 나와야 하며, 종종 한 모델이 다른 모델의 출력을 판단하게 됩니다. 이제 여러분은 첫 번째 문제 위에 새로운 문제를 하나 더 쌓게 된 것입니다.
그 채점자(Scorer) 자체가 하나의 검증(Check)이며, 그 검증 또한 틀릴 수 있습니다. 만약 고장 난 채점자가 루프(Loop)를 주도하게 내버려 둔다면, 단순히 오류를 잡아내지 못하는 것에 그치지 않습니다. 여러분은 확신에 찬 오판을 제조하고, 이를 에이전트의 다음 행동으로 곧장 다시 피드백하게 됩니다. 잘못된 자(Ruler)를 기반으로 구축된 루프는 루프가 없는 것보다 더 나쁩니다. 이는 companion piece에서 설명한 이유와 정확히 일치합니다. 권위를 가진 잘못된 검증은 여러분을 불확실하게 만드는 것이 아니라, 확신에 찬 오판 상태로 만듭니다. 그리고 여기서는 이것이 복리로 쌓이는 사이클 속에서 일어납니다. 따라서 해당 글에서 강조한 규율은 채점자에게도 온전히 적용됩니다. 채점자가 판단할 근거가 되는 주장(Claims)만큼이나 채점자 자체를 엄격하게 검증하십시오. 단순히 숫자만을 내뱉기보다는 앵커(Anchors)와의 비교를 통해 점수를 매기십시오. 그리고 모델이 인간의 합의 수준을 뛰어넘을 것이라고 믿기 전에, 인간 간의 합의가 도달할 수 있는 한계치(Ceiling)를 먼저 파악하십시오.
루프가 존재하는 두 가지 위치
실제 에이전트에서 루프는 두 가지 규모로 나타나며, 성숙한 시스템은 이 두 가지를 모두 실행합니다.
첫 번째는 미들웨어(Middleware)로서의 단계별(Per-step) 루프입니다. 검증기(Validator)는 모델이 결과를 보기 전, 모든 도구 호출(Tool call) 이후에 실행됩니다. 다음 프롬프트로 다시 전달되는 것은 가공되지 않은 API 응답이 아닙니다. 그것은 검증되고, 점수가 매겨지며, 때로는 재작성된 버전입니다. 이것이 단 하나의 잘못된 단계가 하류(Downstream)의 모든 것을 조용히 오염시키는 것을 막아줍니다. 모델이 검증되지 않은 결과 위에서 추론할 기회 자체를 주지 않기 때문입니다. 여기서의 루프는 매우 빠르고 비용이 저렴하며 지속적으로 실행됩니다.
두 번째는 하네스(Harness)로서의 엔드투엔드(End-to-end) 루프입니다. 전체 작업의 경계에서, 여러분은 최종 결과 상태를 의도된 결과와 대조하여 확인하고 그 점수를 회로 차단기(Circuit breaker)로 취급합니다. 만약 엔드투엔드 점수가 임계값(Threshold) 미만이라면, 에이전트는 성공을 보고하지 않습니다. 대신 동작을 멈추고 부분적인 진행 상황을 정직하게 드러냅니다. 즉, 무엇이 변했는지, 무엇이 변하지 않았는지, 어디에서 멈췄는지를 보여줍니다. 이것이 바로 이름 변경(Rename) 오류를 잡아냈을 계층입니다. 단계별 미들웨어는 각 편집을 개별적으로는 통과시켰을지 모르지만, 전체 결과가 여전히 두 곳을 놓치고 있다면, 오직 전체 목표에 대한 검증만이 그 간극을 발견할 수 있습니다.
당신은 두 가지 모두를 원해야 합니다. 왜냐하면 그것들이 서로 다른 실패를 잡아내기 때문입니다. 미들웨어 (Middleware)는 잘못된 단계를 잡아냅니다. 하네스 (Harness)는 모든 단계가 괜찮아 보였음에도 불구하고 결과적으로 잘못된 작업을 잡아냅니다. 미들웨어를 건너뛰면 오류가 인지되기 전에 누적됩니다. 하네스를 건너뛰면 결국 달성하지 못한 성공을 보고하는 에이전트로 되돌아가게 되며, 이는 우리가 시작했던 지점과 같습니다.
분산 시스템 (Distributed-systems) 관점의 재구성
이 모든 것이 익숙하게 느껴진다면, 그것은 당연합니다. 이것은 단지 다른 옷을 입고 있는 '최종 일관성 (Eventual consistency)'일 뿐입니다.
분산 저장소 (Distributed store)에 새로 작성된 행에 대한 단언 (Assertion) 스타일의 체크가 실패하는 이유는 쓰기 (Write) 작업이 잘못되었기 때문이 아니라, 아직 전파 (Propagate)되지 않았기 때문입니다. 올바른 패턴은 결코 단일 단언이 아니었습니다. 그것은 루프 (Loop)였습니다. 읽기 (Read)가 쓰기와 수렴하거나, 타임아웃 (Timeout)이 기다림을 중단하라고 말할 때까지 폴링 (Poll)하는 것입니다. 모델 출력 검증 (Model output validation)도 인지적 불확실성 (Cognitive uncertainty)이 복제 지연 (Replication lag)을 대신하고 있을 뿐, 동일한 패턴입니다. 출력이 아직 확실히 검증된 상태가 아니므로, 관찰하고, 점수를 매기고, 더 나은 출력을 기다릴지 아니면 현재의 것을 수용할지 결정하며, 전체 과정이 종료될 수 있도록 예산 (Budget)을 설정하여 범위를 제한합니다.
이것이 바로 분산 시스템 배경을 가진 팀들이 이 개념이 명명되고 나면 더 빠르게 이를 제대로 수행하는 이유입니다. 당신은 진실이 늦고 불완전하게 도착하는 모든 시스템에서 단판 체크 (One-shot check)가 잘못된 도구라는 것을 이미 알고 있습니다. 당신은 수년 전에 이미 이를 위한 루프를 구축했습니다. LLM은 단지 진실이 늦고 불완전하게 도착하는 또 다른 소스일 뿐입니다. 지수 백오프 (Backoff)를 동반한 재시도 (Retry-with-backoff) 본능이 정답의 절반입니다. 점수 함수 (Scoring function)가 나머지 새로운 절반입니다.
단언 (Assertion)이 여전히 옳은 경우
루프가 만능 업그레이드는 아니며, 모든 곳에 루프를 적용하려는 시도 자체가 실수입니다. 루프가 존재하는 근본적인 이유는 출력이 비결정론적 (Non-deterministic)이기 때문입니다. 출력이 결정론적 (Deterministic)인 경우에는 단언이 단순히 적절할 뿐만 아니라 정확합니다. 결정론적인 단계에 점수 매기기 루프 (Scoring loop)를 감싸는 것은 아무런 이득 없이 비용과 지연 시간 (Latency)만 추가할 뿐입니다.
스키마는 검증되거나 되지 않거나 둘 중 하나입니다. 파서(Parser)는 성공하거나 오류를 던지거나 둘 중 하나입니다. 행(Row)의 개수는 일치하거나 일치하지 않거나 둘 중 하나입니다. 이것들은 단언(Assertion)이며, 시스템의 경계에서 그들이 잘하는 단발성 작업(One-shot job)을 정확히 수행하는 단언으로 남아 있어야 합니다. 기법 경계 (Technique boundary)는 무엇이 무엇인지 결정하는 도구입니다. 가능한 한 많은 시스템을 단순한 단언이 유효한 결정론적(Deterministic) 기반 위에 두고, 루프(Loop)는 진정으로 비결정론적(Non-deterministic)이며 진정으로 판단(Judgement)이 필요한 부분들을 위해 남겨두십시오. JSON 스키마 체크를 통과하기 위해 점수를 매기고 재시도(Retry)를 반복하는 에이전트는 이 글에서 다루는 것과는 정반대의 실수를 저지르고 있는 것이며, 이 또한 똑같이 낭비적입니다.
단 하나의 정직한 질문
다음에 에이전트가 자신만만하게 _완료(Done)_라고 보고한다면, 루프는 묻지만 단언은 물을 수 없는 질문을 던져보십시오. _통과했는가(Did it pass)_가 아니라, _어떻게 아는가, 그리고 그것을 알기 위해 무엇을 관찰했는가_라고 말입니다. 만약 답변이 모델이 그렇게 믿는다는 것이라면, 당신은 모델이 스스로를 위해 작성한 단언을 가진 것입니다. 만약 답변이 목표에 따라 점수가 매겨진 세상에 대한 실제 관찰이며, 그 점수로부터 도출된 결정이라면, 당신은 루프를 가진 것입니다. 이 두 에이전트 중 실패했을 때 당신에게 알려줄 수 있다고 신뢰할 수 있는 것은 오직 하나뿐이며, 그것은 바로 관찰하도록 설계된 에이전트입니다.
이 글은 두 편의 동반 게시물을 확장합니다. 이 글이 전제하는 경계: 가능한 곳에는 결정론을, 필요한 곳에는 판단을 (Determinism Where You Can, Judgement Where Must). 이 글의 스코어러(Scorer)가 의존하는 체크-검증 규율: 잘못된 자는 없는 것보다 못하다 (A Wrong Ruler Is Worse Than No Ruler).
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기