Skip to content

第19章 可观测性与调试

"You can't debug what you can't see."

本章要点

  • Agent 可观测性的三大支柱:日志(Logging)、追踪(Tracing)、指标(Metrics)
  • Trace 是 Agent 调试的核心——还原从用户输入到最终输出的完整决策链路
  • LangSmith/LangFuse 等工具提供了可视化的 Agent Trace 查看器
  • 生产环境必须监控的指标:成功率、延迟、token 消耗、错误率

19.1 为什么 Agent 需要专门的可观测性

传统 Web 服务的可观测性关注请求延迟、错误率、吞吐量。Agent 系统有本质不同:

  • 决策链路长——一个用户请求可能触发 20+ 次工具调用和多次 LLM 交互
  • 行为非确定性——同样的输入可能产生不同的执行路径
  • 失败模式复杂——不只是"报错了",还有"做错了但没报错"
  • 成本可变——一个请求可能消耗 1K token 或 100K token

19.2 三大支柱

日志(Logging)

结构化日志记录每个关键事件:

typescript
// Agent 循环中的关键日志点
logger.info('agent.loop.start', {
  sessionId,
  userMessage: truncate(message, 200),
  contextTokens: countTokens(messages),
})

logger.info('agent.tool.call', {
  sessionId,
  tool: toolName,
  params: sanitizeParams(params),  // 去除敏感信息
  iteration: loopIteration,
})

logger.info('agent.tool.result', {
  sessionId,
  tool: toolName,
  success: !error,
  durationMs,
  resultTokens: countTokens(result),
})

logger.info('agent.loop.end', {
  sessionId,
  iterations: totalIterations,
  totalTokens,
  totalDurationMs,
  toolsCalled: toolCallSummary,
})

追踪(Tracing)

Trace 把一次完整的 Agent 交互组织成树状结构:

一眼就能看出:Agent 在第一次修改后测试失败(红色),自动修复后成功(绿色)。这种可视化对调试至关重要。

Trace 的数据模型

一个 Trace 由嵌套的 Span 组成。每个 Span 记录一个操作单元:

typescript
interface Span {
  id: string
  parentId?: string      // 父 Span(构成树结构)
  name: string           // "llm.call" | "tool.execute" | "agent.task"
  startTime: number
  endTime?: number
  metadata: {
    model?: string       // LLM 调用时记录模型名
    tool?: string        // 工具调用时记录工具名
    inputTokens?: number
    outputTokens?: number
    error?: string       // 失败时的错误信息
    [key: string]: any
  }
  children: Span[]
}

Span 的层级关系自然映射了 Agent 的执行结构:

  • 顶层 Span = 整个任务
  • 二级 Span = 每次 LLM 调用
  • 三级 Span = LLM 调用中的工具执行

OpenTelemetry 集成

越来越多的 Agent 框架开始支持 OpenTelemetry (OTel) 协议——这是可观测性领域的事实标准。使用 OTel 的好处:

  1. 统一协议 — 同一套采集器可以同时收集 Agent Trace、HTTP 请求 Trace、数据库查询 Trace
  2. 生态丰富 — Jaeger、Grafana Tempo、Datadog 等都支持 OTel 数据
  3. 标准化属性gen_ai.systemgen_ai.request.modelgen_ai.usage.input_tokens 等已有标准定义
python
# OpenTelemetry 集成示例
from opentelemetry import trace

tracer = trace.get_tracer("agent-service")

with tracer.start_as_current_span("agent.task") as span:
    span.set_attribute("task.description", user_message)

    with tracer.start_as_current_span("llm.call") as llm_span:
        response = await llm.chat(messages)
        llm_span.set_attribute("gen_ai.request.model", "claude-opus-4-6")
        llm_span.set_attribute("gen_ai.usage.input_tokens", response.usage.input)

    with tracer.start_as_current_span("tool.execute") as tool_span:
        tool_span.set_attribute("tool.name", "Read")
        result = await read_tool.execute(params)

指标(Metrics)

聚合的数值指标用于监控和告警:

typescript
// 关键指标
const METRICS = {
  // 质量
  'agent.task.success_rate': Gauge,        // 任务成功率
  'agent.task.user_satisfaction': Gauge,   // 用户满意度

  // 性能
  'agent.task.duration_ms': Histogram,     // 任务耗时分布
  'agent.llm.ttft_ms': Histogram,          // 首 token 延迟
  'agent.tool.duration_ms': Histogram,     // 工具执行耗时

  // 成本
  'agent.tokens.input': Counter,           // 输入 token 总量
  'agent.tokens.output': Counter,          // 输出 token 总量
  'agent.llm.calls': Counter,              // LLM 调用次数
  'agent.tool.calls': Counter,             // 工具调用次数

  // 错误
  'agent.tool.errors': Counter,            // 工具错误次数
  'agent.llm.errors': Counter,             // LLM 错误次数
  'agent.task.timeout': Counter,           // 任务超时次数
}

19.3 实现 Trace

一个最小的 Trace 实现:

typescript
class Trace {
  private spans: Span[] = []
  private currentSpan: Span | null = null

