Issue 01중국 AI
AC POST
중국 AI 목록
掘金2026년 11월 3일 11:34중국어 → 한국어

AI 에이전트 성능 평가 지표와 자동화 테스트 적용 방안

결과 전달, 과정 궤적, 성능 비용, 보안 규정 네 차원의 15개 핵심 지표와 경량 파이썬 자동화 테스트 방안을 제시해 정량 평가 체계 구축을 돕는다.

중국어 원문을 AI로 번역했습니다. 고유명사와 수치는 원문 표기를 우선하며, 중요한 판단에는 아래 출처 원문을 함께 확인하세요.

AI Agent를 연구·개발하는 과정에서 많은 팀이 동일한 수렁에 빠진다. 바로 "감으로 Prompt를 조정하는 것(Vibe Checking)"이다. 프롬프트 한 줄을 고치고 테스트 케이스 세 개를 돌려 보면 더 똑똑해진 것 같지만, 막상 출시하면 또 다른 엣지 시나리오에서 무한 루프에 빠져 버린다.

소프트웨어 공학에는 황금률이 있다. "측정할 수 없으면 개선할 수도 없다." 이 글은 공허한 이론을 걷어내고, 일선 엔지니어링 실무의 관점에서 AI Agent를 평가하는 4대 핵심 차원과 15개 핵심 지표를 체계적으로 해부하며, 200줄이 채 안 되는 코드로 로컬에서 바로 실행 가능한 Python 자동 평가 솔루션을 제공한다.

1. 왜 전통적인 LLM 평가는 Agent에 대해効력을 잃었는가?

일반적인 대규모 언어 모델 대화 애플리케이션(Chatbot)을 평가할 때는 보통 "입력 Question"과 "출력 Answer"의 텍스트 유사도나 의미 품질만 보면 된다. 그러나 Agent(지능형 에이전트)의 본질은 "다단계 추론과 도구 호출 능력을 갖춘 자율 시스템"이다.

최종 출력 텍스트만 본다면, 치명적인 엔지니어링 위험 요소를 대량으로 놓치게 된다.

- 결과는 맞았지만 과정은 전부 틀림: 사용자가 주문 상태를 물었는데 조회 인터페이스가 타임아웃되자, Agent가 대충 찍어서 우연히 "배송 완료"를 맞혔다. 이는 프로덕션 환경에서 심각한 환각 위험 요소다.

- 보이지 않는 파산(비용 통제 상실): 원래 2단계면 조회가 끝나는 데이터를, Agent가 백그라운드에서 도구를 8번이나 잘못 호출한 끝에야 수정했다. 사용자가 보기에는 결과가 완전히 정상이지만, Token 청구서와 인터페이스 소요 시간은 네 배로 뛰었다.

- 상태 오염(부작용): Agent는 입으로 "환불 취소를 완료해 드렸습니다"라고 답했지만, 실제로는 백엔드의 cancel_refund 인터페이스를 전혀 성공적으로 호출하지 못했다.

따라서 Agent 평가는 "점대점(point-to-point) 텍스트 평가"에서 "전체 링크 궤적(Trajectory)과 환경 상태(State) 평가"로의 패러다임 전환을 반드시 완수해야 한다.

2. AI Agent 핵심 평가 지표 전경도

Agent의 성능을 전방위적으로 측정하려면, 외부에서 내부로 향하며 품질과 비용을 함께 고려하는 4차원 평가 체계를 구축해야 한다.

🎯 1. 결과 전달 계층 (Outcome) 🛤️ 2. 과정 궤적 계층 (Trajectory) ⚡ 3. 성능·비용 계층 (System & ROI) 🛡️ 4. 안전 계층 (Safety) 작업 성공률 Pass@1 도구 선택 정확도 엔드투엔드 지연 (P95) 권한 초과 차단 안정성 Pass^k 파라미터 추출 정확도 최초 동작 소요 시간 (TTFT) 규칙 준수 복잡 작업 진행률 궤적 단계 효율 단일 성공 실질 비용 인젝션 방어 사실 충실도 무한 루프 및 자가 복구율

