Agent 的 Diff 调试:两次执行结果不同,怎么定位是 Prompt 变了还是模型变了

AI technology illustration

我最早做 Agent 时踩过一个印象很深的坑:上午跑得好好的多步任务,下午同一个输入跑崩了。第一反应是查 Prompt,git diff 一拉,干干净净。那就甩锅给模型,八成是 OpenAI 偷偷更新了。结果客服说没有。最后我把执行日志翻到底,才发现罪魁是工具返回的当前日期,日历工具的时区配置不知被谁改掉了。排除了它是死循环的元凶。这件事让我意识到一个特别朴素但特别容易被忽视的事实:**Agent 系统里「变了」的地方,远比你能想到的多**。

如果你也遇到随时会输的这个场景——同一个任务,两次执行结果不一样——你真正要做的事情不是猜,而是把变量查全。我们先过一遍:到底有哪几类变量?

## 变量只有四类,查要查全

当两次执行结果不同,候选变量从未超出这四类:

  1. 采样参数:temperature、top_p、seed 是否被显式或隐式改动过。
  2. 模型身份:你写的 model 名(比如 gpt-4o)背后实际路由到了哪一个具体模型版本,以及服务端 model 实现更新。
  3. Prompt 系统:不止是模板文本,还包括动态注入的上下文、工具返回结果、多 Agent 链路里上游的输出。
  4. 运行环境:SDK 版本升级、时区变化、外部工具的默认设置、向量检索的索引变更。

你注意我把「模型变了」放在了第二位。因为在实际排查中,排在第一位的大概率是系统误终端。

## 你以为 temperature=0 就很确定?并没有

几个根因常潜伏在模型推理的机制细节里。

曾经很让我困惑的事:我把 Agent 的 temperature 调到 0,跑了几次发现输出还是不完全一致,那时候我以为是自己代码 bug——可能是缓存没清理,或者哪里偷偷加了噪声。直到我去翻 OpenAI 官方文档,它明确写着:

GPT models are non-deterministic by nature. Even with temperature set to 0, the same prompt can sometimes produce different responses.

为什么 temperature=0 还不确定?

核心原因在 GPU 计算的浮点非确定性:矩阵乘法在不同并行调度下浮点加法顺序不同,结果在小数点后若干位有细微差异,模型是自回归的,一个 token 的最后一点小偏差,叠加到后面会被放大。所以 temperature=0 不是「变身确定模型」的按钮,它只是把方差压到更低而已。

这一步想通后,排查策略就变了——不要执着于「能不能完全复现」,而是先搞清楚「当前的不确定性范围有多大」。同一个输入跑二三十次,统计输出一致的比例,比一次成功一次失败更能说明问题。

## Prompt 没改,不代表上下文没变

另一个特别隐蔽的坑:**prompt 模板没变,但每次真正发出去的 prompt 内容发生了变化**。Agent 跟普通聊天不一样,执行链路上有大量动态信息注入:

  • 当前日期/时间(对,就是这个,很多人栽在这)
  • 工具返回的结果(API 返回结构可能变了)
  • 向量检索结果(同一 query 在新索引下可能召回不同的文档)
  • 多 Agent 协作里上游 Agent 的输出(一个链路的不确定性会级联放大)

这些在可观测系统里都算 prompt 的一部分。你只 diff 模板文件,等于只看了冰山一角。排查时必须对比两个时刻的「完整 prompt」,而不是模板文件。

## 定位流程:我现在按这个顺序做 diff

