반응형
에이전트가 실패했습니다. 로그를 열었더니 "Error: tool_call failed"입니다. 어느 스텝에서, 왜, 어떤 컨텍스트에서 실패했는지 알 수 없습니다. 단일 LLM 호출 디버깅과 에이전트 디버깅은 완전히 다른 문제입니다. 에이전트는 비결정론적이고, 루프를 돌고, 여러 툴을 호출하고, 세션 간 상태를 유지합니다. 이걸 디버깅하는 도구가 다릅니다.
[핵심 요약]
→ 단일 LLM vs 에이전트 디버깅: 완전히 다른 문제 — 도구도 달라야 함
→ Langfuse: 오픈소스·셀프호스팅, 프롬프트 버전 관리 + 트레이싱 최강. 2026년 1월 Clickhouse 인수
→ AgentOps: 멀티프레임워크 400개+ 지원, 타임트래블 디버깅, 세션 리플레이 — 에이전트 디버깅 특화
→ Braintrust: Eval 퍼스트, CI/CD 게이트, 무료 100만 스팬/월 — 회귀 테스트 특화
→ 오버헤드: AgentOps 12% / Langfuse 15% / LangSmith ~5% (성능 민감 환경 고려)
→ 빠른 선택: 에이전트 디버깅 → AgentOps / 프롬프트+평가 → Langfuse or Braintrust
→ 스타트업 권장 스택: Langfuse(무료 셀프) + AgentOps + Braintrust 무료 티어 조합
→ 프로덕션 스택: Braintrust Pro($249/월) + AgentOps + Datadog
단일 LLM vs 에이전트 — 왜 디버깅이 다른가
[단일 LLM 모니터링 (쉬움)]
요청 → LLM → 응답
→ 입력/출력 로그만 있으면 충분
→ 실패 원인: 대부분 프롬프트 문제
→ Helicone, 기본 로깅으로 충분
[에이전트 디버깅 (전혀 다른 문제)]
요청 →
→ LLM 호출 1 (계획 수립)
→ 툴 A 실행 (DB 쿼리)
→ LLM 호출 2 (결과 분석)
→ 툴 B 실행 (API 호출) ← 여기서 실패
→ LLM 호출 3 (에러 처리)
→ 재시도 × 5회
→ 최종 실패
문제들:
→ 어느 LLM 호출이 잘못된 판단을 했나?
→ 툴 B의 입력이 잘못됐나, 툴 자체가 문제인가?
→ 재시도 5회 중 어디서 무한 루프 시작됐나?
→ 동일 버그가 다른 세션에도 발생하는 패턴인가?
→ 이 실패가 프롬프트 변경 전부터 있었나?
실전 1 — Langfuse: 오픈소스 기반 트레이싱
# pip install langfuse anthropic
from langfuse import Langfuse
from langfuse.decorators import observe, langfuse_context
import anthropic
import json
# Langfuse 초기화
langfuse = Langfuse(
public_key="pk-lf-...",
secret_key="sk-lf-...",
host="https://cloud.langfuse.com" # 또는 셀프호스팅 URL
)
client = anthropic.Anthropic()
# ── 데코레이터 방식 — 가장 간단 ─────────────────────
@observe(name="research-agent") # 트레이스 이름
def research_agent(query: str) -> str:
# 현재 트레이스에 입력 기록
langfuse_context.update_current_observation(
input={"query": query},
metadata={"agent_version": "v2.1"}
)
result = planning_step(query)
return result
@observe(name="planning") # 중첩 스팬 자동 생성
def planning_step(query: str) -> str:
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
messages=[{
"role": "user",
"content": f"다음 쿼리 처리 계획을 세워줘: {query}"
}]
)
plan = response.content[0].text
# 토큰 사용량 기록
langfuse_context.update_current_observation(
usage={
"input": response.usage.input_tokens,
"output": response.usage.output_tokens,
"unit": "TOKENS"
},
output=plan
)
return execution_step(plan, query)
@observe(name="execution")
def execution_step(plan: str, query: str) -> str:
# 툴 실행 추적
tool_span = langfuse_context.update_current_observation(
metadata={"plan": plan[:200]}
)
# 실제 툴 실행
tool_result = call_external_api(query)
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=2048,
messages=[
{"role": "user", "content": query},
{"role": "assistant", "content": plan},
{"role": "user", "content": f"툴 결과: {tool_result}"}
]
)
final = response.content[0].text
# 최종 결과 + 품질 평가 스코어
langfuse_context.score_current_observation(
name="completeness",
value=evaluate_completeness(final), # 0~1
comment="자동 평가"
)
return final
# 실행
result = research_agent("2026년 AI 에이전트 트렌드 분석")
langfuse.flush()
# ── 수동 방식 — 더 세밀한 제어 ──────────────────────
def manual_traced_agent(task: str) -> str:
# 루트 트레이스 생성
trace = langfuse.trace(
name="complex-agent",
input={"task": task},
tags=["production", "v2"],
user_id="user-123",
session_id="session-456"
)
try:
# 스팬 1: LLM 호출
llm_span = trace.span(
name="llm-planning",
input={"prompt": task}
)
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
messages=[{"role": "user", "content": task}]
)
plan = response.content[0].text
llm_span.end(
output={"plan": plan},
usage={
"input": response.usage.input_tokens,
"output": response.usage.output_tokens,
"unit": "TOKENS"
}
)
# 스팬 2: 툴 실행
tool_span = trace.span(name="tool-execution")
tool_result = execute_tools(plan)
tool_span.end(output={"result": str(tool_result)[:500]})
# 스팬 3: 최종 생성
gen_span = trace.generation(
name="final-response",
model="claude-sonnet-4-6",
input={"plan": plan, "tool_result": str(tool_result)[:200]},
)
final_response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=2048,
messages=[
{"role": "user", "content": task},
{"role": "assistant", "content": plan},
{"role": "user", "content": f"결과: {tool_result}"}
]
)
final = final_response.content[0].text
gen_span.end(
output=final,
usage={
"input": final_response.usage.input_tokens,
"output": final_response.usage.output_tokens,
"unit": "TOKENS"
}
)
# 트레이스 완료 + 품질 스코어
trace.score(name="quality", value=0.9)
trace.update(output={"result": final})
return final
except Exception as e:
trace.update(
output={"error": str(e)},
level="ERROR"
)
raise
# 실행
result = manual_traced_agent("복잡한 리서치 태스크")
langfuse.flush()
[Langfuse 대시보드에서 볼 수 있는 것]
→ 전체 에이전트 실행 트리 (스팬 계층 구조)
→ 각 LLM 호출의 입력/출력/토큰/비용
→ 세션별 누적 비용 추이
→ 프롬프트 버전별 품질 비교
→ 실패한 트레이스 필터링 (level="ERROR")
→ 사용자별·세션별 분석
[Langfuse 셀프호스팅 세팅]
docker compose up -d # docker-compose.yml 공식 제공
# → http://localhost:3000 에서 즉시 사용
# → 데이터 완전 소유, 비용 없음
실전 2 — AgentOps: 타임트래블 디버깅
# pip install agentops anthropic
import agentops
import anthropic
from agentops import record_tool, record_action
# AgentOps 초기화
agentops.init(
api_key="your-agentops-key",
# 태그로 세션 분류
tags=["production", "research-agent", "v2.1"],
# 비용 추적
default_tags={"environment": "prod"}
)
client = anthropic.Anthropic()
# ── 자동 계측 — OpenAI/Anthropic SDK 자동 감지 ────────
# AgentOps는 SDK를 자동으로 패치해서 모든 LLM 호출 캡처
# 별도 데코레이터 없이도 기본 동작
def research_agent(query: str) -> str:
# 세션 시작
session = agentops.start_session(tags=["research"])
try:
# LLM 호출 — 자동으로 AgentOps에 기록됨
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
messages=[{"role": "user", "content": query}]
)
plan = response.content[0].text
# ── 툴 실행 추적 ──────────────────────────────
@record_tool("web_search")
def search_web(q: str) -> dict:
return actual_web_search(q)
search_result = search_web(query)
@record_tool("database_query")
def query_db(sql: str) -> list:
return actual_db_query(sql)
db_result = query_db(f"SELECT * FROM reports WHERE topic='{query}'")
# ── 커스텀 액션 기록 ──────────────────────────
@record_action("data_synthesis")
def synthesize(plan: str, web: dict, db: list) -> str:
final_response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=2048,
messages=[
{"role": "user", "content": query},
{"role": "assistant", "content": plan},
{"role": "user",
"content": f"웹: {web}\nDB: {db[:5]}"}
]
)
return final_response.content[0].text
result = synthesize(plan, search_result, db_result)
# 세션 성공 종료
session.end(end_state="Success")
return result
except Exception as e:
# 세션 실패 기록 + 에러 컨텍스트
session.end(
end_state="Fail",
end_state_reason=str(e)
)
raise
result = research_agent("2026년 AI 시장 분석")
# ── AgentOps 타임트래블 디버깅 ────────────────────────
# 특정 세션을 과거 상태로 되돌려서 재실행
import agentops
# 1. 실패한 세션 ID 확인 (대시보드에서)
failed_session_id = "sess-abc123"
# 2. 해당 세션 상태 로드
session_replay = agentops.load_session(failed_session_id)
# 3. 특정 스텝으로 되감기
# "web_search 이후, database_query 이전" 시점으로
checkpoint = session_replay.rewind_to_tool("web_search")
# 4. 그 시점부터 수정된 코드로 재실행
# → 이전 LLM 호출 결과 재사용 (비용 없이)
# → 실패한 툴만 다시 실행
result = checkpoint.replay_from_here(
modified_tool_fn=improved_database_query # 개선된 DB 쿼리 함수
)
# 5. 원래 실행 vs 재실행 비교
session_replay.compare_outputs(original=failed_session_id,
replay=result.session_id)
# ── AgentOps 프레임워크 통합 (LangGraph) ──────────────
import agentops
from langgraph.graph import StateGraph
from typing import TypedDict, Annotated
import operator
agentops.init(api_key="your-key", tags=["langgraph"])
class AgentState(TypedDict):
messages: Annotated[list, operator.add]
task: str
# LangGraph 노드 — AgentOps 자동 계측
def planning_node(state: AgentState) -> dict:
# 이 함수 안의 모든 LLM 호출 자동 추적
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
messages=[{"role": "user", "content": state["task"]}]
)
return {"messages": [response.content[0].text]}
def execution_node(state: AgentState) -> dict:
# 툴 호출도 자동 추적
result = execute_tools(state["messages"][-1])
return {"messages": [result]}
workflow = StateGraph(AgentState)
workflow.add_node("planning", planning_node)
workflow.add_node("execution", execution_node)
workflow.set_entry_point("planning")
workflow.add_edge("planning", "execution")
app = workflow.compile()
# AgentOps가 전체 그래프 실행 자동 추적
result = app.invoke({"task": "분석 태스크", "messages": []})
agentops.end_session("Success")
[AgentOps 대시보드에서 볼 수 있는 것]
→ 세션별 전체 실행 타임라인
→ 툴 호출 순서 + 각 툴의 입력/출력
→ LLM 호출 트리 (어느 LLM이 어느 툴을 트리거했나)
→ 실패 세션 목록 + 실패 원인 분류
→ 타임트래블: 특정 스텝으로 되감아서 재실행
→ 세션 리플레이: 영상처럼 실행 과정 재생
→ 비용: 세션별, 툴별, LLM별 토큰 소비
→ 400+ 프레임워크 자동 지원 (CrewAI, LangGraph, OpenAI Agents SDK 등)
실전 3 — Braintrust: Eval 퍼스트 디버깅
# pip install braintrust autoevals anthropic
import braintrust
from autoevals import LLMClassifier, Factuality
import anthropic
client = anthropic.Anthropic()
# ── 기본 Eval 설정 ────────────────────────────────────
def run_agent(input_data: dict) -> str:
"""평가할 에이전트 함수"""
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=2048,
messages=[{
"role": "user",
"content": input_data["question"]
}]
)
return response.content[0].text
# 테스트 데이터셋
test_cases = [
{
"input": {"question": "파이썬 리스트 컴프리헨션이란?"},
"expected": "리스트를 간결하게 생성하는 문법"
},
{
"input": {"question": "FastAPI와 Django의 차이는?"},
"expected": "FastAPI는 비동기 API 특화, Django는 풀스택 프레임워크"
},
{
"input": {"question": "Docker 컨테이너란?"},
"expected": "앱과 의존성을 격리된 환경에서 실행하는 기술"
},
]
# ── Braintrust Eval 실행 ──────────────────────────────
braintrust.Eval(
name="Tech-QA-Agent",
data=lambda: [
{
"input": case["input"],
"expected": case["expected"],
}
for case in test_cases
],
task=run_agent,
scores=[
# 자동 품질 평가 (LLM-as-Judge)
Factuality,
# 커스텀 스코어 — 길이 적절성
lambda output, expected: {
"name": "length_appropriate",
"score": 1.0 if 50 < len(output) < 500 else 0.5,
},
],
project_name="tech-agent", # Braintrust 프로젝트
api_key="your-braintrust-key",
)
# ── 프로덕션 트레이스 → Eval 데이터셋 변환 ────────────
# 실패한 프로덕션 트레이스를 자동으로 테스트 케이스로
import braintrust
bt_client = braintrust.Client(api_key="your-key")
project = bt_client.get_project("tech-agent")
# 프로덕션에서 실패한 케이스 가져오기
dataset = project.get_dataset("failed-cases-2026-05")
# 새 테스트 케이스 추가 (프로덕션 실패 → 회귀 테스트)
dataset.insert(
input={"question": "실제 실패한 쿼리"},
expected="올바른 답변",
metadata={
"source": "production",
"session_id": "sess-failed-123",
"failure_date": "2026-05-21"
}
)
# ── CI/CD 게이트 설정 ────────────────────────────────
# GitHub Actions에서 자동 실행
# .github/workflows/eval.yml:
# - run: python eval.py
# - 스코어 80% 미만이면 PR 차단
def ci_eval():
"""CI에서 실행되는 평가 — 회귀 방지"""
result = braintrust.Eval(
name="CI-Regression-Check",
data=lambda: list(dataset.fetch()),
task=run_agent,
scores=[Factuality],
api_key="your-key",
)
# 품질 기준 미달 시 실패
avg_score = sum(r.scores["Factuality"] for r in result) / len(result)
if avg_score < 0.8:
raise Exception(
f"품질 기준 미달: {avg_score:.2%} < 80%\n"
"프롬프트 변경이 회귀를 일으켰습니다."
)
print(f"✅ 품질 검증 통과: {avg_score:.2%}")
ci_eval()
# ── Braintrust 트레이싱 (에이전트 실행 중) ────────────
from braintrust import traced, init_logger
logger = init_logger(
project="tech-agent",
api_key="your-key"
)
@traced # 자동 스팬 생성
def agent_step(step_name: str, input_data: dict) -> str:
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
messages=[{"role": "user", "content": str(input_data)}]
)
return response.content[0].text
# 프로덕션 트레이스 → 나중에 Eval 데이터셋으로 클릭 한 번에 변환
with logger.span("production-run") as span:
result = agent_step("planning", {"task": "분석"})
span.log(input={"task": "분석"}, output=result)
# Braintrust 대시보드에서 "Add to Dataset" 버튼 클릭 → 자동 테스트 케이스 생성
실전 4 — 실무 디버깅 패턴
패턴 1: 멀티에이전트 실패 추적
# 여러 에이전트 간 의존성에서 실패 추적
from langfuse import Langfuse
langfuse = Langfuse()
def orchestrator_agent(task: str) -> str:
# 오케스트레이터 트레이스 시작
trace = langfuse.trace(
name="orchestrator",
input={"task": task},
tags=["multi-agent"]
)
# 서브에이전트 A 실행
span_a = trace.span(name="subagent-research")
result_a = research_subagent(task, parent_trace_id=trace.id)
span_a.end(output={"summary": result_a[:200]})
# 서브에이전트 B 실행 (A 결과 기반)
span_b = trace.span(name="subagent-writer")
try:
result_b = writer_subagent(result_a, parent_trace_id=trace.id)
span_b.end(output={"content": result_b[:200]})
except Exception as e:
# 실패 위치 정확히 기록
span_b.end(
output={"error": str(e)},
level="ERROR",
status_message=f"Writer 실패: {e}"
)
# 오케스트레이터 트레이스에도 실패 전파 기록
trace.update(level="WARNING",
status_message="서브에이전트 B 실패, 폴백 실행")
result_b = fallback_writer(result_a)
trace.update(output={"final": result_b})
return result_b
패턴 2: 비결정론적 실패 재현
# 같은 입력인데 가끔만 실패하는 에이전트 디버깅
import agentops
import random
agentops.init(api_key="your-key")
def flaky_agent(task: str, attempt_id: str) -> str:
session = agentops.start_session(tags=[f"attempt-{attempt_id}"])
# 시드 고정으로 재현 가능성 확보
random.seed(42)
try:
result = agent_logic(task)
session.end("Success")
return result
except Exception as e:
# 실패 시 전체 상태 스냅샷 저장
session.end(
end_state="Fail",
end_state_reason=str(e),
# AgentOps가 이 세션의 전체 컨텍스트 저장
# → 나중에 타임트래블로 정확히 동일 지점 재실행 가능
)
raise
# 100번 실행해서 실패 패턴 수집
failures = []
for i in range(100):
try:
flaky_agent("복잡한 태스크", attempt_id=str(i))
except Exception as e:
failures.append(i)
print(f"실패율: {len(failures)/100:.0%}")
# AgentOps 대시보드: 실패 세션만 필터링 → 공통 패턴 분석
패턴 3: 프롬프트 변경 전후 품질 비교
# Braintrust로 A/B 프롬프트 테스트
import braintrust
OLD_SYSTEM = "당신은 AI 어시스턴트입니다."
NEW_SYSTEM = "당신은 10년 경력의 시니어 개발자입니다. 구체적인 코드 예시와 함께 답변합니다."
def run_with_prompt(system_prompt: str, question: str) -> str:
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=2048,
system=system_prompt,
messages=[{"role": "user", "content": question}]
)
return response.content[0].text
# 구버전 vs 신버전 동시 평가
for version, system in [("old", OLD_SYSTEM), ("new", NEW_SYSTEM)]:
braintrust.Eval(
name=f"Prompt-AB-Test-{version}",
data=lambda: test_cases,
task=lambda x: run_with_prompt(system, x["question"]),
scores=[Factuality],
api_key="your-key",
experiment_name=f"prompt-v{version}-2026-05-21"
)
# Braintrust 대시보드에서 두 실험 나란히 비교
# → 어느 케이스에서 개선됐고, 어느 케이스에서 나빠졌는지
도구 선택 가이드
[상황별 최적 선택 — 2026년 5월 기준]
무료·셀프호스팅 원함:
→ Langfuse (MIT 라이선스, Docker 10분 설치)
→ Clickhouse 인수로 쿼리 성능 향상 예정
멀티에이전트 디버깅 (타임트래블):
→ AgentOps (400+ 프레임워크 자동 계측, 세션 리플레이)
CI/CD 품질 게이트·회귀 테스트:
→ Braintrust (무료 100만 스팬/월, 평가 가장 강력)
LangChain/LangGraph 팀:
→ LangSmith (에이전트 IDE + 그래프 시각화 최강)
성능 오버헤드 최소화:
→ Laminar (~5%) 또는 LangSmith
[스타트업 권장 스택]
→ Langfuse 셀프호스팅 (무료) + AgentOps 무료 티어
→ 월 $0 — 핵심 기능 모두 커버
[성장 단계 (월 10K+ 세션)]
→ Braintrust 무료 (100만 스팬) + Langfuse + AgentOps
→ 트레이싱 + 평가 + 디버깅 삼각편대
[프로덕션 스케일]
→ Braintrust Pro ($249/월) + AgentOps + Datadog
→ 품질 알림 + 회귀 테스트 + 인프라 연동
[오버헤드 비교 — 프로덕션 고려사항]
Laminar: ~5% (성능 최우선)
AgentOps: ~12% (에이전트 디버깅 특화)
Langfuse: ~15% (오픈소스, 기능 풍부)
LangSmith: ~5% (LangChain 생태계 최적화)
→ 100ms 응답 API: 15% 오버헤드 = +15ms
→ 대부분의 에이전트 (수초~수분): 무시 가능한 수준
→ 초저지연 요구 환경에서만 고민
마무리
✅ 지금 당장 시작하는 법
→ Langfuse: docker compose up -d → 5분 셋업 → @observe 데코레이터 추가
→ AgentOps: pip install agentops → agentops.init() 한 줄 → 자동 계측
→ Braintrust: 첫 Eval 5분 → 실패한 케이스 → 데이터셋 → CI 게이트
✅ 디버깅 원칙
→ "에러 로그"가 아니라 "실행 트리"를 봐야 한다
→ 어느 LLM 호출이 잘못된 판단을 했는지 추적
→ 실패 케이스는 즉시 회귀 테스트 케이스로 등록
→ 비용 + 품질 동시에 모니터링
❌ 흔한 실수
→ print() 디버깅 — 비결정론적 에이전트에서 패턴 못 잡음
→ 단일 도구에 의존 — 트레이싱·평가·비용 각각 최적 도구 조합
→ 프로덕션 실패를 일회성으로 처리 — 반드시 회귀 테스트 추가
관련 글
반응형
'AI Agent' 카테고리의 다른 글
| Eval-Driven Development 완전 가이드 — AI 에이전트를 TDD처럼 개발하는 법 (0) | 2026.05.21 |
|---|---|
| AI 에이전트 메모리 관리 실전 — 세션 간 상태 유지, 컨텍스트 압축, 레포 재탐색 방지 (0) | 2026.05.21 |
| AI 에이전트 프로덕션 비용 폭탄 — 왜 LLM 청구서가 예상의 10배 나오나 (0) | 2026.05.21 |
| AI 에이전트 보안 완전 가이드 — Double Agent 공격, 에이전트가 내부 위협이 되는 순간 (0) | 2026.05.19 |
| LLMWiki 완전 가이드 — Karpathy가 제안한 AI가 스스로 관리하는 지식 베이스 (0) | 2026.05.19 |