Agent Mentor Learn
검증과 품질 보증: '맞아 보이는 것'을 통과시키지 않기 · 6 / 6강

레슨 6: 실습: 에이전트를 위한 평가 트랙 만들기

학습 목표:

  • 평가 세트, 계층화된 채점, 하네스 루프를 하나로 엮어 실행 가능한 eval-runner.mjs 만들기 — 평가 태스크 하나에 독립적인 루프 하나
  • 리포트에 통과율만이 아니라 태스크별 소요 시간, 도구 호출 횟수, 토큰 소비량, 도구 에러까지 담고, 그 열들로 문제 진단하기
  • 이 트랙으로 시스템 프롬프트 변경의 실제 영향을 측정하고, 올바른 출력을 거절하는 지나치게 엄격한 검증기 잡아내기

전제: 레슨 1–5를 읽었고, 과정 7의 하네스 루프를 바로 돌릴 수 있는 상태 | 이전: 레슨 5 <<

앞의 다섯 레슨은 전부 부품이었습니다. 단계별이 아니라 최종 상태를 검증한다(레슨 2), 결정론적 체크를 먼저 쓰되 지나치게 엄격한 검증기를 조심한다(레슨 3), 자유 형식 텍스트에만 LLM 판정자를 붙인다(레슨 4), 평가 세트는 실제 과제 20개 남짓에서 시작한다(레슨 5). 각각은 그 자체로 말이 되지만, 프롬프트를 바꾼 뒤에 명령 하나로 돌려서 숫자가 '나아졌다 나빠졌다'를 말해 주는 물건은 여전히 없습니다.

이 레슨은 그 부품들을 용접합니다. 결과물은 2초 안에 끝나는 300줄짜리 파일입니다. '평가를 어떻게 돌리는가'에 대한 공식 가이던스는 직설적입니다. 프로그램에서 LLM API를 직접 호출하고, 단순한 에이전틱 루프(LLM 호출과 도구 호출이 번갈아 도는 while 루프)를 쓰되 평가 태스크 하나에 루프 하나를 두라는 것입니다1. 그것이 바로 이 시리즈 과정 7의 stop_reason 기반 루프입니다. 그대로 이식하면 됩니다.

동작하는 모습 먼저 보기

이 레슨 뒤쪽에 있는 eval-runner.mjs 전문을 저장하고 node eval-runner.mjs를 실행합니다.

text
=== 리포트 · 프롬프트 v1 · 검증기 normalized (수정 후) ===태스크            채점 방식     결과      점수  호출  에러      토큰   소요 시간--------------------------------------------------------------------------------t1-total          결정론적      pass      1.00     3     0     1,800       124mst2-pending        결정론적      pass      1.00     1     0       995        82mst3-no-orderid     결정론적      FAIL      0.00     2     1     1,550       124mst4-refund-note    LLM 판정자    FAIL      0.67     1     0     1,432       124mst5-missing-order  결정론적      pass      1.00     1     1       966        83ms--------------------------------------------------------------------------------통과율 3/5 (60%) · 도구 호출 8 · 도구 에러 2 · 토큰 6,743 · 합계 537ms
미통과 케이스:  [t3-no-orderid] 판정 기준: 파라미터가 불완전하면 도구를 한 번도 호출하지 말고 주문번호를 되물어야 한다  에이전트 답변: 주문 SO-1001의 상태는 완료입니다.  [t4-refund-note] 판정 기준: 금액은 주문과 일치하고 톤도 적절하다. 다만 환불 입금 시점이 빠져 있어 고객이 예상 시점을 알 수 없다. 3개 항목 중 1개 누락.  에이전트 답변: 안녕하세요, 주문 SO-1003(금액 ₩320.00)의 취소 요청을 접수했습니다. 환불은 원 결제수단으로 돌려드립니다. 불편을 드려 죄송합니다.
=== 리포트 · 프롬프트 v2 · 검증기 normalized (수정 후) ===태스크            채점 방식     결과      점수  호출  에러      토큰   소요 시간--------------------------------------------------------------------------------t1-total          결정론적      pass      1.00     3     0     1,800       122mst2-pending        결정론적      pass      1.00     1     0       995        82mst3-no-orderid     결정론적      pass      1.00     0     0       487        41mst4-refund-note    LLM 판정자    pass      1.00     1     0     1,518       123mst5-missing-order  결정론적      pass      1.00     1     1       966        82ms--------------------------------------------------------------------------------통과율 5/5 (100%) · 도구 호출 6 · 도구 에러 1 · 토큰 5,766 · 합계 450ms
=== 점수 변화 v1 -> v2 ===태스크                 v1     v2  변화--------------------------------------------------t1-total             1.00   1.00  변화 없음t2-pending           1.00   1.00  변화 없음t3-no-orderid        0.00   1.00  fail => passt4-refund-note       0.67   1.00  fail => passt5-missing-order     1.00   1.00  변화 없음--------------------------------------------------통과율 3/5 -> 5/5