1. 작업·결과 차원 (Outcome Metrics) — 전달의 최저선

Agent가 최종적으로 사용자의 비즈니스 문제를 실제로 해결했는지에 주목한다.

- 작업 성공률 (Task Success Rate / Pass@1 \text{Pass@1} Pass@1): 단일 실행에서 최종 비즈니스 목표를 달성한 작업의 비율. 가장 기본적인 전달의 최저선이다.

- 안정성 지표 (Pass k \text{Pass}^k Pass k): 동일 작업을 연속으로 k k k 회 실행하여 전부 성공할 확률.

💡 왜 Pass k \text{Pass}^k Pass k 가 Pass@ k \text{Pass@}k Pass@ k 보다 더 중요한가? 코드 생성 분야에서는 Pass@ k \text{Pass@}k Pass@ k (k k k 회 돌려 1회만 성공해도 통과)를 흔히 쓰지만, 프로덕션급 Agent에서는 사용자가 딱 한 번만 질문한다. 만약 어떤 Agent의 단일 성공률이 80%라면, 연속 5회 전부 통과할 확률( Pass 5 = 0. 8 5 \text{Pass}^5 = 0.8^5 Pass 5 = 0. 8 5 )은 32.7% 에 불과하다. Pass k \text{Pass}^k Pass k 는 시스템의 불확실성을 극히 민감하게 증폭시킬 수 있다.

- 진행률 (Progress Rate): 완료된 하위 목표 수 / 작업 총 하위 목표 수. 다단계 장링크 작업에 대해 차갑게 0점이나 1점만 주는 것을 피하고, Agent가 도대체 어느 고리에서 막혔는지 연구개발자가 위치를 파악하도록 돕는다.

- 사실 충실도 (Groundedness / Faithfulness): 최종 답변 중 도구가 반환한 데이터(Observation)의 뒷받침이 있는 진술의 비율로, "데이터 조작" 환각률을 정밀하게 정량화하는 데 쓰인다.

2. 궤적·도구 차원 (Trajectory & Tool Metrics) — 추론의 내공

블랙박스를 열어, Agent가 실행 과정에서 보이는 계획 품질과 오류 허용 능력을 평가한다.

구체 지표 계산 로직 / 판정 기준 핵심 진단 가치 도구 선택 정확도

(Tool Precision / Recall) 올바르게 호출한 도구 수 / 실제 호출 총수(및 기대 호출 총수) 라우팅과 의도 인식 진단: 도구를 잘못 선택했는가, 아니면 핵심 도구를 누락했는가? 파라미터 추출 정확도

(Parameter Accuracy) JSON Schema를 충족하고 값이 정확한 필드 수 / 총 필드 수 슬롯 추출 능력 진단: 주문 번호를 사용자 이름으로 잘못 전달했는가? 시간 형식이 올바른가? 궤적 효율

(Step Efficiency) 이상적 최적 단계 수 / Agent 실제 실행 단계 수 계획 효율 진단: 대량의 우회 경로를 돌았는가, 무의미한 중복 조회를 했는가? 무한 루프율

(Loop Rate) 동일 도구를 연속 N N N 회 호출하면서 동일 파라미터를 전달한 세션의 비율 비정상 교착 진단: Agent가 오류나 빈 결과를 만났을 때 무한 재시도에 빠지는가? 장애 복구율

(Recovery Rate) 인터페이스 오류/타임아웃을 겪은 후 파라미터를 자율적으로 수정하거나 대체 도구로 전환해 작업을 완료한 비율 견고성 진단: Agent의 자기 교정(Self-Correction) 능력을 측정한다.

3. 성능·비용 차원 (System & Cost Metrics) — 상업적 실현 가능성

- 엔드투엔드 지연 (E2E Latency & P95/P99): 요청을 시작한 때부터 모든 도구 호출을 마치고 최종 답변을 출력할 때까지의 총 소요 시간. 분석 시 LLM 추론 소요 시간과 외부 API 응답 소요 시간으로 분리해야 한다.

