아래는 서로 다른 세 세션에서 나온 에이전트 완료 보고 세 건입니다. 각각에 대해 어느 부분이 주장이고 어느 부분이 증거인지 판단하세요. 그다음, 각 보고를 '검수 통과'로 취급하기 전에 구체적으로 어떤 증거가 아직 빠졌는지 쓰세요.
레벨 1: 주장과 증거 구별하기글로 답하면 되고 코드는 필요 없습니다.
보고 A
보고 B
보고 C
학습 목표:
- Claude가 작업이 완료된 것처럼 보이는 지점에서 멈추는 이유와, 그럴 때 검증을 결국 누가 떠맡게 되는지 설명하기
- 결정론적 시스템과 비결정론적 시스템의 대비를 사용해, 전통적인 테스트의 "입력 X, 경로 Y, 출력 Z" 전제가 에이전트에서 무너지는 이유 설명하기
- 완료 보고를 받았을 때 어느 문장이 주장이고 어느 문장이 증거인지 구별하고, 검수에 무엇이 아직 빠졌는지 짚어내기
전제: 이 시리즈의 앞선 아홉 개 코스를 마쳤고,
stop_reason이 구동하는 하네스 루프를 손으로 쓸 수 있으며, 체크포인트와 복구를 이해하고 있어야 합니다 | 다음: 레슨 2 >>
화요일 오후, 여러분은 사내 관리자 페이지에 '사용자 일괄 가져오기' 기능을 추가해 달라고 에이전트에 요청합니다. CSV를 업로드하고, 파싱하고, 필드를 검증하고, 데이터베이스에 씁니다. 요구사항을 분명히 적어 준 뒤 회의에 들어갑니다.
돌아와 보니 세션은 멈춰 있습니다. 마지막 메시지는 이렇습니다.
여러분은 diff를 훑어봅니다. 함수는 깔끔하게 분리되어 있고, 이름은 이웃 모듈과 맞춰져 있고, 엣지 케이스도 고려된 것처럼 보입니다 — 빈 파일은 명시적인 에러를 돌려주고, 이메일 정규식도 딱 봐서 망가져 보이지는 않습니다. 머지합니다. 배포합니다.
금요일 오후, 운영팀이 채널에 글을 올립니다. '방금 빈 사용자가 400건이나 들어왔는데 왜죠?'
원인은 간단합니다. 운영팀은 그 CSV를 Excel에서 '다른 이름으로 저장'해서 만들었고, Excel은 UTF-8 파일 앞에 BOM — 눈에 보이지 않는 3바이트 — 을 붙이기를 좋아합니다. 그래서 첫 번째 열 이름이 email 이 아니라 email 로 파싱되었고, 필드 매핑 전체가 헛돌았으며, 모든 행이 '전 필드가 undefined'가 되었습니다. 그 검증 계층은 어땠을까요? 그것은 '이메일 형식이 유효한가'를 확인했지만, undefined 는 다른 분기로 빠져 '이 열은 채워지지 않았다'로 취급되었고, 그래서 통과했습니다.
여기서 손을 뺀 사람은 없습니다. 에이전트는 돌아가는 코드를 썼습니다. 자기가 만든 CSV로 스스로 테스트도 했습니다 — 물론 자기가 만든 CSV에는 BOM이 없습니다. 여러분이 diff를 리뷰할 때 보고 있던 것은 '이 코드가 올바르게 쓰였는가'였지, '이 코드가 현실의 입력과 만나면 무슨 일이 벌어지는가'가 아니었습니다. 양쪽 다 최선을 다했습니다. 그래도 간극은 생겼습니다.
문제는 멈춘 그 순간에 있습니다. 에이전트가 멈췄을 때 손에 있던 것은 '썼고, 한 번 다시 읽었고, 괜찮아 보인다'였습니다. '완료됐음을 확인했다'에서 멈춘 것이 아닙니다. '완료된 것처럼 보인다'에서 멈춘 것입니다. 그리고 대화 히스토리만으로는 그 차이를 구별할 수 없습니다.
Claude Code 문서는 이것을 담담하게 적어 둡니다. Claude는 작업이 완료된 것처럼 보이면 멈춥니다. 스스로 실행할 수 있는 검사가 없으면 '완료된 것처럼 보인다'가 손에 넣을 수 있는 유일한 신호이고, 여러분 자신이 검증 루프가 됩니다. 모든 실수가 여러분이 알아채 주기를 기다리게 됩니다1.
이 문장은 단어 하나하나 두 번 읽을 값어치가 있습니다. 'Claude가 가끔 대충 한다'도 아니고 '모델의 능력이 아직 부족하다'도 아닙니다. 이것은 구조적인 사실을 서술합니다. 파이프라인 전체에서 객관적인 결과를 낼 수 있는 것이 하나도 없다면, '완료된 것처럼 보인다'가 이 시스템에 존재하는 유일한 신호가 됩니다. 모델은 그 신호만으로 판단할 수밖에 없습니다. 다른 것은 아무것도 가지고 있지 않습니다.
같은 문서는 이 현상에 이름을 붙입니다. the trust-then-verify gap(먼저 믿고 그다음에 검증하는 간극) — Claude는 그럴듯해 보이지만 엣지 케이스를 처리하지 않는 구현을 내놓습니다1. 쉽게 말하면 이렇습니다. 먼저 믿고(코드가 좋아 보인다), 검증은 일어나지 않거나 너무 늦게 일어납니다(금요일 오후, 운영 채널에서). 위의 BOM 예시는 이 간극의 표준형입니다. 코드가 틀린 것이 아니라, 'Excel에서 내보낸 파일이면 어떻게 되지?'를 아무도 묻지 않은 것입니다.
여기에는 놓치기 쉬운 두 번째 층이 있습니다. 문서가 제시하는 대책은 이렇게 끝납니다. 검증할 수 없다면, 배포하지 마라1. 강조점은 '검증한다'가 아니라 '배포하지 않는다' 쪽에 있습니다. 이것은 아무리 해도 검증할 수 없는 것이 존재함을 인정합니다. 검증할 수 없을 때 올바른 한 수는 '이번만 감을 믿는다'가 아닙니다. 범위를 좁히거나, 요구사항을 바꾸거나, 배포를 미루는 것입니다.
그 완료 메시지로 돌아갑시다. 한 문장씩 쪼개고, 각각에 같은 질문을 던집니다. 코드를 읽지 않고, 보여 준 것만으로 이 문장을 확인할 수 있는가?
src/importer/parseCsv.js 추가' — 확인할 수 있습니다. 파일이 존재하는지는 한눈에 확인됩니다. 이것은 증거입니다(가장 약한 종류이긴 합니다).경계선은 어디일까요? 증거는 다른 사람이 똑같은 방식으로 다시 돌려 볼 수 있는 것입니다. 명령과 그 원본 출력, 종료 코드, 실패한 테스트 이름 목록, 스크린샷, 변경 전후의 수치 비교. 주장은 믿을지 말지 고를 수밖에 없는 것입니다. '로직은 올바릅니다', '문제없을 겁니다', '이미 최적화했습니다', '다시는 일어나지 않을 겁니다'.
공식 문서는 정확히 이 선을 긋습니다. Claude가 성공을 주장하게 하지 말고 증거를 보여 주게 하라 — 테스트 출력, 실행한 명령과 그것이 돌려준 것, 또는 결과의 스크린샷. 증거를 검토하는 편이 검증을 직접 다시 돌리는 것보다 빠르고, 여러분이 지켜보지 않았던 세션에도 통합니다1.
마지막 반 문장이 핵심입니다. 처음부터 끝까지 지켜보고 있었다면 '주장 대 증거'의 구별은 별 값어치가 없습니다 — 직접 봤으니까요. 하지만 눈을 뗀 순간, 대화 히스토리에 남는 것은 텍스트뿐이고, 텍스트 안에서 주장은 증거만큼이나 자신 있어 보입니다.
'맞아 보이는데 틀렸다'는 전통적인 소프트웨어에서도 봅니다. 왜 에이전트에서는 레슨 하나를 통째로 쓸 만한 문제가 될까요?
전통적인 테스트가 에이전트는 만족시키지 못하는 전제 위에 서 있기 때문입니다.
정의부터 봅시다. 컴퓨팅에서 결정론적 시스템은 동일한 입력이 주어지면 매번 동일한 출력을 내놓지만, 비결정론적 시스템 — 에이전트 같은 — 은 같은 시작 조건에서도 서로 다른 응답을 생성할 수 있습니다2. 이것은 '버그가 있어서 불안정하다'가 아닙니다. 원래 그렇게 동작하는 것입니다. 프롬프트에서 아무것도 바꾸지 않아도 두 실행의 결정이 일치한다는 보장은 없습니다3.
그래서 전통적인 평가의 전제가 무너집니다. 전통적인 평가는 흔히 AI가 매번 같은 단계를 따른다고 가정합니다. 입력 X가 주어지면 시스템은 경로 Y를 따라 출력 Z를 내놓아야 한다는 것입니다3. 멀티 에이전트 시스템은 이렇게 동작하지 않습니다. 시작점이 동일해도 에이전트는 목표에 도달하기 위해 완전히 다른, 그러나 유효한 경로를 택할 수 있습니다 — 한 에이전트는 출처 세 개를 검색하고 다른 에이전트는 열 개를 검색할 수 있고, 같은 답을 찾는 데 서로 다른 도구를 쓸 수도 있습니다3.
구체적으로는 이렇게 보입니다.
어느 쪽 궤적도 '틀렸다'고 부를 수 없습니다. 두 번째 실행은 파일을 하나 더 읽고, 한 군데를 더 고치고, 테스트를 두 번 돌렸습니다 — 돌아간 것일 수도 있고, 첫 번째 실행이 놓친 결합을 잡아낸 것일 수도 있습니다. '반드시 schema.sql 을 먼저 읽어야 한다'는 단언을 쓰면 두 번째 실행은 실패합니다 — 그런데 두 번째 실행이 더 나은 일을 했을 수도 있습니다.
미리 정해 둔 각본에 궤적을 대조하는 방식은 여기서 통하지 않습니다. 올바른 단계가 무엇인지 늘 아는 것이 아니기 때문에, 미리 규정해 둔 '올바른' 단계를 에이전트가 따랐는지 확인하는 것만으로는 대개 충분하지 않습니다3.
층을 하나 더 얹습니다. 에이전트 시스템에서 에러는 복리로 불어납니다. 전통적인 소프트웨어에서는 사소한 버그가, 에이전트에 닿으면 작업 전체를 탈선시킬 수 있습니다 — 한 단계의 실패가 에이전트를 완전히 다른 궤적으로 탐색하게 만들고, 예측 불가능한 결과로 이어집니다3. 이것은 '어떤 함수가 잘못된 값을 돌려주고 그것이 위로 전파된다'는 전통적인 프로그램과는 다릅니다. 에이전트는 잘못된 결과를 받아 그 잘못된 결과를 근거로 새로운 결정을 내립니다. 파일을 잘못 읽으면 '이 모듈은 존재하지 않는다'고 결론 내리고 새 모듈을 만들 수 있고, 그다음에는 그 새 모듈을 중심으로 계속 작업합니다. 최종 출력을 볼 때쯤이면 에러는 처음 있던 자리에 더 이상 없습니다. 다른 무언가로 자라 있습니다.
Anthropic 자신의 결론도 여기에 닿습니다. 에이전트의 자율적인 성격은 더 높은 비용과 에러가 복리로 불어날 가능성을 뜻하므로, 샌드박스 환경에서의 광범위한 테스트를 적절한 가드레일과 함께 권장합니다4. 하나 더, 더 직접적인 문장이 있습니다. LLM은 잠재적으로 여러 턴에 걸쳐 동작하므로, 여러분은 그 의사 결정에 어느 정도의 신뢰를 두어야 합니다4.
'어느 정도의 신뢰'라는 표현에 주목하세요. '신뢰해야만 한다'고 말하는 것이 아닙니다. 이 신뢰가 어딘가에서 와야 한다고 말하는 것입니다. 그리고 신뢰의 출처는 둘뿐입니다. 여러분이 직접 지켜봤거나(그렇다면 에이전트는 여러분의 시간을 하나도 아껴 주지 않았습니다), 무언가가 여러분 대신 지켜봤거나. 이 코스 전체는 두 번째에 대한 이야기입니다.
지금까지의 밑작업은 한 문장으로 모입니다. Claude에게 스스로 실행할 수 있는 검사를 쥐여 주세요 — 테스트, 빌드, 비교할 스크린샷. 그것이 지켜봐야 하는 세션과 자리를 떠도 되는 세션의 차이입니다1.
그 차이는 어디서 생길까요? 통과냐 실패냐를 내놓는 것을 Claude에게 주면 루프가 스스로 닫힙니다. Claude가 작업을 하고, 검사를 돌리고, 결과를 읽고, 검사가 통과할 때까지 반복합니다1.
이 문장은 이 시리즈 7번째 코스에서 다룬 하네스 루프에 그대로 대응시킬 수 있습니다. 먼저 지금의 루프가 어디서 멈추는지 보세요.
end_turn 은 무슨 뜻일까요? 모델이 이번 턴에 할 말을 다 했다고 여긴다는 뜻입니다. 그게 전부입니다. 작업이 올바르다는 뜻이 아니고, 응답이 완결됐다는 보장조차 아닙니다 — 이 루프는 tool_use 만 알아보므로 stop_reason 이 그 밖의 무엇이 되든 빠져나갑니다. 출력이 max_tokens 때문에 문장 중간에서 잘린 경우까지 포함해서요. 종료 조건 안에 '출력의 품질'과 관련된 것은 아무것도 없습니다.
그러면 검사를 연결한다는 것은 어떤 모습일까요? 유효한 자리는 두 곳입니다.
자리 1, 검사를 에이전트가 호출할 수 있는 도구로 만들어 루프 안쪽에서 돌리기.
자리 2, 루프가 빠져나온 뒤에 게이트를 두기. 자기 보고를 믿지 말고 직접 돌리기.
코드 자체에 묘수는 없습니다. 핵심은 종료 조건의 주인이 바뀌었다는 것입니다. '모델이 더는 도구를 부르고 싶지 않다고 말했다'에서 '결정론적인 코드가 0을 돌려줬다'로. 앞의 것은 모델의 자기 평가입니다. 뒤의 것은 아닙니다.
그러면 '검사'는 무엇일 수 있을까요? 공식 문서가 제시하는 범위는 생각보다 넓습니다. 검사란 Claude가 대화 안에서 읽을 수 있는 신호를 돌려주는 것 전부입니다 — 테스트 스위트, 빌드의 종료 코드, 린터, 출력을 픽스처와 diff하는 스크립트, 또는 디자인과 비교하는 브라우저 스크린샷1.
'픽스처'를 설명해 두겠습니다. 미리 저장해 둔 '모범 답안 파일'로, 실행 후 출력을 그것과 비교하며 한 글자도 달라서는 안 되는 방식입니다. 무디게 들리지만 '출력 형식이 안정적이어야 한다'는 종류의 작업에서는 가장 간단하고 가장 믿을 만한 검사의 형태입니다.
이 사고방식은 에이전트 실행에 대한 Anthropic의 권고와 일치합니다. 실행 중에 에이전트가 각 단계에서 환경으로부터 "ground truth" — 도구 호출 결과나 코드 실행 같은 — 를 얻어 자신의 진척을 평가하는 것이 결정적으로 중요합니다4. '환경으로부터'에 주목하세요 — 자기 자신의 추론으로부터가 아닙니다. 모델의 추론은 스스로 생성한 것입니다. 환경의 반환값은 그렇지 않습니다.
'실행할 수 있는 검사를 쥐여 준다'를 본줄기로 삼으면 남은 질문들이 구체적으로 바뀝니다.
레슨 2: 무엇을 검증할 것인가. 미리 정한 각본에 궤적을 대조하는 방식이 통하지 않는다면 무엇을 확인해야 할까요? 답은 종료 상태 우선입니다. 특정 프로세스를 따랐는지가 아니라 올바른 최종 상태에 도달했는지를 평가하고, 복잡한 워크플로라면 특정 상태 변화가 일어났어야 하는 개별 체크포인트로 평가를 쪼갭니다3. 이 레슨에서는 흐릿한 요구사항을 측정 가능한 성공 기준으로 바꾸는 방법도 다룹니다.
레슨 3: 결정론적 검증기. 통과/실패를 낼 수 있는 검사를 어떻게 고르고 어떻게 쓸지. 정확 일치, 스크립트 비교, 테스트 스위트 — 무엇이 어디에 맞는지, 그리고 직관에 반하는 함정 하나: 지나치게 엄격한 검증기는 올바른 답을 거부합니다. 구체적인 검증기 목록과 우선순위는 그 레슨에 있으니 여기서 펼치지는 않습니다.
레슨 4: LLM 판정자. 자유 형식 텍스트는 문자열 비교를 쓸 수 없으니 모델에 점수를 매기게 해야 합니다. 루브릭을 어떻게 쓰는지, 출력 형식을 어떻게 제약하는지, 추론을 먼저 할지 점수를 먼저 낼지, 그리고 작업을 한 모델이 자기 답을 채점하면 안 되는 이유 — 마지막 것은 앞의 퀴즈에서 이미 건드렸습니다. 구체적인 루브릭 설계는 레슨 4에 있습니다.
레슨 5: 평가 세트. 검사 하나는 작업 하나를 담당하고, 작업의 묶음이 평가 세트가 됩니다. 실제 사용에서 사례를 어떻게 모으는지, 엣지 케이스를 어떻게 채우는지, 홀드아웃 세트가 무엇을 하는지, 그리고 '몇 개면 충분한가' — 모두 레슨 5에서 답합니다. 답은 생각보다 작을지도 모릅니다.
레슨 6: 직접 만들기. 앞의 다섯 레슨을 하나로 잇습니다. 평가 작업 하나에 하네스 루프 하나를 붙여 돌리고 리포트를 냅니다. 프롬프트의 버전을 하나 바꿔 점수가 움직였는지 봅니다.
여기까지 오면 반대쪽 극단으로 기울기 쉽습니다. 모든 작업에 테스트와 판정자와 평가 세트가 필요하다고 여기는 것입니다. 그렇지 않습니다.
Anthropic의 원문은 이렇습니다. 다른 모든 LLM 기능과 마찬가지로 성공의 열쇠는 성능을 측정하고 구현을 반복 개선하는 것이며, 다시 말하지만 복잡성은 그것이 결과를 눈에 보이게 개선할 때에만 더할 것을 고려해야 합니다4. 같은 글에는 더 구체적인 경로 권고도 있습니다. 단순한 프롬프트로 시작해 종합적인 평가로 그것을 최적화하고, 더 단순한 해법으로 부족할 때에만 여러 단계의 에이전트 시스템을 더하라는 것입니다4.
검증에 적용하면 판단 기준은 몇 줄로 줄어듭니다.
따로 짚어 둘 값어치가 있는 경우가 하나 더 있습니다. 이미 가지고 있는 검사를 에이전트에 연결하지 않았을 뿐인 경우입니다. 프로젝트에 있는 그 테스트 스위트, 그 lint 명령, 그 빌드 스크립트 — 대개 전부터 존재했을 것입니다. 그것들을 작업 설명에 적어 넣거나 도구로 만드는 비용은 거의 0인데, 세션의 성격은 달라집니다. 이것이 ROI가 가장 높은 한 걸음이고, 이 코스의 다음 몇 레슨의 출발점이기도 합니다.
>> 레슨 2: 무엇을 검증할 것인가: 종료 상태를 먼저, 프로세스는 보조로
Best practices for Claude Code — Claude Code official documentation — https://code.claude.com/docs/en/best-practices ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10 ↩11 ↩12
Writing effective tools for agents — with agents — Anthropic Engineering — https://www.anthropic.com/engineering/writing-tools-for-agents ↩ ↩2
How we built our multi-agent research system — Anthropic Engineering — https://www.anthropic.com/engineering/multi-agent-research-system ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10
Building Effective AI Agents — Anthropic Engineering — https://www.anthropic.com/engineering/building-effective-agents ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9
글로 답하면 되고 코드는 필요 없습니다.
보고 A
보고 B
보고 C
data/contacts.csv에서
이 작업을 에이전트에 넘겼고, 실행됐고, '완료했습니다, 중복을 제거했습니다'라고 보고했다고 합시다.
증거 체크리스트를 설계하세요. 이 작업을 검수하려면 무엇을 봐야 할까요? 항목마다 그것이 어떤 형태를 취하는지 분명히 쓰세요(명령과 그 출력인가? 변경 전후 비교인가? 파일인가?). 그다음 두 번째 질문에 답하세요. 이 중 어느 것이 루프를 스스로 닫게 할 수 있는가 — 즉 여러분이 자리에 없어도 에이전트가 실행하고, 결과를 읽고, 통과할 때까지 반복할 수 있는 것은 어느 것인가.
의사 코드나 명령 예시로 답해도 되고, 완전한 코드는 필요 없습니다.
완료했습니다.
- 파싱과 필드 매핑을 위해 src/importer/parseCsv.js 추가- 이메일 형식과 필수 필드를 확인하는 src/importer/validate.js 추가- POST /api/users/import 에 배치 삽입 연결
구현은 기존 모듈의 코드 스타일을 따릅니다. 실행 중 에러는 없었습니다.같은 작업, 같은 프롬프트, 두 번의 실행
실행 1: read_file(schema.sql) → grep("user_id") → edit(models/user.js) → run_tests → 완료
실행 2: list_dir(src/) → read_file(models/user.js) → read_file(models/order.js) → edit(models/user.js) → edit(models/order.js) → run_tests → run_tests → 완료let response = await client.messages.create({ tools, messages });
while (response.stop_reason === "tool_use") {
messages.push({ role: "assistant", content: response.content });
const toolResults = await runToolUses(response.content);
messages.push({ role: "user", content: toolResults });
response = await client.messages.create({ tools, messages });
}
// 루프는 여기서 멈춘다: stop_reason 이 "tool_use" 에서 "end_turn" 으로 바뀌었다
const tools = [
...editTools,
{
name: "run_checks",
description:
"importer 모듈의 테스트 스위트를 실행한다. 종료 코드와 실패한 테스트 이름을 돌려준다. " +
"src/importer/ 아래의 파일을 수정했다면 반드시 한 번 호출해야 한다.",
input_schema: { type: "object", properties: {}, required: [] },
},
];
// while 루프는 끝났지만, 아직 완료로 취급하지 않는다
messages.push({ role: "assistant", content: response.content }); // 모델의 마무리 답변도 히스토리에 넣는다
let verdict = await runChecks(); // { exitCode, failed: ["BOM 헤더 파싱", ...] }
while (verdict.exitCode !== 0) {
messages.push({
role: "user",
content: `검사가 통과하지 못했어. 실패한 테스트: ${verdict.failed.join(", ")}. 고치고 다시 보고해 줘.`,
});
response = await client.messages.create({ tools, messages });
// …위의 tool_use 루프로 돌아가, 다시 마무리할 때까지…
messages.push({ role: "assistant", content: response.content }); // 마무리 답변은 매번 히스토리에 넣는다
verdict = await runChecks();
}
완료했습니다. src/importer.js 를 리팩터링해 필드 매핑을 mapFields() 라는독립 함수로 추출했고, 로직이 전보다 훨씬 명확해졌습니다. 변경은 기존 동작에영향을 주지 않았습니다.완료했습니다. npm test -- importer 를 실행한 출력은 이렇습니다.
PASS test/importer.test.js Tests: 14 passed, 14 total Time: 1.842 s
새로 추가한 테스트 케이스 3개는 "handles BOM in header", "rejects duplicateemail", "errors on missing column" 입니다. 전체 테스트 스위트는 돌리지 않았고importer 그룹만 돌렸습니다.완료했습니다. BOM 헤더 때문에 필드 매핑이 실패하던 문제를 고쳤습니다. 코드를다시 확인했고 로직은 올바르며, 비슷한 문제가 다시 나오지는 않을 것입니다.하는 김에 파싱 성능도 조금 최적화했습니다.