이것은 손으로 지어낸 예시가 아닙니다. 임시 디렉터리에서 실제로 돌린 결과를 그대로 옮긴 것입니다. 전체 코드를 복사해서 한 번 돌려 보십시오. '소요 시간' 열(실제 벽시계 시간이라 머신 부하에 따라 달라집니다)을 빼면 밀리초 단위까지 일치합니다. 숫자가 같은 이유는 스텁 client가 정해진 응답을 돌려주기 때문입니다.

이 출력 안에 이 레슨이 가르치는 것이 전부 들어 있습니다. 다섯 태스크가 각자의 루프를 돌고, 두 가지 채점 방식이 한 표에 섞여 있고, 통과율에 진단용 네 열이 붙고, 두 버전의 차이가 비교표 하나로 접힙니다. 이 레슨의 나머지는 그것을 하나씩 푸는 일입니다.

트랙의 다섯 조각

  1. 테스트 대상: 도구 정의, 실제 도구 구현, 그 뒤의 데이터. 평가는 '에이전트가 당신의 도구를 써서 일하는 것'을 돌립니다. 도구도 테스트 대상의 일부입니다.
  2. 스텁 client: 고정된 큐에서 정해진 응답을 돌려주는 가짜 messages.create. 트랙 전체를 재현 가능하게 만듭니다.
  3. 평가 세트: tasks 배열이고 각 항목은 {id, prompt, verify}입니다. 공식 요건은 이렇습니다. 모든 평가 프롬프트에는 검증 가능한 응답이나 결과가 짝지어져 있어야 합니다1. 검증기가 없는 프롬프트는 평가 태스크가 아니라 데모입니다.
  4. 채점: 결정론적으로 채점할 수 있는 것은 verify 함수로 가고, 자유 형식 텍스트는 판정자로 갑니다.
  5. 루프와 리포트: 태스크 하나에 while 루프 하나, 끝나면 지표를 표로 집계합니다.

먼저 못 박아 둘 것이 하나 있습니다. 태스크는 messages를 공유하지 않습니다. 각 태스크의 messages는 그 태스크의 사용자 프롬프트 하나로 시작해서 자기 루프를 돌고 버려집니다1. 이것이 왜 그렇게 중요한지는 중간의 퀴즈가 바로 묻습니다.

조각 1: 도구와 그 뒤의 데이터

테스트 대상은 주문 어시스턴트입니다. 주문 4건, 도구 2개입니다. search_orders(고객명이나 상태로 검색해 주문 ID 목록을 반환)와 get_order(주문 ID로 단일 주문 상세를 조회)입니다. 두 가지 세부는 일부러 그렇게 만들었습니다. search_orders는 금액 없이 주문 ID만 반환해서, 에이전트가 주문마다 get_order를 다시 호출하게 만듭니다. 리포트의 '호출' 열이 이 설계 결함을 드러낼 것입니다. 다른 하나는 두 필터 조건이 모두 비어 있으면 에러를 던진다는 점입니다.

이것이 '잘못된 파라미터' 도구 에러입니다. 공식 가이던스는 이런 에러가 몰려 나오면 대개 도구 설명을 더 명확하게 쓰거나 예시가 필요하다는 뜻이라고 말합니다1. 잠시 뒤 리포트에서 보게 됩니다. 도구 에러는 크래시가 아닙니다. 도구 실행 블록이 예외를 잡아서 is_error: true가 붙은 tool_result로 감싸 모델에 돌려주고, 카운터를 하나 올립니다. tool_usetool_resulttool_use_id로 짝지어집니다. 과정 7에서 깔아 둔 토대이고, 여기서는 카운터 두 개만 더 얹습니다.

조각 2: 스텁 client와 검증에 관한 막간

여기서 한 번 멈춰야 합니다. 그러지 않으면 아래의 숫자가 하나도 성립하지 않습니다.

진짜 Claude는 비결정론적입니다. 같은 프롬프트를 두 번 돌려도 경로가 완전히 달라질 수 있습니다2. 프로덕션에서는 좋은 일이지만 데모 레슨에서는 재앙입니다. 오늘 돌리면 3/5, 내일 돌리면 4/5인데 그 차이가 프롬프트 변경 때문인지 모델의 기분 때문인지 알 수 없습니다. 그래서 과정 8과 9의 실습 레슨은 모두 같은 방법을 씁니다. 모델을 고정된 큐에서 정해진 응답을 돌려주는 스텁으로 바꿔 테스트 대상 동작을 통제 변수로 만드는 것입니다. 이렇게 하면 모델의 그날 컨디션이 아니라 당신이 작성한 제어 로직을 검증하게 됩니다.

큐가 소진되면 에러를 던지고 대체 응답을 내놓지 않습니다. 루프가 한 번 더 돌면 곧바로 Error: [stub] v1/t2-pending 응답 큐가 소진되었습니다 (요청 1건 발행)가 보입니다(t2의 큐에서 마지막 응답을 지우고 실제로 받은 에러 문구입니다). 가짜 end_turn이 슬쩍 통과하는 일이 없습니다. 각 응답은 자기 latency_ms를 갖고 있고 스텁이 실제로 그만큼 잠들기 때문에, '소요 시간' 열이 루프가 몇 턴을 돌았는지를 재게 됩니다. 태스크마다 자기 스크립트를 가진 새 client를 받으므로 커서가 태스크를 넘나들지 않습니다.

