에이전트형 평가자
에이전트형 평가자 (Agentic Evaluators)
에이전트의 궤적 -- 즉 최종 출력뿐 아니라 툴 호출의 순서와 인자 -- 을 평가하는 결정적(deterministic) 스팬 기반 평가자예요.
출처: 문서
본문
logfire SDK가 필요해요
이 평가자들은 실행 중에 캡처된 OpenTelemetry 스팬 트리를 읽기 때문에, logfire SDK가 설치되고 구성되어 있어야 해요. Pydantic Logfire 계정은 필요 없어요. SDK가 스팬을 로컬에서 캡처해주니까요.
Terminal
pip install 'pydantic-evals[logfire]'
스팬을 사용할 수 없으면 각 평가자는 예외를 던지는 대신 실패 결과(불리언 평가자는 False, TrajectoryMatch는 0.0)와 함께 logfire 구성으로 안내하는 이유를 반환해요.
로컬에서 실행되는 툴만 해당돼요
이 평가자들은 실행 시 로컬 OpenTelemetry 스팬을 만드는 툴 -- 즉 Pydantic AI가 직접 호출하는 툴 -- 을 봐요. 네이티브 툴(예: OpenAI의 file search나 Anthropic의 web search)은 로컬 스팬을 만들지 않아서 이 평가자들에게 보이지 않아요. 그런 툴을 평가하려면 제공자의 스팬이나 모델 출력을 대상으로 HasMatchingSpan을 사용해요.
무엇이 툴 호출로 간주될까요
모든 실행 시도 는 스팬을 만들며, 다음과 같이 구분돼요:
- 오류로 끝난 시도 -- 툴 본문이 예외를 던지거나
ModelRetry로 재시도를 요청한 경우 -- 는 기본적으로 계수되지 않아요. 모든 시도를 세려면include_failed=True를 전달해요. 예외:MaxToolCalls는 기본적으로 실패한 시도도 계수해요(그것도 예산을 소비하니까요). 거기서 성공 호출만 세려면include_failed=False를 전달해요. - 지연된 호출(
ApprovalRequired/CallDeferred)은 절대 계수되지 않아요: 이번 실행에서 실행되지 않았으니까요. - 캡처된 트레이스의 모든 일치 스팬이 계수돼요. 중첩된 하위 에이전트(agent-as-tool 위임)가 만든 툴 호출도 포함돼요. 하위 에이전트에 위임해서 거기가 자체 툴을 호출한다면 그 호출들을 기대치와 예산에 반영해요.
에이전트형 평가자는 순수한 입력/출력 검사로는 답할 수 없는 "에이전트가 올바른 일을 했나?"라는 질문들에 답해줘요:
- 툴 커버리지 -- 에이전트가 호출해야 할 특정 툴들을 호출했나요? (
ToolCorrectness) - 툴 호출 순서 -- 올바른 순서로 호출했나요, 적어도 올바른 집합을 사용했나요? (
TrajectoryMatch) - 인자 품질 -- 툴이 기대한 입력을 받았나요? (
ArgumentCorrectness) - 예산 규율 -- 에이전트가 툴 호출 및/또는 모델 요청 예산 안에서 끝냈나요? (
MaxToolCalls,MaxModelRequests)
이들 모두 결정적이고, LLM을 호출하지 않으며, 모든 실험의 모든 케이스에서 실행해도 충분히 저렴해요.
ToolCorrectness
에이전트가 특정한 멀티셋의 툴을 호출했는지 판정해요. 이름이 반복되면 반복 호출이 필요해요.
from pydantic_evals import Case, Dataset
from pydantic_evals.evaluators import ToolCorrectness
dataset = Dataset(
name='rag_agent',
cases=[Case(inputs='Summarize the latest papers on X')],
evaluators=[
ToolCorrectness(
expected_tools=['search', 'rerank', 'generate'],
),
],
)
매개변수:
expected_tools(list[str]): 에이전트가 호출할 것으로 기대되는 툴 이름. 순서는 상관없지만 중복은 의미가 있어요 --['search', 'search']는search호출 두 번을 요구해요.allow_extra(bool, 기본False): 기본적으로expected_tools에 없는 툴 호출은 검사를 실패시켜요.True로 설정하면 기대 툴이 호출되기만 하면 되고 추가는 허용돼요.include_failed(bool, 기본False): 오류로 끝난 툴 호출 시도를 계수할지 여부.evaluation_name(str | None): 보고서의 커스텀 이름.
반환: bool 값을 가진 EvaluationReason. reason은 누락되고 예상치 못한 툴을 지목해줘요.
TrajectoryMatch
실제 툴 이름의 정렬된 목록을 기대 목록과 비교하는데, 세 가지 모드 중 하나를 사용해요.
from pydantic_evals import Case, Dataset
from pydantic_evals.evaluators import TrajectoryMatch
dataset = Dataset(
name='ordered_tools',
cases=[Case(inputs='Process and file this request')],
evaluators=[
TrajectoryMatch(
expected_trajectory=['validate', 'enrich', 'submit'],
order='in_order',
),
],
)
매개변수:
expected_trajectory(list[str]): 기대하는 툴 이름의 정렬된 목록.order(Literal['exact', 'in_order', 'any_order'], 기본'in_order'):'exact'-- 수열이 같으면1.0, 아니면0.0.'in_order'-- 최장 공통 부분 수열(LCS)에서 F1을 계산해요. 정밀도 =LCS / len(actual), 재현율 =LCS / len(expected). 기대 순서 사이에 추가 호출이 섞여도 허용되지만 정밀도를 낮춰요.'any_order'-- 멀티셋 교집합에서 F1을 계산해요. 정밀도 =overlap / len(actual), 재현율 =overlap / len(expected). 순서는 무시되지만 추가·누락 호출 모두 점수를 낮춰요.
include_failed(bool, 기본False): 궤적에 오류로 끝난 툴 호출 시도를 포함할지 여부.evaluation_name(str | None): 보고서의 커스텀 이름.
반환: [0.0, 1.0] 범위의 float 값을 가진 EvaluationReason. F1 기반 모드에서는 reason 텍스트가 overlap, 정밀도, 재현율, F1을 풀어써서 불일치로부터 점수를 재현할 수 있어요.
예를 들어 expected = ['a', 'b', 'c']이고 에이전트가 ['a', 'x', 'b']를 호출했다면 LCS는 ['a', 'b'](길이 2)가 되어 정밀도 2/3, 재현율 2/3, F1 ≈ 0.667이 돼요.
기대 궤적과 실제 궤적이 모두 비어 있으면 모든 모드가 1.0을, 둘 중 하나만 비어 있으면 모든 모드가 0.0을 채점해요.
ArgumentCorrectness
특정 툴 호출이 특정 인자들을 받았는지 확인해요.
from pydantic_evals import Case, Dataset
from pydantic_evals.evaluators import ArgumentCorrectness
dataset = Dataset(
name='support_agent',
cases=[Case(inputs='Refund order 12345')],
evaluators=[
ArgumentCorrectness(
tool_name='issue_refund',
expected_arguments={'order_id': '12345'},
match_mode='subset',
occurrence='first',
),
],
)
매개변수:
tool_name(str): 검사할 툴.expected_arguments(dict[str, Any]): 기대하는 인자 키/값.match_mode(Literal['exact', 'subset'], 기본'subset'):'subset'-- 모든 기대 키/값이 실제 인자에 존재해요. 이는 최상위 키에만 적용된다는 점에 주의해요. 기대 값(중첩 딕셔너리 포함)은 실제 값과 완전히 동일해야 해요.'exact'-- 깊은 동등성. 예상치 못한 키도 실패해요.
occurrence(Literal['first', 'last'] | int, 기본'first'): 툴이 여러 번 호출됐을 때 검사할 호출. 정수 인덱스는 0부터 시작해요.include_failed(bool, 기본False): 오류로 끝난 툴 호출 시도를 고려할지 여부.True면 각 시도가 별개의 occurrence로 계수돼요.evaluation_name(str | None): 보고서의 커스텀 이름.
반환: bool 값을 가진 EvaluationReason.
우아한 축소(Graceful degradation): 이 평가자는 인자를 사용할 수 없을 때 크래시하지 않아요. 예를 들어 에이전트가 include_content=False로 계측됐다면 평가자는 상황을 설명하는 이유와 함께 False를 반환해서 보고서가 여전히 의미 있게 해줘요.
MaxToolCalls와 MaxModelRequests
에이전트가 툴 호출 및/또는 모델 요청 예산 안에 머물렀는지 판정해요. 이것들은 MaxDuration처럼 동작해요. 평가자당 하나의 예산이고, 각각 별도의 불리언 판정으로 보고돼요.
from pydantic_evals import Case, Dataset
from pydantic_evals.evaluators import MaxModelRequests, MaxToolCalls
dataset = Dataset(
name='budget_aware',
cases=[Case(inputs='Draft a short reply')],
evaluators=[
MaxToolCalls(max_calls=5),
MaxModelRequests(max_requests=3),
],
)
매개변수:
MaxToolCalls:max_calls(int) -- 허용되는 로컬 실행 툴 호출의 최대 수.include_failed(bool, 기본True)는 오류로 끝난 시도가 예산에 계수되는지 제어해요(기본적으로 계수돼요 -- 여전히 시간과 토큰을 소비하니까요).MaxModelRequests:max_requests(int) -- 허용되는 모델(채팅) 요청의 최대 수. 가능하면ctx.metrics의requests값을 우선하고, 아니면 LLM 요청 스팬을 직접 계수해요(둘 다 같은 기준을 사용해요).- 둘 다
evaluation_name(str | None)을 받아 보고서의 이름을 커스터마이즈할 수 있어요 -- 같은 예산 검사가 데이터셋과 케이스 수준 양쪽에 나타날 때 유용해요.
반환: bool 값을 가진 EvaluationReason. reason은 관측된 개수와 예산을 포함해요.
레시피
RAG 에이전트
검색 파이프라인이 search → rerank → generate 순서로 실행되고 예상치 못한 툴 호출이 없는지 확인해요.
from pydantic_evals import Case, Dataset
from pydantic_evals.evaluators import ToolCorrectness, TrajectoryMatch
dataset = Dataset(
name='rag_pipeline',
cases=[Case(inputs='Find papers on in-context learning')],
evaluators=[
ToolCorrectness(
expected_tools=['search', 'rerank', 'generate'],
),
TrajectoryMatch(
expected_trajectory=['search', 'rerank', 'generate'],
order='exact',
),
],
)
순서가 중요한 멀티 툴 에이전트
가끔 재시도를 허용하되, 주요 단계는 순서대로 일어나도록 요구해요.
from pydantic_evals import Case, Dataset
from pydantic_evals.evaluators import TrajectoryMatch
dataset = Dataset(
name='ordered_with_slack',
cases=[Case(inputs='Process shipment 99')],
evaluators=[
TrajectoryMatch(
expected_trajectory=['validate', 'enrich', 'submit'],
order='in_order', # F1 기반: 추가 호출은 정밀도만 낮추고 순서는 유지돼야 함
),
],
)
ArgumentCorrectness와 예산 검사를 쓰는 지원 에이전트
올바른 입력으로 올바른 조치가 -- 합리적인 단계 수 안에서 -- 이뤄졌는지 확인해요.
from pydantic_evals import Case, Dataset
from pydantic_evals.evaluators import (
ArgumentCorrectness,
MaxModelRequests,
MaxToolCalls,
)
dataset = Dataset(
name='refund_handling',
cases=[
Case(
name='valid_refund',
inputs={'query': 'Refund my order', 'order_id': '12345'},
evaluators=[
ArgumentCorrectness(
tool_name='issue_refund',
expected_arguments={'order_id': '12345'},
),
],
),
],
evaluators=[
MaxToolCalls(max_calls=4),
MaxModelRequests(max_requests=2),
],
)
툴 호출 궤적으로 작업 완료 판단하기
결정적 검사로 충분하지 않은 작업에서는 LLM이 툴 호출 궤적과 함께 작업 결과를 판단하게 할 수 있어요. LLMJudge는 케이스 입력, 출력, 기대 출력만 볼 뿐 다른 평가자의 결과나 스팬 트리는 보지 못해요. 그래서 판단자에게 에이전트가 어떻게 거기에 도달했는지 보여주려면, 스팬 트리에서 궤적을 추출해서 judge_input_output에 직접 전달하는 작은 커스텀 평가자를 작성해요:
from dataclasses import dataclass
from pydantic_evals import Case, Dataset
from pydantic_evals.evaluators import EvaluationReason, Evaluator, EvaluatorContext
from pydantic_evals.evaluators.llm_as_a_judge import judge_input_output
from pydantic_evals.otel import SpanTreeRecordingError
@dataclass
class TrajectoryJudge(Evaluator):
rubric: str
async def evaluate(self, ctx: EvaluatorContext) -> EvaluationReason:
try:
span_tree = ctx.span_tree
except SpanTreeRecordingError:
# 이 페이지의 내장 평가자처럼 우아하게 축소
return EvaluationReason(value=False, reason='No span tree available.')
# 내장 평가자가 기본적으로 툴 호출로 계수하는 것과 일치하도록
# 평문 궤적 요약을 만든다: 툴 스팬은 'running tool'(v2) 또는
# 'execute_tool {name}'(v3+)로 이름이 붙고, 지연 호출은 절대
# 실행되지 않았으며, output function은 툴 스팬 형태를 공유하지만
# 툴 호출이 아니고, 실패 시도(status 'error')는 내장 평가자의
# `include_failed=False` 기본값처럼 제외한다.
tool_names = [
node.attributes['gen_ai.tool.name']
for node in span_tree
if 'gen_ai.tool.name' in node.attributes
and 'pydantic_ai.tool.deferral.name' not in node.attributes
and node.status != 'error'
and (node.name == 'running tool' or node.name.startswith('execute_tool '))
and not str(node.attributes.get('logfire.msg', '')).startswith('running output function:')
]
trajectory = ', '.join(str(n) for n in tool_names) or '(none)'
grading_output = await judge_input_output(
{'query': ctx.inputs, 'tool_trajectory': trajectory},
ctx.output,
self.rubric,
)
return EvaluationReason(value=grading_output.pass_, reason=grading_output.reason)
dataset = Dataset(
name='task_completion',
cases=[Case(inputs='Resolve ticket 42')],
evaluators=[
TrajectoryJudge(
rubric=(
'The agent completed the task correctly, and the tool trajectory '
'included in the input is reasonable for the given query.'
),
),
],
)
이 패턴은 위의 결정적 검사를 저렴하고 재현 가능하게 유지하면서, 질적이고 개방적인 판단은 LLM에게 맡기되 궤적을 판단자가 보는 내용에 명시적으로 포함시켜요.
다음 단계
- 스팬 기반 평가 --
HasMatchingSpan과SpanQuery로 저수준 스팬 쿼리 - 커스텀 평가자 -- 직접 평가 로직 작성
- 내장 평가자 -- 다른 평가자 유형의 완전한 레퍼런스
더 알아보기 (Learn more)
- Pydantic Evals 문서: 에이전트형 평가자