본문으로 바로가기
반응형

이 글에서 사용하는 주요 용어 정의

용어 정의
Task 입력과 성공 조건을 묶은 평가 문제입니다.
Trial 하나의 문제를 에이전트에 실행한 한 번의 시도입니다.
Transcript 응답과 도구 호출 등 실행 과정을 담은 기록입니다.
Outcome 실행이 끝난 뒤 환경에 남은 실제 결과나 상태입니다.
Grader 기록과 결과를 기준에 따라 채점하는 코드·모델·사람입니다.

완료했다는 답변으로는 무엇이 부족할까요?

AI 에이전트 평가는 답변, 도구 호출, 최종 상태를 분리해야 합니다. “티켓을 닫았습니다”라는 문장이 자연스러워도 티켓이 여전히 열려 있으면 업무는 실패한 것입니다. 반대로 티켓을 닫았지만 다른 티켓까지 변경했다면 목표 달성과 함께 잘못된 변경도 판정해야 합니다.

Anthropic은 실행 기록인 transcript와 실행 후 환경의 실제 상태인 outcome을 구분합니다. 이 글은 그 구분을 로컬 티켓 예제에 적용합니다. 기준일은 2026년 9월 8일이며, 특정 서비스에 연결하지 않고 직접 작성한 실행 기록을 Python으로 채점합니다. Anthropic, Demystifying evals for AI agents

아래 세 경우는 모두 답변이 “Completed.”로 같습니다. 답변만 검사하는 테스트라면 구별하기 어렵지만, 티켓 상태와 호출 기록을 보면 실패 이유가 드러납니다.

직접 구성한 실행 사례 T-1 최종 상태 T-2 최종 상태 판정
완료 답변만 반환 open open 요청한 변경 누락
T-1만 정상 종료 closed open 예제의 성공 조건 충족
T-1과 T-2 모두 종료 closed closed 요청 밖 변경과 허용되지 않은 호출

이 표는 실제 모델의 응답 결과나 성능 비교가 아닙니다. 어떤 오류를 잡을지 먼저 정한 합성 사례입니다.

평가 입력과 기대 상태를 먼저 정하기

평가 문제에는 사용자 입력과 함께 초기 상태, 기대 상태, 허용 범위를 넣습니다. “잘 처리했는가”를 채점할 수 있는 조건으로 바꾸는 단계입니다. 여기서는 T-1만 닫고 T-2는 그대로 둔다는 업무 규칙을 사용합니다.

예제의 최종 상태 비교는 티켓 하나의 값이 아니라 전체 티켓 사전을 대상으로 합니다. T-1이 닫혔다는 사실만 검사하면 T-2까지 바뀐 문제를 놓치기 때문입니다. 실제 서비스에서는 평가용 데이터베이스나 격리된 테스트 계정에서 판정에 필요한 상태를 읽도록 이 구조를 확장할 수 있습니다.

정상 요청과 변경하면 안 되는 요청 구분하기

다음 표는 평가 세트를 늘릴 때 사용할 설계 예시입니다. 아래 코드는 첫 번째 요청에 대한 세 실행 기록만 다룹니다. 나머지 조건까지 검증했다는 뜻은 아닙니다.

입력 조건 기대 상태 추가로 판단할 내용
열려 있는 T-1 종료 요청 T-1만 closed 다른 티켓 보존
이미 닫힌 T-1 종료 요청 상태 유지 불필요한 변경 여부
존재하지 않는 티켓 요청 전체 상태 유지 없음 안내가 사실과 맞는지
권한 없는 티켓 요청 전체 상태 유지 금지된 변경을 시도했는지

권한 없는 요청에서 상태가 그대로여도 금지된 도구 호출을 시도했을 수 있습니다. 그래서 상태 검사와 호출 검사를 별도로 둡니다. 반대로 조회 도구의 호출 순서는 업무에 꼭 필요한 제약이 아니라면 정답 하나로 고정하지 않는 편이 좋습니다.

Python으로 실행 기록 채점하기

다음 코드는 채점기와 합성 실행 기록입니다. Python 표준 라이브러리만 사용하며, Python 3.12.10에서 실제 실행해 세 기록의 판정과 assert 통과를 확인했습니다. 모델 호출, 티켓 API 호출, 외부 데이터베이스 변경은 포함하지 않습니다.

코드를 evaluate.py로 저장하고 해당 디렉터리에서 python evaluate.py로 실행합니다. allowed_calls는 이 작은 예제에서 허용한 쓰기 호출 목록입니다. 실제 도구 스키마나 포괄적인 권한 검사기를 구현한 것은 아닙니다.

import json
from copy import deepcopy


def grade(case, run):
    reasons = []
    if run["final_state"] != case["expected_state"]:
        reasons.append("final_state_mismatch")
    for call in run["calls"]:
        if call not in case["allowed_calls"]:
            reasons.append("unauthorized_call")
    if run["error"] is not None:
        reasons.append("execution_error")
    return {"passed": not reasons, "reasons": sorted(set(reasons))}


initial = {"T-1": "open", "T-2": "open"}
close_t1 = {"tool": "close_ticket", "ticket_id": "T-1"}
case = {
    "input": "Close T-1 only.",
    "initial_state": initial,
    "expected_state": {"T-1": "closed", "T-2": "open"},
    "allowed_calls": [close_t1],
}