두 프롬프트 버전의 차이는 스텁의 두 응답 큐에 고정되어 있습니다. 실제 상황이라면 시스템 프롬프트를 바꾸고 모델 동작이 따라오지만, 여기에는 모델이 없으므로 SCRIPT_V1SCRIPT_V2를 미리 써 두고 v2가 두 태스크에서 다른 응답을 내놓게 했습니다. 'v2 프롬프트가 먹혀서 모델이 이렇게 답한다고 가정한다'를 데이터로 인코딩한 것입니다.

전개 문법으로 v1을 물려받고 바뀐 항목만 나열하면, 코드를 읽는 사람이 변경 범위를 한눈에 봅니다. 이 트랙은 트랙 자신을 검증합니다. 검증기가 제대로 채점하는지, 지표가 정확히 기록되는지, 리포트가 옳게 계산하는지, 두 번의 실행을 비교할 수 있는지입니다. 진짜 client로 바꿔 끼워도 트랙은 바뀌지 않습니다. 숫자만 튀기 시작할 뿐입니다.

조각 3: 평가 세트 — 일반 4개와 엣지 케이스 1개

레슨 5는 평가 세트가 실제 분포와 맞아야 하고 엣지 케이스를 덮어야 한다고 했습니다3. 공식도 도구를 충분한 복잡도로 시험하지 못하는 지나치게 단순한 샌드박스 환경을 경계했습니다1. 여기서는 지면 때문에 태스크를 다섯 개만 담지만, 구조는 실제 평가 세트를 따릅니다.

태스크무엇을 테스트하는가채점
t1-total다단계 집계: 목록을 검색한 뒤 각각의 금액을 가져오기결정론적
t2-pending집합 필터링: 주문 ID가 정확히 이것들이어야 하고 더도 덜도 안 됨결정론적
t3-no-orderid엣지 케이스: 사용자가 주문 ID를 주지 않음결정론적
t4-refund-note자유 형식 텍스트: 고객에게 보내는 환불 안내LLM 판정자
t5-missing-order도구 에러 후에 사실대로 보고하고 데이터를 지어내지 않기결정론적

t3-no-orderid는 따로 언급할 만합니다. 프롬프트는 '그 주문 상태 좀 확인해 주세요'인데, 어느 주문입니까? 지정되어 있지 않습니다. 이상적인 동작은 주문 ID를 추측해서 조회하는 대신 되묻는 것입니다. 공식 문서는 이 동작에 대해 조심스럽습니다. 사용자 프롬프트에 도구의 필수 파라미터를 전부 채울 만한 정보가 없으면 Claude Opus는 빠진 파라미터를 알아채고 되묻는 경우가 훨씬 많지만, 이 동작은 보장되지 않으며 특히 더 모호한 프롬프트와 능력이 낮은 모델에서 그렇다고 합니다4. '보장되지 않는' 동작이야말로 평가 세트가 덮어야 할 것입니다. 보장되는 것은 테스트할 필요가 없습니다.

verify가 받는 r에는 answer만이 아니라 toolCalls, toolErrors, tokens도 들어 있어서, 검증기가 텍스트만이 아니라 '최종 상태와 핵심 지표'를 확인할 수 있습니다. t3는 실제로 '도구를 한 번도 호출하지 않았음'을 확인하고, t5는 '에러를 정확히 한 번 보고했고 찾을 수 없다고 사실대로 말했음'을 확인합니다. 레슨 2의 최종 상태 우선이 이 필드들을 통해 실현됩니다. note는 사람을 위한 것입니다. 태스크가 실패하면 리포트가 판정 기준과 에이전트의 실제 답변을 나란히 출력합니다.

레슨 5 숙제에서 {id, prompt, expected, verifier, rubricRef, tags, split} 필드 집합을 썼다면 지금 대응시켜 두어야 헷갈리지 않습니다. 레슨 5의 verifier는 여기서 grader라고 부르고 표시용일 뿐입니다. 실제 채점 유형은 이 태스크에 verify 함수가 있는지 judge: true가 있는지로 정해집니다. expected의 선언적 단언은 여기서 verify 함수 본문에 직접 쓰입니다(태스크마다 단언의 모양이 다르니, 범용 단언 포맷을 설계하는 것보다 함수로 쓰는 편이 간단합니다). rubricRef는 판정자 케이스가 스위트 전체에 하나뿐이라 JUDGE_PROMPT로 인라인했습니다. tagssplit은 지면상 생략했고, 홀드아웃 규율은 늘 그렇듯 '범위' 절에서 다시 반복합니다. 레슨 5의 JSON은 낡은 것이 아닙니다. 그것은 이 TASKS 배열의 선언적 버전입니다. 앞으로 나아간다는 것은 단언 하나하나를 함수로 옮긴다는 뜻입니다.

조각 4: 계층화된 채점, 결정론적 우선

