LLM 에이전트 평가와 디버깅: 비결정적 시스템의 신뢰성 확보 전략
에이전트 개발에서 가장 어려운 부분은 테스트입니다. 같은 입력에도 다른 툴을 호출하거나, 다른 순서로 추론할 수 있습니다. 전통적인 소프트웨어 테스트 방식이 통하지 않습니다.
에이전트 실패 패턴 분류
| 실패 유형 | 증상 | 원인 |
|---|---|---|
| 툴 선택 오류 | 엉뚱한 툴 호출 | description 불명확 |
| 인자 오류 | 잘못된 파라미터 | 스키마 불명확 |
| 무한 루프 | 같은 툴 반복 호출 | stop condition 없음 |
| 조기 종료 | 작업 미완성 상태에서 답변 | 검증 부재 |
| 환각 | 툴 결과를 무시하고 창작 | 프롬프트 설계 문제 |
트레이스 기반 디버깅
import time, json
from dataclasses import dataclass, field, asdict
@dataclass
class ToolCall:
tool_name: str
arguments: dict
result: object
duration_ms: float
error: str = None
@dataclass
class AgentTrace:
session_id: str
user_input: str
final_output: str
tool_calls: list = field(default_factory=list)
total_duration_ms: float = 0
success: bool = True
class TracedAgent:
def __init__(self, agent):
self.agent = agent
def run(self, user_input):
session_id = f"trace_{int(time.time())}"
start = time.time()
trace = AgentTrace(session_id=session_id, user_input=user_input, final_output="")
original_dispatch = self.agent.dispatch_tool
def traced_dispatch(name, args):
t0 = time.time()
try:
result = original_dispatch(name, args)
trace.tool_calls.append(ToolCall(
tool_name=name, arguments=args, result=result,
duration_ms=(time.time() - t0) * 1000
))
return result
except Exception as e:
trace.tool_calls.append(ToolCall(
tool_name=name, arguments=args, result=None,
duration_ms=(time.time() - t0) * 1000, error=str(e)
))
raise
self.agent.dispatch_tool = traced_dispatch
try:
trace.final_output = self.agent.run(user_input)
except Exception as e:
trace.success = False
trace.final_output = str(e)
trace.total_duration_ms = (time.time() - start) * 1000
return trace툴 호출 단위 테스트
import pytest
from unittest.mock import patch
class TestToolSelection:
def setup_method(self):
self.agent = MyAgent()
def test_stock_query_calls_stock_tool(self):
"""주가 관련 질문은 반드시 get_stock_price를 호출해야 함"""
with patch.object(self.agent, 'get_stock_price', return_value={"price": 100}) as mock:
self.agent.run("삼성전자 주가 알려줘")
mock.assert_called_once()
def test_no_tool_for_greeting(self):
"""단순 인사에는 툴을 호출하지 않아야 함"""
with patch.object(self.agent, 'dispatch_tool') as mock:
self.agent.run("안녕하세요")
mock.assert_not_called()
def test_parallel_calls_for_multi_query(self):
"""여러 종목 동시 조회는 병렬 호출해야 함"""
with patch.object(self.agent, 'get_stock_price', return_value={"price": 100}) as mock:
self.agent.run("삼성전자랑 SK하이닉스 주가 비교해줘")
assert mock.call_count == 2LLM-as-Judge 평가
import anthropic, json
eval_client = anthropic.Anthropic()
EVAL_PROMPT = """다음 기준으로 에이전트 응답을 1-5점으로 평가하세요.
[질문] {question}
[에이전트 응답] {response}
[참고 정답] {ground_truth}
평가 기준:
- 정확성 (40%), 완결성 (30%), 간결성 (30%)
JSON으로만 반환: {{"score": 1-5, "reason": "한 줄 이유"}}"""
def evaluate_response(question, response, ground_truth):
result = eval_client.messages.create(
model="claude-sonnet-4-6",
max_tokens=200,
messages=[{
"role": "user",
"content": EVAL_PROMPT.format(
question=question, response=response, ground_truth=ground_truth
)
}]
)
return json.loads(result.content[0].text)
def run_evaluation(test_cases):
scores = []
for case in test_cases:
response = agent.run(case["question"])
eval_result = evaluate_response(case["question"], response, case["expected"])
scores.append(eval_result["score"])
return {
"avg_score": sum(scores) / len(scores),
"pass_rate": sum(1 for s in scores if s >= 4) / len(scores)
}프로덕션 모니터링
핵심 메트릭:
metrics = {
"tool_call_rate": "호출당 평균 툴 사용 횟수 — 급증 시 루프 의심",
"tool_error_rate": "툴 호출 실패율 — 외부 서비스 장애 탐지",
"avg_latency_ms": "평균 응답 시간 — 성능 저하 탐지",
"fallback_rate": "최종 답변 없이 종료된 비율 — 에이전트 미완성 탐지",
"user_retry_rate": "같은 질문 재시도율 — 만족도 프록시",
}
# 알람 조건
alerts = {
"tool_call_rate > 10": "루프 의심, 즉시 알람",
"tool_error_rate > 5%": "외부 서비스 점검",
"avg_latency_ms > 10000": "SLA 위반 위험",
}에이전트 테스트는 완벽할 수 없지만, 트레이스 분석·툴 단위 테스트·LLM-as-Judge 세 가지를 갖추면 프로덕션 문제의 대부분을 사전에 잡을 수 있습니다.
증상별 디버깅 분기표
| 증상 | 먼저 볼 곳 | 대응 |
|---|---|---|
| 같은 입력인데 가끔 실패 | 트레이스의 도구 호출 인자 | temperature 낮추기, 도구 스키마에 예시 추가 |
| 항상 특정 도구에서 실패 | 해당 도구 단위 테스트 | 도구 자체 버그 vs 모델의 인자 생성 오류 분리 |
| 스텝이 비정상적으로 길다 | 계획 단계 출력 | 최대 스텝 제한 + 중간 목표 재확인 프롬프트 |
| 품질 저하가 서서히 진행 | 평가 세트 점수 추이 | 모델·프롬프트·문서 중 최근 변경분 롤백 비교 |
| 비용 급증 | 세션별 토큰 로그 | 루프 감지, 컨텍스트 누적 초기화 정책 점검 |
평가 지표 최소 세트 (이것부터)
- 태스크 성공률 — 대표 시나리오 20~50개의 통과율 (배포 게이트)
- 스텝 효율 — 성공 케이스의 평균 도구 호출 수 (회귀 감지)
- 판정 일치율 — LLM-as-Judge 사용 시, 사람 평가와의 표본 일치율을 분기마다 재검증
- 지표가 없다면 개선도 없습니다 — 자동화 전에 수동 평가 기준부터 문서화하세요.
이 글은 AI 에이전트가 자료 조사와 1차 초안 작성을 담당하고, 사람 편집자가 사실관계·출처·톤과 맥락을 검토한 뒤 발행했습니다. 환경(OS·버전)에 따라 결과가 다를 수 있으니 적용 전 공식 문서를 함께 확인하세요. 오류를 발견하시면 이메일로 제보해 주세요 — 확인 후 신속히 정정합니다.
댓글
첫 번째 댓글을 남겨보세요.