Chapter 03 · Operations
落地:调试、监控、在线评估,与投产陷阱
前两章把 trace 树的词汇和生成机制讲透了;这一章是拿这棵树做事——它不是被动看板,是驱动调试、监控、在线评估的闭环,以及投产时真正咬人的陷阱。
本章你将建立的 schema
- 拿到失败 trace 沿父子链定位是哪个 span 产出坏输入;中间状态(工具输出、检索上下文、路由决策)才是关键证据;replay 目前多是手动重建渲染过的 prompt 加参数。
- 说得出生产该报警什么(cost/run、p95 延迟与 TTFT、错误率、工具失败率),以及这几个数在 LLM 语境下和传统 APM 的含义差异。
- 认得"在线评估"这条 obs→eval 唯一接缝——外部 scorer 对采样生产 trace 异步打分、写回、提拔低分进离线集;不与 Reflexion(agent 推理时自评纠错)混淆。
- 识别投产四族陷阱,尤其推理链 PII 泄漏与朴素头部采样丢掉罕见 bug 这两条最隐蔽的失败模式。
第二章解释了那棵 trace 树不是日志聚合、是 context propagation 造出来的因果结构。这一章的问题是:拿到一棵树之后,工程师到底做什么?文档给的答案是"打开看板"——本章要回答更深一层:一条 trace 摆在面前,怎么从它读出根因;该盯哪几个数能早于用户发现问题;哪条接缝把可观测性和 eval 连起来;以及哪些看起来合理的做法会在三个月后炸。
3.1从一条失败 trace 做根因回溯
面对一条 status=OK 却答错的 trace,根因不在叶子 span,在某个中间 span 的输出污染了后续所有决策。
文档教读者"点开 span 看属性"。根因调试的真正难点是:agent 的失败往往不是哪个 span 抛了异常,而是某个 span 产出了语义上坏的输出——工具返回错误价格、检索召回了无关政策条文、路由 LLM 选错了下一步——后续所有 span 都在这个错误前提上继续推进,直到最终输出,整棵树 status 全是 OK。只有把中间状态(工具输出、检索上下文、路由决策)都存进 span 的 events 或 attributes,才能沿父子链逐级审讯"这一步收到了什么、产出了什么"。
沿父子链逐级回溯
回溯从最终答复 span(根的直接子)开始,向上沿 parent_span_id 追溯,每一级检查三件事:
- 输入来自哪里:该 span 的输入是上一级哪个 span 的输出?若输入已经是错的,根因不在这里。
- 中间状态是否存下来:工具输出、检索到的文档、路由决策用哪个工具,这些是编排层的关键状态。若没被记录进 span,这一层就是盲区。
- 这个 span 的状态与实际输出是否一致:
status=OK只说明调用本身没抛异常,不说明输出语义正确。
找到"第一个产出坏输出的 span"就是根因 span。从那里往下,所有 span 都只是在传播那个错误,修根因才能解决问题,修下游 span 是治标。
差旅助理「成本翻 3 倍」案例
用第二章介绍的差旅助理 agent 具体走一遍。用户看到账单:这次出差订票的 API 调用花费是过去平均值的 3.1 倍。trace 打开,invoke_agent 根 span 下有 9 个 chat span,正常运行只有 5 个。
沿 parent_span_id 从第 5 个 chat span 开始往上看:
- 第 3 个
chatspan 决定调用search_flights——这个execute_toolspan 的 status 是ERROR,gen_ai.response.finish_reasons不在这里,但工具 span 的status.message记录了"航班 API 超时,返回 504"。 - LLM 没有收到重试指令(路由决策 span 没有存下当时 LLM 的推理),却在随后 4 轮
chatspan 里各有一次search_flights调用,每次都返回 504,每次 LLM 重新生成了一段新的 prompt 再试。 - 4 次额外
chatspan 的gen_ai.usage.input_tokens加起来约等于原来 5 次运行的 token 总量——成本翻倍的原因找到了:search_flights失败触发了 LLM 自发的重试循环,每次重试把整段对话历史再喂一遍,token 线性叠加。
根因 span 是第一个失败的 execute_tool: search_flights。修法不是改 LLM prompt,而是在工具层加幂等重试上限,或在编排层注入明确的"已重试 N 次,中止"指令。
Replay:手动重建中间状态
Replay("从这一步重跑")在 agent 领域的现实是:因为 LLM 输出非确定,replay 不等于完全重现,而是手动重建——把存下来的 rendered prompt(完整渲染后的消息列表)+ 采样参数(temperature / seed)+ 工具响应原文,按顺序重新喂给模型,从那个 span 之后重新推进。
不存下 rendered prompt 和工具响应原文,replay 就无从做起。这正是第四节投产陷阱 D 族"可重建"的核心——非确定杀死复现,不存下逐字 rendered prompt 的系统在第 3 步之后就发散。
Good vs bad run diffing
另一个定位根因的手段是把一条失败 trace 和一条成功 trace(同类任务)并排对比:相同层级的 span,token 数差了多少、哪个工具输出在结构上不同、哪个 chat span 的 gen_ai.response.finish_reasons 从 stop 变成了 length(说明 LLM 在那里被截断)。这种 diffing 在 LangSmith / Langfuse 里都有 UI 支持;逻辑上等同于把两棵 span 树做结构性 diff,定位哪条边开始分叉。
差旅助理的失败 trace 里,search_flights 工具 span 的 status 是 ERROR,但根 invoke_agent span 的 status 是 OK。工程师第一眼看 trace 列表时,会不会漏掉这条 trace?原因是什么,怎么防?
展开答案(先停 10 秒再点)
漏掉的概率很高。trace 列表通常按根 span 的 status 筛选和排序,而根 span OK 说明整条 trace "完成了"——agent 最终给了用户一个答复。子 span 的 ERROR 被根 span 的 OK 掩盖,只有点进去才看得见。
防法有两条:① 在告警规则里加 tool_failure_rate(见 §3.2),只要任意子 span 出现 ERROR 就计入,不依赖根 span status。② 工具层出错时,编排逻辑应把错误事件也写进根 span 的 events,或把根 span status 设为 ERROR。这是"只记 what 不记 why"陷阱(§3.4 ⑦)的典型表现。
search_flights span 是根因,右侧重试循环框内每次都把全量对话历史重喂给 LLM。注意:根 invoke_agent span status=OK——不看子 span 的工具失败率,这条 trace 根本不会进告警视野。3.2生产监控:该报警什么
传统 APM 的四个黄金指标(延迟、流量、错误率、饱和度)在 LLM 世界全部变形,工程师需要一套新的基准数集。
文档告诉读者"监控 latency 和 token"。真正的难点是:这些数在 LLM 语境下的含义与传统服务完全不同。延迟的主体是外部 I/O(等 LLM 返回、等工具 API),不是 CPU——火焰图没用;错误率里最危险的一类不会抛异常(工具 504 后 LLM 自发重试,整条 trace status=OK);成本不是 APM 的概念,而是每条 trace 的一等信号。如果没理解这些变形,把传统告警阈值搬过来就会产生大量误报和漏报。
四条核心告警指标
① cost/run(每次运行的总成本):用 gen_ai.usage.input_tokens/output_tokens 上卷到 trace 级后乘价格表计算。这是 APM 从没有的信号。告警阈值建议按历史 p90 × 2 设,超出即触发调查——差旅助理那次"成本翻 3 倍"在这里就能被抓住,不需要等用户投诉。注意:OTel GenAI semconv 不定义 gen_ai.cost.*,成本由各厂商在 ingestion 时自行计算(见第二章机制);告警规则要在 backend 里写,不能靠 span 属性直接报。
② 延迟 p95 与 TTFT:p95 端到端延迟衡量"最慢的那部分用户"体验。TTFT(time to first token,在 span 里是 gen_ai.response.time_to_first_chunk,v1.41.0 加入)衡量"用户等到第一个字"的感知延迟——流式场景里这个比总延迟更重要。这两个数的飙升通常指向:外部 API 变慢(工具超时叠加)、输入 token 数膨胀(全量历史重喂)、或检索层变慢(向量库冷启动)。火焰图对这三种原因都帮不上忙,只有 span 粒度的延迟才能定位到底是哪一跳慢了。
③ error rate(异常错误率):span 级 status=ERROR 的比率。但 §3.1 揭示了一个陷阱——工具失败不一定反映在根 span status 上。正确的做法是分两层报警:根 span error rate(trace 彻底失败)+ 任意子 span error rate(工具/检索层出了问题但 agent 试图恢复)。后者若高但前者低,恰恰说明 agent 在悄悄重试、用户不知道、但成本和延迟都在升。
④ tool-failure rate(工具失败率):execute_tool span 中 status=ERROR 的比率,按工具名分组(search_flights/book_flight/…)。这个数在传统 APM 里没有对应物,但对 agent 最有诊断价值——工具层是 agent 连接外部世界的边界,它的失败模式直接决定 agent 的重试行为和成本爆炸。告警粒度建议按具体工具名设阈值,不要只看总体。
| 传统 APM 指标 | LLM 世界的变形 | 为什么不同 |
|---|---|---|
| 延迟(p99) | p95 端到端 + TTFT | 主体是外部 I/O(LLM/工具),不是 CPU;流式场景首 token 比总延迟更关键 |
| 错误率(5xx) | 根 span 错误率 + 子 span 工具失败率 | 工具 504 后 LLM 自发重试,根 span 可能是 200 OK;语义失败不抛异常 |
| (无) | cost/run | LLM token 是直接成本;重试循环、历史膨胀会让成本指数级上升 |
| 饱和度(CPU/内存) | (工具 API 的)下游限流率 | agent 的瓶颈在外部 API,不在本地进程;火焰图对此无能为力 |
采样策略与告警的关系
这里需要预告 §3.4 会展开的陷阱:告警是建立在采样数据上的,如果采样策略选错(均匀 10% head sampling),工具失败率 22% 的实际问题在采样后只剩 9%,很可能低于阈值不触发。错误感知告警需要基于尾部采样(tail-based sampling,按 span 树最终 status 决定保留哪些 trace)——失败 trace 100% 保留,成功 trace 稀疏采样。这不是可观测性平台的默认配置,需要工程师主动设置。
3.3在线评估:obs 与 eval 的唯一接缝
在线评估是把生产 trace 采样后异步打分、写回、再提拔进离线集的闭环——它是可观测性流入 eval 的那条单向管道。
可观测性记录"发生了什么";eval(agent-eval)回答"这版比上版好吗"——两者的数据流本来是分开的。在线评估是唯一把两者连起来的机制:它在生产 trace 上跑 scorer,把分写回 trace,低分 trace 一键提拔进离线数据集。没有这条管道,离线集只能靠人工标注扩充,而生产里真正困难的 case 永远不会被发现。
底层机制(比文档深一层)
在线评估有五个步骤,每一步都有自己的工程考量:
- 采样生产 trace:不对全量 trace 打分——规模一大,scorer 本身就是成本。典型比例 1–10%,但要用分层采样:低分区域(过去 scorer 认为有问题的 trace)多采,高分区域少采。均匀采样在 bug 密度低时会漏掉几乎所有失败 trace。
- 异步 scorer:scorer 在 trace 落库后异步运行,不阻塞热路径。scorer 可以是 LLM-as-judge(调另一个 LLM 评价当前 trace 的输出质量)、规则 scorer(检查订的航班是否在差旅政策的价格范围内)、或人工标注的 golden-answer 比对。多个 scorer 可并行跑,结果分别写回。
- 把分写回 trace:scorer 的结果写成 trace 的 metadata 字段,可查询可聚合。Langfuse 把这个叫做
score对象(带name/value/comment),LangSmith 叫feedback。写回后,低分 trace 可以在 UI 里用 score 字段过滤,直接跳到"这批 trace 里最差的那 5%"。 - 低分提拔进离线集:一键把低分 trace 加进离线数据集,带上 scorer 的分和评论作为标注。下次迭代模型或 prompt 时,这批 case 进入回归集,防止下一版复现同样的失败。
- 喂回 agent-eval:离线集里的 trace 经过 curated 成为固定 dataset,进入开发期的离线 eval 流程。这是可观测性流入 agent-eval 的那条管道——此后就交给 agent-eval 处理了,不在本章重讲。
在线评估(本节):外部 scorer 对采样的生产 trace 异步打分,写回 trace 字段,供监控和数据集扩充使用。时机=运行结束后异步;主体=外部评估系统;目的=监控 + 发现 bad case。
Reflexion / 运行期自纠(agent-reasoning-patterns):agent 在推理时自己评自己来纠错,把失败翻译成语言教训写进记忆,下轮读回。时机=推理过程中;主体=agent 本身;目的=改善当次推理质量。
两者都用到"评分/判断",但接入点、主体、目的完全不同。在面试或方案评审里,把在线评估说成 Reflexion(或反过来)是常见的混淆点。
3.4投产陷阱四族
可观测性的陷阱不在"接不上",在"接上了却在三个月后以另一种方式炸"——四族陷阱对应隐私、成本、可见性、可重建四个维度。
教科书通常只讲"加可观测性的好处"。真实情况是:一套天真搭建的可观测性系统本身会带来新的风险——trace 库变成了 GDPR 受管仓库、可观测性账单比服务本身更贵、采样策略把最需要看的 bug 丢掉、span 截断让事后复盘建立在残缺证据上。四族框架把 10 条具体陷阱归类,让工程师在设计阶段就能逐族检查。
A 族 · 隐私
陷阱 ①:prompt/工具 I/O 逐字进第三方 SaaS。把 gen_ai.input.messages/output.messages(opt-in events)全量发给 LangSmith、Langfuse 这类 SaaS,等于把用户的每一条输入、agent 的每一段推理、工具返回的每一行数据,都送进了第三方服务器。如果业务涉及医疗、金融、法律,这等于在没有用户知情的情况下建了一个 GDPR/HIPAA 受管的数据仓库。修法:把含 PII 的 events 在 emit 前做 redaction,或只发脱敏版本;对敏感业务考虑自托管 backend。
陷阱 ②:推理链 PII 泄漏(比 ① 更隐蔽)。大模型在 chain-of-thought / reasoning 里会把上下文里的 PII 自然地"用上"——哪怕最终输出已经干净,中间推理步骤里可能出现用户姓名、身份证号、地址。AgentLeak 类研究(EMNLP 2025)证明了这条泄漏路径:输出级脱敏抓不到推理链里的 PII,因为 reasoning tokens 在 v1.41.0 以 reasoning.output_tokens 形式记录在 span 里,若不对这部分单独处理,脱敏就是半截子工程。
对生产 trace 做脱敏时,只对 gen_ai.output.messages(最终输出)做处理是不够的。带推理步骤的模型(o1/R1 类)的中间思维链同样可能包含 PII,且在 OTel semconv v1.41.0 里这部分以 reasoning.output_tokens 独立字段出现。对含 PII 业务的正确做法是:把 gen_ai.input.messages/output.messages/reasoning 相关 events 全部纳入脱敏管道,或直接禁用这三类 opt-in 捕获,改在日志层单独管理(自建私有存储)。
B 族 · 成本
陷阱 ③:全量 prompt+completion 捕获在规模下爆炸。一万对话 × 5 轮 = 20 万次调用/天,单条 span 含完整 prompt 约 2KB+,一天约 400MB 纯 span 数据,进了按 GB 计费的 APM/observability 平台后,账单常涨 40–200%。可观测性系统的账单超过被观测服务的情况不罕见。修法:对 gen_ai.input.messages/output.messages 按采样率有选择地捕获(这两类本就是 opt-in),不需要调试的 trace 不存全文;检查 backend 的定价模型是否适合大体积 span。
C 族 · 可见性
陷阱 ④:基数爆炸。把 user-id、url、prompt 文本当成 metric label(维度),唯一值超过 10 万后压垮时序数据库的索引,静默丢数据。高基数字段应当放进 span 的 attributes(可查询的键值对),不要放进 metric label。
陷阱 ⑤:朴素头部采样丢掉罕见 bug。均匀 10% head sampling 把错误和成功按同比例丢掉——如果工具调用失败率本来只有 1%,采样后大概率一条错误 trace 都不剩,整个监控系统"看不见"这个问题,直到失败率涨到 10% 才可能进入采样。差旅助理案例里:search_flights 失败率 22%,经 10% 采样后 scorer 只能看到 9 条里约 2 条,远低于告警阈值。修法:错误 trace 100% 保留(tail-based sampling 按最终 status),成功 trace 按采样率稀疏保留。
均匀 10% head sampling 是最常见的默认配置,也是生产中最常让团队"看不见问题"的原因。它把错误和成功按同一比例采样,而真正需要调查的失败 trace 恰恰是低频的——低到刚好被采样率丢掉。正确做法是尾部采样:先完整收集 trace,根据该 trace 最终 status(是否有 ERROR span、成本是否异常高)决定是否保留。大多数可观测性 SDK 需要工程师主动配置 tail sampler,默认不开。
陷阱 ⑥:截断掩盖失败。多数 OTel SDK 对 span attributes 有默认大小限制(4–64KB),超出部分静默截断,不报错。复盘时看到的是"残缺"的 span——工具返回了 8KB 的数据,span 里只记录了前 4KB,后半段截断,关键的错误字段就在那后半段。修法:调整 SDK 的 attribute_value_length_limit,或把大体积内容改用 events 方式记录并显式设置截断策略。
陷阱 ⑦:孤儿 span。异步执行、消息队列、跨进程的子 agent 没有正确传递 traceparent,产生的 span 没有 parent_span_id,和主 trace 断链,无法归入同一棵树。结果是:一次运行的完整执行图在 backend 里碎片化成多个孤立 span,无法整体复盘。修法:见第二章机制,context propagation 必须显式传递——每个异步/跨进程调用点都要提取并注入 traceparent。
陷阱 ⑧:只记"what"不记"why"。记了最终输出("agent 说了什么")但没记决策状态("LLM 为什么选这个工具""检索喂了哪些文档给 LLM""路由决策时的候选是什么")。失败恰恰集中在编排层——不是输出错了,而是某个中间决策选错了。没有中间状态,根因调试(§3.1)就无从做起。修法:把工具选择原因、检索上下文、路由决策等中间状态写进对应 span 的 events 或 attributes。
D 族 · 可重建
陷阱 ⑨:同步日志阻塞热路径。在 agent 的每个推理步骤里同步写日志,累计 10–50ms/步,多步工作流复合后总延迟显著上升。修法:用异步 span exporter(OTel SDK 默认提供 batch exporter),把 span 先缓冲在内存,定期批量发送,不阻塞热路径。
陷阱 ⑩:非确定杀死复现。设了 temperature=0 / seed,以为能复现任意一步。现实是:LLM 在服务端可能做负载均衡,相同 seed 不保证逐字一致;工具 API 的响应也可能随时间变化(航班价格是实时的)。不把逐字 rendered prompt + 采样参数 + 工具响应全部存下来,多步工作流在第 3 步就发散,replay 形同虚设。修法:把完整 rendered prompt(非模板,是最终发给 LLM 的消息列表)和工具响应原文存进对应 span 的 events,这才是 replay 的基础材料。
§本章 self-check
先合上教程,把答案写在纸上或编辑器里,再展开对照。直接点开等于把这一节当再读一遍。
- 差旅助理案例里,
invoke_agent根 span status=OK,但真实发生了search_flights四次 504 重试。如果监控系统只按根 span status 触发告警,这个问题会被发现吗?说明原因,并给出正确的告警设计。 - "在线评估"和"Reflexion 运行期自纠"都会对 agent 的输出做判断,但它们在时机、主体、目的三个维度上完全不同。分别说出这三个维度上两者的差异。
- 10% 均匀头部采样下,某工具的实际失败率是 3%。估算采样后每 1000 条 trace 里能观测到的失败 trace 数量,并解释为什么这是问题,以及应该用什么策略替代。
- 工程师在 span 里存下了完整的
gen_ai.output.messages,并对其做了输出级 PII 脱敏。说明这还不够的原因,以及还缺哪一类中间数据没有被脱敏覆盖。
答案(先做完再展开)
- 不会被发现。根 span status=OK 说明 agent 最终给了用户一个答复,监控系统按根 span status 触发的告警对此沉默。工具失败和重试循环全都发生在子 span 层级。正确的告警设计:分两层设独立告警——① 根 span error rate(trace 彻底失败,status=ERROR);②
execute_toolspan error rate 按工具名分组(任意子 span 出现 ERROR 就计入,不依赖根 span)。后者若持续高而前者低,说明 agent 在悄悄重试、成本和延迟在升,用户还没感知到。 - 时机:在线评估在 trace 落库后异步运行,运行已结束;Reflexion 在 agent 推理过程中实时运行。主体:在线评估的主体是外部评估系统(另一个 LLM-as-judge 或规则 scorer);Reflexion 的主体是 agent 本身(自己评自己)。目的:在线评估目的是监控 + 发现 bad case + 扩充离线数据集;Reflexion 的目的是改善当次推理质量(把失败翻译成语言教训,下轮读回)。
- 1000 条 trace × 10% 采样率 = 100 条进入采样;100 条 × 3% 实际失败率 ≈ 3 条失败 trace 能被看到。3 条很可能低于告警阈值,也很可能在统计上被当作噪声。问题在于:低频失败(1–5%)在均匀采样后变得极低频,无法触发任何基于计数的告警。替代策略:尾部采样(tail-based sampling)——先收集完整 trace,对包含 ERROR span 的 trace 100% 保留,对全部 OK 的 trace 按采样率稀疏保留。这保证每一条失败 trace 都进入观测视野,成功 trace 降采以控制存储成本。
- 带推理步骤的模型(o1/R1 类)在 chain-of-thought 中会使用上下文里的 PII。这部分推理链在 OTel semconv v1.41.0 里以
reasoning.output_tokens相关字段独立存在,不在gen_ai.output.messages里。只对输出消息做脱敏,推理链里的 PII 完全没有被覆盖。正确做法是把gen_ai.input.messages、gen_ai.output.messages、以及推理链相关 events 全部纳入脱敏管道,或直接禁用这三类 opt-in 捕获,改用私有存储单独管理。