AI Agent

AI 에이전트 디버깅 실전 — Langfuse·AgentOps·Braintrust 언제 뭘 쓰나

cell-devlog 2026. 5. 21. 09:39
반응형

에이전트가 실패했습니다. 로그를 열었더니 "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() 디버깅 — 비결정론적 에이전트에서 패턴 못 잡음
→ 단일 도구에 의존 — 트레이싱·평가·비용 각각 최적 도구 조합
→ 프로덕션 실패를 일회성으로 처리 — 반드시 회귀 테스트 추가

관련 글


 

반응형