跳到主要内容

AI 原生日志:捕获决策过程,而不仅仅是 I/O

· 阅读需 11 分钟
Tian Pan
Software Engineer

一个客服 Agent 在 12% 的工单中生成了幻觉式的故障排查步骤。HTTP 日志全部显示 200 OK。延迟正常。错误率平稳。从每一项传统指标来看,系统都是健康的——但它却在大规模地悄悄捏造答案。

当工程师最终对决策层进行插桩后,根本原因在几分钟内便浮出水面:检索到的文档块相似度得分全部低于 0.4,对上下文的置信度为 0.28,而模型输出的置信度却显示为 0.91。这是一个巨大的不匹配——在传统日志中完全不可见,但在捕获了决策状态的追踪中一目了然。

这就是将传统日志应用于 LLM 系统时的根本问题。I/O 日志告诉你系统运行了。AI 原生日志告诉你它是否推理正确。

传统日志的根本缺陷

从 Web 服务继承下来的模式很简单:记录输入和输出,记录状态码和延迟,在错误率飙升时发出告警。这之所以有效,是因为确定性服务对相同输入产生相同输出。故障是二元的:服务要么返回了正确的结果,要么没有。

LLM Agent 打破了所有这些假设。

一个产生幻觉的客服 Agent 会返回 HTTP 200,响应看起来合情合理。检索到过时或无关文档的检索步骤会报告成功。选错工具的模型会在没有错误的情况下完成调用。系统在运行,但它没有正常工作。传统日志无法区分这些状态。

还存在一个更深层的结构性问题:传统日志是扁平的。一个 Agent 处理单个用户请求时,可能会调用三个工具、进行四次模型调用、更新两次内存状态,并根据检索结果对推理进行分支。线性日志流将整个层级结构折叠成一系列带时间戳的事件,没有任何因果关联。你看到 Agent 调用了 search_kb,然后调用了 send_response,却看不到搜索返回了垃圾数据、模型置信度崩溃、响应是被捏造出来的。

决策逻辑——真正决定 Agent 是否推理正确的内容——完全存在于那些被记录的事件之间。而在当今大多数生产系统中,它是完全不可见的。

日志行间遗失的信息

思考一下传统日志为一个三步 Agent 交互捕获的内容:

10:22:34 | tool_call | function=search_kb | status=success
10:22:35 | llm_call | model=claude-3-5 | input_tokens=2500 | output_tokens=180
10:22:36 | tool_call | function=send_response | status=success

再思考一下实际发生了什么:

  • 搜索查询与索引之间存在嵌入不匹配
  • 检索到的文档块相似度得分分别为 0.42、0.38 和 0.35——全部低于有效阈值
  • 模型识别到了低质量的上下文(置信度:0.28),但仍然产生了高置信度的输出(0.91)
  • 输出包含了从模型训练中"记住"的某个程序中捏造的步骤

这些都没有出现在日志中。三行 status=success,一个困惑的用户。

在生产故障中,未被捕获的信息类别是一致的:

  • 决策理由:Agent 为何选择这个工具而非它考虑过的备选方案?
  • 被拒绝的备选方案:Agent 考虑并拒绝了什么,置信度水平如何?
  • 置信度信号:模型的确定性在哪里下降?在哪里恢复?输入和输出置信度在哪里出现了分歧?
  • 检索质量:文档块获得了什么相似度得分?模型是否承认对上下文质量存在不确定性?
  • 状态变更:决策前读取了哪些内存?之后写入了什么?状态如何影响了下一步?
  • 中间推理:工具调用之间,模型的思维链产生了什么?

这些不是边缘情况的调试奇观。它们是最常见 Agent 故障模式的主要诊断面:幻觉、错误的工具选择、循环行为、级联检索退化。

树状追踪范式

结构性解决方案是停止将 Agent 日志视为线性流,开始将其视为树。

