跳到主要内容

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)”语义的对象存储,或账本式的追加日志。审计追踪必须比问题本身存在得更久。

保留周期应以月为单位,而非以天计算。一份在违规行为发生 90 天后才收到的错误报告(这在金融、医疗或任何带有延迟人工审核的工作流中都很常见)需要依然存在的证据。请根据你最慢的反馈循环来设定保留周期,而不是根据典型的循环。如果季度合规审计可能会询问上个季度的某次操作,那么你需要一个季度的完整追踪记录,而不是一周。

取证存储是一个隐私保险库,而不是普通的日志文件。它是系统中 PII(个人可识别信息)最集中的地方:完整的用户输入、完整的模型输出、完整的工具执行结果(可能包含账户详情、交易记录或医疗背景)。请以备份保险库的严谨程度来对待其访问控制和保留策略。按敏感度标记 Span,将原始负载的访问权限限制在极少数特定的命名角色中,记录每一次读取操作,并为高风险追踪记录设计独立的保留策略。这也是 GenAI 语义约定所蕴含的权衡——将 Prompt 和响应内容存储在 Event 中而非 Span 属性中,这样 Event 可以在到达索引存储之前在采集器端被过滤或丢弃。

按风险采样,而非按数量采样

对于某些工作负载,捕捉 100% 的数据成本甚至会超过推理本身。解决办法不是进行均匀采样,而是根据无法回放产生的后果来进行采样。

正确的做法是根据路径(而非请求)制定分层采样策略:

  • 对任何包含副作用工具的路径进行 100% 采样。任何涉及写入、发送、支付、删除、发布或更改外部状态的操作。无法重建一笔错误的金融交易或泄露的客户邮件,其代价是无限的。请为完整的记录付费。
  • 对高风险读取路径进行 100% 采样。任何返回受监管答案(医疗、法律、金融)或为人类“橡皮图章”决策提供支持的操作。
  • 对触发了人类介入(Human-in-the-loop)干预或护栏(Guardrail)拦截的追踪记录提高采样率。这些是事故前的信号,务必保留。
  • 对低风险聊天界面进行低采样——例如闲聊、头脑风暴、无副作用的界面。1–10% 是合理的,并保留在调试窗口期间临时切换到 100% 的选项。
  • 始终保留 100% 的元数据,即使你降低了负载的采样率。Token 计数、延迟、模型版本、Prompt 哈希、工具名称及其结果属于每一个追踪记录。这些数据的基数是有限的,成本主要集中在负载内容上。

推论:如果你的网关同时路由具有副作用的工作流和普通的聊天界面,你不能应用统一的全局采样率。记录器需要知道它正处于哪种路径上。请在调用处标记 Span,而不是在采集器处。

除非能够回放,否则记录器并非真实存在

飞行记录仪(黑匣子)有一种比丢失字段更难察觉的失效模式:它捕捉到了看似足够的数据,但实际上无法还原运行过程。Schema 发生了偏移、截断策略发生了变化、工具注册表快照只是一个指向可变文件的指针、系统 Prompt 的哈希是在模板插值前而非插值后计算的——其中任何一点都会让追踪记录变成一个看起来完整实则无用的遗迹。

证明记录器有效的唯一标准是回放(Replay)。获取一段捕获的追踪记录,将其输入到沙箱测试框架中,对每一个非确定性依赖进行打桩(Stub)(使用记录的响应替代模型,使用记录的结果替代工具,使用记录的时间戳替代时钟),并验证 Agent 的行为在采样容差范围内能够重构为字节级一致(Byte equivalence)。如果记录的采样参数和种子(Seed)产生了相同的 Token,那么你的记录就是字节级一致的;如果你在 Temperature 大于零且没有种子的情况下运行,你的记录仍应能重构控制流——即工具调用和决策的顺序——即使具体的 Token 序列有所不同。

将回放测试作为 CI 检查,运行在具有代表性的生产追踪样本上。像对待类型检查失败一样对待回放失败:构建变红。在第一次事故审查中才发现记录器没有捕获系统 Prompt 版本的团队,现在面临的是六个月无法解释的报告。而在 CI 中捕获到这一漏洞的团队,在当周就能发布修复方案。

直到第二次事故才会有人捕捉的字段

有三类证据在第一次尝试时总是被遗漏,但在第二次事故中却至关重要。

框架(Harness)状态,而不仅仅是模型状态。Agent 循环有其自身的状态:当前处于第几步、是哪个子 Agent 调用了它、预算计数器(消耗的 Token、进行的工具调用、实际耗时)、控制本次运行的功能开关(Feature flags)的值,以及选择该模型的路由层的解析配置。当 Bug 是“Agent 提前停止”或“Agent 循环了 40 次”时,模型追踪通常是正常的,框架状态才是“冒烟的枪”(确凿证据)。

任何 LLM-as-judge 步骤中的评判者配置。当 Agent 使用 LLM 来评估或在候选方案中做出选择时,评判者本身就是一个生产模型调用,拥有自己的版本、Prompt 和采样参数。它需要与主 Agent 相同的记录器规范。评判者静默地进行了版本升级,是导致“指标变好了,产品变差了”最常见的原因之一。

检索索引版本和返回的具体分块(Chunks)。不仅是查询语句,也不仅是 Top-k 的 ID——而是具体的分块文本和索引版本。索引会被重建,分块会被重新嵌入(Re-embedded),源文档会发生变化。一个因为检索到的分块是旧版本而产生幻觉的 Agent,与一个因为模型退化而产生幻觉的 Agent,是两种不同的 Bug。如果没有追踪记录中的分块文本和索引版本,你无法区分它们。

在第一次事故发生前建立记录器,而不是在之后

每个经历过这种情况的团队都会学到同样的教训:那些你没能捕捉到的字段,往往才是最重要的字段。模型版本、Prompt 哈希、完整的工具执行结果 —— 这些并不是什么奇怪的要求。在每个 span 中多写几行代码就能搞定。它们之所以被忽略,是因为目前还没有出过错,而团队正忙于交付功能。一旦出事,缺乏这些记录的代价将是信任的丧失,而不仅仅是工程时间的损耗。

标准正在趋于统一。OpenTelemetry GenAI 语义规范为模型调用层提供了一个可移植的 schema。一份关于智能体(Agent)审计追踪的 IETF 草案为自主 AI 系统提议了一种 JSON 记录格式。《欧盟 AI 法案》要求从 2026 年 8 月起,高风险 AI 系统必须自动记录事件。记录器的具体形式已不再是一个研究课题。真正的问题在于,你是在被迫解决事故之前还是之后才将它接入系统。

最廉价的方案是在你的供应商 SDK 外套一层包装(Wrapper),将上述八个字段添加到结构化日志行中,并写入一个具有 90 天生命周期的只增不减(Append-only)存储桶。而昂贵的方案则是一个完整的智能体治理框架,包含基于回放的 CI、内容寻址的 Prompt 和工具注册表,以及按风险等级划分的分层采样策略。大多数团队需要的方案介于两者之间。但没有团队应该什么都不做。智能体是一个非确定性的分布式系统。请像支付系统对待账本一样对待它的追踪(Trace)—— 这样当某天有人打电话问你“它为什么会那样做”时,你就能给出答案。

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