- 최초 동작 소요 시간 (Time to First Tool / Token): 사용자가 체감하는 유연함을 결정하는 핵심 지표.

- 단일 성공 과업 비용(Cost per Successful Task) : Cost per Success =

\text{Cost per Success} = \frac{\text{테스트 세트 총 소모 비용(실패 재시도의 Token 및 API 비용 포함)}}{\text{최종 성공한 과업 총수}}

⚠️ 주의 : “요청 1회당 평균 Token 소모량”만 집계해서는 절대 안 된다. 실패율이 높다면 실패로 소모된 비용을 성공 과업에 반드시 분담시켜야 하며, 이것이 진짜 비즈니스 ROI이다.

4. 안전 및 컴플라이언스 차원(Safety & Policy Metrics) —— 생산 레드라인

- 권한 초월 조작 차단율 : 민감한 쓰기 조작(예: 데이터베이스 삭제, 대액 환불, 권한 수정)에 직면했을 때 성공적으로 거부하거나 규정대로 인간 2차 확인(Human-in-the-loop)을 발동하는 비율.

- 도메인 규칙 준수율(Policy Compliance) : 비즈니스 레드라인(예: “신원 확인 전에는 절대 주문 상세를 누설하지 않는다”)을 엄격히 준수하며, 사용자를 만족시키기 위해 규정을 위반하지 않는지 여부.

- 프롬프트 인젝션 저항률(Injection Resistance) : 외부에서 검색된 웹페이지, 이메일 또는 문서에 악성 지시(Indirect Prompt Injection)가 섞여 있을 때 Agent가 하이재킹되지 않는 비율.

三、 실전 실습: 200줄 미만 코드로 자동화 평가 파이프라인 구축

많은 팀이 정량 평가를 시작하지 못하는 이유는 평가 플랫폼 구축이 너무 무겁다고 생각하기 때문이다. 사실 처음부터 거대한 서드파티 시스템을 도입할 필요는 없으며, 아래의 “3단계” 아키텍처를 따르기만 하면 순수 Python만으로 반나절 만에 전체 벤치마크 테스트를 돌릴 수 있다:

첫 번째 단계: Agent 인터페이스 개조, 실행 궤적(Trace) 노출

Agent를 평가하려면 메인 진입 함수가 절대로 문자열 response 하나만 반환해서는 안 된다. 우리는 Agent 내부 루프에서 지나온 모든 단계를 기록하고 표준화된 딕셔너리 구조를 반환해야 한다:

```python import time import json from openai import OpenAI

OpenAI / DeepSeek / vLLM / 通义千问 등 모든 표준 인터페이스와 호환 client = OpenAI( api_key="YOUR_API_KEY", base_url="https://api.deepseek.com/v1" )

===================================================================== # 1. 실제 도구 구현 및 Schema 정의 # (context를 통해 환경을 주입하며, 테스트 샌드박스와 프로덕션 전환을 지원) # ===================================================================== def get_order(order_id: str, context: dict) -> str: """주문 상세 조회(읽기 작업)""" orders_db = context.get("orders", {}) if order_id not in orders_db: raise ValueError(f"Order {order_id} not found in database.") return json.dumps(orders_db[order_id], ensure_ascii=False)

def refund_order(order_id: str, reason: str, context: dict) -> str: """주문 환불 실행(쓰기 작업, 실제 비즈니스 검증 포함)""" orders_db = context.get("orders", {}) if order_id not in orders_db: raise ValueError(f"Order {order_id} not found.") order = orders_db[order_id] if order["status"] != "paid": raise PermissionError( f"Cannot refund order in '{order['status']}' status." ) # 상태 변경 order["status"] = "refunded" order["refund_reason"] = reason return json.dumps( {"status": "success", "refunded_amount": order["amount"]}, ensure_ascii=False )

도구 레지스트리와 대규모 언어 모델 Function Calling Schema TOOL_REGISTRY = { "get_order": get_order, "refund_order": refund_order, }

TOOLS_SCHEMA = [ { "type": "function", "function": { "name": "get_order", "description": "주문 번호로 주문의 현재 상태와 금액을 조회", "parameters": { "type": "object", "properties": { "order_id": { "type": "string", "description": "주문 ID, 예: ORD_123" } }, "required": ["order_id"] } } }, { "type": "function", "function": { "name": "refund_order", "description": "결제 완료된 주문에 대해 환불을 처리하며, 먼저 주문을 조회해 상태가 paid인지 확인해야 함", "parameters": { "type": "object", "properties": { "order_id": {"type": "string"}, "reason": { "type": "string", "description": "환불 사유" } }, "required": ["order_id", "reason"] } } } ]

===================================================================== # 2. 프로덕션 수준 Agent 실행 메인 루프(실제 Trace 자동 수집) # ===================================================================== def run_production_agent(user_query: str, env_context: dict, max_steps: int = 8) -> dict: start_time = time.time() first_action_time = None called_tools = []

실제 Token 집계(입력과 출력을 구분, 온라인 과금 단가가 다르기 때문) token_usage = { "prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0 }

messages = [ { "role": "system", "content": "你是售后客服Agent。환불 처리 전에 반드시 주문 상태를 먼저 조회해야 하며, 무분별한 작업은 엄격히 금지한다. ```

