☰
AI Agent 可观测性实战:用 Trace ID 与 Span ID 告别黑盒排查
2026/10/5 5:08:50 网站建设 项目流程

1. 从一次线上事故说起:对话记录为什么不够用

去年冬天,我负责的一个客服类 AI Agent 在凌晨两点突然开始给用户回复完全无关的内容。用户问退货政策,它回答天气;用户问物流进度,它开始背诵产品说明书。值班同事第一反应是打开对话记录,一条条翻看用户和 Agent 的往来消息。看了半小时,只看到“输入正常、输出异常”,但完全不知道问题出在哪一步——是意图识别错了?是工具调用返回了脏数据?还是记忆模块把上一轮的内容串了?

这就是只盯着对话记录做排查的典型困境。对话记录(Conversation Log)本质上只是 Agent 与用户之间的输入输出快照,它告诉你“发生了什么”,但几乎不告诉你“为什么发生”。一个 AI Agent 在运行时,内部可能经历了意图解析、上下文组装、工具选择、参数生成、外部 API 调用、结果解析、回复生成等七八个环节,对话记录只记录了首尾两端,中间全是黑盒。

可观测性(Observability)这个词在传统后端服务里已经讲了很多年,但放到 AI Agent 场景下,它的内涵完全不一样。传统服务的可观测性主要靠 Metrics、Logs、Traces 三件套,而 AI Agent 的可观测性还要额外关注推理链路——也就是模型在每一步“想了什么”“选了哪个工具”“传了什么参数”“拿到了什么结果”。没有这层观测能力,排查故障就像在黑暗中找一根掉在地上的针。

这篇文章适合两类人看:一类是正在搭建或维护 AI Agent 的工程师,另一类是团队里负责 AI 应用稳定性的 SRE 或技术负责人。我会从实际故障场景出发,拆解为什么对话记录不够用、trace ID 和 span ID 在 Agent 场景下怎么落地、以及如何用一套可观测性体系把“黑盒排查”变成“白盒定位”。中间会穿插大量实操细节和踩坑经验,你可以直接抄作业。

2. 对话记录的三个致命盲区

2.1 盲区一:看不到中间步骤的决策依据

对话记录最致命的问题是它只记录最终输出。假设用户问“帮我查一下上周的订单”,Agent 最终回复“您上周没有订单”。这个回复可能是对的,也可能是错的。如果它是错的,问题可能出在:

  • 意图识别把“查订单”误判成了“查物流”
  • 时间解析把“上周”算成了“上上周”
  • 工具调用时传入了错误的用户 ID
  • 订单查询 API 返回了空结果,但 Agent 没有做二次确认

这四个环节在对话记录里全部不可见。你只能看到用户输入和 Agent 输出,中间发生了什么完全靠猜。更麻烦的是,AI Agent 的决策往往带有概率性——同一个输入,不同时间可能走不同的工具调用路径。没有中间步骤的记录,你连复现都做不到。

我试过在一个 LangChain 项目里只靠对话记录排查,结果花了三个小时才定位到一个简单的参数拼接错误。后来接入了 trace 体系,同样的故障五分钟就找到了根因。这个效率差距不是线性提升,是数量级的差异。

2.2 盲区二:无法关联外部依赖的调用状态

AI Agent 和普通聊天机器人最大的区别在于它会调用外部工具。查数据库、调 API、读文件、发消息,这些外部依赖的调用状态在对话记录里几乎不可见。我遇到过好几次这样的情况:Agent 回复“查询失败,请稍后重试”,但对话记录里只有这句回复,没有记录它调了哪个接口、返回了什么状态码、耗时多久。

如果外部 API 返回了 500 错误,但 Agent 的容错逻辑把它吞掉了,你从对话记录里根本看不出来。更隐蔽的是超时场景——外部 API 响应慢,Agent 等了三秒后放弃并返回兜底话术,对话记录里只会显示兜底话术,你不会知道背后有一次超时。

注意:很多 Agent 框架默认不记录工具调用的详细日志,你需要显式开启或自己埋点。LangChain 的verbose=True只能打印到控制台,生产环境需要接入结构化日志系统。

2.3 盲区三:缺少跨会话的上下文追踪

