跳到主要内容

你的 Agent 没有留下的那本证据档案

· 阅读需 10 分钟
Tian Pan
Software Engineer

你的调用链路会记录每一个 token。它记录每一次工具调用、每一次重试、每一次检索延迟、每一个模型 id。看起来无所不包。然后,某个监管机构、某个客户、或者你自己内部的 incident 频道,抛出一个本该轻松回答的问题:模型在决策那一刻究竟看到了什么? 你这才发现,调用链路记下了"问题",却没记下模型当时正在看着回答的"答案"。

那次检索命中的 chunks 早就因为上周二的重建索引轮换出了向量存储。那次工具响应是一段流式 payload,你只保留了最终态摘要,因为完整流会让账单翻三倍。那段 system prompt 是运行时按一个 feature flag 拼装出来的,而那个 flag 此后又翻转了两次,你的 flag 服务并不按时间戳保留历史值。你对发生了什么有完整的可观测性——调用图、token 数、延迟。但你对模型当时在针对什么作答,一无所知。这道缺口,就是调用链路与决策记录之间的差别。大多数团队没意识到自己只造了两者中的一个。

调用链路告诉你形状,决策记录告诉你实质

调用链路是运维向的。它是为 SRE 的问题而造的——请求去了哪里、耗时多久、哪个调用失败了。schema 也是围着这个优化的:spans、parent id、duration、错误码、每个 span 配上稀疏的几个属性。每个现存的可观测性厂商都为 LLM 扩展了这套 schema——加上 token 数、模型 id、prompt 与 completion 字符串。这种扩展是必要的,但它与决策记录不是同一种产物。

决策记录是取证向的。它是为另一个问题而造的——这个 Agent 行动那一刻,锁定了关于世界的什么状态。这个状态包含:被检索到的那批文档,按它们被检索时的版本(而非指向可变语料库的指针);Agent 收到的工具输出原样(而非可能已经变过的二次抓取);运行时拼装出来的 system prompt(而非"模板加上 flag id");模型版本——包括任何静默的服务端切版;以及每一个守护 Agent 行为的 guardrail 与 feature flag 的配置状态。调用链路告诉你这次运行的形状。决策记录告诉你 Agent 当时手上有什么实质。

这两种产物有重叠,这正是缺口难被察觉的原因。你的调用链路里已经有了 prompt 和 completion。看起来就是决策记录。

它不是。链路里的 prompt 是在 flag 翻转之后捕获的渲染字符串。completion 是最终答案,链路告诉你模型返回了这个 completion——但它没法告诉你:产生那段 prompt 中上下文块的检索,源自一个此后已被重新嵌入、现在排在第 7 名而非第 1 名的 chunk。调用链路记下了表面。表面之下的实质,已经移动了。

你为成本而丢掉的那张快照

每一个把 LLM 可观测性做到非平凡程度的团队,都有过一段以"我们不保存完整检索 payload,只存 chunk id 和分数"收尾的对话。这个推理表面上无懈可击:一次向量检索能返回几十 KB,一个 Agent 循环每次运行能发起几十次检索,每月处理百万级决策的系统,面对的是 TB 级证据外加一条访问长尾。只存 chunk id、查询时再从向量存储里重新生成内容,是 50 倍的成本下降。于是上线的就是这个版本。

为成本而被丢掉的那张快照,恰恰就是让你能重建 Agent 行动当下"认知状态"的那张快照。半年后向量存储重建了两次。链路里的 chunk id 指向不再存在的内容——或者更糟,指向一段虽然还在、但已被重新嵌入和重新切分、现在文本略有不同的内容。

你可以拿当前的存储重跑一次检索,得到某个结果,但那个结果不是 Agent 当时看到的。你已经丧失了回答原始问题的能力——不是因为你缺可观测性,而是因为你的可观测性是一套指针系统,而它指向的东西动了。

同样的模式到处都在重演。工具响应只存最终态摘要,就丢掉了 Agent 反应过的那些流式中间态。Feature flag 只按 id 引用、不按值快照,等 flag 服务回收旧配置时,历史状态就丢了。System prompt 按"模板 id + 变量"引用,等模板被原地编辑、旧版本被覆盖时,原来的含义就丢了。每一种情况,团队都有站得住脚的成本理由。每一种情况,团队都在不全然自知地把那些决策变得不可复现。

能弥合缺口的几种模式

