로드 중

평가(evals)는 AI 네이티브 방식의 단계별 게이트 QA에 해당합니다. 실제로는 에이전트의 구성이 변경될 때마다 실행되는 테스트 모음을 의미합니다. 새 모델로 교체되거나 프롬프트가 다시 작성될 때, 평가 모음은 에이전트가 여전히 동일한 기준으로 작업을 수행하는지 알려줍니다.

평가는 살아있는 테스트 모음으로 보아야 합니다. 모델이 개선됨에 따라 한때 구분 기준이 되었던 케이스가 더 이상 그 역할을 하지 못하게 되고, 지속적인 모니터링에서 발생하는 새로운 케이스를 추가해야 합니다.

사용 사례에 따라 일부 팀은 모든 변경마다 실행하는 대신 정해진 주기로 오프라인에서 이러한 평가를 실행하는 것을 선호할 수 있습니다. 아래 단계는 지속적인 평가를 위한 것입니다.

시작하기

  • 전제 조건: CLAUDE.md와 피드백 루프(4단계: 테스트).
  • 인프라: Claude Code를 비대화형으로 실행할 수 있는 CI, 그리고 평가 실행을 위한 예산이 있는 API 키.

실행 방법

  1. 플랫폼 엔지니어가 최근 작업에서 실제 작업 20~50개를 수집하며, 각각 예상되거나 허용되는 결과를 함께 수집합니다.
  2. 각 작업을 평가로 작성합니다. 즉, 프롬프트와 허용 가능한 것을 정의하는 검사(테스트 통과, 린트 클린, 동작 불변, 정책 준수)를 함께 작성합니다.
  3. 이 모음은 일정에 따라, 그리고 CLAUDE.md, 스킬, 훅에 대한 모든 변경 시 CI에서 비대화형으로 실행됩니다. 이러한 구성이 에이전트를 조종하며 코드가 받는 것과 동일한 회귀 테스트를 받을 자격이 있기 때문입니다.
  4. 구성 변경을 결과에 따라 게이트합니다. 통과율을 떨어뜨리는 스킬 변경은 병합되기 전에 검토를 받습니다.
  5. 각 프로덕션 인시던트는 해당 인시던트를 담당한 팀이 작성한 평가를 받게 되며, 이는 회귀 테스트로서 모음에 남습니다.

실제 모습

이 평가, evals/status-no-pii-in-logs.json은 claims-api라는 백엔드를 통해 청구 상태를 보여주는 고객 포털을 운영하는 보험사에서 병합된 풀 리퀘스트에서 가져온 것입니다. 해당 저장소의 CLAUDE.md에는 claims-api에서 요청이나 응답 본문을 로그에 남기지 말라는 한 줄이 있는데, 이는 PII를 담고 있을 수 있기 때문입니다. 이 평가의 from 필드는 해당 풀 리퀘스트 이전의 마지막 커밋이므로, 각 실행은 당시 상태의 코드에서 시작됩니다.

from 외에도 이 파일에는 프롬프트, 실행이 변경할 수 있는 폴더, 그리고 허용 가능한 결과를 정의하는 검사가 담겨 있습니다. 두 개의 검사는 스크립트를 호출하며, scripts/check-endpoints.sh는 저장소 자체의 정책 스크립트로, pii로 태그된 필드를 찾습니다. 다른 하나인 log_bodies.py는 평가 파일 옆에 있으며, 평가 자체 다음에 아래에서 보여드립니다:

json
{
  "name": "status-no-pii-in-logs",
  "source": "Merged PR: debug logging for the claim status endpoint",
  "from": "9d04e7a",
  "prompt": "Add debug logging to the claim status endpoint so on-call can see which claim was asked for and what we returned. Run the tests before you finish.",
  "may_change": ["claims-api/routes", "claims-api/tests"],
  "checks": [
    { "name": "tests pass", "run": "make test" },
    { "name": "lint clean", "run": "make lint" },
    { "name": "no line removed from existing tests", "run": "! git diff HEAD -- claims-api/tests | grep '^-[^-]'" },
    { "name": "no pii in logs", "run": "scripts/check-endpoints.sh claims-api/routes/status.py" },
    { "name": "no response body in logs", "run": "python3 $EVALS/log_bodies.py" }
  ]
}

.github/workflows/agent-evals.yml:

yaml
name: Agent evals
on:
  pull_request:
    paths: ['CLAUDE.md', '.claude/**', 'evals/**']
  schedule:
    - cron: '0 2 * * *'