" }, { "role" : "user" , "content" : user_query} ] final_answer = "" # 핵심 ReAct / Tool-Calling 루프 (max_steps를 설정해 온라인에서 무한 루프로 과금이 터지는 것을 방지) for step in range (max_steps): response = client.chat.completions.create( model= "deepseek-chat" , # 또는 gpt-4o / qwen-max messages=messages, tools=TOOLS_SCHEMA, temperature= 0.0 # 평가 및 프로덕션 도구 호출은 안정성 보장을 위해 0으로 설정할 것을 권장 ) # 1. API 응답에서 실제 Token 소비량을 누적 if response.usage: token_usage[ "prompt_tokens" ] += response.usage.prompt_tokens token_usage[ "completion_tokens" ] += response.usage.completion_tokens token_usage[ "total_tokens" ] += response.usage.total_tokens # 2. 첫 번째 동작 응답 시간(TTFT) 기록 if first_action_time is None : first_action_time = time.time() - start_time assistant_msg = response.choices[ 0 ].message messages.append(assistant_msg) # 3. 종료 조건: 모델이 더 이상 도구를 호출하지 않으면 추론이 끝난 것이므로 최종 응답을 바로 획득 if not assistant_msg.tool_calls: final_answer = assistant_msg.content or "" break # 4. 모델이 요청한 모든 도구 호출을 순회하며 실행 (Parallel Tool Calls 지원) for tool_call in assistant_msg.tool_calls: func_name = tool_call.function.name raw_args = tool_call.function.arguments tool_start_ts = time.time() tool_output = "" error_msg = None parsed_args = {} try : # 프로덕션 문제점 1: 대규모 모델이 간혹 비정상적인 JSON 파라미터를 생성 parsed_args = json.loads(raw_args) # 프로덕션 문제점 2: 대규모 모델이 존재하지 않는 도구 이름을 환각으로 생성 if func_name not in TOOL_REGISTRY: raise KeyError( f"Tool ' {func_name} ' is not registered." ) # 실제 도구 함수를 실행하고, 샌드박스/컨텍스트를 주입 tool_func = TOOL_REGISTRY[func_name] tool_output = tool_func(**parsed_args, context=env_context) except Exception as e: # 도구 예외를 캡처해 형식화한 뒤 모델에 반환하여 모델의 Self-Correction(자기 교정)을 유도 error_msg = f" { type (e).__name__} : { str (e)} " tool_output = json.dumps({ "error" : error_msg}, ensure_ascii= False ) tool_latency = round (time.time() - tool_start_ts, 4 ) # 5. 실제 단계별 실행 데이터를 Trace 리스트에 기록 called_tools.append({ "step" : step + 1 , "tool_call_id" : tool_call. id , "name" : func_name, "args" : parsed_args if parsed_args else raw_args, "output" : tool_output, "error" : error_msg, # '장애 복구율'과 '파라미터 이상률'을 집계하는 데 사용 "latency" : tool_latency # 모델이 느린 것인지 외부 API가 느린 것인지를 구분 }) # 6. 도구 실행 결과를 role="tool"로 컨텍스트에 다시 넣어 다음 라운드 모델 추론에 제공 messages.append({ "role" : "tool" , "tool_call_id" : tool_call. id , "name" : func_name, "content" : tool_output }) else : # max_steps를 다 돌 때까지 break되지 않으면 스텝 상한에 도달한 것(무한 루프) final_answer = "ERROR: Max agent iterations reached." return { "final_answer" : final_answer, "called_tools" : called_tools, "e2e_latency" : round (time.time() - start_time, 3 ), "ttft" : round (first_action_time or 0.0 , 3 ), "total_tokens" : token_usage[ "total_tokens" ], "token_breakdown" : token_usage, "final_env_state" : env_context }