修复方案在概念上很简单,在架构上很烦人。原则是:决策记录必须自包含——能在不依赖任何"决策时刻存在的可变系统"的情况下被还原。能做到这一点的几种模式:

  • 每次决策一份不可变上下文包。 对每一个值得保留的 Agent 决策,落一份单独的 bundle,内含:完整拼装好的 system prompt、完整的检索内容(不是指针)、Agent 观测到的完整工具响应、模型 id(含 provider 版本)、以及 Agent 读过的每一个 flag 的配置快照。把 bundle 写入冷存储,用内容哈希寻址,在调用链路里引用它。链路保持便宜,bundle 才是证据。
  • 检索结果哈希 + 内容寻址查找。 检索到一个 chunk 时,把 chunk 内容哈希一下,把哈希存进调用链路,然后保证这个 chunk 被保留在一个以该哈希为键的内容寻址存储里。向量存储重建索引不会让 chunk 失效——它只是为同一份内容生成了新的向量,旧内容仍然可寻址。这就是为 LLVM 的内容寻址存储、以及每一个能挺过 refactor 的构建缓存撑腰的那个技巧:身份由内容决定,而非由位置决定。
  • 配置状态要快照,不要引用。 Feature flag、prompt 模板、模型版本、工具 schema,都应该以"决策时刻的值"写进调用链路,而不是以 id 引用。"按时间 T 去查 flag X"的便利,正是 flag 服务退役旧配置时最先垮掉的便利。内联存值,代价是字节;按引用存,代价是回答"过去发生了什么"的能力。
  • 按证据等级分层保留,而不是按时间。 运维链路过了 30 天就丢掉是便宜的,因为它回答的是 SRE 问题,而 SRE 问题的半衰期就是 30 天。决策记录过了 30 天再丢掉就不便宜了,因为审计、监管、或者原告律师上门的时间表,跟你的留存策略毫无关系。正确的分区不是按时间新旧,而是按关键度:例行决策可以过期,incident 级别的决策永久保留,而区分两者的策略要落在系统里,而不是某个人的记忆里。

烦人的部分是,以上没有一条是一行配置就能搞定的。每一家现存的可观测性厂商都围绕调用链路模型搭了 schema,而决策记录模型是另一种 schema——写入模式不同、留存经济学也不同。建出这一层的团队基本都是自建的,通常是在一次 incident 之后——他们在那次 incident 里发现自己回答不了那个真正要命的问题。

你与审计员之间的那段对话

让这件事从"架构观点"升级为"运营必需"的,是某个团队之外的人来找你重建一次决策的场景。可能是监管者,依据 2026 年 8 月生效的欧盟 AI 法案中针对高风险系统的审计轨迹条款——它要求的是持续的结构化合规证据,而不是政策文档。可能是诉讼里的原告,指控你的 Agent 拒绝了他们的某项服务、或批准了一笔欺诈操作。也可能是你自己的事故复盘,要追问 Agent 为什么在某个周二把某个错误答案发给了某个特定用户。

每一种情况下,对话的形状都一样。你被要求重建 Agent 当时手上有什么。你打开你那套出色的可观测性工具,自信地把调用链路调出来——spans、模型调用、工具调用、延迟。

审计员读完,问一个后续问题:Agent 检索了什么,检索内容包含什么? 你指向那些 chunk id。审计员要求看这些 chunk。你解释说向量存储已经重建过索引了。审计员在本子上写了点什么。这场对话已经暴露了一个事实:你的系统在运维意义上是可观测的,在证据意义上是不可问责的——而这两者不是同一种属性。

能体面挺过这场对话的团队,不是那些日志记得更狠的团队。是那些在这场对话还没出现之前,就已经认定"决策记录是和调用链路不一样的产物",并搭了基础设施同时产出两者的团队。挺得难看的团队,会自信地把调用链路掏出来,然后看着审计员实时地把对他们的信任度往下调。

这门必须存在的取证学科

更宏观的点是:决策时刻的证据保全,是一门有别于运维日志的学科,需要被配人、配预算、并有人 own。它不是你可观测性栈的一个特性——你的可观测性栈是为另一个问题而造的。它不是你数仓的一个特性——数仓存的是事实,不是这些事实赖以推导出来的底质。它不是你 prompt 管理工具的一个特性——那个工具给模板做了版本,但它没快照运行时拼装出来的结果,而运行时拼装出来的结果才是模型真正看到的东西。

它是它自己的一层,有自己的 schema、自己的留存策略、自己的成本模型、自己的 SLO。这个 SLO 不是 uptime;是可复现性——给我一个决策 id 和一个关于 Agent 在那一刻"认知状态"的问题,你能不能给出一个足够好的答案?能给的团队,将能用 2026 年的证据回答 2027 年的问题。不能给的团队,将发现"我们有完整可观测性"是一句现在时的话,而他们正被追问的事在过去时,而过去时,正是他们的架构没有保留下来的那个时态。

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