一个用户可能在一个会话里先问“我的订单在哪”,然后问“帮我退了吧”,再问“退款什么时候到”。这三轮对话在业务上是强关联的,但在对话记录里它们是三条独立的消息。如果退款环节出了问题,你需要把三轮对话串起来看,才能理解 Agent 为什么在第三轮调用了退款查询接口。

更复杂的是多用户并发场景。当几十个用户同时和 Agent 交互时,对话记录是交织在一起的。没有 trace ID 做关联,你根本分不清哪条工具调用属于哪个用户。我见过一个团队在排查并发问题时,把两个用户的对话记录混在一起分析,得出了完全错误的结论。

3. Trace ID 与 Span ID:给 Agent 装上“行车记录仪”

3.1 用生活化类比理解 Trace 和 Span

把一次 AI Agent 的完整处理过程想象成一次快递配送。Trace ID 就是快递单号,它唯一标识这一次配送任务。Span ID 则是配送过程中的每一个节点:揽件、分拣、运输、派送、签收。每个节点有自己的开始时间、结束时间、状态和附加信息。

当用户投诉“快递没收到”时,你不可能只看“已签收”这个最终状态就下结论。你需要看每个节点的记录:揽件时间对不对?分拣有没有出错?运输途中是否滞留?派送员是谁?这些信息对应到 AI Agent 场景,就是:

  • Trace ID:一次用户请求的全局唯一标识
  • Span ID:请求在 Agent 内部经过的每个环节(意图识别、工具选择、API 调用、回复生成)
  • Span 属性:每个环节的输入、输出、耗时、状态、错误信息

有了这套体系,排查故障时你不再需要“猜”,而是可以沿着 trace 树一层层往下看,直到找到异常的那个 span。

3.2 在 Agent 中落地 Trace ID 的三种方式

方式一:框架原生支持。LangChain、LlamaIndex 等主流框架已经内置了 callback 机制,可以接入 LangSmith、LangFuse 等可观测性平台。你只需要配置环境变量,框架会自动为每次请求生成 trace ID 并上报 span 数据。这种方式最省事,但灵活性受限,某些自定义环节可能覆盖不到。

方式二:手动埋点。在 Agent 的每个关键环节手动创建 span,通过 OpenTelemetry SDK 上报。这种方式最灵活,但工作量大,需要你对 Agent 的执行流程非常熟悉。我一般建议在核心链路上用这种方式,比如工具调用和外部 API 请求。

方式三:混合模式。框架原生支持覆盖通用环节,手动埋点补充自定义环节。这是我在生产环境最常用的方案。比如用 LangChain 的 callback 自动记录 LLM 调用,同时在工具函数里手动加 span 记录参数和返回值。

from opentelemetry import trace tracer = trace.get_tracer(__name__) def query_order(user_id: str, time_range: str): with tracer.start_as_current_span("query_order") as span: span.set_attribute("user_id", user_id) span.set_attribute("time_range", time_range) try: result = order_api.query(user_id, time_range) span.set_attribute("result_count", len(result)) return result except Exception as e: span.set_attribute("error", str(e)) span.record_exception(e) raise

这段代码的关键在于:span 不仅记录了“调用了 query_order”,还记录了入参、出参数量和异常信息。当订单查询返回空结果时,你可以立刻判断是 API 真的没数据,还是参数传错了。

3.3 Span 的粒度怎么定:太粗没用,太细爆炸

Span 粒度是实操中最容易踩坑的地方。我见过两种极端:一种是只记录一个顶层 span,相当于什么都没记;另一种是每个函数调用都加 span,结果一次请求产生上千个 span,存储成本爆炸,排查时也找不到重点。

我的经验法则是:按“可能出错的独立环节”来划分 span。具体来说:

  • LLM 调用:必须单独一个 span,记录 prompt、模型名、token 数、耗时
  • 工具调用:每个工具一个 span,记录工具名、入参、出参、耗时
  • 外部 API 请求:每个请求一个 span,记录 URL、状态码、耗时
  • 记忆读写:如果用了向量数据库或缓存,读写各一个 span
  • 意图识别/路由:如果这一步逻辑复杂,单独一个 span

其他辅助函数、格式化逻辑、字符串拼接之类的,不需要单独 span,否则噪音太大。

4. 从零搭建一套 Agent 可观测性体系

4.1 整体架构设计

一套完整的 Agent 可观测性体系包含四个层次:

采集层:负责在 Agent 运行时收集 trace、log、metric 数据。常用工具是 OpenTelemetry SDK,它支持多种语言和框架,数据格式统一。