每个用户请求都会产生一个根追踪。每个 Agent 步骤都会从中分支:每次模型调用、工具调用、检索查询和内存操作都成为子 Span。结构化属性附加到每个 Span——不仅仅是输入和输出,还有决策元数据。

Root Span: user_query "billing issue"
├─ Span: agent_step_1
│ ├─ decision_point: tool_selection
│ ├─ intent: "retrieve customer billing information"
│ ├─ alternatives_considered: [query_crm, search_kb, escalate]
│ ├─ selected: query_crm
│ ├─ confidence: 0.92
│ └─ Span: tool_call:query_crm (latency: 342ms, result: customer_record)
├─ Span: agent_step_2
│ ├─ decision_point: response_generation
│ ├─ intent: "explain billing discrepancy to user"
│ ├─ context_confidence: 0.87
│ └─ Span: model_call (tokens: 1250→220, confidence: 0.85)
└─ Span: send_response

这种结构能做到扁平日志无法做到的几件事。首先,因果关系变得清晰:你可以看到步骤 2 的推理受到了步骤 1 检索结果的影响。其次,故障自然浮现:一个 context_confidence: 0.28 的 Span 紧挨着 output_confidence: 0.91 的 Span,立即标记出值得调查的不匹配。第三,完整的决策路径可以被重建:你不仅知道 Agent 做了什么,还知道它本可以做什么。

树状表示并非装饰性的。关于多 Agent 系统故障的研究发现,最先进的开源 Agent 正确率低至 25%,故障包括格式错误的工具调用、误解指令,以及在 15 次以上工具调用后失去连贯状态。树状追踪能精确揭示是哪个分支点导致了故障。而线性日志只是产生一堵事件墙,最后跟着一个超时。

有用的 Agent 日志条目是什么样的

一个插桩良好的 Span 同时捕获三个层面。

认知层面记录 Agent 的推理:意图、它考虑的备选方案、选择了哪个以及置信度如何。对于扩展推理模型,这包括思维链片段以及模型修正其思路的节点。这些条目看起来存储开销大,但实际上很廉价——每个决策点只有几百字节——并且是诊断 Agent 为何选择错误的主要诊断面。

操作层面记录执行情况:工具名称、参数、结果、延迟、重试次数。这是大多数团队已经在插桩的层面。它是必要的,但不充分。

上下文层面记录环境:使用了哪个提示模板版本、模型参数、检索相似度得分、Token 用量、用于关联的用户和会话标识符。这一层使追踪可重现。当你想了解 Agent 为何在周二的行为与周一不同时,提示版本和检索元数据会告诉你答案。

具体来说,一个有用的工具选择决策 Span 包含:

{
"trace_id": "a1b2c3d4",
"span_id": "x1y2z3",
"name": "agent_tool_selection",
"decision_point": "tool_selection",
"intent": "retrieve customer billing information",
"selected_tool": "query_crm",
"alternatives": ["search_kb", "escalate"],
"confidence": 0.92,
"model": "claude-sonnet-4-6",
"input_tokens": 1250,
"latency_ms": 12
}

与大多数团队实际记录的内容相比:函数名、状态、耗时。有用的 Span 大约大四倍。考虑到 LLM 可观测性数据的存储成本与推理成本相比可以忽略不计——每月几百 GB 只需几美元——这种权衡是显而易见的正确选择。

OpenTelemetry 作为新兴标准

插桩领域已基本收敛于 OpenTelemetry 作为结构性基础。OTel GenAI 语义约定——于 2024 年正式确定并现已成为官方规范的一部分——定义了标准化的属性名称,以便所有工具能一致地解释 Span:gen_ai.request.modelgen_ai.usage.input_tokensgen_ai.response.finish_reason 等。

大多数主流 Agent 框架现在都原生通过 OpenTelemetry 发出追踪。Pydantic AI、LangChain 和 smolagents 都支持 OTEL。平台层——LangChain 密集型技术栈的 LangSmith、作为开源选项的 Langfuse、RAG 密集型系统的 Arize Phoenix——消费这些追踪并在其上添加评估和告警。