채점 방식에는 순서가 있습니다. 코드 기반 채점은 가장 빠르고 가장 신뢰할 수 있으며 확장성도 대단히 좋지만 복잡한 판단에서는 뉘앙스가 부족합니다. LLM 기반 채점은 빠르고 유연하며 복잡한 판단을 다룰 수 있지만 먼저 신뢰성을 테스트하고 그다음에 확장해야 합니다. 사람 채점은 가장 유연하고 품질도 가장 높지만 느리고 비싸서 가능하면 피합니다3.

그래서 규칙은 이렇습니다. 코드로 채점할 수 있는 것은 절대 판정자에게 보내지 않습니다. 여기 다섯 태스크 중 넷이 verify를 씁니다. 자유 형식 텍스트인 t4-refund-note만 판정자로 갑니다. '이 문단을 고객에게 보내도 되는가'는 문자열 일치로 답할 수 없기 때문입니다. 판정자의 모양은 레슨 4를 따릅니다. 루브릭은 세 항목으로 고정, 출력 형식은 JSON으로 고정, 근거를 먼저 쓰고 점수를 나중에 냅니다.

각 항목에는 출처가 있습니다. 판정자에게 근거를 먼저 쓰게 하고 점수를 낸 뒤 그 추론은 버립니다. 채점 품질이 올라가며 특히 복잡한 판단이 필요한 과제에서 그렇습니다3. 출력은 경험적이거나 구체적이어야 하고 순수하게 정성적인 평가여서는 안 됩니다3. 그리고 '단일 LLM 호출, 단일 프롬프트, 0.0–1.0 점수와 통과/실패 출력'은 공식이 자사 멀티 에이전트 리서치 시스템에서 여러 판정 방식을 시도한 끝에 가장 일관되고 사람의 판단과도 가장 잘 맞는다고 확인한 조합입니다2.

여기서도 판정자는 스텁입니다. v1의 답변에는 입금 시점이 빠져 있어 세 항목 중 둘만 맞아 0.67, 판정은 fail입니다. v2는 그것을 추가해서 셋 다 맞아 1.00, 판정은 pass입니다. 점수는 루브릭과 자기 일관적입니다. 이분 항목 셋의 평균은 0, 0.33, 0.67, 1.00에만 떨어질 수 있습니다. 0.85라는 점수가 나왔다면 판정자가 루브릭의 계산을 따르지 않았다는 뜻입니다. 판정자 자신도 토큰을 태우고 그 소비는 그 태스크의 토큰에 더해집니다. t4가 도구를 한 번만 호출하는데도 토큰이 낮지 않은 이유가 그것입니다.

레슨 4의 규율을 하나 더 옮깁니다. 일한 모델이 자기를 채점해서는 안 됩니다. 공식은 새 모델 인스턴스가 결과를 반박해 보게 하라고 말합니다. 일하는 쪽이 채점하는 쪽이 아닙니다5. 코드에서는 이렇습니다. 판정자는 자기 client, 자기 시스템 프롬프트, 자기 messages 배열을 쓰고, 과제 프롬프트와 채점 대상 답변만 볼 뿐 에이전트의 도구 호출 트랜스크립트는 보지 않습니다.

조각 5: 루프와 리포트

루프는 과정 7의 루프 그대로이고 뼈대는 바뀌지 않았습니다. 실제 API가 요구하는 modelmax_tokens를 더했고(스텁은 무시합니다) 카운터로 감쌌을 뿐입니다.

messagesrunTask 안의 지역 변수입니다. 함수가 반환되면 사라집니다. 그것이 '태스크가 컨텍스트를 공유하지 않는다'의 구현 전부입니다. 별도의 장치가 필요 없고, 밖으로 끌어올리지만 않으면 됩니다.

지표에 대한 공식의 체크리스트는 이렇습니다. 최상위 정확도 외에 개별 도구 호출과 태스크의 총 실행 시간, 총 도구 호출 횟수, 총 토큰 소비량, 도구 에러도 함께 수집하라는 것입니다1. 리포트 표의 열은 이 체크리스트를 그대로 따릅니다. 통과율은 '통과했는가'만 알려 주지만 이 열들은 '어떻게 통과했는가'를 알려 줍니다. 통과하면서 도구를 열두 번 호출한 태스크와 두 번 호출한 태스크는 품질 등급이 다릅니다. 이 열들은 스스로 진단서 역할도 합니다. 중복 도구 호출이 많으면 대개 페이지네이션이나 토큰 한도 파라미터를 적정 크기로 조정할 필요가 있다는 뜻이고, 잘못된 파라미터로 인한 도구 에러가 많으면 대개 도구 설명이 더 명확해지거나 더 나은 예시가 필요하다는 뜻입니다1. 연습에서 이것을 바로 씁니다.

