Appearance
第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 的好处:
- 统一协议 — 同一套采集器可以同时收集 Agent Trace、HTTP 请求 Trace、数据库查询 Trace
- 生态丰富 — Jaeger、Grafana Tempo、Datadog 等都支持 OTel 数据
- 标准化属性 —
gen_ai.system、gen_ai.request.model、gen_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 可观测性让黑盒变成灰盒:
- 三大支柱——日志记录事件,Trace 还原链路,指标驱动告警
- Trace 是核心——树状结构可视化整个决策过程
- 工具支持——LangSmith/LangFuse 提供生产级方案
- 调试策略——重放、对比、渐进式缩小范围
- 持续监控——告警和仪表盘确保问题被及时发现
下一章讨论 Agent 系统的成本控制与性能优化。