
LLM 에이전트 평가를 레이어화하기: Google ADK의 12가지 지표를 모두 CI에 올리면 안 되는 이유
요약
Google ADK의 12가지 에이전트 평가 지표를 CI(지속적 통합) 환경에 무분별하게 도입할 때 발생하는 문제점과 해결 방향을 다룹니다. 레이턴시, 비용, Flakiness 문제를 지적하며 지표의 특성에 따른 계층적 평가 구조의 필요성을 강조합니다.
핵심 포인트
- LLM-as-a-Judge 기반 지표는 높은 레이턴시와 비용을 유발함
- Judge나 Simulator의 변덕으로 인한 CI Flakiness 발생 위험
- 결정론적 지표(ROUGE 등)도 모델의 효율적 개선을 방해할 수 있음
- 지표의 특성에 따라 실행 빈도를 조절하는 레이어화된 평가가 필요함
Google의 Agent Development Kit (ADK)는 AI 에이전트를 평가하기 위한 12가지 지표 (Criteria)를 제공하고 있다 (https://adk.dev/evaluate/criteria/). 도구 호출 (Tool calling)의 정확성, 최종 응답의 품질, 할루시네이션 (Hallucination), 안전성, 멀티 턴 (Multi-turn)에서의 태스크 달성도 등 상당히 포괄적이다.
그런데 이것을 보고 가장 먼저 저지르기 쉬운 실수가 "모두 PR의 CI에 집어넣는" 판단이다. 이는 높은 확률로 사고로 이어진다. 그 이유와 대신 어떻게 구성해야 하는지를 실례와 함께 정리한다.
12가지는 "정답 데이터가 필요한가", "루브릭 (Rubric)이 필요한가", "LLM-as-a-Judge를 사용하는가"의 3개 축으로 정리할 수 있다.
| Criteria | 정답 데이터 | 루브릭 | LLM Judge | 사용자 시뮬레이션 |
|---|---|---|---|---|
tool_trajectory_avg_score | 필요 | 불필요 | 미사용 | 미사용 |
response_match_score | 필요 | 불필요 | 미사용 | 미사용 |
final_response_match_v2 | 필요 | 불필요 | 사용 | 미사용 |
rubric_based_final_response_quality_v1 | 불필요 | 필요 | 사용 | 사용 |
rubric_based_tool_use_quality_v1 | 불필요 | 필요 | 사용 | 사용 |
rubric_based_multi_turn_trajectory_quality_v1 | 불필요 | 필요 | 사용 | 사용 |
hallucinations_v1 | 불필요 | 불필요 | 사용 | 사용 |
safety_v1 | 불필요 | 불필요 | 사용 | 사용 |
per_turn_user_simulator_quality_v1 | 불필요 | 불필요 | 사용 | 사용 |
multi_turn_task_success_v1 | 불필요 | 불필요 | 사용 | 사용 |
multi_turn_trajectory_quality_v1 | 불필요 | 불필요 | 사용 | 사용 |
multi_turn_tool_use_quality_v1 | 불필요 | 불필요 | 사용 | 사용 |
12종 중 10종이 LLM Judge에 의존한다. 이 시점에서 "PR마다 전부 돌리는 것"은 파탄이 날 것임이 보인다.
3가지 구조적인 벽이 있다.
레이턴시 (Latency): LLM-as-a-Judge 나 User Simulation 을 사용하는 케이스는 1건당 수십 초~수 분이 걸린다. 테스트 케이스 수를 곱하면, PR 피드백이 수 분에서 심하면 1시간 이상으로 늘어난다.
비용 (Cost): 테스트 케이스 수 N, 턴 수 T라고 하면, User Simulation + Judge의 총 토큰 소비는 대략 O(N×T)로 증가한다. 커밋마다 이를 돌리는 것은 FinOps 관점에서 성립하지 않는다.
Flaky화 (Flakiness): temperature=0.0으로 설정해도 Judge나 Simulator의 출력은 흔들린다. CI가 빨간색(실패)이 되어도 "코드의 버그"인지 "Judge의 변덕"인지 구분해야 하며, 그러다 보면 결국 아무도 빨간색을 신뢰하지 않게 된다.
결정론적 (Deterministic)이라고 여겨지는 2가지 지표에도 함정이 있다. response_match_score는 ROUGE-1 (단어 중복률)이므로, 포맷이나 말투를 바꾼 것만으로도 쉽게 오탐 (False Negative)을 발생시킨다. tool_trajectory_avg_score 역시 EXACT나 IN_ORDER로 엄격하게 판정하면, 모델 업데이트로 도구 호출이 병렬화되는 등 "더 효율적인 절차"를 발견했을 경우 테스트가 부당하게 실패한다. 엄격한 일치는 개선을 저해한다.
"실행 빈도 (높음 $\rightarrow$ 낮음)"와 "결정론성 (높음 $\rightarrow$ 낮음)"을 축으로 3층 피라미드로 나눈다.
Layer 3: E2E / 시나리오 평가 [주간 / 릴리스 전]
- User Simulation 고비용 / 비결정적
- multi_turn_task_success
...
사용하는 것은 tool_trajectory_avg_score 뿐이며, 그것도 EXACT가 아니라 ANY_ORDER로 "필수 도구가 호출되었는가"만 본다. 외부 API는 모킹 (Mocking)한다. LLM 호출 제로, 비용 제로, 실행 시간은 1분 미만.
import pytest
from google.adk.evaluation import AgentEvaluator
from my_agent.agent import root_agent
...
layer1_ci.json
이 측면에서 결정론적 (Deterministic) 특성을 보장하는 설정을 고정한다.
{
"criteria": {
"tool_trajectory_avg_score": {
...
여기서 처음으로 LLM-as-a-Judge가 등장한다. 단, 대상은 전체가 아니라 버전 관리된 고정 데이터셋(수백 건 규모)이다. 중요한 것은 Judge로 사용할 모델과 temperature를 고정하는 것, 그리고 절대 점수뿐만 아니라 "이전 버전 대비 성능 저하율"을 보는 상대 평가도 포함하는 것이다.
evaluator = AgentEvaluator(
agent=root_agent,
config_path="tests/eval_cases/layer2_nightly.json",
...
루브릭 (Rubric) 계열의 지표는 모호한 형용사가 아니라 명시적인 가중치 기반 루브릭으로 정의해야 한다. 이 부분이 가장 사고가 나기 쉬운 포인트다.
{
"rubric_based_final_response_quality_v1": {
"threshold": 0.8,
...
루브릭의 기술은 판정 가능한 객관적 조건으로 구체화해야 한다.
- Bad: "이해하기 쉬운 언어로 해설되어 있을 것"
- Good: "전문 용어를 사용할 때, 괄호 표기 또는 직후 문장에서 그 정의를 1문장 이내로 보충하고 있을 것"
Judge LLM은 쿼리(Query), 응답(Response), 가중치 기반 루브릭 목록을 전달받아 각 항목의 점수와 가중 평균을 반환한다. assert가 실패했을 때도 reasoning
문자열이 그대로 남기 때문에, "왜 실패했는지"를 수치뿐만 아니라 이유와 함께 추적할 수 있다.
User Simulator LLM이 에이전트와 여러 턴(Multi-turn) 동안 대화하며, 목표 달성도(multi_turn_task_success_v1)와 대화 프로세스의 품질(multi_turn_trajectory_quality_v1, multi_turn_tool_use_quality_v1)을 평가한다.
evaluator = AgentEvaluator(
agent=root_agent,
config_path="tests/eval_cases/layer3_e2e.json",
...
per_turn_user_simulator_quality_v1에 의한 스킵(Skip) 처리에 주목해 주길 바란다. 이 레이어에는 구조적인 함정이 있다.
이 레이어는 "사용자 역할을 수행하는 Simulator LLM", "테스트 대상인 Agent", "채점하는 Judge LLM"의 세 주체가 얽혀 있다. 즉, "Judge가 (Simulator라는) Judge를 판정하는" 중첩 구조가 된다. Simulator가 폭주하면 (루프를 돌거나, 스스로 설정한 목표를 무시하거나, 존재하지 않는 제약 조건을 만들어내는 등), task_success 점수는 에이전트의 실패가 아니라 Simulator의 실패를 반영하게 된다.
per_turn_user_simulator_quality_v1이 임계값(Threshold)을 밑도는 세션을 태스크 성공 판정 전에 제외하는 것은 만약을 위한 방어 코드다. 이를 넣지 않으면, Simulator 모델의 상태가 좋지 않았던 날에 에이전트 자체는 정상임에도 불구하고 릴리스 게이트(Release Gate)만 떨어지는 사고가 조용히 발생한다.
name: Agent Evaluation Pipeline
on:
pull_request:
...
PR을 차단하는 것은 Layer 1뿐이다. Layer 2와 3은 스케줄 실행(Scheduled Execution)으로 설정하여, 실패했을 때 PR을 빨간색으로 만드는 것이 아니라 인간의 트리아지(Triage)로 넘긴다.
자주 나오는 질문으로 "평가(Eval)용 코드를 tests/에 넣어야 하는가"가 있다. 결론부터 말하자면, tests/(결정적 테스트)와 evals/(확률적 품질 측정)는 본질적으로 다른 프로시저이므로, 루트 디렉토리에서 분리하는 것을 추천한다.
.
├── src/
│ └── my_agent/
...
tests/는 로컬에서 부담 없이 돌릴 수 있는 속도와 비용을 유지해야 한다. 만약 반드시 tests/ 하위에 두어야 한다면 pytest.mark.eval 등으로 마커(Marker)를 구분하여, pytest -m "not eval"로 실행해야 한다.
가 기본 로컬 루프(local loop)가 되도록 한다.
ADK의 12가지 지표 중, PR(Pull Request)을 차단해도 되는 현실적인 지표는 1~2개뿐이다(tool_trajectory_avg_score의 ANY_ORDER + 모킹(Mocking)을 통한 결정론적 어서션(Deterministic Assertion)). LLM Judge나 User Simulator가 관여하는 항목은 전부, 버전 관리된 고정 데이터셋(Fixed Dataset)을 대상으로 나이트리(Nightly) 빌드나 릴리스 전 레이어에서 실행하며, Judge 모델과 temperature를 고정하여 재현성(Reproducibility)을 확보해야 한다. 평가 목록을 하나의 리스트로 만들어 커밋마다 전부 실행하려고 하면, Flaky한 레드 빌드(Red build)와 아무도 신뢰하지 않는 PR 큐(PR queue)가 기다리고 있을 것이다. 고쳐야 할 것은 어서션(Assertion)을 작성하는 방식이 아니라, 레이어를 나누는 방식 그 자체이다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기