리포트가 사람이 읽을 수 있다는 것 자체에 고유한 가치가 있습니다. 공식의 제안은 이렇습니다. Claude에게 성공했다는 주장 대신 증거를 보여 주게 하십시오. 테스트 출력, 실행한 명령과 그 반환값, 또는 결과 스크린샷입니다. 증거를 검토하는 편이 검증을 직접 다시 돌리는 것보다 빠르고, 당신이 지켜보지 않은 세션에도 통합니다5. 이 리포트 표가 바로 그 증거입니다. PR 설명에 붙이거나 동료에게 보내면, 다시 돌려 보지 않고도 판단할 수 있습니다. (출력에서 유일한 함정은 한글 같은 전각 문자가 폭 2로 계산된다는 것입니다. 날것의 padEnd로는 열이 어긋나므로 코드에는 폭을 인식하는 pad가 들어 있습니다.)

eval-runner.mjs 전문

복사해서 eval-runner.mjs로 저장하고 node eval-runner.mjs를 실행하면 바로 돌아갑니다. 의존성 없음, package.json 없음, Node 18 이상입니다(최상위 await를 쓰므로 확장자는 .mjs여야 합니다).

레슨 3의 함정 되짚기: 지나치게 엄격한 검증기

레슨 3은 함정을 하나 다뤘습니다. 공식의 표현 그대로입니다. 형식, 문장 부호, 타당한 다른 표현 같은 사소한 차이 때문에 올바른 응답을 거절하는 지나치게 엄격한 검증기를 피하십시오1. 상식처럼 들리지만 코드에서는 거의 피할 수 없습니다. 지나치게 엄격한 검증기가 쓰기 가장 쉽기 때문입니다.

이 트랙에도 하나 심어 두었습니다. t1-total에는 검증기가 두 버전 있고, 옛 것은 pass: r.answer.includes("1280.00")입니다. 빈틈없어 보입니다. 정답이 1280.00이니 답변에 그 문자열이 있는지 확인하면 된다는 것입니다. node eval-runner.mjs --strict-verify를 돌려 보십시오(아래에는 v1 리포트만 붙입니다. v2 리포트와 변화표는 평소대로 출력됩니다).

text

=== 리포트 · 프롬프트 v1 · 검증기 strict (옛 버전, 정규화 없음) ===태스크            채점 방식     결과      점수  호출  에러      토큰   소요 시간--------------------------------------------------------------------------------t1-total          결정론적      FAIL      0.00     3     0     1,800       123mst2-pending        결정론적      pass      1.00     1     0       995        83mst3-no-orderid     결정론적      FAIL      0.00     2     1     1,550       123mst4-refund-note    LLM 판정자    FAIL      0.67     1     0     1,432       123mst5-missing-order  결정론적      pass      1.00     1     1       966        83ms--------------------------------------------------------------------------------통과율 2/5 (40%) · 도구 호출 8 · 도구 에러 2 · 토큰 6,743 · 합계 535ms
미통과 케이스:  [t1-total] 판정 기준: 답변에 문자열 1280.00이 그대로 들어 있어야 한다  에이전트 답변: 고객 계명테크의 2026년 8월 완료 주문은 2건(SO-1001, SO-1002)이고 합계 ₩1,280.00입니다.  [t3-no-orderid] 판정 기준: 파라미터가 불완전하면 도구를 한 번도 호출하지 말고 주문번호를 되물어야 한다  에이전트 답변: 주문 SO-1001의 상태는 완료입니다.  [t4-refund-note] 판정 기준: 금액은 주문과 일치하고 톤도 적절하다. 다만 환불 입금 시점이 빠져 있어 고객이 예상 시점을 알 수 없다. 3개 항목 중 1개 누락.  에이전트 답변: 안녕하세요, 주문 SO-1003(금액 ₩320.00)의 취소 요청을 접수했습니다. 환불은 원 결제수단으로 돌려드립니다. 불편을 드려 죄송합니다.

이것도 실제 실행 결과입니다. t1-total의 상세를 보십시오. 에이전트는 '합계 ₩1,280.00'이라고 답했습니다. 금액도 맞고 주문도 맞고 표현도 정상입니다. 유일한 죄는 1과 280 사이에 천 단위 구분자 쉼표를 넣었다는 것이고, 그래서 includes("1280.00")이 false를 반환해 완전히 올바른 답이 실패로 채점됩니다.

이 지점에서는 에이전트가 아니라 검증기를 고칩니다. 리포트는 't1 실패'만 알려 줄 뿐 누구 잘못인지는 알려 주지 않습니다. 가려내는 방법은 상세에 적힌 에이전트의 실제 문장을 읽는 것이고, 리포트가 원본 답변을 출력하는 이유가 바로 그것입니다. 해법은 정규화입니다. 정확 일치에 대한 공식의 설명에 이미 이 단계가 들어 있습니다. 정확 일치 평가는 보통 공백과 대소문자를 정규화한 뒤에 모델 출력이 미리 정해진 정답과 일치하는지를 측정합니다3. 금액 시나리오는 더 많이 씻어 내야 합니다. 통화 기호, 천 단위 구분자, 단위가 있으므로, 수정된 검증기는 잡음을 먼저 씻어 내고 숫자를 뽑아 수치로 비교합니다.

