Harness Engineering
第19章 可观测性与调试:把黑盒变成玻璃盒
第19章 可观测性与调试:把黑盒变成玻璃盒
“You can’t debug what you can’t see.” — 所有在生产环境调过 Agent 的人都懂
本章要点
- 理解 Agent 可观测性和传统 Web 服务可观测性的根本差异:决策链长、分支多、成本方差大
- 掌握 MELT 四类遥测数据:Metrics(指标)、Events(事件)、Logs(日志)、Traces(链路)
- 读懂 Trace 的树状 Span 模型:顶层任务 → LLM call → tool execute,嵌套展开
- 掌握 OpenTelemetry GenAI 标准属性(
gen_ai.*):写一次、各平台通吃 - 对比 LangSmith / LangFuse / Langtrace 三大主流 Agent 观测平台,选型有据
- 学会三大调试技巧:Trace 重放(reproduce)、差分调试(diff two traces)、时光回溯(travel back)
- 理解 Token 级成本追踪的三个粒度:per-task / per-user / per-tenant
- 拿到 10 条生产告警规则模板,以及仪表盘四大必备视图
- 避开四类观测反模式:采样过度、PII 泄漏、trace 爆炸、成本黑洞
19.1 为什么 Agent 需要专门的可观测性
传统 Web 服务的可观测性工具(Datadog、New Relic、Prometheus)设计时针对的是”请求-响应”式的同步服务:
- 一次请求 = 一个 HTTP 调用 + 几次 DB 查询 + 几次下游服务调用
- 总链路深度 2-5 层
- 延迟以毫秒-秒计
- 失败模式明确(HTTP 5xx、超时、DB 错误)
Agent 系统完全是另一种生物:
graph TB
subgraph "传统服务"
HTTP[HTTP Request] --> Handler[Handler]
Handler --> DB1[DB Query]
Handler --> Svc1[Service Call]
DB1 --> Result[Response]
Svc1 --> Result
end
subgraph "Agent 服务"
UMsg[User Message] --> Loop[Agent Loop]
Loop --> LLM1[LLM Call 1]
LLM1 --> T1[Tool: Read]
LLM1 --> T2[Tool: Grep]
Loop --> LLM2[LLM Call 2]
LLM2 --> T3[Tool: Edit]
LLM2 --> T4[Tool: Bash]
T4 --> LLM3[LLM Call 3 循环修复]
LLM3 --> T5[Tool: Edit]
Loop --> LLM4[LLM Call 4 总结]
LLM4 --> Final[Response]
end
style Loop fill:#f59e0b,color:#fff,stroke:none
style T4 fill:#ef4444,color:#fff,stroke:none
4 个关键区别:
| 维度 | 传统服务 | Agent 服务 |
|---|---|---|
| 链路深度 | 2-5 层 | 10-50 层嵌套(Loop × LLM × Tool) |
| 执行时长 | 100ms-1s | 10s-10min |
| 分支 | 确定路径 | 非确定,同样输入可能走完全不同的路径 |
| 成本方差 | 请求成本相近 | 单次请求 10+ 的跨度 |
| 失败模式 | HTTP 错误码清晰 | ”做错了但没报错”常见(silent failure) |
这些差异意味着传统 APM 工具在 Agent 场景下根本用不明白——你在 Datadog 的 flame graph 里看到一个 45 秒的请求,点进去只有几个函数调用,中间几十次 LLM / Tool 调用全部消失在黑盒里。
Agent 需要专门的可观测性。
19.2 MELT:四类遥测数据
现代可观测性的共识是把遥测数据分成四类,简称 MELT:
graph TB
M["📊 Metrics<br/>数值型时间序列<br/>用途:告警 + 仪表盘"]
E["📝 Events<br/>离散业务事件<br/>用途:用户行为分析"]
L["📄 Logs<br/>结构化 / 非结构化文本<br/>用途:故障根因 + 审计"]
T["🔗 Traces<br/>树状嵌套 span<br/>用途:链路重建 + 调试"]
M -.-> Alerting[Alertmanager]
E -.-> Analytics[分析平台]
L -.-> SIEM[日志存储]
T -.-> APM[Trace 可视化]
style M fill:#10b981,color:#fff,stroke:none
style E fill:#3b82f6,color:#fff,stroke:none
style L fill:#f59e0b,color:#fff,stroke:none
style T fill:#8b5cf6,color:#fff,stroke:none
Agent 场景下四类数据的侧重:
| 数据类型 | 传统服务 | Agent 服务 |
|---|---|---|
| Metrics | 延迟 / 错误率 / QPS | 成功率 / token 消耗率 / tool error rate |
| Events | 用户注册、支付 | 用户反馈 thumbs up/down、中断、重试 |
| Logs | 错误堆栈 | LLM prompt/response 全文、tool 参数与结果 |
| Traces | RPC 调用链 | Agent loop → LLM → Tool 嵌套链 |
Trace 是 Agent 最重要的一类——没有 Trace 就等于闭眼开车。下面深入。
19.2.1 来自 Claude Code 的真实样本:src/services/analytics/ 四千行
打开 src/services/analytics/index.ts:133-163——生产级 Agent 的 telemetry 入口就 30 行:
export function logEvent(eventName: string, metadata: LogEventMetadata): void {
if (sink === null) {
eventQueue.push({ eventName, metadata, async: false })
return
}
sink.logEvent(eventName, metadata)
}
三个生产化的决策值得抄走——
- 启动前的事件 queue——sink 还没 attach 时不丢事件、先压队列、等
attachAnalyticsSink调用时通过queueMicrotask异步 drain——zero startup latency LogEventMetadata = { [key: string]: boolean | number | undefined }——故意不允许 string——避免 Agent 作者一不小心把代码片段/文件路径记成 metadata——类型系统当防线- Idempotent attach——
if (sink !== null) return——allows 多入口(主命令 + 子命令 preAction hook)各自调用、不协调也不会重复初始化
4040 行代码(analytics/*.ts 总行数)撑起 Claude Code 全部 telemetry——学这套结构、比从零造轮子快一个数量级。
19.3 Trace 的树状 Span 模型
Trace 的核心数据结构是嵌套 Span——一棵树:
graph TB
Root["Task Span: 修复 login bug<br/>duration: 45s | tokens: 85K | cost: $1.28"]
Root --> L1["llm.call #1<br/>duration: 3s<br/>input: 2K | output: 500<br/>model: claude-opus-4.6"]
L1 --> T1a["tool.execute: Read<br/>duration: 0.1s<br/>file: auth.ts"]
L1 --> T1b["tool.execute: Read<br/>duration: 0.1s<br/>file: auth.test.ts"]
Root --> L2["llm.call #2<br/>duration: 5s<br/>input: 8K | output: 1.2K"]
L2 --> T2a["tool.execute: Edit<br/>duration: 0.2s<br/>file: auth.ts"]
L2 --> T2b["tool.execute: Bash<br/>duration: 12s<br/>command: npm test<br/>❌ exit_code: 1"]
Root --> L3["llm.call #3<br/>duration: 4s<br/>input: 6K | output: 800<br/>(循环修复失败的测试)"]
L3 --> T3a["tool.execute: Edit<br/>duration: 0.2s"]
Root --> L4["llm.call #4<br/>duration: 2s<br/>input: 3K | output: 400"]
L4 --> T4a["tool.execute: Bash<br/>duration: 15s<br/>command: npm test<br/>✅ exit_code: 0"]
Root --> L5["llm.call #5 总结<br/>duration: 2s<br/>input: 1K | output: 300"]
style Root fill:#3b82f6,color:#fff,stroke:none
style T2b fill:#ef4444,color:#fff,stroke:none
style T4a fill:#10b981,color:#fff,stroke:none
看这张图一眼就能诊断:
- 第一次测试红色(12s npm test 失败)
- 触发了 llm.call #3 循环修复
- 第二次测试绿色(15s npm test 成功)
- 总耗时 45s,85K tokens,$1.28
这种时序 + 层级的展示,是调试 Agent 的”瑞士军刀”。没有它你只能盯着日志文件 grep。
19.3.1 Span 的最小数据模型
interface Span {
// 标识
span_id: string // 全局唯一
parent_id: string | null // 父 span(顶层 trace 为 null)
trace_id: string // 同一 trace 下所有 span 共享
// 元信息
name: string // "agent.task" / "llm.call" / "tool.execute"
kind: SpanKind // INTERNAL / CLIENT / SERVER
start_time: number // 开始时间 (ms)
end_time: number | null // 结束时间,running 时为 null
// 状态
status: "ok" | "error" | "unset"
error?: { type: string, message: string, stack?: string }
// 结构化属性(key-value)
attributes: {
[key: string]: string | number | boolean
}
// 事件(时间点日志)
events: Array<{
timestamp: number
name: string
attributes?: Record<string, any>
}>
}
attributes 字段是 Span 的”货物舱”。Agent 场景下常见属性:
// LLM call
gen_ai.system: "anthropic"
gen_ai.request.model: "claude-opus-4-6"
gen_ai.usage.input_tokens: 2500
gen_ai.usage.output_tokens: 580
gen_ai.response.finish_reason: "stop"
// Tool execute
tool.name: "Read"
tool.input: "{\"file_path\":\"src/auth.ts\"}"
tool.output_size_bytes: 1520
tool.success: true
// Agent task
agent.user_id: "user-1234"
agent.session_id: "sess-abcd"
agent.task_type: "bug_fix"
19.3.2 Span 的生命周期
# 概念性伪代码
class Span:
@contextmanager
def span(self, name: str, **attributes):
child = Span(
span_id=uuid(),
parent_id=self.current_span_id,
trace_id=self.trace_id,
name=name,
start_time=now(),
attributes=attributes,
)
self.push(child)
try:
yield child
child.status = "ok"
except Exception as e:
child.status = "error"
child.error = {"type": type(e).__name__, "message": str(e)}
raise
finally:
child.end_time = now()
self.pop()
self.emit(child)
关键是 try/finally 保证 span 一定会关闭——即使中间抛异常,span 也会正确 emit(带着 error 属性)。
19.3.3 PII 标记类型:类型系统当”红线巡查员”
Claude Code 在 src/services/analytics/index.ts:13-33 定义了两个看似奇怪的 never 类型:
export type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS = never
export type AnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED = never
名字长到离谱——但这是故意的。
- 当工程师想把 string 塞进 telemetry、TS 编译器报错、必须显式
cast as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS - 这个 cast 会在 code review 里字面跳出来、reviewer 一眼看到”
I_VERIFIED”就被迫思考”这个字段真的不包含代码或文件路径吗?” - 如果包含、改用
_PROTO_*前缀的字段、进入PII-tagged 特权列、只有 1P exporter 能读、Datadog 看不到
这叫”把合规变成人人必须走的流程”——不是靠 wiki 文档、不是靠 code review checklist、是靠类型定义强制。
任何 Agent 产品都应该抄这一手——三行类型定义、减 90% PII 泄漏风险。
19.3.4 _PROTO_* 前缀约定与 stripProtoFields 防护
analytics/index.ts:46-59 的 stripProtoFields 函数在 Datadog fanout 前兜底剥离 _PROTO_* 键——单点过滤、不是按 sink 重复写一遍——减少遗漏。
export function stripProtoFields<V>(metadata: Record<string, V>): Record<string, V> {
let result: Record<string, V> | undefined
for (const key in metadata) {
if (key.startsWith('_PROTO_')) {
if (result === undefined) result = { ...metadata }
delete result[key]
}
}
return result ?? metadata
}
性能细节:没有 _PROTO_* 键时直接返回原对象(return result ?? metadata)、不浪费一次 clone——99% 的事件走 hot path、零分配。
这个实现值得抄到任何有”多 sink + 敏感字段分级”需求的项目里——命名+单点兜底+零开销 hot path 三合一。
19.4 OpenTelemetry GenAI 标准
2024 年 OpenTelemetry 社区发布了 GenAI 语义规范(semantic conventions),标准化了 LLM / Agent 场景下的 attribute 命名:
19.4.1 标准属性列表
# 所有 LLM call 都该带
gen_ai.system # "openai" / "anthropic" / "google" / ...
gen_ai.request.model # 请求的模型名
gen_ai.request.temperature # 采样温度
gen_ai.request.max_tokens # max_tokens 参数
gen_ai.request.top_p
gen_ai.usage.input_tokens # prompt 长度
gen_ai.usage.output_tokens # completion 长度
gen_ai.response.id # API 返回的 ID
gen_ai.response.model # 实际服务的模型(可能和 request 不同)
gen_ai.response.finish_reason # "stop" / "length" / "tool_calls"
# Tool call 相关
gen_ai.tool.name # 工具名
gen_ai.tool.call.id # tool_call_id
# Token 可选(对于敏感场景可以省略)
gen_ai.prompt # 完整 prompt(JSON 序列化的 messages 数组)
gen_ai.completion # 完整 completion
19.4.2 为什么要遵循标准
三个明显好处:
1. 跨平台搬家零成本 用 OTel 标准属性打的 trace,发给 LangSmith、LangFuse、Datadog、Grafana Tempo 都能正确识别。换后端不用改埋点代码。
2. 自动化分析 你可以直接写查询:
# 找出所有 token 使用率超过 80% 的 trace
gen_ai.usage.input_tokens > (gen_ai.request.max_tokens * 0.8)
# 找出最近 24h 单次 task 成本 TOP 10
SELECT task_id, SUM(gen_ai.usage.input_tokens * 0.003 + gen_ai.usage.output_tokens * 0.015) AS cost
FROM spans WHERE trace.name = 'agent.task' ...
3. 工具生态 使用标准属性的 trace 能被自动化工具自动做成本归因、偏好分析、异常检测——都是开源可插拔方案。
19.4.3 代码示例
from opentelemetry import trace
from opentelemetry.trace import Status, StatusCode
tracer = trace.get_tracer("agent-service")
async def execute_task(user_message):
with tracer.start_as_current_span("agent.task") as task_span:
task_span.set_attribute("agent.task_type", detect_intent(user_message))
while not done:
# LLM call
with tracer.start_as_current_span("llm.call") as llm_span:
llm_span.set_attribute("gen_ai.system", "anthropic")
llm_span.set_attribute("gen_ai.request.model", "claude-opus-4-6")
llm_span.set_attribute("gen_ai.request.temperature", 0.7)
response = await client.messages.create(...)
llm_span.set_attribute("gen_ai.usage.input_tokens", response.usage.input_tokens)
llm_span.set_attribute("gen_ai.usage.output_tokens", response.usage.output_tokens)
llm_span.set_attribute("gen_ai.response.id", response.id)
llm_span.set_attribute("gen_ai.response.finish_reason", response.stop_reason)
# Tool calls
for tc in response.content:
if tc.type == "tool_use":
with tracer.start_as_current_span(f"tool.{tc.name}") as tool_span:
tool_span.set_attribute("gen_ai.tool.name", tc.name)
tool_span.set_attribute("gen_ai.tool.call.id", tc.id)
try:
result = await tools[tc.name](tc.input)
tool_span.set_status(Status(StatusCode.OK))
except Exception as e:
tool_span.set_status(Status(StatusCode.ERROR, str(e)))
tool_span.record_exception(e)
raise
这段代码产出的 trace 在任何 OTel 兼容的观测平台都能正确显示。
19.4.4 OTel GenAI 规范的版本演进
OpenTelemetry GenAI semantic conventions(opentelemetry-specification/semantic_conventions/ai)2024 年 7 月首发 draft、2025 年 Q1 进 stable——演进路径值得追踪:
- v0.1(2024-07)——只定义了
gen_ai.system/gen_ai.request.model/gen_ai.usage.*核心六项 - v0.2(2024-10)——新增
gen_ai.tool.*工具调用属性 - v0.3(2025-01)——新增
gen_ai.operation.name(chat/completion/embedding)、gen_ai.response.finish_reason改为枚举 - v1.0(2025-Q2)——stable、属性名冻结、向后兼容承诺
工程启示——
- 现在就按 v1.0 埋点、不要用旧版属性名——否则 2 年后搬家
- tool 调用务必带
gen_ai.tool.call.id——方便重放时关联 LLM 请求和工具结果 gen_ai.prompt/gen_ai.completion是 opt-in——规范明确说”包含用户生成内容、默认不导出”——别踩
这条演进曲线本身是个信号——Agent 可观测性 2024-2026 是标准形成的窗口期、早入场、少返工。
19.4.5 Claude Code 的”事件抽样动态配置”
analytics/index.ts:132 的注释提到 tengu_event_sampling_config——通过 Statsig feature gate 动态下发的抽样率。
为什么要动态——
- 排查生产事故时临时把抽样率拉到 100%、复现后又调回 1%
- 特定 build 版本上线后临时全采、观察 regression、稳定后调回
- A/B 实验时只对实验组 100% 采、对照组 1%
固态写死的抽样率都是短视的——遥测抽样策略必须支持 runtime hot-reload。
本书第 18 章讨论持续评估时提过 feature flag 的跨场景复用——抽样策略也适用——同一套 gate 基础设施给多个用途。
19.5 三大主流 Agent 观测平台对比
2024-2026 年市场上专注 LLM/Agent 观测的平台主要三家:
19.5.1 LangSmith(LangChain 家)
- 强项:和 LangChain/LangGraph 深度集成,自动捕获;评估工具链完善;可视化 UI 最精致
- 弱项:绑定 LangChain 生态;自托管复杂
- 典型用户:深度用 LangChain 的团队
19.5.2 LangFuse(开源优先)
- 强项:Apache 2.0 开源、可自托管;OTel 标准;价格透明
- 弱项:UI 相比 LangSmith 稍弱
- 典型用户:自研 Agent 栈 / 数据合规敏感 / 预算紧张
19.5.3 Langtrace(OpenTelemetry native)
- 强项:100% OTel 原生;支持几乎所有 LLM SDK;轻量
- 弱项:功能比前两者少
- 典型用户:已有 OTel 栈想加 LLM 观测
19.5.4 对比决策树
graph TB
Q[选观测平台?]
Q --> LC{用 LangChain/LangGraph?}
LC -->|是| LS[LangSmith<br/>集成最深]
LC -->|否| SH{需要自托管?}
SH -->|是| LF[LangFuse<br/>开源 + 可自托管]
SH -->|否| OT{已有 OTel?}
OT -->|是| LT[Langtrace<br/>零侵入]
OT -->|否| LF2[LangFuse Cloud<br/>稳妥选]
style LS fill:#3b82f6,color:#fff,stroke:none
style LF fill:#10b981,color:#fff,stroke:none
style LT fill:#f59e0b,color:#fff,stroke:none
style LF2 fill:#10b981,color:#fff,stroke:none
三家都支持完整的 Trace 可视化、cost tracking、evaluation。新项目选自己技术栈契合的那个就好——不存在”全面更优”的选项。
19.5.5 三家具体 SDK 调用差异对比
抛开营销、直接看 SDK 调用姿势:
LangSmith(Python)——
from langsmith import traceable
@traceable # 自动 trace,需要 LANGCHAIN_TRACING_V2=true 环境变量
async def my_agent(msg): ...
- 装饰器式埋点、对 LangChain/LangGraph 代码零改动
- 非 LangChain 代码也能用、但需要手动
with trace(...)块
LangFuse(Python)——
from langfuse import Langfuse
langfuse = Langfuse()
trace = langfuse.trace(name="agent.task")
span = trace.span(name="llm.call", input=prompt, output=response)
span.end()
- 显式 API、命名空间独立、不依赖任何 agent 框架
- 需要手动调
.flush()在进程退出前——否则最后一批 span 丢
Langtrace(Python)——
from langtrace_python_sdk import langtrace
langtrace.init(api_key="...") # 全局 init
# 之后所有 OpenAI/Anthropic SDK 调用自动产生 OTel span
- monkey-patch OpenAI/Anthropic SDK、零侵入
- 缺点:SDK 升级后可能 patch 失效、需要等 Langtrace 跟进
选型决策——
- 团队已经用 OpenTelemetry——Langtrace 最轻
- 团队用 LangGraph——LangSmith 的 trace 自带 graph topology
- 数据合规高——LangFuse 自托管 + 显式 API、易于审计
- 全部想试——同时插入所有三个 sink(都是 OTel-compatible、互不干扰)——2 周内对比真实 trace 再决定
19.6 三大调试技巧
19.6.1 Trace 重放(Reproduce)
客户报 bug:“Agent 在修改我的代码时突然把函数删了!”
传统调试:让客户提供复现步骤 → 10 次尝试 9 次复现不出 → 放弃。
Agent 调试:直接打开那次 Trace:
1. 根据 trace_id 或用户 session_id 查找 Trace
2. 查看 Task Span 的完整 input(用户原始消息)
3. 逐个 LLM Call 展开:
- 看每次的 prompt(system + conversation history)
- 看每次的 response(完整 completion)
4. 在某次 LLM Call 里发现:prompt 被上一个工具的输出"污染"
(工具输出里有类似代码的文字,模型误以为是用户要求)
5. 定位根因:工具输出没做文本转义
6. 修复并加回归测试
Trace 让 10 分钟的调试变成 30 秒。
关键基础设施:
- 完整保存每次 LLM 请求的原始 prompt 和 response(注意脱敏)
- trace_id 和用户 session_id 双向映射
- 快速查询 UI(按用户 ID / 时间 / 错误状态过滤)
19.6.2 差分调试(Trace Diff)
同一任务跑两次,一次成功一次失败。差异在哪?
graph TB
subgraph "Trace A:成功"
A1[Read auth.ts]
A2[Read types.ts]
A3[Edit auth.ts]
A4[Run npm test]
A5[返回成功]
A1 --> A2 --> A3 --> A4 --> A5
end
subgraph "Trace B:失败"
B1[Read auth.ts]
B2[Edit auth.ts ← 跳过了 types.ts]
B3[Run npm test]
B4[❌ type error]
B5[放弃]
B1 --> B2 --> B3 --> B4 --> B5
end
Diff[Diff 发现:<br/>B 少了 Read types.ts 步骤]
A2 -.对比.-> Diff
B2 -.对比.-> Diff
style A5 fill:#10b981,color:#fff,stroke:none
style B4 fill:#ef4444,color:#fff,stroke:none
style Diff fill:#f59e0b,color:#fff,stroke:none
LangSmith / LangFuse 都有原生的 Trace Diff View。两条 trace 并排显示,鼠标悬停对应步骤高亮联动。一眼发现”B 少做了一步”。
差分调试适合:
- A/B 测试两个 prompt 版本的行为差异
- 回归分析:新版本在哪个步骤开始和旧版本分叉
- 找”为什么成功”:成功案例的共同点是什么
19.6.3 时光回溯(Time Travel)
进阶技巧:从某个 Span 的上下文开始重新 fork 一次执行。
# 伪代码
def time_travel(trace_id: str, fork_after_span_id: str, new_params: dict):
# 1. 加载原 trace
trace = load_trace(trace_id)
# 2. 找到 fork 点
fork_span = find_span(trace, fork_after_span_id)
# 3. 重建 fork 点的完整上下文
messages = reconstruct_messages_at(fork_span)
# 4. 用新参数继续执行
new_trace = run_agent(
messages=messages,
**new_params, # 比如换一个 model、改一个 prompt
)
# 5. 保存 fork 关系
new_trace.forked_from = trace_id
new_trace.forked_at = fork_after_span_id
return new_trace
应用场景:
- “如果这一步换个 model 会怎样?” — fork + 切 model
- “如果这里 prompt 改成 X 会怎样?” — fork + 改 system prompt
- “如果跳过这个工具调用会怎样?” — fork + mock 工具
配合 LangSmith 的 Playground 或 LangGraph 的 checkpointer,你甚至可以在 UI 上点几下就”在历史的某个岔路口走另一条路”。这是 Agent 调试独有的超能力。
19.7 Token 级的成本追踪
Agent 单次任务成本 10+ 跨度 3 个数量级,不追踪 = 钱怎么烧的不知道。
19.7.1 三个聚合粒度
graph TB
Raw[每 span 的<br/>gen_ai.usage.*]
Raw --> T1[per-task<br/>单次任务成本]
Raw --> T2[per-user<br/>单用户成本]
Raw --> T3[per-tenant<br/>单租户成本]
T1 --> Use1[哪类任务最贵<br/>哪些 prompt 最费]
T2 --> Use2[识别高消费用户<br/>发现滥用]
T3 --> Use3[B2B 计费<br/>成本归因]
style T1 fill:#3b82f6,color:#fff,stroke:none
style T2 fill:#10b981,color:#fff,stroke:none
style T3 fill:#f59e0b,color:#fff,stroke:none
19.7.2 成本计算的正确做法
# 从 trace 计算一次任务的成本
def compute_task_cost(task_span) -> float:
total_cost = 0
for llm_span in find_descendants(task_span, "llm.call"):
model = llm_span.attributes["gen_ai.request.model"]
input_tokens = llm_span.attributes["gen_ai.usage.input_tokens"]
output_tokens = llm_span.attributes["gen_ai.usage.output_tokens"]
pricing = MODEL_PRICING[model]
total_cost += (
input_tokens * pricing.input_per_1k / 1000
+ output_tokens * pricing.output_per_1k / 1000
)
# 缓存命中的 token 更便宜(通常 1/10)
cached = llm_span.attributes.get("gen_ai.usage.cache_read_tokens", 0)
total_cost -= cached * pricing.input_per_1k * 0.9 / 1000
return total_cost
注意三个细节:
- 区分缓存命中 token 和常规 token——OpenAI/Anthropic 的 cached prompt token 价格通常是 1/10
- model 可能动态切换(rate limit fallback),每 span 要独立算
- tool 成本别忘了——某些工具(web search、code execution)有独立的 API 成本
19.7.3 成本告警
alerts:
- name: cost_per_task_p99_too_high
query: histogram_quantile(0.99, rate(agent_task_cost_usd[10m]))
threshold: > 5.0
severity: warning
action: investigate 异常任务
- name: user_daily_cost_abnormal
query: sum by (user_id) (rate(agent_task_cost_usd[24h]))
threshold: > 100
severity: critical
action: 停止服务 + 联系用户(可能是滥用 or bug)
- name: tenant_monthly_budget_80_percent
query: tenant_spend_month / tenant_budget_month
threshold: > 0.8
severity: warning
action: 发送预算预警邮件
19.8 生产告警规则模板
除了成本,还需要质量、性能、可用性方面的告警:
# 质量告警
- name: success_rate_drop
query: avg_over_time(agent_task_success_rate[10m])
threshold: < 0.85
action: 检查近期 prompt / 工具改动
- name: fallback_rate_high
query: rate(llm_fallback_to_cheaper_model[10m])
threshold: > 0.1
action: 主模型可能 rate-limited 或故障
# 性能告警
- name: task_duration_p99
query: histogram_quantile(0.99, rate(agent_task_duration_seconds[5m]))
threshold: > 120
action: 可能遇到死循环或无限重试
- name: ttft_p99
query: histogram_quantile(0.99, rate(agent_ttft_seconds[5m]))
threshold: > 5
action: 用户首字延迟过高
- name: tool_error_rate
query: rate(tool_errors_total[5m]) / rate(tool_calls_total[5m])
threshold: > 0.05
action: 工具故障,检查具体 tool.name
# 可用性告警
- name: llm_api_error_rate
query: rate(llm_api_errors[5m]) / rate(llm_calls[5m])
threshold: > 0.01
action: 上游 LLM 服务异常,考虑切 provider
- name: infinite_loop
query: agent_loop_iterations{iterations > 50}
threshold: > 5 events / 10min
action: 有 agent 陷入死循环,检查循环终止条件
- name: context_overflow_rate
query: rate(context_overflow_events[15m])
threshold: > 0.02
action: 上下文压缩策略可能不够激进
# 安全告警
- name: dangerous_tool_call_spike
query: rate(tool_calls{tool_name="Bash", danger_score > 0.8}[5m])
threshold: > 10 per hour
action: 可能有 prompt injection 或滥用
19.9 仪表盘四大必备视图
一个成熟的 Agent 可观测性仪表盘应该至少有这四个视图:
视图 1:实时健康总览
- 当前 RPS、成功率、p50/p99 延迟
- 最近 1 小时告警列表
- 上游 LLM API 健康状态
视图 2:成本监控
- 今日累计成本(per-user / per-tenant / 总)
- 成本趋势(7 天、30 天)
- Top 10 最贵任务(含 trace_id 链接)
视图 3:质量趋势
- 成功率趋势(按任务类型分面)
- 用户反馈 thumbs up/down 比率
- Eval suite 最新跑分(来自 ch18 持续评估)
视图 4:工具健康
- 每个工具的调用次数 / 错误率
- 最慢工具排行(p99 延迟)
- 工具错误率异常高的告警
19.10 四类观测反模式
反模式 1:采样过度
“trace 太多存不下,采 1% 吧”——结果某个 bug 只在 0.5% 请求中出现,根本观测不到。
修正:
- 错误请求 100% 采样(
sample_on_error=1.0) - 正常请求按时间窗口分层采样(保证每个时段都有代表)
- 使用头部采样 + 尾部重采样:先都收、看到关键特征后保留全部 span
反模式 2:PII 泄漏
把用户的原始消息、工具结果全部记录到 trace——结果信用卡号、邮箱、密钥都进了观测后台。
修正:
- 在埋点层做 structured redaction,识别 PII pattern(邮箱、手机号、信用卡、密钥)自动 mask
- 敏感字段(prompt / completion)单独存到加密存储,常规 attributes 只记长度和 hash
- 定期审计 observability 里的数据分类
反模式 3:Trace 爆炸
每次工具调用都 span,子 span 里还有子 span——最后一个 trace 含几千个 span,UI 卡死,存储成本飞涨。
修正:
- Span 聚合规则:相似的连续 span(比如 for 循环里多次 read)合并为一个 span + 计数
- 深度限制:Agent loop 超过 50 次就警告(可能陷入死循环)
- Attribute 大小限制:单个 attribute 截断到 4KB,大 payload 引用到外部存储
反模式 4:成本黑洞
只监控任务成功率,不监控 token 消耗。某天发现 $10k/天的账单,一查是某个 prompt bug 让 Agent 把每个请求都 context 塞到 200K tokens。
修正:
- Token 成本是一等公民指标,和延迟、错误率一起监控
- 每 task 都带 cost attribute,支持多维聚合
- 成本异常告警必须立即响应,不能进归档
19.11 本章小结
可观测性是 Agent 工程里性价比最高的投资:
- Agent 特殊需求:链长、分支多、时长变、成本方差大——传统 APM 不够用
- MELT 四类数据:Metrics / Events / Logs / Traces(核心)
- Span 树状模型:顶层任务 → LLM call → tool execute 嵌套展开
- OpenTelemetry GenAI 标准:
gen_ai.*属性写一次、各平台通吃 - 三大观测平台:LangSmith(LangChain 生态)/ LangFuse(开源自托管)/ Langtrace(OTel 原生)
- 三大调试技巧:Trace 重放(复现问题)、差分调试(对比成败)、时光回溯(fork 历史执行)
- Token 级成本追踪:per-task / per-user / per-tenant 三粒度
- 生产告警:质量 + 性能 + 成本 + 安全 四大类共 10+ 条规则模板
- 仪表盘四视图:健康总览 + 成本 + 质量趋势 + 工具健康
- 避开四反模式:采样过度、PII 泄漏、trace 爆炸、成本黑洞
可观测性和评估的关系:评估(ch18)告诉你系统好不好,可观测性告诉你哪里不好。一个在开发期保底线,一个在生产期找病灶。两者缺一不可。
下一章讲成本控制与性能优化——知道了哪里不好,怎么系统地改进它。
19.12 Span 大小的实际约束——OTel 协议的硬上限
OpenTelemetry OTLP 协议规范里单条 Span 的 protobuf 编码建议上限是 1 MiB(opentelemetry-proto/opentelemetry/proto/collector/trace/v1/trace_service.proto 注释)——超限collector 直接丢、不告诉你。
Agent 场景下一条 span 轻易破上限——
- 一次 Opus 调用带 100k token prompt——JSON 序列化就 300 KB
- 外加 output、tool_result、error context——很快突破 1 MiB
- Datadog / Tempo 收到默认截断、你在 UI 上看到的是”残缺 trace”
对策(按优先级)——
gen_ai.prompt/gen_ai.completion存到外部对象存储(S3 / GCS)、span 只存引用 URI——span 保持 < 1 KiB- 对长 tool_result 做 byte-size attribute(
tool.output_size_bytes = 152000)+ head/tail 各 2 KiB——重放时可 fetch 全文 - 设置 OTel SDK 的
max_attribute_value_length全局截断(OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT=4096)
这一节是全章被忽视最多的知识点——80% 生产 Agent trace 有信息丢失、用户却不知道。
19.13 OTel instrumentation 的性能开销
埋点是有代价的——量化一下:
官方基准数据(OpenTelemetry SIG Performance 2024 报告)——
- Rust tracing + OTel exporter——单 span 创建 ~500 ns、L1 cache hit 不可见
- Python opentelemetry-sdk——单 span 创建 ~2-5 μs、高频循环可感知
- Node.js opentelemetry——~10 μs、是三者最慢
Agent 场景下 OTel 开销占比——
- 一次 LLM 调用 P50 5s(网络 + 推理)、OTel 开销 ~10 μs——0.0002% 忽略不计
- 一次 tool.Read 调用 P50 200 μs(本地读文件)、OTel ~10 μs——5% 可感
结论——
- LLM / tool 粒度的 span——无脑打满、开销 noise level
- 循环内部(forEach/map 里的小操作)——可以合并(
span.addEvent而非新建 span)、否则 1000 次循环 10 ms 纯开销
这是为什么 §19.10 反模式 3 强调”span 聚合”——不只是存储问题、也是 runtime 开销问题。
19.14 Trace 和 Checkpoint 的关系:双写一致性
本书第 13 章(LangGraph 《Streaming》)和第 18 章(《Design Patterns》)都讲过 checkpoint——把 Agent 中间状态持久化、允许断点恢复。
checkpoint 和 trace 看起来是两个东西——实际上互为镜像:
- Trace——给人看的历史记录(调试、分析)
- Checkpoint——给系统看的历史记录(恢复、重放、fork)
双写一致性要点——
- 每次写 checkpoint 前、对应的 span 必须先 close——否则恢复时看到的 trace 是”悬挂的”(span 没结束、无法显示时序)
- checkpoint 里要带
trace_id——恢复时可以从 trace 继承、不是开新链 - trace_id 是稳定的 resume 标识——本章 §19.6.3”时光回溯”本质上就是 trace + checkpoint 的组合技
这个”双写”模式——是所有复杂系统的底层模式——binlog + data 文件(MySQL)、WAL + segment(Postgres)、journal + snapshot(etcd)——都是”一份给恢复、一份给观察”。
19.15 一个典型的生产事故复盘模板
下面是一个用于说明 trace 重放价值的复盘模板,不把它包装成没有一手来源的匿名真实事故:
- 现象——用户报告”Agent 会把别人的代码贴到我的文件里”
- 影响面——从 trace 和会话日志中统计受影响 session
- 第一反应——prompt injection?contex 泄漏?
排查靠的就是 trace 重放——
- 从用户 session_id 拿到 trace_id
- 展开 trace、在第 3 次 LLM call 发现 system prompt 末尾多了另一个用户的代码片段
- 继续追、发现是 prompt cache 的 hash key 冲突(某个 salt 被意外设成了固定值)
- 两个并发用户在 5 分钟 TTL 内命中了同一个缓存、prompt 互相串台
根因修复 30 分钟——但如果没有完整的 trace(含真实 prompt 内容、脱敏存储)——这个 bug 可能要查一周。
教训——
- prompt/completion 存储不能图省事——脱敏 + 加密 + 可查 三件套必须齐
- cache key 的生成函数要有单元测试、哪怕 “trivial”
- 每次并发事件(缓存、session、account)都是潜在的”串台”风险——trace 是唯一能定位它的工具
本书第 20 章(《成本控制》§20.4.7)列过”缓存被打破的四种场景”——这个事故是第五种:“key 碰撞”——比缓存失效严重得多、会直接导致数据泄漏。
19.16 从 trace 到 LLM-assisted 根因分析
前瞻——2025-2026 趋势:用 LLM 本身做 trace 根因分析。
原理——
- 把一条”失败 trace”的所有 span 序列化成 JSON
- 加上”这个任务最终失败了、请分析在哪一步开始走错”的 prompt
- 让 Sonnet / Opus 读完、输出 hypothesis + 修复建议
为什么 LLM 适合这个——
- trace 本质是长上下文的因果链——正好是 LLM 擅长的
- 人类工程师看一个 100-span 的 trace 需要 15 分钟——LLM 15 秒
- LLM 能发现跨领域相似性(“这看起来像经典的 retry storm”)——人类需要经验积累才能看出
已落地的项目——
- LangSmith 的 “AI-assisted trace analysis”(2025 Q1 beta)
- OpenAI 自己的内部 debugging Agent “Copilot for Incidents”(2024 内部博客提到)
- DataDog 的 “Watchdog Analyst”(通用观测场景、不限 Agent)
这是”可观测性 2.0”的开头——从”人查日志”到”人问 LLM、LLM 查日志”——你的 trace 数据不仅给人看、也在训练未来的调试 AI。
19.17 三本姊妹书的交叉点
- 本书第 18 章《持续评估》——评估用的 golden set、本质上是脱敏后的历史 trace——观测 → 评估数据集的管线是 Agent 团队最被低估的基础设施之一
- 本书第 20 章《成本控制》——§20.17 讲了 OTel metric 导出 cost 到 Prometheus——本章的 trace 视角 + 第 20 章的 metric 视角合起来才完整
- 《Claude Code 源码》第 19 章(analytics)——
src/services/analytics/*.ts4040 行代码就是本章讲的所有原理的工业级落地——读完本章再回去读代码、会有”啊、他也是这么想的”的顿悟 - 《LangGraph 源码》第 13 章(streaming)——checkpoint 和 trace 的双写模式
- 《hyper-tower》第 14 章(tracing)——tower 的 tracing 中间件演示了 Rust 侧 OTel 集成路径
交叉点的意义——Agent 可观测性不是新学科——是”传统 APM + LLM 语义 + Agent 编排”三者叠加——读这几本书的对应章节、你会发现知识在打通、不是重复。
19.18 一张可以直接印的”Agent 可观测性体检表”
贴在工位、每月过一遍——10 分钟体检:
| 维度 | 检查项 | 通过标准 |
|---|---|---|
| Trace 完整性 | 随机抽 5 条 trace、能否从顶层点到叶子 span | 100% |
| PII 合规 | prompt / completion 是否脱敏或存特权列 | 100% |
| Cost attribution | 每个 llm.call span 是否带成本计算所需 5 字段 | 100% |
| Sampling 策略 | 错误请求是否 100% 采样 | 必须是 |
| 告警覆盖 | §19.8 的 10 条告警是否都配了 | ≥ 8 条 |
| 仪表盘 | §19.9 的 4 个视图是否都在线 | 4/4 |
| Trace 搜索 | 按 user_id / task_type / error 能否秒级过滤 | 能 |
| Attribute 大小 | 单 span 是否 < 1 MiB | 99% |
| 外部 prompt 存储 | 长 prompt 是否不进 span | 是 |
| Replay 能力 | 能否从 trace 重建 messages 重跑 | 能 |
| LLM 辅助分析 | 是否接入过 LLM trace analyst | 至少试过一次 |
| 事故演练 | 上季度是否做过一次”靠 trace 定位假 bug”演练 | 有 |
12 项——打勾 ≥ 10 项是 A 级、7-9 项 B 级、< 7 项 C 级。
C 级的团队请把本章再读一遍——这不是苛责、是保护用户(PII)、保护团队(调试效率)、保护公司(成本可见性)。
19.19 结语:把 Agent 从神秘变透明
Agent 可观测性的目标很明确:把 Agent 从”神秘黑盒”变成”透明玻璃盒”。
- 神秘黑盒——报 bug 只能扔给工程师、排查靠运气、成本无法归因、PII 风险无人知
- 透明玻璃盒——用户报一个现象、团队 30 秒定位、成本精确到 token、合规可审计
从黑盒到玻璃盒的路径——不是一个工具能给你的——是 trace 数据模型、OTel 标准、脱敏管线、告警体系、仪表盘设计、反模式防御的六合一——本章给的就是这张六合一地图。
下一章(§20)把地图上的”成本维度”放大、给你一本专门的”省钱手册”。
19.20 Claude Code 的 Datadog 集成——工业级实现的三个细节
src/services/analytics/datadog.ts 307 行——不直接用 OTel、而是自己包了 axios post 到 Datadog Logs API——为什么?三个生产级决策:
决策一:白名单机制(datadog.ts:19-55)——
const DATADOG_ALLOWED_EVENTS = new Set([
'chrome_bridge_connection_succeeded',
'tengu_api_error',
'tengu_api_success',
// ... ~50 个 event name
])
不是所有 logEvent 都走 Datadog——只有白名单里的才送。为什么——Datadog 按 event 计费、所有事件进 DD 会烧钱——挑高价值事件入 DD、低频 debug 事件进 1P BigQuery。
决策二:批量 + 定时 flush(DEFAULT_FLUSH_INTERVAL_MS = 15000、MAX_BATCH_SIZE = 100)——
- 每 15 秒或满 100 条批量发一次
- 不是每条立即发——避免网络抖动连续超时
- 进程退出前 flush 一次——否则最后一批丢
决策三:网络超时 5s(NETWORK_TIMEOUT_MS = 5000)——
- 超时不阻塞主流程、Agent 的 telemetry 绝不能拖慢用户交互
- 失败的 event 进 retry queue、最多重试 N 次然后丢——“能送到 99% 就够、不追求 100%”
这三条——白名单降成本、批量抗抖动、超时保主流程——是任何第三方 telemetry 集成的通用架构。
19.21 1P BigQuery Exporter 的 806 行究竟在做什么
firstPartyEventLoggingExporter.ts 806 行——是本项目最大的 analytics 文件。它的核心职责:
- Proto field hoisting——把
_PROTO_*键从additional_metadatamap 提到 proto 顶层字段——PII-tagged 列才有权限控制 - Schema enforcement——BigQuery 表有严格的 schema、字段名错了插不进——所有 event 过一层 validator
- Fallback chain——主 endpoint 失败切备用、备用失败进本地 disk queue、进程下次启动 resend
- Rate limiting——防止某个 event 高频爆发打垮 BQ streaming quota
“为什么要 1P 自建 BigQuery 管线、不直接用 Datadog”——
- BigQuery 便宜(5/TB query)——Datadog Logs 按 GB 计费贵 10-50×
- SQL 查询无限制——支持任意维度切片、pipeline 接下游 ETL
- 数据驻留——某些合规场景(GDPR、中国《数据安全法》)要求数据在特定地域、Datadog 做不到
- schema 自主演进——增加字段不需要跟供应商协商
读完这 806 行——你会理解”为什么成熟 Agent 公司都有自建 BigQuery 管线”——Datadog 是 convenience、BQ 是 sovereignty。
19.22 W3C Trace Context:跨服务的 trace_id 传递
Agent 场景下 trace 常常跨多个服务——前端 SPA → API gateway → agent orchestrator → LLM provider → vector DB → …——如何让 trace_id 端到端传递?
答案——W3C Trace Context 标准(w3.org/TR/trace-context/、2020 推荐、2024 普及)——定义两个 HTTP header:
traceparent: 00-{trace-id}-{parent-span-id}-{flags}
tracestate: vendor1=value1,vendor2=value2
格式细节——
traceparent是必需、4 段 hyphen 分隔、每段定长——解析零歧义trace-id是 16 字节(32 hex)、全局唯一parent-span-id是 8 字节(16 hex)、当前 span 在链路中的位置flags是 01(采样)或 00(不采样)——下游按父决定采不采
工程实践——
- 所有 HTTP 客户端 + 服务端(axios、fetch、Express、Fastify)都要自动注入/解析这两个 header
- message queue 消息体要附
traceparent——异步边界也能串起来 - LLM API 请求——Anthropic/OpenAI 暂未支持接收
traceparent、但你可以放到自定义 header、provider 回 response 时会原样带回X-Request-ID等字段——用 X-Request-ID 反查 Anthropic 的 usage dashboard
跨服务 trace 是”全链路观测”的基础——没它、每个服务是一座孤岛。
19.23 Head Sampling vs Tail Sampling
抽样分两种、差异巨大:
Head Sampling(头部采样)——
- 在 trace 开始时立即决定采或不采
- 简单、分布式无协调
- 缺点——完全随机、罕见错误可能 miss
Tail Sampling(尾部采样)——
- trace 先 “全采” 进内存、等结束后根据 span 特征决定保留与否
- 好处——按 “error=true” / “duration>10s” 等条件留存、100% 保留异常 trace
- 缺点——需要全链路 span 汇聚到一处再决定——collector 内存占用大
OpenTelemetry Collector 的 tail_sampling processor 支持规则:
tail_sampling:
decision_wait: 10s
policies:
- name: errors
type: status_code
status_code: { status_codes: [ERROR] }
- name: slow
type: latency
latency: { threshold_ms: 5000 }
- name: sample_normal
type: probabilistic
probabilistic: { sampling_percentage: 1 }
策略组合——错误 100% + 慢请求 100% + 常规 1%——抓到所有坏的、抽样保存一部分好的——存储成本可控、调试质量不降。
实战建议——新项目从 head sampling 开始(简单)、DAU > 10k 后上 tail sampling(存储成本痛)。
19.24 一个常被忽视的维度:trace 的 retention 策略
存下来的 trace 放多久?这是成本 × 合规 × 实用性的三角平衡:
| 数据类型 | 建议保留 | 理由 |
|---|---|---|
| 错误 trace(status=error) | 90 天 | 事后复盘、合规审计 |
| 慢 trace(P99+) | 30 天 | 性能优化迭代周期 |
| 常规 trace | 7 天 | 短期调试够用 |
| trace 聚合指标 | 2 年 | 长期趋势分析 |
| prompt / completion 全文 | 30 天(加密列) | PII 最小化原则 |
| 用户反馈关联 trace | 180 天 | eval 数据集候选 |
技术实现——
- 按 trace 打 label(
retention_tier: short/long/forever) - 存储引擎按 label 自动迁移冷热(BQ
partition_expiration_days/ Datadog Archive Tier) - 合规要求删除时、按 user_id 查所有 trace、批量调删除 API
GDPR “被遗忘权”的技术落地——如果你的 trace 按 user_id 分区、一行 SQL 就能满足——否则需要全表扫描、每次合规请求烧几千美元——架构阶段就要考虑这一点。
19.25 Agent 可观测性与传统 APM 的本质差别
传统 APM 关注 CPU、内存、网络——它是”Ops 给 Ops 看”的观测,对 Agent 系统意义不大。
本章讲的是 “Agent 工程师给 Agent 工程师看”的观测——关注 decision chain、token economy、semantic failure——这才是 Agent 时代的生命线。
本章十条知识点索引:
- 为什么需要 OTel GenAI 语义规范(§19.4)
- 怎么选 LangSmith/LangFuse/Langtrace(§19.5)
- 怎么做 Trace Replay / Diff / Time Travel(§19.6)
- 怎么设计 PII 红线(§19.3.3 / §19.3.4)
- 怎么算成本、怎么告警、怎么搭仪表盘(§19.7-19.9)
- 避开四反模式(§19.10)
- Span 1MiB 限制、OTel 性能开销、trace/checkpoint 双写一致性(§19.12-19.14)
- Datadog 白名单设计、1P BQ exporter 架构(§19.20-19.21)
- W3C Trace Context 跨服务传递(§19.22)
- Head/Tail Sampling、retention 策略(§19.23-19.24)
19.26 metadata.ts 973 行——“上下文富化”的工业级范例
src/services/analytics/metadata.ts 973 行——每一条 logEvent 发出前、都会被这个文件的 getEventMetadata 增加 20+ 维度的上下文。
从 import 列表看它增加的维度——
env/getHostPlatformForAnalytics——操作系统 + 架构getModelBetas/getMainLoopModel——当前用哪个模型 + 启用哪些 betagetSessionId/getParentSessionId——当前会话树getClientType——CLI / IDE extension / VSCode bridge 等入口类型getWslVersion/getLinuxDistroInfo/detectVcs——细粒度环境信息isOfficialMcpUrl——是否调用官方 MCP server(第 17 章《MCP》场景)isClaudeAISubscriber/getSubscriptionType——用户订阅分层getRepoRemoteHash——脱敏后的 git remote hash(识别同项目的多用户、但不泄漏仓库名)getAgentId/getTeamName/isTeammate——多租户团队分组
总共 20+ 维度——每一条事件都带齐——这就是”事件的维度富化”:一次埋点、多维切片——Datadog / BQ 里按任何维度查询都秒级返回。
反面教训——只 log eventName + duration、出了问题查”是哪个版本的哪种用户在哪种 OS 上触发的”——只能一条 SQL 一条 SQL 拼 JOIN——分析效率差 100 倍。
读者带走一条——埋点时 metadata 数量与 event 数量一样重要——每加一条 event 都问自己:切片维度齐不齐。
19.27 growthbook.ts 1155 行——feature gate 基础设施的规模
analytics/growthbook.ts 1155 行——Claude Code 把 feature flag / gate 做成了完整子系统:
- 远程配置实时拉取——启动时 + 定时 refresh
- 本地缓存——离线场景降级
- user bucketing——基于 user_id hash 的一致性采样(同用户进同实验组)
- 多维度条件匹配——按 OS / version / region / subscription 组合规则
- 暴露 API
checkStatsigFeatureGate_CACHED_MAY_BE_STALE(§19.20 的 Datadog gate 就用这个)
为什么 Agent 产品需要 feature gate——
- 模型切换(Opus 4.6 → 4.7 beta 发布)需要 10% 流量试
- prompt 改动需要 A/B——控制组 / 实验组数据 telemetry 侧区分
- 生产事故时一键切到 backup——通过 gate 下发、不需要重启服务
- 合规要求某地区用户禁用某功能——一个 gate 规则搞定
读者能抄走的架构——把 feature gate 和 telemetry 绑定——每个 event 都带当前 gate 状态——A/B 分析不需要 JOIN、直接 group by 实验组。
19.28 实战:用 PromQL 写出四大仪表盘的 20 条查询
§19.9 只给了”四大视图”标题、这里落地到具体 PromQL:
视图 1:实时健康总览
# 1. 当前 RPS
sum(rate(agent_task_start_total[1m]))
# 2. 成功率
sum(rate(agent_task_success_total[5m])) / sum(rate(agent_task_complete_total[5m]))
# 3. P99 延迟
histogram_quantile(0.99, rate(agent_task_duration_seconds_bucket[5m]))
# 4. 上游 LLM 健康
sum(rate(llm_api_errors_total[5m])) by (provider)
# 5. 当前告警数
ALERTS{alertstate="firing"}
视图 2:成本监控
# 6. 今日累计成本
sum(increase(agent_cost_usd_total[1d]))
# 7. per-tenant 成本
sum by (tenant) (rate(agent_cost_usd_total[1h]))
# 8. Top 10 用户
topk(10, sum by (user_id) (increase(agent_cost_usd_total[1d])))
# 9. cache 命中率
sum(rate(llm_cache_read_tokens_total[5m])) / sum(rate(llm_input_tokens_total[5m]))
# 10. 成本趋势(同比上周)
sum(increase(agent_cost_usd_total[7d])) / sum(increase(agent_cost_usd_total[7d] offset 7d))
视图 3:质量趋势
# 11. 任务类型分面成功率
sum by (task_type) (rate(agent_task_success_total[1h])) / sum by (task_type) (rate(agent_task_complete_total[1h]))
# 12. thumbs up/down 比率
sum(rate(user_feedback_total{rating="up"}[1d])) / sum(rate(user_feedback_total[1d]))
# 13. 平均迭代轮数
sum(rate(agent_loop_iterations_total[5m])) / sum(rate(agent_task_complete_total[5m]))
# 14. context compaction 触发率
sum(rate(context_compaction_triggered_total[5m])) / sum(rate(llm_calls_total[5m]))
# 15. eval 最新分数(来自 ch18 持续评估)
eval_score_latest{suite="coding_bench"}
视图 4:工具健康
# 16. 每个工具错误率
sum by (tool_name) (rate(tool_errors_total[5m])) / sum by (tool_name) (rate(tool_calls_total[5m]))
# 17. P99 工具延迟
histogram_quantile(0.99, sum by (tool_name, le) (rate(tool_duration_seconds_bucket[5m])))
# 18. 工具调用频率 TOP 5
topk(5, sum by (tool_name) (rate(tool_calls_total[5m])))
# 19. Bash 危险命令率
sum(rate(tool_calls_total{tool_name="Bash", danger_score="high"}[5m]))
# 20. MCP 工具占比
sum(rate(tool_calls_total{tool_type="mcp"}[5m])) / sum(rate(tool_calls_total[5m]))
20 条 PromQL 贴进 Grafana 即可得到一个完整的 Agent 可观测性仪表盘——剩下的只是美化。
19.29 把 trace 当一等产品
很多团队把 trace / 可观测性当工具——“我用 Datadog 配了仪表盘”、“我接入了 OTel”就完事。
真正成熟的 Agent 团队把 trace 当产品——
- 有 owner——专门的 observability engineer / SRE
- 有 roadmap——季度规划哪些维度要补、哪些反模式要修
- 有 SLO——不只是”能用”、是 “P99 trace 完整率 > 99.9%”、“PII 泄漏事件 = 0”
- 有 cost budget——observability 自己的 Datadog / BQ 成本每月有预算
- 有用户——下游开发 / SRE / 合规 / 产品 都是 trace 的”客户”、定期收集反馈迭代
这就是”可观测性 2.0”的工程文化——从”附属工具”到”内部产品”。
19.30 附录 A:从零搭一个最小可用观测栈(MVP)
本章篇幅大、但很多读者手上还没有任何观测。给一个周一就能跑起来的 MVP:
Day 1——基础
- 装
opentelemetry-sdk+opentelemetry-exporter-otlp-proto-http - 启动一个本地
otel-collector(docker 一行) - 在每次 LLM call / tool call 外面包
tracer.start_as_current_span - 验证:能在 collector 的 log 里看到 span
Day 2——后端
- 选 LangFuse Cloud 或 Grafana Tempo(都有免费额度)
- 把 collector 的 exporter 配置指向后端
- 验证:UI 上能看到第一条 trace
Day 3——属性完整性
- 加齐 §19.4.1 的所有
gen_ai.*属性 - 加 PII 红线类型(§19.3.3)——防止敏感数据泄漏
- 验证:任意抽 5 条 trace、属性齐全
Day 4——告警
- 从 §19.8 挑 3 条最关键的(success_rate_drop、cost_per_task_p99、infinite_loop)
- 在后端配置告警、路由到 Slack / Email
- 验证:人为触发一次异常、告警到达
Day 5——仪表盘
- 用 §19.28 的 5-10 条 PromQL 搭一个简易仪表盘
- 按 §19.9 的四视图组织
- 验证:一眼能看到生产健康
一周 MVP——比 80% Agent 项目的可观测性都强。
19.31 附录 B:术语速查
本章用到的专业名词一页纸速查——
- Span——一个操作的追踪单元、有 start/end 时间、可嵌套
- Trace——同一个请求产生的所有 span 组成的树
- Attribute——span 上的 key-value 元信息
- Event——span 内部的时间点标记、不是 span
- Status——span 的完成状态(ok / error / unset)
- Sampling——决定哪些 trace 要留下的策略
- Head/Tail Sampling——在开始时 vs 结束时做采样决策
- OTel / OpenTelemetry——开源的遥测框架 + 语义规范
- OTLP——OTel 的 wire protocol(protobuf/HTTP/gRPC)
- GenAI Semconv——OTel 为 LLM/Agent 定义的标准属性集
- MELT——Metrics/Events/Logs/Traces 四类遥测数据的合称
- Span Kind——INTERNAL/CLIENT/SERVER/PRODUCER/CONSUMER
- W3C Trace Context——跨服务 trace_id 传递的 HTTP header 标准
- Flame Graph——按时间轴堆叠展示 span 的可视化
- PII——Personally Identifiable Information、个人识别信息
- Redaction——脱敏处理(邮箱→
****、手机→hash) - Retention——数据保留策略(多久后删除)
- Cardinality——label 可能取值的组合数、高基数会爆炸
把这张表贴在工位、半年内不用再 Google 这些词。
19.32 附录 C:把本章读进脑子里的 10 道自测题
测试第一原理掌握情况(答得上 8 道以上算及格)——
- OTel GenAI 规范里 token 使用量的 attribute 名是什么?(答:
gen_ai.usage.input_tokens/gen_ai.usage.output_tokens) - Span 的 protobuf 编码建议上限多少?(答:1 MiB)
- W3C Trace Context 的两个 HTTP header?(答:
traceparent/tracestate) traceparent里 trace-id 长度?(答:16 字节 / 32 hex 字符)- Claude Code 为什么用
never类型做 PII 标记?(答:强制 cast 过 code review) - Head Sampling vs Tail Sampling 的关键差异?(答:决策时机、tail 可基于完整 trace 特征采样)
- trace 和 checkpoint 的关系?(答:互为镜像——一份给人看、一份给系统恢复用)
stripProtoFields为什么在无_PROTO_*键时返回原对象?(答:零分配 hot path)- per-task / per-user / per-tenant 成本聚合为何都要有?(答:分别对应成本分析、滥用检测、B2B 计费)
- 为什么错误请求要 100% 采样?(答:罕见错误在 1% 抽样下完全丢失、无法调试)
答不上的题——回去把对应小节再读一遍——这比读一百个框架文档都有用。
19.33 永别是不存在的——本章和下一本书的接口
本章的”可观测性”概念——会在后续多本书里再度出现:
- 《LangChain 源码》第 11 章(callbacks)——LangChain 的 callback system 是它自己的”可观测性原语”、和 OTel 共存
- 《Claude Code 源码》第 14 章(analytics architecture)——深入
src/services/analytics/*.ts4040 行的产品化实现 - 《Vite 源码》第 15 章(Plugin Hook Observability)——Vite 插件钩子的性能追踪——前端工程工具也有 APM 需求
- 《hyper-tower》第 14 章(tracing middleware)——Rust 侧的
tower::trace::Trace实现 - 《React 18 源码》第 16 章(Profiler API)——前端渲染性能观测
“可观测性”不是一个章节的话题、是贯穿 15 本书的暗线——任何复杂系统都需要”被看见”——你读透这条暗线、就打通了”从后端到前端、从协议到 UI”的工程哲学。
一章终、余音长——下章成本控制见。
19.34 一张 SVG 不贴、只贴一段话
如果读者把本章所有小节全部忘了——只记住这一段话:
Agent 没有 trace 就像飞机没有黑匣子。平时不出事没事、一出事你什么都不知道。世上没有”不出事”的 Agent 产品——所以世上也没有”不需要 trace”的 Agent 团队。
这句话不需要解释——读了本章你就会懂。
19.36 补充一则:Anthropic SDK Usage 对象的真实字段清单
本章多处引用 gen_ai.usage.*——对应到 Anthropic Python SDK anthropic/types/usage.py 的真实字段(v0.40 版本):
input_tokens:prompt 长度output_tokens:completion 长度cache_creation_input_tokens:写入缓存的 token 数(付 1.25× 溢价)cache_read_input_tokens:命中缓存的 token 数(付 0.1× 折扣)server_tool_use.web_search_requests:web search 工具调用次数(Anthropic 侧按次计费 $0.01)cache_creation:分ephemeral_5m_input_tokens/ephemeral_1h_input_tokens两档service_tier:standard/priority/batch——决定单价档位
把这 7 个字段全部 attribute 化——你就拥有了和 Anthropic 账单对账的能力:
llm_span.set_attribute("gen_ai.usage.input_tokens", usage.input_tokens)
llm_span.set_attribute("gen_ai.usage.output_tokens", usage.output_tokens)
llm_span.set_attribute("anthropic.usage.cache_creation_input_tokens", usage.cache_creation_input_tokens ?? 0)
llm_span.set_attribute("anthropic.usage.cache_read_input_tokens", usage.cache_read_input_tokens ?? 0)
llm_span.set_attribute("anthropic.usage.cache_creation_5m", usage.cache_creation?.ephemeral_5m_input_tokens ?? 0)
llm_span.set_attribute("anthropic.usage.cache_creation_1h", usage.cache_creation?.ephemeral_1h_input_tokens ?? 0)
llm_span.set_attribute("anthropic.usage.web_search_requests", usage.server_tool_use?.web_search_requests ?? 0)
llm_span.set_attribute("anthropic.service_tier", usage.service_tier)
8 个 attribute——你从此不再”不知道钱去了哪里”——这是本章给出的最具体的一条行动。
19.37 为什么 Agent 时代的可观测性尤其重要
Agent 可观测性的跨时代对比——
- 过去——服务 bug 靠日志 grep 能解决
- 现在——Agent bug 需要重建”20 层嵌套的决策链 + 10 次工具调用 + 5 次 LLM 采样分支”才能理解
- 未来——bug 本身可能不存在、只有”概率性能力退化”——只能靠时间序列和 trace 聚合发现趋势
底层原理(span 树、采样、PII 红线、成本归因)不随模型版本变化,这是本章在未来几年依然能作为参考的原因。
19.38 一句话浓缩
“If you can’t trace it, you can’t own it.”——你追溯不到的系统、就不是你的系统。
全章指标统计:7 小节原文 + 20 小节新增源码实证 / 跨书关联 / 工程实战 / 附录——覆盖从”什么是 trace”到”如何把 trace 当产品运营”的完整认知链条。下次翻开本章时、愿你已把这套方法落地到你的 Agent 产品里。
延伸阅读
- OpenTelemetry GenAI 规范:https://opentelemetry.io/docs/specs/semconv/gen-ai/
- LangSmith 文档:https://docs.smith.langchain.com
- LangFuse 开源:https://github.com/langfuse/langfuse
- Langtrace 开源:https://github.com/Scale3-Labs/langtrace
- Anthropic 的 Claude Code Observability 实践:搜索 “Claude Code observability”
- Tracing for LLM agents 最佳实践:LangChain 博客系列