传输层:负责把采集到的数据送到后端。可以用 OpenTelemetry Collector 做中转,它支持批处理、重试、采样等策略,避免数据丢失或压垮后端。

存储层:负责持久化数据。Trace 数据通常存到 Jaeger、Tempo、Zipkin 等系统;Log 数据存到 Elasticsearch、Loki;Metric 数据存到 Prometheus。

展示层:负责查询和可视化。Grafana 是最常用的选择,它可以同时展示 trace、log、metric,支持关联查询。

对于中小团队,我建议直接用 LangFuse 或 LangSmith 这类专门为 LLM 应用设计的平台,它们开箱即用,省去了搭建和维护基础设施的成本。对于有自建能力的大团队,OpenTelemetry + Jaeger + Grafana 的组合更灵活,数据也完全可控。

4.2 关键埋点位置与参数设计

埋点位置决定了你能否定位到问题。以下是我在实践中总结的必埋点清单:

埋点位置关键属性排查用途
请求入口trace_id, user_id, session_id, input_text关联用户和会话
意图识别intent, confidence, candidates判断意图是否识别错误
上下文组装context_length, memory_hits, truncated判断上下文是否丢失或超长
LLM 调用model, prompt_tokens, completion_tokens, latency判断模型是否超时或输出异常
工具选择tool_name, tool_args, selection_reason判断工具是否选错
工具执行tool_result, status, latency, error判断工具是否失败
回复生成output_text, finish_reason判断回复是否被截断
请求出口total_latency, status, error整体成功率统计

这张表里的每一行都对应一个真实的故障场景。比如“上下文组装”这一行,如果truncated=true,说明上下文被截断了,Agent 可能因为丢失关键信息而做出错误决策。再比如“工具执行”的status字段,如果大量出现timeout,说明外部依赖不稳定,需要加熔断或重试。

4.3 采样策略:全量还是抽样

生产环境全量采集 trace 数据成本很高,尤其是 LLM 调用频繁的场景。我的建议是分层采样:

  • 错误请求全量采集:任何 status 为 error 的 trace 必须完整保留
  • 慢请求全量采集:超过 P99 延迟的请求完整保留
  • 正常请求按比例采样:比如 10% 或 1%,用于统计和趋势分析
  • 特定用户全量采集:VIP 用户或测试账号的请求全量保留

OpenTelemetry Collector 支持基于属性的采样策略,你可以根据status、latency、user_tier等属性动态决定是否采样。这样既控制了成本,又保证了关键数据不丢失。

提示:采样策略不是一成不变的。上线初期建议全量采集,摸清正常和异常的分布后,再逐步调整采样率。我见过团队一上来就设 1% 采样,结果故障发生时发现关键 trace 没被采集到,排查陷入僵局。

5. 实战排查:三个真实故障的定位过程

5.1 故障一:工具调用参数错位

现象:用户反馈 Agent 查询订单时经常返回别人的订单信息。

对话记录:只显示用户问“查订单”,Agent 回复了订单详情,看起来正常。

Trace 排查:打开 trace 树,发现query_orderspan 的user_id属性值和请求入口的user_id不一致。进一步看,发现是上下文组装环节把上一个请求的user_id缓存了下来,没有随新请求更新。

根因:Agent 用了全局变量存储用户 ID,并发场景下发生串号。

修复:把用户 ID 改为请求级变量,通过参数传递而不是全局存储。

这个故障如果只看对话记录,你甚至不会意识到有问题——因为 Agent 确实返回了订单信息,只是返回了错误的订单。只有 trace 里的user_id对比才能暴露这个 bug。

5.2 故障二:LLM 输出被截断导致回复不完整

现象:Agent 回复经常在句子中间断掉,比如“您的订单预计明天”就没了。

对话记录:显示 Agent 输出确实不完整,但不知道为什么。

Trace 排查:查看 LLM 调用 span,发现finish_reason是length,说明输出达到了 max_tokens 限制被截断。再看completion_tokens数值,正好等于配置的 max_tokens。

根因:max_tokens 设置太小,复杂回复被截断。

修复:调大 max_tokens,同时在回复生成后加校验,如果finish_reason=length则触发续写或兜底话术。

这个故障的排查关键在于finish_reason这个属性。对话记录不会告诉你输出为什么断掉,但 trace 里的 LLM span 会明确记录截断原因。

