智能体事件取证:在需要之前即刻捕获
周二,客户给支持团队发了一张截图。他们的账户显示六天前有一笔他们从未要求的退款。你的 CRO 转发了这张截图,并问了一个问题:“这是怎么产生的?”你知道是智能体(agent)干的——审计日志显示 actor: refund-agent-v3。但自那以后,提示词(prompt)已经修改了四次。由于财务部门为了追求 12% 的成本削减而更换了供应商,模型 ID 在上周四进行了轮换。系统提示词是根据三个检索到的文档生成的模板,而检索索引在周一重新进行了索引。对话历史被运行时(runtime)裁剪,以适应更小的上下文窗口。
你可以告诉 CRO 是智能体做的。你无法告诉他们为什么。这种差距——即知道发生了某个操作与能够重建导致该操作的输入之间的差距——是大多数智能体团队在工程团队之外的人提出真正的取证问题时发现的。
经典的答案是“我们有日志”。经典的答案是错误的。智能体“做了”什么的日志(14:23:09 发放了 84.20 美元的退款)并不是“产生”该操作的日志。后者需要模型在决策时看到的每个输入的快照,在写入时捕获,冻结,并按一周后可以透视的内容进行索引。大多数团队在第一次事故中发现,他们捕获的是结论而不是前提。
重建究竟需要什么
要重新推导智能体的操作,你需要一个元组,在发出操作时原子化地捕获,其中包含模型所依赖的每一个变量。OpenTelemetry GenAI 语义规范在 2025 年正式确定了这些字段的基准——gen_ai.request.model、gen_ai.usage.input_tokens、gen_ai.provider.name、gen_ai.operation.name——但这些规范只是底线,而非上限。一份完整的取证记录需要更多内容。
最小可捕获集合包括:模型接收到的完整提示词(在模板化、检索注入和工具输出交错之后)、来自供应商的模型 ID 和确切版本字符串、每一个解码参数(temperature、top-p、top-k、max tokens、频率和存在惩罚、停止序列、如果固定了的话还有 seed)、系统提示词版本哈希、请求时序列化的工具 schema、模型在对话中直到该轮次所摄取的每个工具结果、调用时你自有的数据库输入记录、用户身份和租户、智能体框架(harness)的运行时版本,以及任何影响输出过滤的安全策略或护栏配置。
反复出现的错误是假设“我记录了提示词”就足够了。编写代码时的提示词不是模型看到的提示词。当它到达模型时,它已经被检索、前几轮的工具结果、上下文溢出时的截断、框架添加的隐藏后缀,以及安全分类器将“用户请求 X”重写为“用户请求 X(已过滤)”所改变。取证日志捕获的是突变后的形式,而不是突变前的形式。如果你只能记录其中之一,请记录突变后的形式。
解码参数:沉默的第三轴
大多数团队会对提示词和模型进行版本化。几乎没有人以同样的纪律对解码配置(decode config)进行版本化,而解码配置的重要性超乎人们的想象。Temperature 从 0.2 变为 0.4 会实质性地改变输出分布;top_p 从 0.95 变为 1.0 会拓宽长尾;将存在惩罚(presence penalty)从 0 翻转到 0.5 会导致模型避免重复,其表现看起来就像是行为退化。如果你用错误的解码配置重现事故,你会得到不同的输出,并错误地得出模型“变好了”或“变差了”的结论,而事实上你只是改变了那个你忘记固定的变量。
务实的规则是:将解码配置视为提示词的一部分。将其与提示词主体一起哈希,将哈希值存储在追踪(trace)中,并将完整的解码配置存储在以该哈希值为键的内容寻址表(content-addressed table)中。当相同的配置在数百万次请求中重复出现时,你只需存储一次。当你重现时,通过哈希获取完整配置并重新创建确切的推理路径。
内容寻址的提示词版本
提示词版本通常作为命名标签(如 "prompt-v3.1")或内联字符串进行管理。这两者在取证上都是失败的。命名标签可以被重写——队友修复了一个拼写错误并在不增加版本号的情况下重新发布了 "prompt-v3.1"。内联字符串 体积庞大,会让每一行追踪数据变成在数百万次请求中重复的数 KB 文本。正确的原语是内容寻址:获取规范化提示词模板的 SHA-256,将提示词主体一次性存储在以该哈希为键的 prompt_revs 表中,并在追踪中仅存储哈希。
这同时为你提供了三样东西:去重(无论流量如何,相同的提示词版本只存储一次)、篡改证据(如果主体改变,哈希也会改变,因此队友无法悄悄重写历史),以及事故响应期间琐碎的差异比对(提示词版本 a3f9... vs 提示词版本 b2c4...——提取两个主体并运行文本比对)。哈希链审计追踪(Hash-chain audit trails)扩展了这一思路:每个条目的哈希都包含前一个条目的哈希,因此攻击者无法在两个时间戳之间插入事件而不破坏链。AuditableLLM 是一个使用这种结构的已发布框架;你可以在不采用框架的情况下,用五十行应用程序代码实现核心思想。
对话历史是那个陷阱
在生产环境中,最令团队头疼的问题是:第 N 轮的对话历史并不等同于 Agent 在第 N 轮所“看到”的历史。在上下文窗口的压力下,运行时(Runtimes)会进行截断、总结和压缩。有些 Agent 每隔 K 轮就进行一次总结并丢弃原始记录;有些会将工具结果内联(inline)并在增长过快时进行修剪;还有些会预置一个滚动的“记忆”缓冲区,根据近期性(recency)进行条目置换。
当你重构某个事故并尝试用完整的对话记录进行回放时,往往无法复现当时的动作,因为模型从未见过完整的对话记录。你必须捕获真正发送给模型的线上传输数据,做到逐字节一致,且包含已经应用过的截断和总结。这很繁琐,因为截断后的形式比“相对于上一轮的增量”要大得多,而大多数团队默认只记录增量。请拒绝这种默认做法。线上的原始形式是唯一可以进行回放的形式。
工具结果即证据
在 Agent 系统中,模型的行为处于工具输出的下游。搜索工具返回了过时的文档;数据库查询返回了已被更新的行;定价 API 返回了旧的报价。Agent 的“错误”决策在它收到的工具结果前提下可能是正确的 —— 这通常也是你的 CRO 真正需要听到的答案,因为它将复盘的主题从“模型坏了”转变为“我们的检索索引在周二过时了”。
将每一个工具的输入和输出都作为追踪(trace)的一部分进行捕获,并采用相同的内容寻址方法。如果工具结果很大(例如来自搜索索引的 4MB JSON 对象),请将正文存储在对象存储中,并在追踪记录中保存其 SHA 哈希值。加拿大航空(Air Canada)聊天机器人的事件中,机器人引用了与官网其他页面矛盾的退款政策。如果你在决策时刻捕获了检索结果,这类事件的调查就会变得非常简单;如果没有捕获,则根本无法解决。没有捕获的检索结果,你无法判断是模型幻觉(hallucinated)了错误的政策,还是检索系统塞给了它错误的文档。有了结果,复盘报告就能信手拈来。