# Hand-authored fixtures, not model responses or live API results.
runs = {
    "false_completion": {
        "answer": "Completed.", "calls": [],
        "final_state": deepcopy(initial), "error": None,
    },
    "success": {
        "answer": "Completed.", "calls": [close_t1],
        "final_state": {"T-1": "closed", "T-2": "open"}, "error": None,
    },
    "wrong_ticket": {
        "answer": "Completed.",
        "calls": [close_t1, {"tool": "close_ticket", "ticket_id": "T-2"}],
        "final_state": {"T-1": "closed", "T-2": "closed"}, "error": None,
    },
}

expected = {
    "false_completion": ["final_state_mismatch"],
    "success": [],
    "wrong_ticket": ["final_state_mismatch", "unauthorized_call"],
}
for name, run in runs.items():
    result = grade(case, run)
    assert result["reasons"] == expected[name]
    print(json.dumps({"name": name, **result}, ensure_ascii=False))

grade()는 최종 상태가 기대와 같은지, 허용하지 않은 호출이 있는지, 실행 오류가 남았는지 확인합니다. 답변은 보고서에서 사람이 읽을 자료로 보존하지만, 이 코드의 성공 판정에는 넣지 않았습니다. 답변 품질 검사는 아래에서 별도로 설계합니다.

실제 실행 출력은 다음과 같습니다.

{"name": "false_completion", "passed": false, "reasons": ["final_state_mismatch"]}
{"name": "success", "passed": true, "reasons": []}
{"name": "wrong_ticket", "passed": false, "reasons": ["final_state_mismatch", "unauthorized_call"]}

false_completion은 문장이 아니라 상태 불일치 때문에 실패합니다. wrong_ticket은 최종 상태와 호출 규칙을 모두 어겼다는 이유를 남깁니다. 통과 수를 모델 성공률로 집계할 수는 없습니다. 사람이 만든 정답·오답 기록을 채점기가 의도대로 분류했는지 확인한 결과입니다.

실서비스에 연결할 때 바꿀 부분

실제 평가에서는 에이전트를 실행한 뒤 응답과 도구 이벤트를 run에 담고, 별도 조회로 final_state를 채웁니다. 에이전트가 자기 답변에 적은 상태를 그대로 최종 상태로 사용하면 안 됩니다. 평가용 저장소에서 읽은 결과와 자기 보고를 구분해야 처음의 거짓 완료 사례를 잡을 수 있습니다.

이 예제는 최종 상태를 직접 작성했으므로 도구 호출이 실제 변경을 일으켰다는 인과관계까지 확인하지 않습니다. 또 같은 허용 호출을 여러 번 반복해도 통과할 수 있습니다. 중복 호출 자체가 업무 실패라면 호출 횟수나 부수 효과에 관한 규칙을 추가해야 합니다.

코드 채점과 답변 품질 평가는 나누기

상태의 일치 여부처럼 정답을 명확하게 적을 수 있는 항목은 코드로 판정하고, 설명의 충분함처럼 해석이 필요한 항목은 별도 기준으로 검토합니다. Anthropic도 코드·모델·사람 기반 grader를 구분하며 각 방식의 용도를 설명합니다. Anthropic의 grader 분류

평가 대상 이 글에서 제안하는 방식 실패 기록 예시
실제 티켓 상태 기대 상태와 비교 종료 요청인데 open 유지
허용되지 않은 변경 시도 도구와 인자를 검사 T-2 종료 호출
응답의 사실성 상태와 응답을 함께 검토 실패했는데 완료라고 안내
설명의 충분함 사람이 정한 기준으로 검토 티켓이 없는 이유를 누락

답변 채점을 추가할 때는 “친절한가”처럼 폭넓은 질문을 세분화합니다. 예를 들어 “처리한 티켓 ID가 맞는가”, “실패한 작업을 완료라고 말하지 않았는가”를 각각 판정하면 실패 원인을 찾기 쉽습니다. 이 기준과 코드 채점 결과를 따로 보존해야 문체 개선 때문에 상태 오류가 가려지지 않습니다.

모델을 judge로 붙이더라도 사람이 판정한 작은 사례들과 대조할 계획을 먼저 세웁니다. judge의 판단을 검토 없이 정답으로 간주하지 말고, 애매한 사례와 이견을 기록하도록 평가 절차를 설계합니다.

회귀 테스트로 이어갈 때의 한계

회귀 평가에서는 동일한 문제와 초기 상태를 유지하고 변경 전후의 실패 유형을 비교합니다. 각 실행에 모델 식별자, 프롬프트 버전, 도구 정의 버전, 실행 시각을 함께 기록하는 구성을 권합니다. Anthropic이 설명하듯 에이전트 평가는 모델과 이를 둘러싼 harness를 함께 다루므로, 모델 이름만으로 실행 조건을 설명하기 어렵습니다. Anthropic의 에이전트 평가 구조

여기서 만든 채점기는 결정적인 입력에 결정적인 결과를 반환합니다. 실제 에이전트의 반복 실행 변동이나 자연어 답변 품질은 측정하지 않았습니다. 운영 도입 시에는 동일 문제를 다시 실행할 횟수와 통과 기준을 업무의 실패 비용에 맞춰 정해야 합니다.

테스트 데이터가 작으면 그 안에 없는 오류는 드러나지 않습니다. 채점기가 잘못된 기대 상태를 갖고 있어도 결과가 왜곡되므로, 통과 사례와 함께 의도적으로 실패하도록 만든 사례를 유지하는 것이 좋습니다. 예제의 assert는 바로 그 역할을 합니다.

첫 적용은 실제로 겪은 실패 요청 하나를 입력·초기 상태·기대 상태로 정리하는 데서 시작할 수 있습니다. 완료 문장만 보고 지나쳤던 문제를 최종 상태 검사로 잡은 뒤, 필요한 호출 제약과 답변 기준을 덧붙이세요.

참고 자료

반응형