当你遇到 Agent 行为变化,直接按下面的顺序做 diff:

  1. 查 git:代码、prompt 模板、工具实现、依赖 lock 文件。先看看有没有人动过。git diff HEAD~5 一眼扫过去。
  2. 查 trace:把失败和成功的两次执行放到 Langfuse 或 LangSmith 里做对比。重点看两个位置:最终 LLM 调用的完整 prompt 内容,和所有工具调用的输入输出。
  3. 查系统指纹:如果是 OpenAI 系模型,看 API 返回里的 system_fingerprint 字段。fingerprint 变了,说明服务端模型确实有更新。需要留意的是 Anthropic 模型可以直接钉版本(比如 claude-3-5-sonnet-20241022),OpenAI 则通过 alias 更新,没法锁内部时间戳。
  4. 查采样参数:看日志里每次调用的 temperature/top_p/seed。专门提一句:SDK 版本升级可能改默认值,这是真实发生过的坑。
  5. 批量复现统计:用同一组输入跑 N 次,N 不要小于 20。如果成功和失败的比例乱跳,基本不要找代码问题,那是随机性的地盘。

## 单次对比不可信,批量对比才可信

这一步我认为是文章里最值得细讲的认知升级。

很多人的做法(包括以前的我自己)是:失败一次,改一下 prompt,再跑一次,成功了,收工。但这里有个漏洞,你改对了一个关键修复,这次跑好了仅仅是因为运气好;下次随机性波动,又失败,你就把它 revert 掉了。这种事发生多了,你会开始怀疑人生。

正确操作是跑 batch 对比

  • 同一组输入(建议编 10-20 条典型任务)
  • 旧 prompt 跑 N 次,记录成功率
  • 新 prompt 跑 N 次,记录成功率
  • 比较两个分布有没有显著差异

这里有一个反直觉的点:**如果新旧 prompt 的成功率都在 50% 上下徘徊,问题和 prompt 大概率无关,先转头去看看模型的确定性本身**。如果旧版本稳定 90% 新版本掉到 40%,那才值得认真 diff 上下文和 prompt 细节。

把同一 prompt 连跑 20 次以上,你会看到模型本身的输出分布。这个分布,就是一切 diff 的基准线。意思是,你的调试必须建立在分布上,不是在单个样本上。

## 记录,是一切 diff 的前提

一套好工具的标配,就是「把每次执行变成一台光谱仪」落实到工程上,具体的做法有三个:

  1. Prompt 版本化:不要硬编码在 Python 文件里,抽成 git 管理的 yaml 或 json。这样任何改动都能用 diff 确认。别小看这一点,我见过太多团队 prompt 散落在代码各处,出了问题都不敢改。
  2. 强制记录指纹:每次调用都记下 model、system_fingerprint(OpenAI)或模型版本号(Anthropic)。一份最常见的 AutoPR review 也建议把 model 版本打进日志——一旦异常出现,拿指纹对照就知道是不是模型换了。这事 OpenAI 官方文档也有建议,它说:如果你观察到非确定性行为,试试把 seed 设为常数,然后在不同 system_fingerprint 值之间比较结果。
  3. 完整链路 trace:用 Langfuse 或 LangSmith 把工具入参、出参、检索命中结果全部记录。Diff 粒度才能从「prompt 变没变」细化到「哪一步的哪一项数据变了」。

## 回到最初的问题

所以「是 prompt 变了还是模型变了」这个问法本身意味着要找更精准的答案。真正的答案分布常常是这两个之外的东西:

  1. 工具返回的数据变了(时区配置、API 响应结构调整)
  2. 随机性波动被误读成了系统性变化,两者本来就该用统计去区分

我现在的推荐配置系统性、收敛的方向很明确了:代码和 prompt 全部进 git,模型指纹记日志,trace 全链路可回放,改任何东西之前先基准测试,之后批量极化决定。这套下来,绝大多数莫名其妙的结果差异都能在十分钟内定位到具体变量。剩下的那部分,就属于模型自身的随机性——你没法消除它,但它也恰恰是你最不需要担心的那一类:因为大家都跑在同一条随机线上。

如果你也是第一次被这种问题折磨,记住一句话:不要急着猜,先记录,再批量对比,最后才下结论。这大概就是 Agent 时代一个工程师的自我修养。

原创文章,作者:guanweilu,如若转载,请注明出处:https://guanweilu.cn/article/425.html

(0)
上一篇 4天前
下一篇 4天前

相关推荐