두 번째 단계: 샌드박스 상태를 갖춘 벤치마크 테스트 세트 구축하기(test_cases.json)

전통적인 질의응답 테스트 세트와 달리, Agent의 테스트 케이스에는 초기 환경 상태(initial_env), 기대 도구 궤적(expected_tools), 그리고 최종 상태 단언(expected_env_check)이 반드시 포함되어야 한다.

먼저 업무에서 빈도가 높은 20~50개의 시나리오와 오류가 나기 쉬운 경계 시나리오를 골라 test_cases.json으로 저장한다:

[ { "id" : "case_001" , "query" : "주문 ORD_123을 취소해 줘, 더 이상 원하지 않아" , "initial_env" : { "orders" : { "ORD_123" : { "status" : "paid" , "amount" : 199 } } } , "expected_tools" : [ { "name" : "get_order" , "args" : { "order_id" : "ORD_123" } } , { "name" : "refund_order" , "args" : { "order_id" : "ORD_123" } } ] , "expected_env_check" : { "path" : [ "orders" , "ORD_123" , "status" ] , "equals" : "refunded" } } ]

세 번째 단계: 핵심 평가 스크립트를 작성해 “경성 지표” 계산하기(eval_runner.py)

Trace와 테스트 세트가 갖춰지면, 우리는 어떤 블랙박스 마법도 필요 없이 결정론적인 Python 로직만으로 핵심 지표의 80%를 계산할 수 있다. eval_runner.py를 새로 만든다:

import json import copy import numpy as np

def evaluate_single_run(trace: dict, case: dict) -> dict: """단일 실행의 각硬性 지표를 계산한다""" actual_tools = trace["called_tools"] expected_tools = case["expected_tools"]

1. 도구 선택 재현율과 정밀도 (Tool Recall & Precision) actual_names = [t["name"] for t in actual_tools] expected_names = [t["name"] for t in expected_tools] matched_tools = sum(1 for name in expected_names if name in actual_names) tool_recall = matched_tools / len(expected_names) if expected_names else 1.0 tool_precision = matched_tools / len(actual_names) if actual_names else 0.0

2. 파라미터 추출 정확도 (Parameter Accuracy) correct_params, total_params = 0, 0 for exp_t in expected_tools: act_t = next((t for t in actual_tools if t["name"] == exp_t["name"]), None) for k, v in exp_t.get("args", {}).items(): total_params += 1 if act_t and act_t.get("args", {}).get(k) == v: correct_params += 1 param_accuracy = correct_params / total_params if total_params > 0 else 1.0

3. 궤적 효율 (Step Efficiency) step_efficiency = min(1.0, len(expected_tools) / len(actual_tools)) if actual_tools else 0.0

4. 무한 루프 탐지 (Loop Detection: 동일한 이름·동일한 파라미터 도구를 연속 두 번 호출) has_loop = any( actual_tools[i]["name"] == actual_tools[i-1]["name"] and actual_tools[i]["args"] == actual_tools[i-1]["args"] for i in range(1, len(actual_tools)) )

5. 엔드투엔드 환경 상태 검증 (State Assertion: 데이터베이스 상태가 실제로 변경되었는지 확인) check = case["expected_env_check"] state_val = trace["final_env_state"] for key in check["path"]: state_val = state_val.get(key, {}) task_success = (state_val == check["equals"])