  startSpan(name: string, metadata?: Record<string, any>): Span {
    const span: Span = {
      id: crypto.randomUUID(),
      parentId: this.currentSpan?.id,
      name,
      startTime: Date.now(),
      metadata: metadata || {},
      children: [],
    }
    if (this.currentSpan) {
      this.currentSpan.children.push(span)
    } else {
      this.spans.push(span)
    }
    this.currentSpan = span
    return span
  }

  endSpan(result?: Record<string, any>): void {
    if (!this.currentSpan) return
    this.currentSpan.endTime = Date.now()
    this.currentSpan.duration = this.currentSpan.endTime - this.currentSpan.startTime
    if (result) Object.assign(this.currentSpan.metadata, result)
    // 回到父 span
    this.currentSpan = this.findParent(this.currentSpan.parentId)
  }

  toJSON(): object {
    return { spans: this.spans, totalDuration: this.getTotalDuration() }
  }
}

// 使用
const trace = new Trace()

trace.startSpan('agent.task', { task: userMessage })
  trace.startSpan('llm.call', { model: 'claude-opus-4-6' })
  trace.endSpan({ tokens: 5000 })

  trace.startSpan('tool.execute', { tool: 'Read' })
  trace.endSpan({ success: true })
trace.endSpan({ result: 'success' })

19.4 LangSmith / LangFuse

生产级的 Agent 可观测性平台:

LangSmith(LangChain 官方):

  • 自动捕获 LangChain/LangGraph 的每一步
  • 可视化 Trace 树
  • 支持评估和人工标注
  • 对比不同版本的 Agent 表现

LangFuse(开源替代):

  • 兼容 OpenTelemetry 协议
  • 自托管或 Cloud
  • 支持 cost tracking 和 user feedback
python
# LangFuse 集成示例
from langfuse import Langfuse

langfuse = Langfuse()

trace = langfuse.trace(name="fix-login-bug", user_id="user-123")

# LLM 调用
generation = trace.generation(
    name="analyze-code",
    model="claude-opus-4-6",
    input=messages,
    output=response,
    usage={"input": 5000, "output": 2000},
)

# 工具调用
span = trace.span(name="read-file", input={"path": "src/auth.ts"})
span.end(output={"lines": 150, "success": True})

19.5 调试策略

重放调试

保存完整的 Trace 后,可以在本地重放 Agent 的决策过程:

typescript
// 加载历史 Trace
const trace = await loadTrace(traceId)

// 查看每一步的输入和输出
for (const span of trace.spans) {
  console.log(`Step: ${span.name}`)
  console.log(`Input: ${JSON.stringify(span.metadata.input)}`)
  console.log(`Output: ${JSON.stringify(span.metadata.output)}`)
  console.log(`Duration: ${span.duration}ms`)
  console.log('---')
}

对比调试

同一任务运行两次,对比 Trace 差异:

Trace A (成功):                    Trace B (失败):
├── Read src/auth.ts               ├── Read src/auth.ts
├── Read src/types.ts              ├── Edit src/auth.ts  ← 没读 types.ts
├── Edit src/auth.ts               │   (修改不完整)
├── npm test (pass)                ├── npm test (fail)
└── Done                           └── 放弃

一眼看出问题:Trace B 跳过了读取 types.ts,导致修改不完整。

渐进式调试

当 Agent 行为异常时,逐步缩小问题范围:

1. 查看 Trace 概览 → 在第 3 步出了问题
2. 查看第 3 步的 LLM 输入 → system prompt + 前两步结果
3. 查看第 3 步的 LLM 输出 → 模型选择了错误的工具
4. 分析原因 → 第 2 步的工具结果太长,关键信息被截断
5. 修复 → 改进工具结果的截断策略

19.6 告警与仪表盘

生产环境需要自动告警:

yaml
alerts:
  - name: agent-success-rate-drop
    condition: agent.task.success_rate < 0.85
    for: 10m
    severity: warning
    message: "Agent 任务成功率低于 85%"

  - name: agent-token-spike
    condition: rate(agent.tokens.input[5m]) > 1000000
    severity: critical
    message: "Token 消耗速率异常,可能存在无限循环"

  - name: agent-error-rate
    condition: rate(agent.tool.errors[5m]) / rate(agent.tool.calls[5m]) > 0.1
    severity: warning
    message: "工具错误率超过 10%"

仪表盘应该展示:

  • 实时任务成功率和趋势
  • Token 消耗的分布(P50/P95/P99)
  • 最常调用的工具和最常失败的工具
  • 平均任务耗时和工具调用次数

19.7 本章小结

Agent 可观测性让黑盒变成灰盒:

  1. 三大支柱——日志记录事件,Trace 还原链路,指标驱动告警
  2. Trace 是核心——树状结构可视化整个决策过程
  3. 工具支持——LangSmith/LangFuse 提供生产级方案
  4. 调试策略——重放、对比、渐进式缩小范围
  5. 持续监控——告警和仪表盘确保问题被及时发现

下一章讨论 Agent 系统的成本控制与性能优化。

基于 VitePress 构建