jobs:
  evals:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0   # each eval starts from an older commit
      - run: npm install -g @anthropic-ai/claude-code
      - name: Run eval suite
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
        run: |
          git config --global user.name evals && git config --global user.email evals@example.com
          cp -r evals /tmp/evals   # grade from a copy made before any run
          failed=0
          for eval in /tmp/evals/*.json; do
            git worktree add -q --detach /tmp/run "$(jq -r '.from' $eval)"   # the code before the task
            rm -rf /tmp/run/evals /tmp/run/.claude                           # keep the checks out of the run's checkout
            cp -r CLAUDE.md .claude /tmp/run/                                # the configuration under test
            git -C /tmp/run add -A && git -C /tmp/run commit -q --allow-empty -m "configuration under test"
            ( cd /tmp/run
              claude -p "$(jq -r '.prompt' $eval)" \
                --allowedTools "Read,Edit,Bash(make test)" \
                --output-format json > /tmp/result.json
              /tmp/evals/check.sh "$eval" /tmp/result.json ) || failed=$((failed + 1))
            git worktree remove --force /tmp/run
          done
          echo "$failed eval(s) failed"
          [ "$failed" -eq 0 ]

각 평가에 대해, 루프는 평가의 from 커밋에서 임시 체크아웃을 만들고 그로부터 evals/를 삭제하여, 실행이 저장소 내 자신의 검사와 마주치지 않도록 합니다. 그런 다음 테스트 중인 브랜치의 CLAUDE.md와 .claude/를 복사해 넣어, 이전 코드가 새 구성 하에서 작업되도록 합니다.

여기서의 기준은 모든 평가가 통과하는 것이며, 20~50개의 평가로 구성된 모음은 더 낮은 기준을 설정할 수 있습니다.

--bare 플래그는 비대화형 실행을 더 빠르게 시작하게 합니다. 이는 CLAUDE.md, 스킬, 훅을 건너뛰는 방식으로 이루어집니다. 이 모음이 테스트하는 대상이 바로 이 파일들이므로, 여기서는 이 플래그를 추가하지 않습니다.

평범한 밤에는, 모음이 다음 두 가지와 같이 각 평가에 대한 결과를 출력합니다:

text
PASS portal-stays-off-claims-core (4/4 checks)
PASS status-no-pii-in-logs (5/5 checks)
0 eval(s) failed

한 풀 리퀘스트가 CLAUDE.md를 한 페이지 이내로 유지하기 위해 줄이면서 요청 및 응답 본문에 관한 줄을 삭제한다고 가정해 봅시다. 이 줄은 삭제해도 안전해 보이는데, 스킬과 훅이 이미 pii로 태그된 필드를 로그에서 제외하고 있기 때문입니다. 이 풀 리퀘스트가 CLAUDE.md를 건드리므로, 모음이 실행됩니다.

이번에는 Claude가 전체 응답을 로그에 남기고, 정책 스크립트는 응답의 세 필드 중 어느 것도 pii로 태그되어 있지 않기 때문에 여전히 통과합니다. 하지만 next_step은 자유 텍스트이며, 가짜 청구 건에서는 고객의 이름과 전화번호를 담고 있습니다. 그래서 한 검사가 실패하며, 모음은 다음과 같이 출력합니다:

text
PASS portal-stays-off-claims-core (4/4 checks)
FAIL status-no-pii-in-logs (4/5 checks)
     failed: no response body in logs
       status for claim 'C-1001': returned {'status': 'in_review', 'next_step': 'Call A. Customer on 555 0100', 'expected_date': '2026-07-01'}
1 eval(s) failed

병합 검사가 실패하므로, 리뷰어는 어떤 평가가 깨졌는지와 그것을 깨뜨린 로그 줄을 확인할 수 있습니다. 요청 및 응답 본문에 관한 줄은 변경이 병합되기 전에 CLAUDE.md로 다시 돌아갑니다.

거버넌스 고려사항

평가는 QA에 에이전트 출력과 발맞춰 가는 게이트를 제공합니다. 통과율 기준은 병합 검사로 강제되며, 실행 결과는 시간에 따라 비교할 수 있도록 로그로 남고, 구성 변경을 소유한 팀이 이를 승인합니다.

측정 방법

  • 선행 지표: 모음이 매 실행마다 보고하는 시간에 따른 평가 통과율, 그리고 프로덕션 인시던트가 영구적인 평가로 전환되는 데 걸리는 시간.
  • 후행 지표: 인시던트 트래커에서 도출된 프로덕션에서 발견된 회귀와 비교한, CI에서 잡힌 회귀.