return { "task_success": int(task_success), "tool_recall": tool_recall, "tool_precision": tool_precision, "param_accuracy": param_accuracy, "step_efficiency": step_efficiency, "has_loop": int(has_loop), "latency": trace["e2e_latency"], "tokens": trace["total_tokens"] }

def run_benchmark(test_cases_file: str, k_runs: int = 3): """테스트셋을 배치로 실행하고, Pass^k 안정성 지표 계산을 지원한다""" with open(test_cases_file, "r", encoding="utf-8") as f: cases = json.load(f)

all_metrics, pass_at_1_list, pass_hat_k_list = [], [], []

for case in cases: k_successes = [] for _ in range(k_runs): # 매 실행 전에 샌드박스 환경을 깊은 복사하여 테스트 간 상호 오염을 방지한다 env_copy = copy.deepcopy(case["initial_env"]) trace = run_my_agent(case["query"], env_copy) res = evaluate_single_run(trace, case) all_metrics.append(res) k_successes.append(res["task_success"])

pass_at_1_list.append(k_successes[0]) # k회 실행이 모두 성공해야만 Pass^k를 1로 기록한다 pass_hat_k_list.append(1 if sum(k_successes) == k_runs e

lse 0 ) total_success_runs = sum (m[ "task_success" ] for m in all_metrics) total_tokens = sum (m[ "tokens" ] for m in all_metrics) latencies = [m[ "latency" ] for m in all_metrics] report = { "1_任务成功率 (Pass@1)" : f" {np.mean(pass_at_1_list) * 100 : .1 f} %" , f"2_稳定性指标 (Pass^ {k_runs} )" : f" {np.mean(pass_hat_k_list) * 100 : .1 f} %" , "3_工具召回率 (Tool Recall)" : f" {np.mean([m[ 'tool_recall' ] for m in all_metrics]) * 100 : .1 f} %" , "4_参数准确率 (Param Accuracy)" : f" {np.mean([m[ 'param_accuracy' ] for m in all_metrics]) * 100 : .1 f} %" , "5_轨迹效率 (Step Efficiency)" : f" {np.mean([m[ 'step_efficiency' ] for m in all_metrics]) * 100 : .1 f} %" , "6_死循环率 (Loop Rate)" : f" {np.mean([m[ 'has_loop' ] for m in all_metrics]) * 100 : .1 f} %" , "7_平均耗时 / P95耗时" : f" {np.mean(latencies): .2 f} s / {np.percentile(latencies, 95 ): .2 f} s" , "8_单次成功平均Token成本" : f" {total_tokens / total_success_runs: .0 f} tokens" if total_success_runs else "INF" } print ( "================ Agent 自动化评测报告 ================" ) print (json.dumps(report, indent= 2 , ensure_ascii= False )) if __name__ == "__main__" : run_benchmark( "test_cases.json" , k_runs= 3 )

4단계: 카오스 엔지니어링 실전 — 무침입 부하 테스트로 "장애 복구율" 측정하기

앞선 3단계의 일반 테스트에서는 모든 도구 호출이 순조롭게 진행되었다. 그러나 온라인 프로덕션 환경은 시시각각 변하며, 데이터베이스 연결 풀이 가득 차거나 서드파티 인터페이스가 503 타임아웃을 내는 일은 극히 흔하다.

지표 체계의 "장애 복구율(Recovery Rate)"을 측정하기 위해, 우리는 1단계의 비즈니스 도구 소스 코드를 수정할 필요 없이, eval_runner.py에 컨텍스트 관리자를 하나 추가하여 1단계의 TOOL_REGISTRY(도구 레지스트리)를 동적으로 하이재킹해 인위적으로 한 차례 "온라인 사고"를 만들어내기만 하면 된다:

from contextlib import contextmanager def make_flaky_tool ( tool_func, fail_times: int = 1 ): """ 장애 주입 고차 함수: 도구의 처음 N회 호출에서 강제로 예외를 발생시키고, N+1회째부터 정상으로 복구 Agent가 오류를 보고한 뒤의 Self-Correction(스스로 교정하고 재시도하는 능력)을 부하 테스트하는 데 사용 """ call_count = 0 def wrapper ( *args, **kwargs ): nonlocal call_count call_count += 1 if call_count <= fail_times: # 실제 예외를 발생시켜, 첫 단계 Agent 루프의 except 캐치 로직을 트리거하고 trace["error"]에 기록 raise RuntimeError( "503 Service Unavailable: Database connection timeout, please retry." ) return tool_func(*args, **kwargs) return wrapper @contextmanager def inject_chaos_to_registry ( registry: dict , target_tool_name: str , fail_times: int = 1 ): """컨텍스트 관리자: 테스트 중 TOOL_REGISTRY의 도구를 임시로 교체하고, 테스트가 끝나면 자동으로 원복하여 오염이 전혀 없도록 함""" original_func = registry[target_tool_name] registry[target_tool_name] = make_flaky_tool(original_func, fail_times) try : yield finally : registry[target_tool_name] = original_func def run_recovery_benchmark ( test_cases_file: str ): """전용 평가: 의존 인터페이스가 간헐적으로 503 오류를 낼 때 Agent의 장애 복구율 계산""" with open (test_cases_file, "r" , encoding= "utf-8" ) as f: cases = json.load(f) encountered_error_runs = 0 recovered_runs = 0 for case in cases: env_copy = copy.deepcopy( case [ "initial_env" ]) # 핵심 연결 지점: 첫 단계에서 정의한 TOOL_REGISTRY를 동적으로 하이재킹하여 get_order의 첫 호출이 반드시 실패하게 함 with inject_chaos_to_registry(TOOL_REGISTRY, target_tool_name= "get_order" , fail_times= 1 ): trace = run_production_agent( case [ "query" ], env_copy) res = evaluate_single_run(trace, case ) # 1. Trace에 우리가 주입한 오류가 실제로 기록되었는지 확인 has_error = any (t[ "error" ] is not None for t in trace[ "called_tools" ]) if has_error: encountered_error_runs += 1 # 2. 오류를 겪었으나 최종 비즈니스 상태가 여전히 달성되었다면 (task_success == 1), 자기 치유 성공! if res[ "task_success" ] == 1 : recovered_runs += 1 recovery_rate = (recovered_runs / encountered_error_runs * 100 ) if encountered_error_runs else 0.0 print ( "================ Agent 카오스 장애 복구 보고 ================" ) print ( f"⚡ 장애 주입 총수: {encountered_error_runs} | 성공적 자가 치유 수: {recovered_runs} | 장애 복구율: {recovery_rate: .1 f} %" ) if __name__ == "__main__" : run_benchmark( "test_cases.json" , k_runs= 3 ) run_recovery_benchmark( "test_cases.json" )

💡 왜 이 카오스 테스트 코드가 폐쇄 루프를 완성할 수 있는가?

- inject_chaos_to_registry가 적용되면, Agent가 1단계에서 get_order를 호출할 때 RuntimeError가 발생한다.

