跳到主要内容

确定性重放:如何调试永远不会以相同方式运行两次的 AI Agent

· 阅读需 12 分钟
Tian Pan
Software Engineer

你的 Agent 上周二在生产环境出了故障。一个客户报告了错误的回答。你调出日志,看到最终输出,也许还有几条中间的 print 语句——然后你就卡住了。你无法重新运行 Agent 来重现同样的故障,因为模型不会产生相同的 token,你的工具调用的 API 现在返回了不同的数据,嵌入在提示词中的时间戳也已经变了。Bug 消失了,你只能盯着间接证据发呆。

这就是 AI Agent 的根本调试问题:传统软件是确定性的,所以你可以通过重建输入来复现 bug。Agent 系统不是。每次运行都是模型采样、实时 API 响应和时间依赖状态的独特组合。没有专门的工具,事后调试就变成了取证猜测。

确定性重放通过在执行过程中记录每个非确定性来源,并在重放时替换这些记录来解决这个问题——把你无法复现的 Agent 运行变成你可以像调试器一样逐步跟踪的东西。

为什么"把 Temperature 设为零就行了"行不通

大多数工程师的第一直觉是从源头消除随机性:设置 temperature=0,然后宣称它是确定性的。这是一个让团队浪费无数调试时间的误区。

Temperature 只控制 token 采样步骤——它让贪婪解码选择最高概率的 token。但输入到选择过程的 logits 本身就是可变的。主要原因是批处理组合。云推理提供商将多个请求捆绑到同一个 GPU 上以提高吞吐量。你的请求与其他用户的请求共享计算资源,而它落入的具体批次会影响浮点中间结果。一个以 0.0000001 的微弱优势领先的 token 可能在不同的批处理上下文中失败。argmax 翻转,选择了不同的词,剩余的生成就偏离到一条全新的路径上。

硬件异构性使情况更糟。云提供商的 GPU 集群包含多种架构——H100、A100,有时还有更旧的卡。不同的 GPU 架构以略有不同的数值行为实现矩阵运算。你的请求可能在一次运行中命中 H100,在下一次命中 A100。跨异构集群的逐位可复现性在实践中是不可能的。

即使 OpenAI 和 Anthropic 也承认这一点。OpenAI 的 seed 参数改善了可复现性,但明确表示不保证它。Anthropic 的文档声明,即使 temperature=0.0,结果也不会完全确定。研究人员对五个配置为确定性输出的 LLM 进行调查,发现跨运行的准确率变化高达 15%,最佳和最差性能之间的差距达到 70%。

对于单次 LLM 调用,这种变化可能是可容忍的。对于多步 Agent 工作流——每一步的输出成为下一步的输入——微小的偏差会复合放大。第 2 步中略有不同的措辞可能导致第 3 步中完全不同的工具调用,而这会向第 4 步输入不同的数据。到了第 10 步,两次运行已经面目全非。

架构:记录一切,精确重放

确定性重放借鉴了系统工程中一个成熟的模式:记录-重放调试。游戏引擎用于重放、分布式系统用于故障诊断的同一思想,直接适用于 Agent 系统。

核心架构有两种模式:

记录模式正常运行 Agent,使用真实的 LLM 和真实的工具,但拦截并记录每一次外部交互。重放模式将这些记录的交互按精确顺序回放给 Agent,用记录的响应替代实时调用。

每一步需要记录的内容:

  • LLM 交互:发送的完整提示词、所有采样参数(temperature、top_p、max_tokens)、模型标识和版本,以及返回的精确响应 token
  • 工具调用:函数名、传递的参数,以及完整的响应——包括错误、超时和部分结果
  • 系统状态:时间戳(使时间依赖逻辑能相同地重放)、随机种子,以及 Agent 读取的任何环境变量
  • 决策元数据:如果你的 Agent 有路由逻辑、规划器或分类器,将它们的输入和输出作为独立事件捕获

每个记录的事件获得一个单调递增的步骤 ID、一个用于关联的运行 ID,以及结构化的输入/输出负载。格式应该是追加写入和可流式的——JSONL 在实践中效果很好。

关键洞察是,在重放期间,你不重新调用模型或工具。你替换成确定性存根——代理对象,在被匹配的输入调用时逐字返回记录的响应。模型存根返回记录的精确 token。工具存根返回记录的精确 API 响应。如果 Agent 尝试进行未在记录中的调用,重放引擎会大声失败,而不是静默穿透到实时系统。

检查点重放 vs. 完整重放

确定性重放有两种风格,正确的选择取决于你的调试需求。

完整重放从 Agent 运行的开头开始,按顺序重放每一步。这给你完整的执行路径,但对于长时间运行的 Agent 可能很慢——想象一下为了检查第 47 步的故障而重放一个 50 步的研究 Agent。

检查点重放在每个步骤边界捕获完整的 Agent 状态,让你可以直接跳转到执行中的任何点。这就是 LangGraph 的时间旅行等系统所实现的:每个状态转换都作为检查点持久化,你可以从任何保存的检查点分叉执行,探索替代路径。

检查点重放在交互式调试中更强大。你可以:

  • 直接跳到故障点而不重放早期步骤
  • 从检查点分叉并注入不同的输入来测试假设
  • 通过从同一检查点用不同的模型响应分支来比较执行路径

代价是存储。完整重放只存储事件日志——外部调用的输入和输出。检查点重放在每一步存储完整的 Agent 状态,可能包括大型上下文窗口、累积的工具结果和内部规划状态。对于处理文档的 RAG Agent,单个检查点可能是几兆字节的累积上下文。