--strict-verify를 빼고 다시 돌리면 t1-total이 0.00에서 1.00으로 뒤집히고 v1 기준선은 2/5에서 3/5로 돌아옵니다. 그사이 에이전트는 한 글자도 바뀌지 않았고 스텁의 응답 큐도 한 글자도 바뀌지 않았습니다. 점수는 바뀌었는데 테스트 대상은 바뀌지 않았다 — 그것이 '검증기 문제'를 가려내는 리트머스 시험지입니다.

범위에 관해 한마디 덧붙입니다. 정규화는 느슨할수록 좋은 것이 아닙니다. '1280이 들어 있어 보이면 통과'까지 느슨하게 하면, 에이전트가 '주문 1280건, 금액은 알 수 없음'이라고 답해도 통과합니다. 검증기는 '무관한 차이는 통과시키고 실질적인 오류는 막는' 자리에 있어야 하고, 그 자리를 찾는 유일한 방법은 실제 답변으로 시도해 보는 것입니다.

프롬프트 한 군데를 바꾸고 점수가 움직이는 것 보기

트랙을 교정했으니 이제 실전입니다. 한 군데만 바꿨습니다. 시스템 프롬프트에 v1 뒤로 규칙 두 개를 더한 것입니다.

이 두 규칙은 지어낸 것이 아닙니다. v1 리포트의 '미통과 케이스'에서 읽어 낸 것입니다. t3는 파라미터가 불완전한데 주문번호를 추측해서 실패했고, t4는 입금 시점이 빠져서 감점됐습니다. 리포트가 말하는 것을 고친다 — 그것이 트랙이 있고 없고의 가장 구체적인 차이입니다. 트랙이 없으면 프롬프트를 바꾼 뒤 출력을 훑어보며 '나아진 것 같다'고 느끼는 데 그치지만, 트랙이 있으면 '무엇이 좋아졌고, 무엇이 그대로고, 퇴행한 것이 있는가'가 숫자 세 줄이 됩니다.

다시 돌리면 변화표는 첫머리 출력의 마지막 구간과 같습니다. 통과율이 60%에서 100%로 오르고, 두 태스크가 fail에서 pass로 뒤집히고, 나머지 셋은 꿈쩍하지 않습니다. 마지막 반쪽 문장이 앞쪽만큼 중요합니다. 이 변경이 이미 잘 돌아가던 것을 망가뜨리지 않았다는 뜻이기 때문입니다. 트랙이 없으면 프롬프트를 바꾸고 출력을 한 번 들여다보며 '나아 보인다'고 생각하는 것이 전부지만, 트랙이 있으면 '무엇이 좋아졌는가 / 무엇이 그대로인가 / 퇴행이 있는가'가 숫자 세 줄로 남습니다.

공식은 이것을 이렇게 표현합니다. 평가가 있으면 프롬프트 엔지니어링의 영향을 더 높은 확신을 갖고 측정할 수 있고, 도구 설명을 조금만 다듬어도 극적인 개선을 낳을 수 있습니다1. 여기서 챙길 이득이 하나 더 있습니다. 에이전트 개발 초기에는 낮게 달린 열매가 아직 많아서 변경이 극적인 영향을 주는 경향이 있습니다. 프롬프트를 한 번 손대는 것만으로 성공률이 30%에서 80%로 오를 수 있고, 효과 크기가 이 정도라면 테스트 케이스 몇 개만으로도 변화를 알아볼 수 있습니다2. 지금 태스크가 다섯 개뿐이라는 것은 결핍이 아니라 출발점입니다.

지표 열을 다시 보십시오. v2의 도구 호출은 8에서 6으로, 도구 에러는 2에서 1로, 토큰은 1,000 가까이 줄었습니다. t3가 더 이상 아무렇게나 추측해서 도구를 부르지 않기 때문입니다. 같은 변경이 정확도와 비용을 동시에 개선했다 — 이런 것은 이 열들을 함께 기록해야만 보입니다.

범위: 이 트랙이 맡는 것과 맡지 않는 것

맡는 것: 에이전트 하나, 태스크 한 묶음, 당신 머신에서 한 번 실행, 사람이 읽을 수 있는 리포트 산출.

진짜 모델로 바꿔 끼우기 — 트랙 구조는 바뀌지 않습니다. stubClient(...)@anthropic-ai/sdk의 진짜 client로 바꾸면 됩니다. runTask 안의 while 루프는 한 줄도 바뀌지 않습니다. 이미 실제 API의 stop_reason / tool_use / tool_result 모양에 맞춰 쓰여 있고, 필수 파라미터인 modelmax_tokens도 이미 들어 있습니다(스텁은 무시하고 진짜 client는 씁니다). 바꾼 뒤에는 두 가지가 달라집니다. 에이전트는 프롬프트가 동일해도 실행마다 비결정론적이라 점수가 흔들릴 것이므로2 단일 실행을 과하게 해석하지 마십시오. 그리고 한 바퀴 도는 데 돈과 시간이 듭니다. 태스크 다섯 개는 상관없지만 200개라면 동시성과 비용을 고려해야 합니다.

