《Harness Engineering --- AI Agent 工程方法论》完整目录
- 前言
- 第1章 Agent 不等于大模型:Harness 的价值
- 第2章 Agent 架构模式全景
- 第3章 Agent Loop:心跳与决策循环
- 第4章 上下文工程:比 Prompt Engineering 更重要的事
- 第5章 Tool Design:给 Agent 造趁手的兵器
- 第6章 工具编排与并发执行
- 第7章 工具结果处理与错误恢复
- 第8章 System Prompt 分层设计
- 第9章 指令优先级与冲突消解
- 第10章 Few-shot、CoT 与动态提示策略
- 第11章 短期记忆:上下文窗口管理
- 第12章 长期记忆:持久化与检索
- 第13章 多轮对话与会话状态机
- 第14章 Agent 权限模型设计
- 第15章 沙箱、隔离与防御性编程
- 第16章 多 Agent 协调模式
- 第17章 Human-in-the-Loop:人机协作设计
- 第18章 评估与测试方法论
- 第19章 可观测性与调试(当前)
- 第20章 成本控制与性能优化
- 第21章 设计模式与架构决策
第19章 可观测性与调试
"You can't debug what you can't see."
:::tip 本章要点
- Agent 可观测性的三大支柱:日志(Logging)、追踪(Tracing)、指标(Metrics)
- Trace 是 Agent 调试的核心------还原从用户输入到最终输出的完整决策链路
- LangSmith/LangFuse 等工具提供了可视化的 Agent Trace 查看器
- 生产环境必须监控的指标:成功率、延迟、token 消耗、错误率 :::
19.1 为什么 Agent 需要专门的可观测性
传统 Web 服务的可观测性关注请求延迟、错误率、吞吐量。Agent 系统有本质不同:
- 决策链路长------一个用户请求可能触发 20+ 次工具调用和多次 LLM 交互
- 行为非确定性------同样的输入可能产生不同的执行路径
- 失败模式复杂------不只是"报错了",还有"做错了但没报错"
- 成本可变------一个请求可能消耗 1K token 或 100K token
graph TD
subgraph Pillars["可观测性三大支柱"]
L["📝 Logging\n记录每个事件"] --> T["🔗 Tracing\n还原决策链路"]
T --> M["📊 Metrics\n聚合指标告警"]
end
L -->|"事件流"| Store["日志存储"]
T -->|"Trace 树"| Viz["可视化查看器\nLangSmith / LangFuse"]
M -->|"时间序列"| Alert["仪表盘 + 告警"]
style L fill:#dbeafe,stroke:#3b82f6
style T fill:#fef3c7,stroke:#f59e0b
style M fill:#dcfce7,stroke:#22c55e
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 交互组织成树状结构:
flowchart TD
Root["Trace: 修复登录 bug\n总耗时 45s | 85K tokens"]
Root --> L1["LLM Call #1 (3s, 2K t)\n决定先读代码"]
L1 --> T1a["Tool: Read auth.ts\n0.1s"]
L1 --> T1b["Tool: Read auth.test.ts\n0.1s"]
Root --> L2["LLM Call #2 (5s, 8K t)\n分析并修改"]
L2 --> T2a["Tool: Edit auth.ts\n0.2s"]
L2 --> T2b["Tool: Bash npm test\n12s ❌ 失败"]
Root --> L3["LLM Call #3 (4s, 6K t)\n修复测试失败"]
L3 --> T3a["Tool: Edit auth.ts\n0.2s"]
Root --> L4["LLM Call #4 (2s, 3K t)\n再次验证"]
L4 --> T4a["Tool: Bash npm test\n15s ✅ 通过"]
Root --> L5["LLM Call #5 (2s, 1K t)\n生成总结"]
style T2b fill:#fee2e2,stroke:#ef4444
style T4a fill:#dcfce7,stroke:#22c55e
style Root fill:#dbeafe,stroke:#3b82f6,stroke-width:2px
一眼就能看出: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 差异:
bash
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 行为异常时,逐步缩小问题范围:
markdown
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 系统的成本控制与性能优化。