1. 从一次线上事故说起:对话记录为什么不够用
去年冬天,我负责的一个客服类 AI Agent 在凌晨两点突然开始给用户回复完全无关的内容。用户问退货政策,它回答天气;用户问订单状态,它开始背诵产品说明书。值班同事第一反应是打开对话记录,一条条翻看用户输入和 Agent 输出,试图找到"哪句话触发了异常"。
翻了两个小时,结论是:对话记录看起来完全正常。用户输入没问题,Agent 输出虽然答非所问,但格式、语气、长度都在合理范围内。没有任何一条记录能解释"为什么它突然开始胡言乱语"。
这就是我今天想聊的核心问题:AI Agent 出故障时,只看对话记录远远不够。对话记录是"结果层"的数据,它告诉你 Agent 说了什么,但不告诉你 Agent 为什么这么说。要定位一个 Agent 的故障,你需要的是"过程层"的可观测性——trace ID、span ID、工具调用链、检索命中情况、模型推理耗时、上下文窗口占用率这些指标。
这篇文章适合两类人看:一类是正在搭建或维护 AI Agent 的工程师,另一类是团队里负责 Agent 稳定性但还没建立起完整可观测性体系的技术负责人。我会从实际故障场景出发,拆解为什么对话记录不够、需要补哪些数据、怎么用 trace ID 和 span ID 把一次 Agent 调用串起来,最后给出一套可以直接参考的落地思路。
2. 对话记录的三个致命盲区
2.1 盲区一:看不到工具调用的中间状态
一个典型的 AI Agent 处理一次用户请求,内部可能经历这样的流程:接收用户输入 → 判断意图 → 决定调用哪个工具 → 执行工具 → 拿到工具返回 → 把工具结果塞回上下文 → 再次调用模型生成最终回复。
对话记录只记录了最外层的"用户输入"和"最终输出"。中间那五步,它一概不知。
我遇到过这样一个案例:Agent 调用了一个查询订单的 API,API 返回了 200 状态码,但返回体里是一个空数组。Agent 拿到空数组后,模型"自作主张"地编造了一个订单状态回复给用户。从对话记录看,Agent 回复得很流畅、很自信,完全看不出它其实拿到的是空数据。只有当你去看工具调用的 span 记录,才会发现"工具返回为空"这个关键事实。
工具调用的入参、出参、耗时、状态码、异常信息,这些都必须作为独立的 span 记录下来。否则你永远不知道 Agent 是在"有依据地推理"还是在"无依据地编造"。
2.2 盲区二:看不到检索和上下文组装过程
现在大部分 Agent 都带 RAG(检索增强生成)能力。用户问一个问题,Agent 先去向量库或搜索引擎检索相关文档,把检索结果拼进 prompt,再让模型生成回答。
对话记录里,你只能看到用户问题和最终回答。但检索命中了哪些文档、相似度分数是多少、有没有命中空结果、上下文窗口被塞了多少 token、有没有因为超长被截断——这些信息全部丢失。
我踩过的一个坑:Agent 回答一个产品参数问题时,引用了错误型号的数据。查对话记录,回答本身逻辑通顺、格式正确。后来加上检索 span 才发现,向量库返回的 top-3 文档里,第一条是正确型号,但相似度分数只有 0.62,第二条是错误型号,相似度 0.61。模型在低置信度的情况下"随机"选了第二条。如果当时能看到相似度分数,我只需要把检索阈值从 0.6 调到 0.65 就能避免这个问题。
2.3 盲区三:看不到模型层面的性能与异常
对话记录不记录模型的推理耗时、token 消耗、是否触发了限流、是否发生了重试、是否命中了缓存。而这些恰恰是 Agent 故障的高频原因。
举个真实场景:某天下午 Agent 响应突然变慢,用户侧感知是"回复要等十几秒"。看对话记录,内容都正常,只是时间戳间隔变大了。但如果你有模型调用的 span 数据,会立刻发现:模型 API 的 P99 延迟从 800ms 飙升到了 12s,原因是触发了供应商侧的限流,Agent 框架在自动重试。重试三次才成功,每次重试都消耗了完整的上下文 token。
没有这层数据,你只能猜"是不是网络问题",然后白白浪费几个小时。
3. 可观测性的核心武器:trace ID 与 span ID
3.1 用生活化类比理解这两个概念
把一次 AI Agent 处理请求的过程想象成一次快递配送。
trace ID就是这次配送的运单号。从用户下单那一刻生成,贯穿整个配送流程。无论这个包裹经过多少个中转站、多少个快递员,运单号不变。在 Agent 场景里,一次用户请求从进入到返回,生成一个唯一的 trace ID,所有相关的日志、指标、事件都挂在这个 ID 下。
span ID是配送过程中每一个环节的编号。比如"揽收"是一个 span,"分拣"是一个 span,"运输"是一个 span,"派送"是一个 span。每个 span 有自己的开始时间、结束时间、状态、属性。在 Agent 场景里,"意图识别"是一个 span,"工具调用"是一个 span,"检索"是一个 span,"模型生成"是一个 span。
有了这两个 ID,你就能把一次 Agent 调用完整地"串"起来。出故障时,先通过 trace ID 找到这次请求的所有 span,然后按时间顺序看哪个 span 出了问题、耗时异常、或者返回了错误状态。
3.2 一个 Agent 请求应该拆成哪些 span
根据我的实践经验,一个生产级 AI Agent 的请求,至少应该拆成以下几类 span:
| Span 类型 | 记录内容 | 排查价值 |
|---|---|---|
| 请求入口 span | 用户 ID、会话 ID、输入文本、时间戳 | 定位是哪个用户、哪个会话出问题 |
| 意图识别 span | 识别结果、置信度、耗时 | 判断是否意图误判导致后续全错 |
| 检索 span | 查询语句、命中文档 ID、相似度分数、耗时 | 定位检索质量问题 |
| 工具调用 span | 工具名、入参、出参、状态码、耗时、异常 | 定位外部依赖问题 |
| 模型生成 span | 模型名、prompt token 数、completion token 数、耗时、是否重试 | 定位模型层性能与成本问题 |
| 上下文组装 span | 最终 prompt 长度、截断情况、各来源占比 | 定位上下文窗口溢出问题 |
| 响应出口 span | 最终输出、总耗时、状态 | 汇总本次请求整体情况 |
这张表不是让你一次全上,而是给你一个"应该覆盖哪些维度"的清单。初期可以先上请求入口、工具调用、模型生成这三个最关键的 span,后续再逐步补齐。
3.3 trace ID 怎么生成和传递
trace ID 的生成时机很关键。我的建议是在请求进入 Agent 系统的第一跳就生成,通常是在 API 网关或 Agent 的入口控制器里。生成方式用 UUID v4 或者雪花算法都可以,核心要求是全局唯一。
传递方式取决于你的架构。如果是单体应用,用线程上下文(ThreadLocal)传递即可。如果是微服务或分布式架构,需要把 trace ID 放进请求头,逐层透传。Agent 调用外部工具时,也要把 trace ID 带过去,这样工具侧的日志也能关联回来。
一个容易忽略的点:Agent 内部如果有异步任务(比如后台反思、记忆写入),这些异步任务也必须继承主请求的 trace ID,否则它们产生的日志会成为"孤儿数据",排查时根本找不到。
4. 落地一套 Agent 可观测性体系:从零到可用
4.1 技术选型:不要重复造轮子
可观测性领域已经有成熟的标准和工具,没必要自己从零写一套。核心选择是OpenTelemetry作为数据采集标准,后端存储和展示可以选择 Jaeger、Grafana Tempo、或者商业化的观测平台。
OpenTelemetry 的好处是它定义了统一的 trace、span、metric、log 数据模型,并且有各语言的 SDK。你用 Python 写 Agent,就用 opentelemetry-python;用 Java 写,就用 opentelemetry-java。采集到的数据格式统一,后端可以自由替换。
对于 AI Agent 场景,还需要额外关注GenAI 语义约定。OpenTelemetry 社区已经在制定针对大模型调用的 span 属性标准,比如gen_ai.system、gen_ai.request.model、gen_ai.usage.input_tokens这些字段。遵循这些约定,能让你的数据在通用观测工具里直接可用,不用自己定义一套私有格式。
4.2 最小可用方案:三个 span 先跑起来
如果你现在什么都没有,我建议先上最小可用方案,只埋三个 span:
- 入口 span:记录用户输入、会话 ID、trace ID 生成。
- 模型调用 span:记录模型名、输入 token 数、输出 token 数、耗时、是否重试。
- 工具调用 span:记录工具名、入参、出参、耗时、异常。
这三个 span 覆盖了 Agent 故障的 80% 场景。入口 span 帮你定位"是谁、在什么会话里出的问题";模型 span 帮你定位"是模型慢、模型贵、还是模型被限流";工具 span 帮你定位"是外部依赖挂了还是返回了脏数据"。
埋点方式用装饰器或上下文管理器最省事。比如在 Python 里,可以写一个@trace_span("tool_call")装饰器,自动给函数加上 span 记录。这样业务代码改动最小,侵入性低。
4.3 数据采集后的存储与查询
trace 数据的量级比普通日志大得多。一次 Agent 请求可能产生 10 到 50 个 span,每个 span 又有若干属性。如果 QPS 是 100,一天就是几千万到几亿条 span 记录。
存储选型上,Jaeger 适合中小规模,Grafana Tempo 适合和现有 Grafana 体系集成,ClickHouse 适合自建大规模存储。我的经验是:如果团队已经有 Grafana,直接上 Tempo 最省事;如果是从零开始且数据量大,ClickHouse 的压缩比和查询性能更优。
查询时,核心入口是 trace ID。用户报障时提供 trace ID(或者通过会话 ID 反查 trace ID),你就能拉出完整的调用链。此外,建议建立几个常用查询视图:按错误状态过滤、按耗时排序、按工具名聚合。这些视图能让你在故障发生时快速缩小范围。
5. 实战排查:三个真实故障的完整定位过程
5.1 故障一:Agent 突然开始"胡说八道"
现象:某天下午,客服 Agent 对部分用户的问题开始给出无关回答,比例大约 5%。
排查过程:先按时间范围过滤出异常 trace,发现这些请求有一个共同特征——模型生成 span 的input_tokens数值异常高,达到了 12000 以上,而正常请求只有 2000 左右。
进一步看上下文组装 span,发现检索环节返回了 20 多篇文档,全部被塞进了 prompt。原因是向量库的 top_k 参数被误改成了 20(正常是 3)。上下文过长导致模型注意力分散,开始忽略真正的用户问题。
解决:把 top_k 改回 3,同时加上上下文长度上限的硬性截断。另外在检索 span 里增加了一个告警规则:单次检索返回文档数超过 5 篇时触发告警。
5.2 故障二:工具调用超时导致 Agent 卡死
现象:用户反馈 Agent 有时"转圈圈"很久才回复,有时直接超时无响应。
排查过程:按耗时排序 trace,找到 P99 耗时超过 30s 的请求。看工具调用 span,发现某个查询物流的 API 耗时达到了 28s,而正常情况是 200ms。Agent 框架没有设置工具调用超时,一直在等。
解决:给所有工具调用加上超时配置,默认 5s,超时后返回降级结果(比如"暂时查不到物流信息,请稍后再试")。同时在工具 span 里记录超时事件,方便后续统计哪些工具最不稳定。
5.3 故障三:模型限流引发的连锁反应
现象:晚高峰时段 Agent 错误率飙升,但错误信息五花八门,有超时、有格式错误、有空回复。
排查过程:看模型生成 span,发现大量 span 的status是ERROR,错误类型是rate_limit_exceeded。Agent 框架在自动重试,但重试间隔太短,三次重试全部撞在限流窗口上。重试失败后,框架返回了空结果,下游的格式化逻辑拿到空结果又抛出了格式错误。
解决:调整重试策略,改为指数退避,初始间隔 1s,最大重试 3 次。同时在模型 span 里记录重试次数,当单次请求重试超过 2 次时触发告警,提示可能需要扩容或切换模型供应商。
6. 常见问题与排查技巧速查
6.1 trace 数据太多,怎么快速定位问题
这是最常见的抱怨。我的经验是建立"三层过滤"机制:
第一层按时间过滤,先锁定故障发生的时间窗口。第二层按状态过滤,只看ERROR或耗时超过阈值的 trace。第三层按业务维度过滤,比如按用户 ID、会话 ID、工具名。三层下来,通常能把范围从几万条缩小到几十条。
另外,建议给 trace 打上业务标签,比如tenant_id、agent_version、channel。这样出问题时可以快速判断"是某个租户的问题还是全局问题"、"是新版本引入的还是老问题"。
6.2 span 属性记什么、不记什么
记什么:能帮你定位问题的关键信息。工具名、状态码、耗时、token 数、检索命中数、模型名、重试次数。
不记什么:完整的用户输入和模型输出。这些内容体积大、可能含敏感信息,而且对话记录里已经有了。span 里最多记一个摘要或哈希值,需要看全文时再去关联对话记录。
一个实用技巧:在 span 里记录
input_hash和output_hash,排查时用哈希值去对话记录里反查原文。这样既保证了可观测性,又避免了敏感数据在 trace 系统里扩散。
6.3 采样率怎么设置
全量采集 trace 成本很高。我的建议是分层采样:
- 错误请求:100% 采集。
- 慢请求(超过 P95):100% 采集。
- 正常请求:按 1% 到 10% 采样,具体比例看你的流量和存储预算。
这样既保证了问题请求不丢失,又控制了整体成本。OpenTelemetry 支持基于属性的采样策略,可以很方便地实现这套规则。
6.4 异步任务和后台反思怎么追踪
Agent 经常有后台任务,比如记忆写入、反思总结、定时清理。这些任务如果不继承 trace ID,就会变成"孤儿"。
解决方案是在创建异步任务时,显式传递 trace ID 和 parent span ID。在 Python 的 asyncio 里,可以用contextvars来传递上下文。在 Java 里,可以用ExecutorService的装饰器来包装任务提交。核心原则是:任何由主请求触发的异步工作,都必须继承主请求的 trace 上下文。
7. 我踩过的坑和几条实在建议
第一个坑是"埋点太多导致性能下降"。初期我恨不得每个函数都加 span,结果 Agent 的响应时间增加了 15%。后来精简到只埋关键路径,性能影响降到了 2% 以内。span 不是越多越好,关键是覆盖故障高发环节。
第二个坑是"trace ID 在跨服务调用时丢失"。Agent 调用外部工具时,如果工具是另一个团队维护的服务,他们可能不认你的 trace ID。解决办法是在请求头里带上标准化的traceparent字段,推动上下游一起遵循 OpenTelemetry 标准。如果推不动,至少在自己的日志里记录"出站请求的 trace ID",方便人工关联。
第三个坑是"只看 trace 不看 metric"。trace 适合定位单次请求的问题,但发现趋势性问题要靠 metric。比如"工具调用失败率从 0.1% 涨到 2%",这种趋势在 trace 里很难看出来,但在 metric 图表里一目了然。我的建议是 trace 和 metric 配合使用,trace 定位个案,metric 发现趋势。
最后一个建议:可观测性建设要趁早。很多团队是出了大故障才开始补,但那时候往往已经积累了大量技术债,补起来很痛苦。如果你现在正在搭建 Agent,哪怕功能还没完善,也先把 trace ID 和基础 span 埋上。这东西就像买保险,平时感觉不到价值,出事时能救命。
关于后续扩展,我最近在尝试把 Agent 的决策过程也纳入可观测性范围,比如记录模型在每一步的"思考摘要"(不是完整思维链,而是关键决策点)。这样排查时不仅能看到"调用了什么工具",还能看到"为什么选择这个工具"。这块还在摸索阶段,等有成熟经验了再单独写一篇分享。