맡지 않는 것: 평가를 CI에 걸기, 커밋마다 돌리기, 과거 버전과 비교하기, 점수가 임계값 아래로 떨어지면 병합 막기. 이것들은 흔한 엔지니어링 관행이고 실제로 잘 동작하지만 이 레슨에서는 다루지 않습니다. 연습 레벨 2가 '리포트 두 개 비교하기'까지는 데려다주고, 나머지 오케스트레이션은 당신 CI의 몫입니다.

레슨 5의 규율을 하나 더 반복합니다. 홀드아웃 세트를 상대로 튜닝하지 마십시오. 리포트를 따라 프롬프트를 고치면 몇 바퀴 뒤에 점수는 분명히 오르지만, 그 상승이 '이 다섯 태스크에서의 점수'일 수도 있습니다. 공식의 실천은 '학습용' 평가에 과적합되지 않았음을 확인하기 위해 홀드아웃 테스트 세트에 의존하는 것입니다1. 그러니 실제 구성에서는 태스크를 두 무더기로 나누십시오. 한쪽은 매일 돌려 방향을 잡고, 다른 쪽은 잠가 두었다가 '이번 버전은 될 것 같다' 싶을 때만 엽니다. 첫 무더기의 점수는 항법이고, 둘째 무더기의 점수는 판결입니다.

마지막으로 오래된 주의 하나. 자동 평가는 놓치는 것이 있습니다. 사람 테스터는 언제나 평가가 놓치는 엣지 케이스에 부딪힙니다. 흔치 않은 질의에서의 환각, 시스템 차원의 장애, 미묘한 출처 선택 편향입니다2. 트랙이 매끄럽게 돌아간다고 해서 직접 써 보는 일을 그만두어도 된다는 뜻은 아닙니다.

💻 연습

정리

  • 평가를 돌리는 표준적인 모양은 프로그램에서 API를 직접 호출하고 단순한 에이전틱 루프를 쓰되 평가 태스크 하나에 루프 하나를 두는 것이다. 태스크는 messages를 공유하지 않는다. 공유하면 앞 태스크가 다음 태스크를 오염시켜 결과를 더 이상 비교할 수 없게 된다1.
  • 모든 평가 프롬프트에는 검증 가능한 결과가 짝지어져야 한다. 검증기는 정확한 문자열 비교부터 모델에게 판정을 맡기는 것까지 스펙트럼을 이룬다. 코드로 채점할 수 있는 것은 절대 판정자에게 보내지 않는데, 코드 기반 채점이 가장 빠르고 가장 신뢰할 수 있으며 확장성도 대단히 좋기 때문이다1 3.
  • 자유 형식 텍스트는 판정자로 간다. 모양은 단일 호출, 단일 프롬프트, 0.0–1.0 점수와 통과/실패 출력이다. 루브릭은 근거를 먼저 쓰고 점수를 나중에 내게 해야 하며 출력 형식은 고정한다2 3.
  • 리포트는 통과율 외에 태스크 소요 시간, 도구 호출 횟수, 토큰 소비량, 도구 에러를 기록해야 한다. 이 열들은 스스로 진단서가 된다. 중복 호출은 페이지네이션/반환량 파라미터 조정을 가리키고, 잘못된 파라미터 에러는 도구 설명이 명확해져야 함을 가리킨다1.
  • 지나치게 엄격한 검증기는 올바른 답을 거절한다. 형식, 문장 부호, 타당한 다른 표현이 전부 문자 그대로의 비교를 걸어 넘어뜨릴 수 있으므로, 정확 일치 전에 정규화를 한다1 3. 점수는 바뀌었는데 테스트 대상은 바뀌지 않았다면 검증기 잘못이다.
  • 트랙이 있으면 프롬프트 변경의 영향이 측정 가능해진다. 작은 다듬기도 극적인 개선을 낳을 수 있고, 초기에는 효과 크기가 커서 케이스 몇 개로도 차이를 알아볼 수 있다1 2. 리포트 자체가 남이 검토할 수 있는 증거이며, 검증을 직접 다시 돌리는 것보다 빠르고 지켜보지 않은 세션에도 통한다5.
  • 리포트를 따라 프롬프트를 고치면 점수는 오르지만 그 상승이 이 태스크 묶음에서만의 것일 수 있다. 홀드아웃 세트를 잠가 과적합을 막는다1. 자동 평가에는 사각지대가 있고, 사람 테스터는 여전히 평가가 놓치는 엣지 케이스를 잡아낸다2.

이 과정을 마치며

