평가(evals)는 AI 네이티브 방식의 단계별 게이트 QA에 해당합니다. 실제로는 에이전트의 구성이 변경될 때마다 실행되는 테스트 모음을 의미합니다. 새 모델로 교체되거나 프롬프트가 다시 작성될 때, 평가 모음은 에이전트가 여전히 동일한 기준으로 작업을 수행하는지 알려줍니다.
평가는 살아있는 테스트 모음으로 보아야 합니다. 모델이 개선됨에 따라 한때 구분 기준이 되었던 케이스가 더 이상 그 역할을 하지 못하게 되고, 지속적인 모니터링에서 발생하는 새로운 케이스를 추가해야 합니다.
사용 사례에 따라 일부 팀은 모든 변경마다 실행하는 대신 정해진 주기로 오프라인에서 이러한 평가를 실행하는 것을 선호할 수 있습니다. 아래 단계는 지속적인 평가를 위한 것입니다.
시작하기
- 전제 조건:
CLAUDE.md와 피드백 루프(4단계: 테스트). - 인프라: Claude Code를 비대화형으로 실행할 수 있는 CI, 그리고 평가 실행을 위한 예산이 있는 API 키.
실행 방법
- 플랫폼 엔지니어가 최근 작업에서 실제 작업 20~50개를 수집하며, 각각 예상되거나 허용되는 결과를 함께 수집합니다.
- 각 작업을 평가로 작성합니다. 즉, 프롬프트와 허용 가능한 것을 정의하는 검사(테스트 통과, 린트 클린, 동작 불변, 정책 준수)를 함께 작성합니다.
- 이 모음은 일정에 따라, 그리고
CLAUDE.md, 스킬, 훅에 대한 모든 변경 시 CI에서 비대화형으로 실행됩니다. 이러한 구성이 에이전트를 조종하며 코드가 받는 것과 동일한 회귀 테스트를 받을 자격이 있기 때문입니다. - 구성 변경을 결과에 따라 게이트합니다. 통과율을 떨어뜨리는 스킬 변경은 병합되기 전에 검토를 받습니다.
- 각 프로덕션 인시던트는 해당 인시던트를 담당한 팀이 작성한 평가를 받게 되며, 이는 회귀 테스트로서 모음에 남습니다.
실제 모습
이 평가, evals/status-no-pii-in-logs.json은 claims-api라는 백엔드를 통해 청구 상태를 보여주는 고객 포털을 운영하는 보험사에서 병합된 풀 리퀘스트에서 가져온 것입니다. 해당 저장소의 CLAUDE.md에는 claims-api에서 요청이나 응답 본문을 로그에 남기지 말라는 한 줄이 있는데, 이는 PII를 담고 있을 수 있기 때문입니다. 이 평가의 from 필드는 해당 풀 리퀘스트 이전의 마지막 커밋이므로, 각 실행은 당시 상태의 코드에서 시작됩니다.
from 외에도 이 파일에는 프롬프트, 실행이 변경할 수 있는 폴더, 그리고 허용 가능한 결과를 정의하는 검사가 담겨 있습니다. 두 개의 검사는 스크립트를 호출하며, scripts/check-endpoints.sh는 저장소 자체의 정책 스크립트로, pii로 태그된 필드를 찾습니다. 다른 하나인 log_bodies.py는 평가 파일 옆에 있으며, 평가 자체 다음에 아래에서 보여드립니다:
{
"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:
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, 스킬, 훅을 건너뛰는 방식으로 이루어집니다. 이 모음이 테스트하는 대상이 바로 이 파일들이므로, 여기서는 이 플래그를 추가하지 않습니다.
평범한 밤에는, 모음이 다음 두 가지와 같이 각 평가에 대한 결과를 출력합니다:
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은 자유 텍스트이며, 가짜 청구 건에서는 고객의 이름과 전화번호를 담고 있습니다. 그래서 한 검사가 실패하며, 모음은 다음과 같이 출력합니다:
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에서 잡힌 회귀.