关键的插桩原则是在每个决策边界处进行封装,而不是在每个函数调用处。捕获 Span 的开销来自结构和传输,而非决策元数据本身。对每一行都进行插桩的滥用方法会产生噪音,使调试更加困难。一种有针对性的方法——捕获模型调用、工具选择、检索步骤和状态变更——能产生既可用于调试又可经济存储的追踪。

一个实用的模式:对错误情况以及置信度低于阈值的 Span 采用 100% 采样;对常规成功操作应用采样。这在你需要的地方提供完整保真度,而不会产生 PB 级的冗余数据。

置信度不匹配信号

有一个特定模式值得单独指出,因为它在各种生产故障模式中反复出现:输入上下文质量与输出置信度之间的置信度不匹配。

摄入了低质量上下文的模型——无关的检索结果、被截断的工具输出、对话中相互矛盾的信息——有时仍会产生高置信度的输出。这种不匹配是幻觉的指纹。在 I/O 日志中它是不可见的,因为检索成功和模型响应成功看起来与正确的交互完全相同。

当你将检索相似度得分与模型置信度信号一同捕获时,这种模式就变得可检测。一个 retrieval_confidence: 0.31 的 Span 紧接着 output_confidence: 0.89 的 Span 应触发调查。一旦这种不匹配被捕获为结构化数据,你就可以对其构建自动告警。

同样的模式出现在工具选择中:一个 Agent 以 confidence: 0.48 选择了某个工具,而在该领域它通常以 confidence: 0.85+ 操作,这表明存在值得关注的不确定性。如果没有决策元数据,你就无法区分自信的正确行动和碰巧成功的不确定猜测。

详细追踪的实际理由

反对更丰富日志的阻力通常是存储成本或性能开销。但仔细审查后,两者都站不住脚。

存储是廉价的。一次 Agent 交互的完整详细追踪——包括所有三个层面、完整的 Span 元数据——大约为 2KB。在每月 10 万次请求的情况下,这是 200GB,在对象存储中只需几美元。与此同时,这些请求的推理成本高达数千美元。比例如此悬殊,以至于存储成本对任何认真尝试调试 Agent 行为的团队来说都不是真正的约束。

性能开销是真实存在的,但可以管理。解决方案是异步追踪发射:同步地将 Span 写入本地缓冲区,异步地刷新到可观测性后端,并接受追踪可用性上的有界延迟。大多数可观测性 SDK 默认这样做。替代方案——阻塞 Agent 执行以等待向慢速存储写入日志——才是真正的性能问题,而且它与详细日志记录无关。

更丰富追踪的实际理由是它所预防的调试成本是巨大的。在 I/O 日志中不可见的生产幻觉可能需要数小时的追踪重放和复现才能诊断。而在置信度不匹配 Span 中可见的生产幻觉只需几分钟。

你需要改变什么

AI 原生日志不是一个新库——它是应用于你已在构建的插桩中的一种规范。这种转变需要三件事。

第一,将决策点视为一等日志事件。当 Agent 选择工具时,发出包含备选方案和置信度的 Span。当检索步骤返回结果时,发出相似度得分。当模型调用完成时,将输入置信度与输出一起发出。

第二,分层结构化你的日志。事件的扁平流不是追踪。具有父子关系的 Span 树才是。大多数现代可观测性框架原生支持这一点;使用它。

第三,跨层面进行关联。贯穿每个 Span 的 trace_id——模型调用、工具调用、检索步骤、状态变更——是当出现问题时重建完整决策路径的关键。没有它,你只有一堆事件。有了它,你才有了一个故事。

在生产中可靠调试的 Agent 是那些工程师可以查看追踪,看到的不只是发生了什么,而是 Agent 做出每个选择的原因、它对什么感到不确定、以及推理在哪里出错的 Agent。实现这一点不需要奇特的工具。它需要将决策状态视为日志数据——并交付能够捕获它的插桩。

References:Let's stay in touch and Follow me for more thoughts and updates