레슨 3: 결정론적 검증기: 통과/실패를 내는 검사만이 유효하다
학습 목표:
- 후보 검증기들을 '가장 빠르고, 가장 믿을 만하고, 가장 확장 가능한' 순으로 줄 세우고 특정 출력에 맞는 것 고르기
- 에이전트가 읽고 반복할 수 있는 형태로 통과/실패를 돌려주는 결정론적 검증 스크립트 쓰기
- 지나치게 엄격한 검증기가 올바른 출력을 거부하는 거짓 음성을 알아보고 정규화로 고치기, 그리고 결정론적 검사의 천장이 어디인지 이해하기
전제: 레슨 1과 2(실행할 수 있는 검사가 없으면 '완료된 것처럼 보인다'가 유일한 신호라는 것, 종료 상태를 먼저 검증하고 프로세스는 보조로 둔다는 것, 성공 기준은 측정 가능해야 한다는 것) | 이전: << 레슨 2 | 다음: 레슨 4 >>
'무엇을 검증할 것인가'에서 '어떻게 검증할 것인가'로
레슨 2를 끝낸 시점이면 구체적인 성공 기준을 손에 적어 두었을 것입니다. 이 레슨에서 쓸 예시를 봅시다. 에이전트가 판매 CSV 한 묶음을 읽어, 제목과 채널별 항목과 합계를 담은 report.json 으로 집계합니다. 레슨 2가 가르친 것은 이 기준을 종료 상태로 짜는 것이었습니다 — 파일이 존재한다, 필드가 갖춰져 있다, 합계가 항목 건수의 총합과 같다 — '파일을 읽고, 그다음 합계를 계산하고, 그다음 파일을 쓴다' 같은 턴별 프로세스 확인이 아니라요.
기준은 손에 있습니다. 다음 질문은 실무적입니다. 그 기준에 무엇으로 대조할 것인가?
눈으로 볼 수도 있습니다. 다른 모델에 읽히고 피드백을 받을 수도 있습니다. 아니면 JSON을 읽어 건수를 더하고 맞지 않으면 0이 아닌 값으로 종료하는 Node 코드 열두어 줄을 쓸 수도 있습니다. 셋 다 결론에 이르지만 비용과 신뢰성은 크게 다릅니다.
공식 문서는 줄 세우기 원칙을 줍니다. 가장 빠르고, 가장 믿을 만하고, 가장 확장 가능한 채점 방법을 고르라1. 그 잣대로 보면 세 부류의 순서는 분명합니다.
- 코드 기반 채점 — 가장 빠르고 가장 믿을 만하며 확장성이 대단히 좋습니다. 약점은 규칙에 덜 얽매인 판단이 필요한 복잡한 사안에서 결이 부족하다는 것입니다1.
- LLM 기반 채점 — 빠르고 유연하며, 확장 가능하고 복잡한 판단에 적합하지만, 먼저 신뢰성을 시험해 보고 나서 확장해야 합니다1. (레슨 4의 주제입니다.)
- 사람 채점 — 가장 유연하고 품질이 높지만 느리고 비쌉니다. 가능하면 피하세요1.
여기서 '믿을 만하다'는 같은 입력에는 매번 같은 판정을 뜻합니다. 코드 기반 채점이 1위인 것은 똑똑해서가 아니라 예측 가능하게 멍청하기 때문입니다 — 오늘은 기분이 좋아 통과시키고 내일은 표현이 마음에 안 들어 떨어뜨리는 일이 없습니다. 컴퓨팅에서 결정론적 시스템은 동일한 입력이 주어지면 매번 동일한 출력을 내놓지만, 비결정론적 시스템 — 에이전트 같은 — 은 같은 시작 조건에서도 서로 다른 응답을 생성할 수 있습니다2. 이 레슨이 다루는 '결정론적 검증기'는 바로 그 예측 가능하게 멍청한 검사입니다. 결정론적인 것이 비결정론적인 것을 평가하는 것이지요.
평가 작업을 설계할 때 관련된 원칙이 하나 있습니다. 자동 채점이 가능하도록 문제를 구성하라(예를 들어 객관식, 문자열 일치, 코드 채점, LLM 채점)1. 자동화할 여지가 있는 곳에서는 잡으세요.
검증기 메뉴: 신호를 돌려주는 것이면 무엇이든
'검증기'라고 하면 전용 프레임워크처럼 들리지만 문턱은 훨씬 낮습니다. 공식 문서의 정의는 거의 무뚝뚝합니다. 검사란 Claude가 대화 안에서 읽을 수 있는 신호를 돌려주는 것이면 무엇이든이다. 테스트 스위트, 빌드의 종료 코드, 린터, 출력을 픽스처와 diff하는 스크립트, 또는 디자인과 비교하는 브라우저 스크린샷3.
이 목록을 풀어 봅시다. npm test 를 돌려 전부 초록이면 통과이고, 코드 산출물에 딱 맞습니다 — 코드 해법은 자동 테스트로 검증 가능합니다4. 빌드의 종료 코드는 가장 쉽습니다. 툴체인이 단언을 이미 대신 써 둔 셈이니까요. 린터만으로는 부족하지만 '하지 말아야 할 실수'의 바닥으로는 훌륭합니다. diff 스크립트는 이번 실행의 출력을 미리 준비한 픽스처 파일(정답으로 알려진 표본)과 비교하며, 출력이 안정적이고 형식이 고정된 경우에 맞습니다. 스크린샷 비교는 프런트엔드 스타일을 손볼 때 씁니다.
엔지니어링 실무에 관한 참고 한 줄. 컴파일러와 정적 타입 검사기(tsc --noEmit 같은)도 실제 프로젝트에서 이런 식으로 자주 쓰입니다. 그것들 역시 종료 코드와 읽을 수 있는 에러 위치를 내놓기 때문입니다. 이것은 제가 덧붙인 것이지 공식 목록에 있는 것이 아니니, 공식이 보증한 것으로 여기지 마세요.
이 방법들은 서로 배타적이지 않습니다. 검증기는 스펙트럼을 이룹니다 — 한쪽 끝은 '픽스처와의 정확한 문자열 일치'이고, 다른 쪽 끝은 'Claude에 판정을 맡기기'입니다2. 이 레슨은 왼쪽 절반을 다루고, 레슨 4가 오른쪽으로 갑니다.
최소 형태: output == golden_answer
스펙트럼의 왼쪽 가장자리는 이렇게 생겼습니다1.
동등 비교 하나뿐입니다. 이것을 정확 일치라고 부릅니다. 모델의 출력이 미리 정해 둔 올바른 답과 일치하는지를 재며, 보통 공백과 대소문자를 정규화한 뒤에 비교합니다. 단순하고 모호하지 않은 지표라, 감정 분석(positive, negative, neutral)처럼 답이 뚜렷하게 갈리고 범주형인 작업에 완벽하게 맞습니다1.
여기서 '정규화'는 평범한 뜻입니다. 비교하기 전에, 의미를 담고 있지 않은 차이를 지우는 것입니다. 코드로 보면 이렇습니다.
두 번째 호출의 결과를 한참 들여다볼 값어치가 있습니다. trim() 과 toLowerCase() 는 줄바꿈과 대소문자를 구해 냈지만 저 마침표는 구하지 못했습니다. 정규화를 어디까지 밀고 갈지는 여러분의 작업에 어떤 차이가 무의미한지에 달렸고, 그 판단은 어떤 라이브러리도 대신해 주지 않습니다. 이 레슨 뒤쪽의 함정 절은 그 판단을 잘못했을 때 무슨 일이 벌어지는지에 관한 이야기입니다.
검증기의 출력을 읽을 수 있게 만들기
그 정의에는 자주 건너뛰는 절이 하나 있었습니다. 신호는 Claude가 대화 안에서 읽을 수 있는 것이어야 한다는 것입니다3. 그 절이 여러분의 검증 스크립트가 출력을 어떻게 써야 하는지를 결정합니다. 실패 메시지 둘을 비교해 보세요.
첫 번째는 에이전트에 '너 틀렸다'고 말하고 그다음은 짐작에 맡깁니다. 두 번째는 어떤 필드가 실패했는지, 무엇을 기대했는지, 실제는 무엇이었는지를 말해 줍니다 — 다음 턴에 그 숫자를 곧장 고치러 갈 수 있습니다. 같은 통과/실패인데 정보량은 자릿수가 다릅니다. 공식 안내의 다른 대목도 같은 말을 합니다. Claude가 성공을 주장하게 하지 말고 증거를 보여 주게 하라 — 테스트 출력, 실행한 명령과 그것이 돌려준 것, 또는 결과의 스크린샷. 증거를 검토하는 편이 검증을 직접 다시 돌리는 것보다 빠르고, 지켜보지 않았던 세션에도 통합니다3. 여러분의 검증 스크립트가 바로 그 증거를 생산하는 쪽입니다. 그것이 흐릿하면 증거도 흐릿합니다.
실무 규칙 둘. 통과에는 종료 코드 0, 실패에는 0이 아닌 값을 쓰세요(CI와 셸의 && 가 그대로 씁니다). stdout에는 실패 하나에 한 줄씩, '어떤 필드가, 무엇을 기대했고, 무엇이 왔는지'를 밝히세요.
통과/실패가 있으면 에이전트의 행동이 달라진다
실행 중에 에이전트에는 각 단계에서 환경으로부터 오는 "ground truth"(도구 호출 결과나 코드 실행 같은)가 진척을 평가하는 데 필요합니다4. 검증기가 없으면 얻을 수 있는 유일한 진척 신호는 방금 자기가 쓴 문단뿐입니다 — 다 됐다고 여기면 다 된 것입니다. 검증기가 있으면 환경 안에 자기 판단과 독립적인 사실의 출처가 생깁니다.
그래서 행동의 사슬이 바뀝니다. 통과냐 실패냐를 내놓는 것을 Claude에게 주면 루프가 스스로 닫힙니다. Claude가 작업을 하고, 검사를 돌리고, 결과를 읽고, 검사가 통과할 때까지 반복합니다3. 달리 말하면, 에이전트는 테스트 결과를 피드백 삼아 해법을 반복 개선할 수 있습니다4.
이 시리즈 7번째 코스에서 손으로 썼던 종류의 하네스 루프에서는, 검사를 도구로 노출하는 것이 구현이 됩니다.
검증기의 출력은 tool_result 를 통해 대화로 흘러들어옵니다. 모델은 total 불일치: 선언값 48, item count 합계 50 을 읽고 다음 턴에 그것을 고치러 갑니다. 여러분이 참여한 단계는 하나도 없습니다.
편리한 확장이 둘 있습니다. 첫째, 검사는 끝에만 놓일 필요가 없습니다. 레슨 2에서 복잡한 워크플로는 모든 중간 단계를 검증하는 대신 특정 상태 변화가 일어났어야 하는 개별 체크포인트로 쪼갤 수 있다고 다뤘습니다5. 그 검증 체크포인트들은 결정론적 검사를 놓기에 자연스러운 착지점입니다 — '모든 CSV를 읽은 뒤, 행 수는 개별 파일 행 수의 총합과 같아야 한다'처럼요. 같은 회고 글은 에이전트의 적응력을 재시도 로직이나 정기적인 체크포인트 같은 결정론적 안전장치와 결합하는 것도 언급합니다5(거기서 말하는 '체크포인트'는 이 시리즈 9번째 코스에서 다룬 상태 저장형 복구 체크포인트를 가리킨다는 점에 주의하세요).
둘째, 한 부류의 검증은 API 계층으로 앞당길 수 있습니다. 도구 정의에 strict: true 를 더하면 Claude의 도구 호출이 언제나 여러분의 스키마와 정확히 일치하도록 보장됩니다6. 스키마는 파라미터 구조에 대한 여러분의 선언입니다.
그 한 줄로 '필드 이름 오타'나 'count 를 문자열로 넘김' 같은 구조적 문제가 '코드를 써서 확인할 것'에서 플랫폼 차원의 보장으로 넘어갑니다. 도구는 결정론적 시스템과 비결정론적 에이전트 사이의 계약이고2, strict 는 그 계약을 인터페이스에 써 넣는 방법입니다.
다만 이것이 단속하는 것은 구조이지 의미가 아닙니다. total 이 실제로 모든 count 값의 합과 같은지에 대해 스키마는 할 말이 없습니다 — 그 부분은 여전히 여러분이 검증해야 합니다.
함정: 지나치게 엄격한 검증기는 올바른 출력을 거부한다
결정론적 검사가 실패하는 가장 흔한 방식이고, 벌어져도 알아채지 못할 때가 많습니다.
도구 평가에 대한 공식 안내는 날카롭습니다. 형식, 문장 부호, 유효한 다른 표현 같은 겉도는 차이 때문에 올바른 응답을 거부하는, 지나치게 엄격한 검증기를 피하라2.
'겉도는 차이'가 핵심 표현입니다. 같은 정답이 끝에 공백 하나를 달고 올 수도 있고, positive 를 Positive 로 쓸 수도 있고, 단어 사이에 공백이 하나가 아니라 둘일 수도 있습니다. 이 차이들은 작업에 아무 의미도 없지만, 바이트 단위 비교 검증기에는 재앙입니다. 실패의 방향도 음험합니다. 에러를 통과시키는 것이 아니라 올바른 출력을 벌주는 것 — 그것이 거짓 음성입니다.
구체적인 파탄과 수정을 봅시다. 픽스처의 제목은 2026년 Q1 채널 집계 입니다. 에이전트의 report.json 은 데이터가 전부 올바르고, 제목 앞뒤에 공백이 있고 단어 사이에 공백이 둘 더 있을 뿐입니다. 순진하게 엄격한 검증기는 이렇게 생겼습니다.
돌려 보면 실제 출력은 이렇습니다.
내용이 완전히 올바른 리포트가 끝의 줄바꿈 하나로 떨어졌습니다. 이 결과가 에이전트에 되돌아가면 에이전트는 제목의 공백을 만지작거리러 갑니다 — 작업과는 무관한 방향으로요.
수정은 함수 하나입니다.
replace(/\s+/g, " ") 가 연속된 공백을 하나로 접고, trim() 이 앞뒤를 떼고, toLowerCase() 가 대소문자를 통일합니다. 바꾸고 나면 같은 파일이 통과합니다 — 전체 스크립트와 실제 실행 결과는 아래 연습의 레벨 2에 있습니다.
반대쪽 주의도 있습니다. 정규화는 '많을수록 좋다'가 아닙니다. 문장 부호까지 지워 버리면 total: 48 과 total: 4.8 같은 진짜 에러를 매끄럽게 덮어 버릴 수 있습니다. 판단 기준은 언제나 같습니다. 이 차이가 의미를 담고 있는가? 담고 있으면 엄격하게. 담고 있지 않으면 정규화로.
결정론적 검사의 천장은 어디인가
결정론적 검증기에는 잘 정의된 적용 경계가 있습니다.
첫 번째 경계는 자유 텍스트입니다. 조사의 산출물은 자유 형식 텍스트이고 정답이 하나인 경우가 드물어 프로그램으로 평가하기 어렵습니다5. 요약문에 output == golden_answer 를 쓸 수는 없습니다 — 같은 자료를 두고 잘 쓴 요약문 둘이 완전히 다른 표현을 쓸 수 있으니까요. 그런 종류의 판정은 레슨 4의 영역으로 갑니다.
두 번째 경계는 '더 넓은 시스템 요구사항에 맞는가'입니다. 코드 해법은 자동 테스트로 검증 가능하지만, 해법이 더 넓은 시스템 요구사항에 부합함을 보장하려면 사람의 리뷰가 여전히 결정적으로 중요합니다4. 패치 하나가 모든 테스트를 통과하면서도 모듈 전체를 유지 보수 불가능하게 만드는 나쁜 설계일 수 있습니다. 멀티 에이전트 회고도 같은 말을 울립니다. 자동 평가가 있는 세상에서도 손으로 하는 테스트는 여전히 필수입니다5.
결정론적 검사는 '틀리지 말아야 할 것이 틀리지 않았다'는 바닥을 담당합니다. 그 바닥 위쪽에는 다른 도구가 필요합니다.
비례: 모든 것이 검증기를 가질 값어치가 있는 것은 아니다
반대 방향의 실수도 흔합니다. 한 번 쓰고 버릴 스크립트에 온전한 검증 세트를 짓는 것입니다.
에이전트가 한 번 돌리고 지울 데이터 마이그레이션 스크립트를 썼는데, 여러분은 구조 검증에 픽스처 비교에 회귀 표본까지 마련합니다 — 검증기를 쓰는 데 든 시간이 출력을 그냥 눈으로 보는 시간을 넘어섭니다.
판단의 잣대는 여전히 그 오래된 문장입니다. 복잡성은 그것이 결과를 눈에 보이게 개선할 때에만 더할 것을 고려해야 합니다4. 검증기에 한정하면 스스로에게 이렇게 물으세요.
- 이 검사는 몇 번이나 돌아갈 것인가? 한 번뿐이고 바로 옆에서 지켜보고 있다면 여러분의 눈이 더 빠를 수 있습니다.
- 그것이 없으면 에러는 얼마 만에 발견되는가? '즉시, 지금 보고 있으니까'와 '하류가 깨질 때, 이틀 뒤'는 완전히 다른 결론을 냅니다.
- 이 작업에 프롬프트를 몇 번이나 손볼 계획인가? 한 번을 넘는 순간, 전후를 비교할 안정적인 잣대가 필요합니다. 아니면 '이게 나아진 건가'는 언제나 짐작으로 남습니다.
반대로 반드시 써야 할 때에 대한 공식 안내는 단호합니다. 언제나 검증을 제공하라(테스트, 스크립트, 스크린샷). 검증할 수 없다면, 배포하지 마라3.
💻 연습
정리
- 채점 방법의 줄 세우기 원칙은 '가장 빠르고, 가장 믿을 만하고, 가장 확장 가능한'이다. 코드 기반 채점이 셋 다에서 1위이며, 대가는 규칙에 덜 얽매인 판단이 필요한 복잡한 사안에서 결이 부족하다는 것이다1
- 검사는 Claude가 대화 안에서 읽을 수 있는 신호를 돌려주는 것이면 무엇이든 될 수 있다. 테스트 스위트, 빌드의 종료 코드, 린터, 출력을 픽스처와 diff하는 스크립트, 디자인과 비교하는 스크린샷3
- 검증기는 스펙트럼을 이룬다. 왼쪽 가장자리는 픽스처와의 정확한 문자열 일치이고 오른쪽 가장자리는 Claude에 판정을 맡기기다2. 최소 형태는
output == golden_answer 뿐이고, 보통 공백과 대소문자를 정규화한 뒤에 비교하며, 답이 뚜렷하게 갈리고 범주형인 작업에 완벽하게 맞는다1
- 통과/실패가 있으면 루프가 스스로 닫힌다. 작업을 하고, 검사를 돌리고, 결과를 읽고, 통과할 때까지 반복한다3. 에이전트는 진척을 평가하기 위해 각 단계에서 환경으로부터 오는 ground truth가 필요하기 때문이고4, 그래서 테스트 결과를 피드백 삼아 반복 개선할 수 있는 것이다4
- 복잡한 워크플로는 모든 중간 단계를 검증하는 대신 특정 상태 변화가 일어났어야 하는 개별 검증 체크포인트로 쪼갤 수 있다5. 한 부류의 구조 검증은
strict: true 로 API 계층까지 앞당겨 도구 호출이 스키마를 엄격히 따르게 할 수도 있다6
- 가장 큰 함정은 지나치게 엄격한 검증기다. 형식, 문장 부호, 유효한 다른 표현 같은 겉도는 차이 때문에 올바른 응답을 거부한다2. 고치는 방법은 먼저 정규화하고 그다음 비교하며, 엄격함은 정말로 의미를 담은 부분에 아껴 두는 것이다
- 결정론적 검사에는 천장이 있다. 조사의 산출물은 프로그램으로 평가하기 어렵고5, 자동 테스트는 기능을 검증하지만 해법이 더 넓은 시스템 요구사항에 부합함을 보장하려면 사람의 리뷰가 여전히 결정적으로 중요하며4, 자동 평가가 성숙해도 손으로 하는 테스트는 여전히 필수다5
- 한 번 쓰고 버릴 스크립트에 온전한 검증기를 짓지 마라. 복잡성은 그것이 결과를 눈에 보이게 개선할 때에만 더할 것을 고려해야 한다4. 다만 반대 방향으로는, 검증할 수 없다면 배포하지 마라3
>> 레슨 4: LLM 판정자: 루브릭, 출력 형식, 그리고 판정을 맡기면 안 되는 것