돌아보면 본줄기는 사실 짧습니다. 레슨 1은 '완료된 것처럼 보인다'와 '완료됐다'를 갈라놓았습니다. 실행 가능한 체크가 없으면 '완료된 것처럼 보인다'가 유일하게 얻을 수 있는 신호이고, 그러면 당신이 검증 단계가 됩니다5. 레슨 2는 무엇을 검증할지를 정했습니다. 에이전트는 같은 목표에 대해 완전히 다른 합리적 경로를 걸을 수 있으므로 최종 상태를 평가하고 궤적을 단계별로 확인하지 않습니다2. 레슨 3은 '체크'를 통과/실패를 내놓는 실행 가능한 결정론적 검증기로 만들었고, 동시에 지나치게 엄격한 검증기가 올바른 답을 거절한다고 경고했습니다1. 레슨 4는 자유 형식 텍스트를 다뤘습니다. 루브릭, 출력 형식, 그리고 일한 모델이 자기를 채점해서는 안 된다는 것입니다2 5. 레슨 5는 '몇 개의 케이스로 검증할 것인가'를 풀었습니다. 실제 과제 20개 남짓이면 시작할 수 있고, 수백 개가 쌓일 때까지 기다리지 않습니다2. 이 레슨은 앞의 다섯을 300줄짜리 파일 하나로 용접했습니다.

그 파일은 복잡하지 않고 2초 안에 끝나지만, 바꾸는 것은 구체적입니다. 오늘부터 프롬프트 버전을 바꿀 때 '출력 몇 문단을 읽고 나아진 것 같다'는 느낌에 기대어 판단하지 않아도 됩니다. 명령 하나를 돌리면 v1에서 v2로의 변화표가 당신 대신 말해 줍니다. 이번에 t3t4가 초록으로 바뀌고 나머지 셋은 그대로였던 것처럼 말입니다. 다음번에 당신의 에이전트가 '완료했습니다'라고 말할 때, 그 주장을 검증할 명령 두 개와 종료 코드 하나가 당신에게 있습니다.

다음번에 당신의 에이전트가 '완료했습니다'라고 말할 때, 그것을 검증할 실행 가능한 트랙이 당신에게 있습니다.

Footnotes

  1. Writing effective tools for agents — with agents — Anthropic Engineering — https://www.anthropic.com/engineering/writing-tools-for-agents 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17

  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 11

  3. Define success criteria and build evaluations — Claude API documentation — https://platform.claude.com/docs/en/test-and-evaluate/develop-tests 2 3 4 5 6 7 8

  4. Tool use with Claude — Claude API documentation — https://platform.claude.com/docs/en/agents-and-tools/tool-use/overview

  5. Best practices for Claude Code — Claude Code official documentation — https://code.claude.com/docs/en/best-practices 2 3 4 5

연습

01

코드 없음. 레슨 첫머리의 두 리포트(v1 기준선과 v2 변경 후)로 돌아갑니다. 요약 줄은 이렇습니다.

레벨 1: 리포트를 읽고, 서둘러 코드를 고치지 않기
text
v1: 통과율 3/5 (60%)  · 도구 호출 8 · 도구 에러 2 · 토큰 6,743v2: 통과율 5/5 (100%) · 도구 호출 6 · 도구 에러 1 · 토큰 5,766

두 표 전체를 놓고 세 질문에 각각 세 문장에서 다섯 문장으로 답하십시오.

  1. t1-total은 두 리포트 모두에서 통과하지만 도구 호출 횟수가 3으로 가장 많습니다. 이것은 어떤 문제를 가리킵니까? 무엇을 바꿔야 합니까?
  2. v1은 도구 에러가 2건, v2는 1건입니다. 이 두 에러는 같은 부류의 문제입니까? 각각 무슨 뜻이고, 각각 고쳐야 합니까?
  3. 이 레슨에는 세 번째 리포트(--strict-verify 쪽)가 있고 거기서 t1-total은 0.00입니다. 같은 태스크가 한 리포트에서는 0.00, 다른 리포트에서는 1.00입니다. 이 점수 차이가 에이전트가 아니라 검증기의 문제라는 것을 어떻게 가려냅니까?
완료 기준 · 로컬에서 확인
02

코드를 씁니다. 반드시 돌아가야 합니다. eval-runner.mjs에 두 가지를 더하십시오.

레벨 2: 트랙에 '두 번의 실행 비교' 붙이기
  1. 리포트 영속화: writeJsonAtomic(file, obj)를 추가하고, 과정 9의 원자적 쓰기(.tmp에 먼저 쓰고 rename)로 한 번의 실행 리포트를 JSON으로 저장하십시오. 명령줄은 --version v1 --out reports/v1.json을 지원합니다.
  2. compare.mjs 작성: 리포트 JSON 두 개를 읽어 태스크별 점수 차이(기준 점수, 신규 점수, 델타, 상태)를 출력하고, 마지막에 통과율 변화를 출력하십시오. pass에서 fail로 뒤집힌 태스크가 하나라도 있으면 요약을 stderr로 출력하고 0이 아닌 코드로 종료합니다.

다음 네 명령을 실행하고 출력을 붙이십시오.

text
node eval-runner.mjs --version v1 --out reports/v1.jsonnode eval-runner.mjs --version v2 --out reports/v2.jsonnode compare.mjs reports/v1.json reports/v2.json   # 0으로 종료해야 함node compare.mjs reports/v2.json reports/v1.json   # 1로 종료해야 함

(파라미터 순서를 바꾸는 것은 '새 버전이 기준선보다 나쁘다'를 흉내 내어, 0이 아닌 종료 경로가 실제로 동작하는지 확인하기 위한 것입니다.)

완료 기준 · 로컬에서 확인