사용자 시뮬레이션 기반 멀티턴 대화 테스트, 워크플로우 완료 검증, 반복 호출 및 모순적 계획 감지 등 E2E 시나리오 테스트의 전체 방법론을 다룹니다.
3장에서 다룬 도구 호출 검증은 에이전트의 개별 "행동"을 검증했습니다. 그러나 실제 사용자는 하나의 도구 호출로 끝나는 단순한 요청만 하지 않습니다. "여행을 계획해 줘"라는 한 마디 뒤에는 항공편 검색, 호텔 예약, 일정 조율, 예산 확인 등 수십 번의 상호작용이 이어집니다.
엔드투엔드(End-to-End, E2E) 테스트는 이러한 전체 시나리오를 처음부터 끝까지 시뮬레이션합니다. 에이전트가 복잡한 멀티스텝 작업을 성공적으로 완수하는지, 그 과정에서 일관성을 유지하는지를 검증합니다.
E2E 테스트의 핵심은 **시뮬레이션 사용자(Simulated User)**입니다. 실제 사용자를 대신하여 에이전트와 대화하는 LLM 기반 사용자입니다. 좋은 시뮬레이션 사용자는 다음 요소를 갖춰야 합니다.
시뮬레이션 사용자의 특성과 행동 패턴을 정의합니다.
PERSONAS = {
"tech_savvy": {
"description": "기술에 능숙한 30대 개발자",
"behavior": [
"간결하게 요청한다",
"기술 용어를 자연스럽게 사용한다",
"불필요한 설명은 건너뛴다",
],
},
"non_technical": {
"description": "기술에 익숙하지 않은 60대 사용자",
"behavior": [
"쉬운 말로 질문한다",
"한 번에 이해하지 못하면 다시 물어본다",
"과정을 하나씩 확인하려 한다",
],
},
"impatient": {
"description": "바쁜 직장인, 빠른 결과를 원함",
"behavior": [
"짧게 요청한다",
"느린 응답에 재촉한다",
"선택지가 많으면 '아무거나' 고른다",
],
},
"adversarial": {
"description": "시스템을 테스트하려는 사용자",
"behavior": [
"모호하거나 모순된 요청을 한다",
"갑자기 주제를 바꾼다",
"에이전트의 한계를 시험한다",
],
},
}시뮬레이션 사용자가 달성하려는 목표와 대화 중 지켜야 할 제약 조건을 명시합니다.
from agentest import AgentTest, SimulatedUser, LLMJudge
async def test_complete_booking_workflow():
test = AgentTest(
agent=travel_agent,
simulatedUser=SimulatedUser(
persona=PERSONAS["non_technical"],
goal="3월 25일 서울에서 도쿄로 2박 3일 여행 예약하기",
constraints=[
"예산은 100만 원 이하여야 한다",
"직항만 원한다",
"호텔은 시내 중심가를 선호한다",
"영어를 못하므로 한국어 서비스가 되는 곳을 원한다",
],
completion_hint="모든 예약이 확정되고 일정표를 받으면 완료",
),
judge=LLMJudge(
criteria=[
"예산 100만 원 이하를 충족했는가",
"직항 조건을 반영했는가",
"최종 일정표가 제공되었는가",
"사용자 눈높이에 맞게 설명했는가",
],
),
maxTurns=20,
)
result = await test.run()
assert result.passed
assert result.score >= 0.8시뮬레이션 사용자의 completion_hint는 대화의 자연스러운 종료를 돕습니다. 이 힌트가 없으면 시뮬레이션 사용자가 언제 대화를 끝내야 할지 판단하기 어려워, maxTurns에 도달할 때까지 불필요한 대화가 이어질 수 있습니다.
멀티턴 대화 테스트는 단일 요청-응답이 아닌, 여러 턴에 걸친 상호작용을 검증합니다. 이 과정에서 주의해야 할 구조적 요소가 있습니다.
class ConversationConfig:
"""대화 테스트 설정"""
min_turns: int = 2 # 최소 턴 — 너무 빨리 끝나면 불충분
max_turns: int = 20 # 최대 턴 — 무한 루프 방지
timeout_seconds: int = 120 # 전체 시간 제한
# 종료 조건
end_conditions = [
"사용자가 목표를 달성했다고 판단",
"에이전트가 작업 완료를 선언",
"max_turns에 도달",
"timeout에 도달",
"에이전트가 더 이상 진행할 수 없다고 판단",
]멀티턴 대화에서 에이전트가 이전 턴의 맥락을 올바르게 유지하는지 검증하는 것은 매우 중요합니다.
@pytest.mark.asyncio
async def test_context_is_retained():
"""이전 턴의 정보가 후속 턴에서 유지되는지 검증"""
result = await Scenario(
name="컨텍스트 유지 검증",
description="""
1턴: 사용자가 '김철수'로 예약하겠다고 말합니다.
2턴: 사용자가 '예약자 이름 확인해 줘'라고 요청합니다.
에이전트는 1턴에서 전달한 이름을 기억하고 있어야 합니다.
""",
agent=booking_agent,
success_criteria=[
"에이전트가 2턴에서 '김철수'를 정확히 언급했는가",
"에이전트가 이름을 다시 물어보지 않았는가",
],
).run()
assert result.success is TrueE2E 테스트의 최종 목표는 워크플로우가 성공적으로 완료되었는지 확인하는 것입니다. 이를 위해 워크플로우의 필수 단계를 정의하고, 각 단계의 완료 여부를 추적합니다.
class WorkflowStep:
def __init__(self, name: str, required_tools: list, validation_fn=None):
self.name = name
self.required_tools = required_tools
self.validation_fn = validation_fn
BOOKING_WORKFLOW = [
WorkflowStep(
name="항공편 검색",
required_tools=["search_flights"],
),
WorkflowStep(
name="항공편 선택",
required_tools=["select_flight"],
),
WorkflowStep(
name="승객 정보 입력",
required_tools=["set_passenger_info"],
),
WorkflowStep(
name="결제",
required_tools=["process_payment"],
),
WorkflowStep(
name="확인서 발송",
required_tools=["send_confirmation"],
),
]
def verify_workflow_completion(tool_calls: list, workflow: list) -> dict:
"""워크플로우의 각 단계가 완료되었는지 검증"""
tool_names = [call.name for call in tool_calls]
results = {}
for step in workflow:
completed = all(
tool in tool_names for tool in step.required_tools
)
results[step.name] = {
"completed": completed,
"required_tools": step.required_tools,
"found_tools": [t for t in step.required_tools if t in tool_names],
}
return results
@pytest.mark.asyncio
async def test_booking_workflow_complete(booking_agent):
result = await Scenario(
name="예약 워크플로우 완료 검증",
description="항공편 예약의 전체 과정을 수행합니다",
agent=booking_agent,
).run()
completion = verify_workflow_completion(
result.state.get_all_tool_calls(),
BOOKING_WORKFLOW,
)
incomplete_steps = [
name for name, info in completion.items()
if not info["completed"]
]
assert not incomplete_steps, \
f"미완료 단계: {incomplete_steps}"E2E 테스트에서는 워크플로우 완료 여부뿐만 아니라, 실패를 나타내는 패턴도 능동적으로 감지해야 합니다.
에이전트가 같은 작업을 반복적으로 시도하는 것은 "막혀 있다"는 신호입니다.
def detect_loop(tool_calls: list, window_size: int = 3) -> bool:
"""도구 호출 시퀀스에서 반복 패턴을 감지"""
if len(tool_calls) < window_size * 2:
return False
names = [call.name for call in tool_calls]
for i in range(len(names) - window_size * 2 + 1):
window = names[i:i + window_size]
next_window = names[i + window_size:i + window_size * 2]
if window == next_window:
return True
return False
@pytest.mark.asyncio
async def test_no_infinite_loops(agent):
"""에이전트가 무한 루프에 빠지지 않는지 검증"""
result = await Scenario(
name="무한 루프 감지",
description="복잡한 요청을 처리하며 루프에 빠지지 않아야 합니다",
agent=agent,
).run()
tool_calls = result.state.get_all_tool_calls()
assert not detect_loop(tool_calls), \
"도구 호출에서 반복 패턴이 감지되었습니다"에이전트가 스스로 세운 계획과 모순되는 행동을 하는 경우를 감지합니다.
@pytest.mark.asyncio
async def test_no_contradictory_actions(agent):
"""에이전트가 모순적 행동을 하지 않는지 검증"""
result = await Scenario(
name="모순적 행동 감지",
description="호텔을 예약한 뒤 같은 날짜에 다른 호텔을 예약하지 않아야 합니다",
agent=agent,
success_criteria=[
"동일 날짜에 중복 예약이 발생하지 않았는가",
"이전 행동과 모순되는 행동이 없는가",
"에이전트가 일관된 계획을 따랐는가",
],
).run()
assert result.success is True
# 도구 호출 레벨에서도 검증
tool_calls = result.state.get_all_tool_calls()
book_calls = [c for c in tool_calls if c.name == "book_hotel"]
if len(book_calls) > 1:
dates = [c.parameters.get("check_in_date") for c in book_calls]
assert len(dates) == len(set(dates)), \
"동일 날짜에 중복 호텔 예약이 감지되었습니다"모순적 계획 감지는 도구 호출 레벨의 결정론적 검증과 LLM-as-Judge의 의미론적 평가를 결합해야 합니다. 도구 호출만으로는 "에이전트가 예산 초과를 인지하고도 예약을 진행했는가"와 같은 논리적 모순을 잡기 어렵습니다.
반복적으로 사용되는 시나리오 정의를 재사용 가능한 패턴으로 구성합니다.
from dataclasses import dataclass, field
@dataclass
class ScenarioTemplate:
"""재사용 가능한 시나리오 템플릿"""
name: str
category: str
description_template: str
base_criteria: list = field(default_factory=list)
persona: str = "default"
max_turns: int = 15
def create(self, **kwargs) -> dict:
return {
"name": self.name.format(**kwargs),
"description": self.description_template.format(**kwargs),
"success_criteria": self.base_criteria.copy(),
"max_turns": self.max_turns,
}
# 템플릿 정의
CRUD_TEMPLATE = ScenarioTemplate(
name="{resource} CRUD 워크플로우",
category="crud",
description_template="{resource}을(를) 생성하고, 조회하고, 수정하고, 삭제하는 전체 흐름",
base_criteria=[
"생성 작업이 성공했는가",
"조회 시 생성한 데이터가 반환되는가",
"수정 사항이 정확히 반영되었는가",
"삭제 후 조회 시 존재하지 않는가",
],
)
# 템플릿 사용
booking_scenario = CRUD_TEMPLATE.create(resource="항공편 예약")import pytest
@pytest.fixture
def travel_agent():
"""여행 에이전트 픽스처"""
agent = create_travel_agent(
model="claude-sonnet-4-20250514",
tools=["search_flights", "book_flight", "search_hotels",
"book_hotel", "send_confirmation"],
temperature=0.1, # 테스트 시에는 낮은 temperature
)
yield agent
agent.cleanup()
@pytest.fixture
def mock_apis():
"""외부 API 모킹 픽스처"""
with MockAPIServer() as server:
server.register("flights_api", mock_flight_data)
server.register("hotels_api", mock_hotel_data)
server.register("payment_api", mock_payment_success)
yield server
@pytest.fixture(params=["tech_savvy", "non_technical", "impatient"])
def user_persona(request):
"""다양한 사용자 페르소나를 순회하는 픽스처"""
return PERSONAS[request.param]E2E 테스트 결과는 단순한 pass/fail을 넘어 상세한 분석 정보를 제공해야 합니다.
def analyze_e2e_result(result) -> dict:
"""E2E 테스트 결과를 상세히 분석"""
tool_calls = result.state.get_all_tool_calls()
return {
"passed": result.success,
"score": result.score,
"total_turns": result.turn_count,
"tool_calls": {
"total": len(tool_calls),
"unique_tools": len(set(c.name for c in tool_calls)),
"by_tool": {
name: len([c for c in tool_calls if c.name == name])
for name in set(c.name for c in tool_calls)
},
},
"issues": {
"loops_detected": detect_loop(tool_calls),
"excessive_turns": result.turn_count > 15,
"tool_errors": len([c for c in tool_calls if c.error]),
},
"duration_seconds": result.duration,
}이번 장에서는 E2E 시나리오 테스트의 전체 방법론을 살펴보았습니다.
5장에서는 에이전트 테스트의 가장 도전적인 영역인 비결정적 출력 평가를 다룹니다. 결정론적 검증만으로는 불가능한 응답 품질 평가를 위해, LLM-as-Judge, 품질 차원별 점수 산출, 임계값 설정, pass@k 전략 등을 상세히 살펴봅니다.
이 글이 도움이 되셨나요?
관련 주제 더 보기
LLM-as-Judge 패턴으로 에이전트의 비결정적 출력을 평가하는 방법, 품질 차원별 점수 산출, 임계값 설정, pass@k 전략을 상세히 다룹니다.
에이전트의 도구 호출 정확성을 이름, 파라미터, 출력의 3단계로 검증하는 방법과 모킹 전략, 도구 체인 순서 검증, 불필요한 호출 감지 기법을 다룹니다.
평가를 회귀 테스트로 졸업시키는 패턴, Golden Dataset 관리, 롤링 성공률 모니터링, 베이스라인 관리와 변경 영향 분석을 다룹니다.