5.3 故障三:外部 API 超时引发级联失败

现象:Agent 在高峰期大量返回“系统繁忙,请稍后重试”。

对话记录:全是兜底话术,看不出具体原因。

Trace 排查:按时间范围筛选 trace,发现大量query_logisticsspan 的latency超过 5 秒,status为timeout。进一步看,物流 API 的 P99 延迟从平时的 200ms 飙升到 8 秒。

根因:物流 API 在高峰期过载,Agent 没有设置合理的超时和熔断策略,导致请求堆积。

修复:给物流 API 调用设置 2 秒超时,超时后走缓存或降级话术;同时接入熔断器,连续失败达到阈值后直接跳过调用。

这个故障的排查完全依赖 trace 数据。对话记录只能告诉你“失败了”,trace 才能告诉你“哪一步失败了、失败了多少次、耗时多久”。

5.4 常见问题速查表

现象可能原因排查入口
Agent 回复答非所问意图识别错误或上下文丢失查看 intent span 和 context span
工具调用返回空结果参数错误或 API 异常查看 tool span 的 args 和 status
回复不完整max_tokens 截断或超时查看 LLM span 的 finish_reason
响应时间过长LLM 慢或外部 API 慢查看各 span 的 latency 分布
并发场景下数据串号全局变量或缓存污染对比 trace 间的 user_id 一致性
偶发性失败无法复现概率性决策或竞态条件按 trace_id 回放完整链路

6. 工具选型:自建还是用现成平台

6.1 主流方案对比

方案优势劣势适用场景
LangSmith与 LangChain 深度集成,开箱即用闭源,数据在云端,按量收费快速验证,小团队
LangFuse开源可自建,功能全面自建需要维护基础设施中大型团队,数据敏感
OpenTelemetry + Jaeger标准化,灵活可控需要自己埋点和搭建有 SRE 能力的大团队
自研方案完全定制开发成本高,容易重复造轮子有特殊合规要求

我的建议是:先用 LangFuse 或 LangSmith 快速跑起来,等业务规模上来后再考虑自建。很多团队一上来就追求自建,结果花了两个月搭基础设施,真正用来排查故障的时间反而少了。工具是手段,不是目的。

6.2 选型时容易忽略的三个点

数据保留策略:Trace 数据量很大,默认保留 30 天可能不够排查历史问题,保留一年又成本太高。我一般建议热数据保留 7 天,温数据保留 30 天,冷数据归档到对象存储保留一年。

查询性能:当 trace 数量达到千万级时,查询性能会成为瓶颈。选型时要关注平台是否支持索引优化、是否支持按属性过滤、是否支持聚合分析。

告警集成:可观测性不只是事后排查,还要能事前告警。选型时要确认平台是否支持基于 trace 属性的告警规则,比如“工具调用失败率超过 5% 时触发告警”。

7. 我踩过的坑和总结的经验

第一个坑是埋点太多导致性能下降。早期我在每个函数入口都加了 span,结果 Agent 的响应时间增加了 30%。后来精简到只保留关键环节,性能影响降到 5% 以内。埋点本身有开销,尤其是序列化和网络上报,一定要控制粒度。

第二个坑是trace 数据没有关联 log。Trace 告诉你“哪一步慢了”,但有时候你需要看那一步的详细日志才能知道“为什么慢”。后来我在 span 里加了log_ref属性,指向对应的日志 ID,排查时可以一键跳转。

第三个坑是忽略了采样策略的动态调整。有一次线上故障,我需要看某个时间段的完整 trace,结果发现那段时间正好被采样策略过滤掉了。后来我加了一个机制:当错误率超过阈值时,自动临时切换到全量采集,持续 10 分钟后恢复。

最后一个经验是:可观测性建设要趁早。很多团队等到出了大故障才想起来补埋点,但那时候已经积累了大量技术债,补起来很痛苦。我的建议是在 Agent 开发的第一天就把 trace 体系搭好,哪怕一开始只记录最基础的几个 span,后面再逐步完善。这就像盖房子先打地基,后面加楼层才稳。

如果你现在还在靠对话记录排查 Agent 故障,我强烈建议你从下一个项目开始接入 trace 体系。刚开始可能会觉得麻烦,但当你第一次在五分钟内定位到一个困扰团队一周的 bug 时,你会觉得所有投入都值得。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询