跳到主要内容

智能体事件取证:在需要之前即刻捕获

· 阅读需 12 分钟
Tian Pan
Software Engineer

周二,客户给支持团队发了一张截图。他们的账户显示六天前有一笔他们从未要求的退款。你的 CRO 转发了这张截图,并问了一个问题:“这是怎么产生的?”你知道是智能体(agent)干的——审计日志显示 actor: refund-agent-v3。但自那以后,提示词(prompt)已经修改了四次。由于财务部门为了追求 12% 的成本削减而更换了供应商,模型 ID 在上周四进行了轮换。系统提示词是根据三个检索到的文档生成的模板,而检索索引在周一重新进行了索引。对话历史被运行时(runtime)裁剪,以适应更小的上下文窗口。

你可以告诉 CRO 是智能体做的。你无法告诉他们为什么。这种差距——即知道发生了某个操作与能够重建导致该操作的输入之间的差距——是大多数智能体团队在工程团队之外的人提出真正的取证问题时发现的。

经典的答案是“我们有日志”。经典的答案是错误的。智能体“做了”什么的日志(14:23:09 发放了 84.20 美元的退款)并不是“产生”该操作的日志。后者需要模型在决策时看到的每个输入的快照,在写入时捕获,冻结,并按一周后可以透视的内容进行索引。大多数团队在第一次事故中发现,他们捕获的是结论而不是前提。

重建究竟需要什么

要重新推导智能体的操作,你需要一个元组,在发出操作时原子化地捕获,其中包含模型所依赖的每一个变量。OpenTelemetry GenAI 语义规范在 2025 年正式确定了这些字段的基准——gen_ai.request.modelgen_ai.usage.input_tokensgen_ai.provider.namegen_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)了错误的政策,还是检索系统塞给了它错误的文档。有了结果,复盘报告就能信手拈来。

分层存储,让账单保持理智

为每个请求捕获所有这些数据产生的字节量将比你现在的日志多出好几个数量级。人们的直觉是进行采样,但采样对于取证来说是错误的 —— 你采样掉的那个请求,可能就是客户下个月要投诉的那个。正确的模式是基于尾部(tail-based)的:捕获所有内容并进行分层存储。

一个实用的布局方案:热层(Hot tier)存储在 SSD 上,保留最近 24–72 小时,方便工程师在处理活动事故时进行交互式查询;温层(Warm tier)存储在更便宜的块存储上,保留 30–90 天,用于复盘和解决近期争议;冷层(Cold tier)存储在对象存储(如 S3、GCS)上,仅保留索引元数据,存储 6 个月到数年,用于合规性检查和罕见的深度取证。冷层的字节成本比热层低大约两个数量级,而且大多数取证查询(如“给我退款编号 8472 的追踪记录”)可以通过哈希查找(hash lookup)而非全量扫描来完成,因此冷层检索的延迟代价并不重要。

这对合规性至关重要。欧盟人工智能法案(EU AI Act)要求高风险系统运营商至少保留六个月的日志,并确保这些日志允许“对算法决策进行完全的可重构性” —— 这一表述在解释时不会有任何缩水。按冷层费率计算,六个月的存储是负担得起的;按热层费率计算则不然。如果你打算在 2026 年和 2027 年实现合规而不烧光你的可观测性预算,分层存储的决策必须先于合规性决策。

按调查人员真正关注的维度建立索引

存储数据只是问题的一半,另一半是能够找到它。默认的可观测性架构按追踪 ID(trace id)和时间戳建立索引,这对于“Agent 在 14:23:09 做了什么”这样的请求没问题,但对于“显示过去 90 天内所有涉及客户记录 41822 的 Agent 决策”这样的请求则毫无用处。取证索引需要以业务实体为中心 —— 客户 ID、交易 ID、工单 ID、文档 ID —— 而不仅仅是可观测性标识符。

务实的方法是在捕获请求期间提取这些实体 ID,并将其作为单独的列或字段建立索引。当 CRO 问“Agent 对这个客户做了什么”时,你进行一次实体查询,就能找回所有涉及该客户的追踪记录,按时间排序,并包含每个记录的 Prompt 版本(rev)和工具结果。如果没有这个索引,同样的查询将演变成对一年数据的大规模全量扫描。

回放是证据,而非目标

捕获所有这些数据的理由不仅仅是为了阅读,而是为了回放。确定性回放意味着将捕获的 Prompt、解码配置、模型 ID 和工具结果输入到一个测试框架中,并得到相同的输出(或衡量指标上相似的输出,因为某些模型即使在 temperature 为 0 时也不是位级确定的)。回放将取证记录从静态日志变成了可执行的产物:你可以修改 Prompt 并重新运行,更换模型并重新运行,或者修复工具漏洞并验证 Agent 现在能否在错误的输入上产生正确的动作。

这也是你在进行模型迭代前进行回归测试的方法。在更换供应商之前,将过去 30 天的 Agent 决策通过新模型进行回放并对比差异。那些产生分歧的决策就是你原本会盲目发布到生产环境中的回归缺陷。拥有这套机制的团队将模型迭代视为常规操作;而没有的团队则将其视为一场豪赌。

这种原则在于:在需要之前就完成捕获

上面列出的每项取证能力在稳态下几乎不需要任何成本——只需在追踪行中增加几个列、一个内容寻址表或一个分层存储策略。但在你第一次需要它却发现没有它时,成本将变得极其高昂,因为没有任何事后追溯的方法能捕获模型在上周二看到的内容。发出错误退款的智能体已经消失了;Prompt 版本被覆盖了;工具结果只是一个瞬时 API 调用;对话历史被总结后就丢弃了。

在发生第一次事故之前就建立捕获机制。做到这一点的团队在第一季度看起来很慢,但在之后的每个季度都会显得很快,因为每起事故处理起来都只是对追踪存储库的一次查询,而不是由六名工程师参与、并在长长的 Slack 讨论串中进行的“考古项目”。而没做到这一点的团队最终会在一场关键会议上被问到:“是什么产生了这种输出?”而他们只能回答:“我们不知道。”

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