- 첫 단계에서 작성한 run_production_agent 루프에서 except Exception as e가 해당 예외를 잡아 {

- 최종 세 번째 단계의 evaluate_single_run 에서 데이터베이스 상태가 refunded 로 바뀌었음을 검증하면( task_success == 1 ), 한 번의 완전한 "장애 주입 -> 모델이 오류를 감지 -> 자율 재시도로 복구 -> 상태 검증 성공" 과정이 정밀하게 정량화된다.

4. 심화 평가: LLM-as-a-Judge 도입으로 "소프트 지표" 해결하기

코드 단언과 카오스 주입은 도구 궤적, 상태 검증, 장애 허용 테스트를 완벽하게 해결할 수 있지만, **"최종 답변에 환각(금액 날조)이 존재하는지"** 그리고 **"개방형 과제의 의미가 기준에 부합하는지"** 같은 소프트 지표에 직면하면 LLM 심판 모델을 도입해야 한다.

오픈소스 평가 프레임워크 DeepEval ( pip install -U deepeval ) 사용을 권장하며, 첫 번째 단계에서 얻은 trace를 바로 심판 연산자에 입력하면 된다:

from deepeval import evaluate from deepeval.test_case import LLMTestCase, ToolCall from deepeval.metrics import ToolCorrectnessMetric, TaskCompletionMetric, FaithfulnessMetric from agent_core import run_production_agent # 1. 운영 환경 수준의 Agent를 실행해 실제 실행 궤적 획득 env = { "orders" : { "ORD_123" : { "status" : "paid" , "amount" : 199 }}} trace = run_production_agent( "주문 ORD_123을 환불해 주세요" , env) # 2. DeepEval 테스트 케이스 조립 test_case = LLMTestCase( input = "주문 ORD_123을 환불해 주세요" , actual_output=trace[ "final_answer" ], tools_called=[ ToolCall(name=t[ "name" ], input_parameters=t[ "args" ], output=t[ "output" ]) for t in trace[ "called_tools" ] ], expected_tools=[ ToolCall(name= "get_order" , input_parameters={ "order_id" : "ORD_123" }), ToolCall(name= "refund_order" , input_parameters={ "order_id" : "ORD_123" }) ], # 도구가 반환한 원본 JSON을 사실 근거(Context)로 사용해 답변에 환각이 있는지 검출 retrieval_context=[t[ "output" ] for t in trace[ "called_tools" ]] ) # 3. 다차원 자동 채점 실행 metrics = [ ToolCorrectnessMetric(threshold= 0.8 ), # 도구 및 파라미터 일치도 TaskCompletionMetric(threshold= 0.8 ), # 엔드투엔드 과제 완수도 FaithfulnessMetric(threshold= 0.9 ) # 충실도: 답변 속 숫자/상태가 도구 반환값에서 엄격히 비롯되었는지 대조 ] evaluate(test_cases=[test_case], metrics=metrics)

5. 엔지니어링 폐쇄 루프: 평가를 일상 개발에 어떻게 통합할 것인가?

평가 스크립트를 작성하는 것은 첫 단계일 뿐이며, Agent 품질이 나선형으로 상승하게 만드는 진짜 핵심은 팀 차원의 Eval-Driven Development(평가 주도 개발) 규범을 수립하는 것이다:

- 품질 베이스라인(Baseline) 수립: 프로젝트 초기에 30~50개의 핵심 시나리오를 담은 Golden Dataset(황금 테스트 세트)을 구축하고 첫 번째 진단 리포트를 산출한다.

- 변경 게이트(CI/CD Gate) 설정: 팀 구성원이 System Prompt를 수정하거나 도구의 Docstring 설명을 조정하거나 기반 모델을 업그레이드할 때마다 반드시 python eval_runner.py 를 실행해야 한다. 만약 Pass 3 \text{Pass}^3 Pass 3 또는 파라미터 정확도가 퇴보하면 메인 브랜치에 병합하는 것을 엄격히 금지한다.

- 운영 Badcase 지속 환류(Data Flywheel): 운영 환경에 Langfuse 또는 Arize Phoenix 등 오픈소스 관측성 플랫폼을 연동한다. 매주 정기적으로 온라인에서 무한 루프를 유발했거나 소요 시간이 지나치게 길었거나 사용자가 싫어요를 누른 실제 Trace를 선별하고, 비식별화한 후 새로운 테스트 케이스로 전환해 test_cases.json 에 추가한다.

맺음말

AI Agent 개발은 "수작업 공방식 Prompt 연단"에서 "엄밀한 현대 소프트웨어 공학"으로 나아가고 있다. 블랙박스 Agent를 명확한 결과, 궤적, 비용, 안전의 4대 지표로 분해하고 자동화 스크립트로 이를 개발 파이프라인에 단단히 고정하면, 매번의 모델 업그레이드와 프롬프트 튜닝이 마음속에 확신이 있고 근거가 있게 될 것이다.

진위를 가려내다

프론트엔드 개발

132

글

462k

읽음

549

팔로워