在实践中,混合方法效果很好:为每次运行存储完整的事件日志(相对紧凑),仅为触发警报、失败或被标记审查的运行存储完整检查点。

存储成本:重放的实际开销

对确定性重放的一个常见反对意见是存储成本。让我们算一算。

一个典型的 Agent 步骤涉及一次 LLM 调用,包含一个提示词(可能 2-4K token 的上下文加上新消息)和一个响应(几百个 token)。原始文本大约是每步 10-15 KB。加上工具调用负载(API 请求和响应体),大约每次工具交互 1-5 KB。对于 10 步的 Agent 运行,你需要大约 100-200 KB 的跟踪数据。

按云存储价格计算,这基本上是免费的——每百万次 Agent 运行几分钱。真正的成本不是原始存储,而是围绕它的可观测性基础设施:索引跟踪以供搜索、按合规窗口保留、以及运行重放引擎本身。

昂贵的情况是处理大型文档或图像的 Agent。如果你的 Agent 获取一个 50 页的 PDF 并通过多次 LLM 调用传递片段,单次运行的跟踪可能达到几兆字节。解决方案是基于引用的记录:只存储文档一次,在跟踪中记录内容寻址的哈希值,而不是在每一步都复制完整内容。

对于已经运行 Langfuse、LangSmith 或 Arize 等可观测性平台的团队,启用重放级跟踪的边际成本是适中的。重放所需的数据(提示词、响应、工具调用)与你已经为监控和评估收集的数据高度重叠。差距通常在完整性上——监控可能会采样或截断,而重放需要逐字捕获。

构建重放系统:实用模式

如果你要在现有 Agent 系统中构建重放功能,以下是重要的模式。

将记录与执行分离。 你的 Agent 代码不应该知道自己是否在被记录。用记录代理包装你的 LLM 客户端和工具接口,透明地拦截调用。这意味着不要在 Agent 逻辑中散布 if recording: log(...)——检测代码存在于基础设施层。

对大负载使用内容寻址存储。 对文档、图像和大型 API 响应进行哈希,只存储一次,并在跟踪中通过哈希引用它们。这在处理相同输入的运行之间去重,并保持跟踪文件紧凑。

对跟踪格式进行版本化。 你的跟踪模式会演进。在每个跟踪文件中包含版本字段,这样你的重放引擎可以处理新旧格式。跟踪格式的破坏性变更会使你的历史重放库失效,这很痛苦。

构建偏差检测。 重放时,将 Agent 的内部决策与记录的内容进行比较。如果 Agent 的路由逻辑或提示词构建本身是确定性的(给定相同输入),记录路径和重放路径之间的任何偏差都表明代码变更改变了行为。这将重放变成了回归测试工具——用本周的代码重放上周的生产流量,标记任何偏差。

不要忘记时间。 如果你的 Agent 在提示词、日志或决策逻辑中使用时间戳,你的重放引擎必须拦截系统时钟调用并替换为记录的时间戳。一个包含"今天是 2026 年 4 月 12 日"的提示词,如果明天用"今天是 2026 年 4 月 13 日"重放,会产生不同的模型行为。

重放作为回归测试

确定性重放最被低估的用途不是调试——而是回归测试。传统软件测试依赖于具有确定性断言的单元测试和集成测试。但如何测试一个输出本质上可变的系统?

重放给出了一个具体的答案:捕获生产运行作为黄金跟踪,然后对新代码版本重放它们。你不是检查精确的输出匹配(模型可能产生不同的 token)。相反,你检查结构等价性:Agent 是否以相同的顺序调用了相同的工具?是否提取了相同的关键信息?是否得出了相同的结论?

这种方法捕获了一类几乎不可见的 bug:提示词回归。你更新一个系统提示词来改善一个场景,却无意中破坏了五个其他场景。没有基于重放的回归测试,你只有在用户报告问题时才会发现。有了它,你在每次提示词更改后重放你的黄金跟踪库,在部署前捕获回归。

同样的模式适用于模型升级。在从一个模型版本切换到另一个之前,重放生产跟踪的代表性样本并比较行为指标。这比基准测试分数更可靠,因为它衡量的是你实际工作负载上的性能,而不是合成任务。

重放无法拯救你的情况

确定性重放有真实的局限性,值得理解。

它无法重放未记录的内容。 如果你的 Agent 有未被追踪的副作用——写入数据库、发送邮件、修改外部状态——这些不会被捕获或重放。全面的检测是前提条件。

它无法解释模型为什么这样说。 重放向你展示精确的输入和输出序列,但不提供机制性的可解释性。你会知道第 5 步产生了错误答案,但不知道哪些注意力头或神经元驱动了那个响应。

跟踪存储成为数据治理问题。 你的跟踪包含来自生产的完整提示词和响应,通常包含用户数据。跟踪存储必须符合你的数据保留政策、访问控制和隐私法规。在受监管行业中,这是一个特性(审计追踪),但需要审慎处理。

模型停用会破坏历史重放。 如果你对 GPT-4 记录了跟踪,而该模型后来被停用,你仍然可以使用记录的输出进行重放——但你无法对不再存在的模型进行反事实重放(注入新输入并获取模型响应)。对于长期的合规要求,这意味着你的记录跟踪是永久记录,而不是可复现的实验。

尽管有这些局限性,确定性重放仍然是我们对非确定性系统最接近调试器的东西。它不会给你 LLM 的 gdb,但它给你几乎同样有价值的东西:当你的 Agent 在生产中失败时,能够说"这就是一步步发生的事情"。对于大规模运行 Agent 的团队,这种能力将事件响应从猜测变成工程。

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