跳到主要内容

Agent 飞行记录仪:在第一次事故发生前必须捕获的字段

· 阅读需 14 分钟
Tian Pan
Software Engineer

当 agent 在生产环境中第一次失控时——它删错了行,给错误的客户发了邮件,在单个任务上烧掉了 400 美元的推理费用,或者对受监管的用户说了法律风险极高的话——团队打开日志,却发现他们实际上拥有的是:一串参数被截断的 CloudWatch 工具调用名,一个只捕获了最新一轮对话的“用户提示词”字段,而且没有记录实际运行的是哪个模型版本。供应商在两周前滚动更新了别名。系统提示词存在于一个没有快照的配置服务中。由于框架默认值是 0.7 且“人尽皆知”,因此没有记录温度。触发错误操作的工具结果超过了日志行大小限制,并被截断为“...”。

你无法重现决策过程。你只能猜测。六个月后,你堆积了一堆无解的“它为什么这么做”的报告,团队开始像对待天气一样对待 agent——把它当作一种发生在你身上的事情,而不是你可以调试的东西。

飞行记录仪准则(Flight recorder discipline)是你为了防止这种情况所能交付的最廉价的东西,但如果你等到第一次事故发生才开始,它也将是你交付的最昂贵的东西。以下字段是最低要求,存储形式不容商量,采样和隐私边界必须同步设计,而不是事后修补。

为什么现有的日志无法重现决策过程

经典的请求日志记录假设请求是输入,响应是输出。对于 LLM agent 来说,这两者都不成立。“输入”是一个组合体:一个本身就是大型版本化产物的系统提示词(system prompt),一个模式(schema)属于契约一部分的工具注册表,一个由非确定性截断策略组装的上下文窗口,从本身也会漂移的索引中提取的 RAG 注入分块,改变输出分布的采样参数,以及通常是一个供应商可以在不通知的情况下重新指向的别名模型标识符。“输出”是一个序列:一系列通过多轮模型交织在一起的工具调用和工具结果,可能还带有供应商仅在有条件时才暴露的思考令牌(thinking tokens)。

如果记录中缺失了这些输入中的任何一项,你就无法重现决策过程。如果你无法重现它,你就无法判断 agent 做错事是因为模型退化、提示词漂移、索引过时、工具返回了超预期的字节,还是采样时的随机性。于是每一次复盘最终都变成了“我们更新了提示词,现在看起来好多了”。

经典的 SRE 习惯——记录指标、延迟和采样部分请求体——是不够的。Agent 是一个非确定性的分布式系统,其可观测性需求更接近支付账本,而不是无状态 API。你需要一份只增(append-only)记录,包含跨越边界进出模型的所有字节,以及产生这些字节的版本化上下文,并保留足够长的时间以支撑到 bug 报告产生的时刻。

记录仪必须捕获的最小字段集

这里没有可选字段。以下每一项都曾是某人复盘中的关键证据。

解析后的模型标识,而非别名。 记录供应商返回的实际模型版本,而不是你发送的别名。claude-sonnet-4 是一个别名,claude-sonnet-4-20250929 是一个版本。别名会滚动,版本不会。如果你的供应商在响应中返回了 model 字段,请记录该字段,而不是你在请求中填写的那个。如果没有返回,请在请求中固定到特定版本标识符,永远不要在生产环境中使用裸别名。

系统提示词内容哈希及指向不可变产物的指针。 系统提示词是一个经常变动的大对象。在每个 span 中存储全文是浪费的,且会增加 PII(个人身份信息)暴露面。在每次调用时存储一个内容哈希(所有模板变量填充后的渲染提示词的 SHA-256),并将提示词正文本体存储在以该哈希为键的不可变注册表中。这与 Git 的内容寻址存储模式相同,原因也一样:你希望每条追踪都能精准指向运行时的内容,即使提示词之后已经更新了 40 次。

完整的工具注册表快照,而非仅名称。 工具的模式(schemas)是输入的一部分——它们影响模型的选择和参数生成。上周二增加了一个可选参数的工具即使没有改动 agent 代码,也会改变 agent 的行为。通过内容哈希对完整的工具注册表(名称、描述、JSON schemas)进行快照,就像处理系统提示词一样,并在每次 agent 运行时固定该哈希。

每一个采样参数。 Temperature、top-p、top-k、max tokens、存在感惩罚(presence penalty)和频率惩罚(frequency penalty)、停止序列(stop sequences),以及如果你设置了 seed(种子)。OpenTelemetry GenAI 语义规范将这些大部分形式化为 gen_ai.request.* 属性;如果你是从零开始,请遵循该模式,以便你的记录仪可以在不同供应商之间迁移。

发送的完整上下文。 不仅仅是最新的一轮用户输入。包括完整的消息数组:所有之前的轮次、所有传回的工具结果,以及注入的确切 RAG 分块——每个分块都要标记来源标识符及其所属的索引版本。如果你的截断策略丢弃了之前的轮次,请记录丢弃了什么以及原因。如果你无法在字节层面等价地回答“模型看到了什么”,你的记录仪就是不完整的。

完整响应,包括供应商暴露的思考令牌。 有些事故只有通过阅读模型的推理链才能调试。当 API 返回思考输出时捕获它,并使用与可见响应相同的访问控制进行存储,以同样的严谨性对待其保留期限。

带有完整参数和完整结果的每一次工具调用。 模型输出的参数、处理后的最终参数、工具返回的结果(完整负载,不要截断)以及延迟。具有副作用的工具——任何涉及写入、发送、支付或删除的操作——需要 100% 捕获。没有例外,不进行采样,不设日志行大小限制。

连接整个任务的稳定会话和追踪标识符。 单个用户任务可能会在多轮 agent 对话中产生数十次模型调用。它们必须能关联起来。OpenTelemetry GenAI 规范为此提供了 gen_ai.conversation.idgen_ai.agent.id。请使用它们。

仅追加、数月的保留周期、独立的保险库

存储的形式与字段本身同样重要。有三个属性是不可逾越的底线。

存储必须是**仅追加(Append-only)**的。可以被编辑的取证记录称不上是取证记录。如果同一次事故审查会因为查看者的先后顺序不同而产生不同的证据,那么在受监管的语境下,这条记录将毫无价值;在任何语境下,其价值也极其有限。请使用带有版本控制和“单次写入(write-once)”语义的对象存储,或账本式的追加日志。审计追踪必须比问题本身存在得更久。

加载中…
References:Let's stay in touch and Follow me for more thoughts and updates