第 7 章 非确定性测试

同一个用例,这次跑过了,下次跑挂了。代码没变、输入没变、连天气都没变——它就是挂了。这是 Agent 测试最让人崩溃的地方,也是传统测试方法论彻底失效的地方。
开篇:一条用例的三天翻转
我有一个 eval 用例,测的是信息合并——用户先说了一件事,后来补充了细节,Agent 应该把两次信息合并成一条,而不是存两条。
这个用例的测试结果,五月初的那几天是这样的:
- 5 月 8 日下午 5:12 —— FAIL
- 5 月 8 日下午 5:17(五分钟后)—— FAIL
- 5 月 9 日早上 8:59(第二天)—— PASS
同一个用例,同一份代码,什么都没改,结果从 FAIL 变成了 PASS。
这不是个例。翻 eval 日志,这种翻转到处都是:同一个用例上午 FAIL、两小时后自己翻成 PASS;另一个用例反过来,从过变挂。
最诡异的是 LLM 裁判的判定。同一个用例的同一轮回复,裁判第一次判了 PASS,理由是一大段纠结的推理("应判断为通过。但仔细看……所以通过");第二次同样的回复,裁判直接判了 FAIL,理由完全不同。
裁判自己都在摇摆,你怎么信它?
这就是 Agent 测试的核心难题:非确定性。传统软件测试的整个方法论,都建立在"同样输入得到同样输出"这个假设上。这个假设在 Agent 时代被彻底打破了。
为什么 Agent 的输出不确定
先说清楚为什么会这样。
Agent 的每一次决策,背后都是一次 LLM 调用。LLM 是概率模型——它不是在"计算"答案,而是在"采样"答案。同一个 prompt,模型内部的概率分布是固定的,但每次采样可能采到不同的位置。大多数时候差异很小,但偶尔会差到影响行为——比如这次它选择调工具,下次它选择直接回复。
再加上温度参数、上下文长度的微小变化、甚至请求到达服务器的时间差,都会让输出产生波动。这不是 bug,是 LLM 的本质特性。
Galileo AI 有个数据说,93% 使用 LLM 裁判的团队都遇到过重大的可靠性问题,最主要的原因就是"一致性失败"。这个"一致性失败"不是偶发的,它是系统性的——你永远无法保证"这次和上次一样"。
第一个策略:跑多次,看通过率
既然单次结果不可靠,那就跑多次。
我在 eval 框架里实现的策略是:每个用例跑 5 次,通过率 ≥ 80%(也就是 5 次里至少过 4 次)才算通过。
为什么是 5 次和 80%?这不是精确计算出来的,是试出来的。跑 3 次太少——2 比 1 和 1 比 2 都可能出现,没法区分"偶尔抖动"和"稳定失败"。跑 10 次太多——跑一轮 eval 的时间会爆炸。5 次是个平衡点:足够平滑掉偶尔的抖动,又不至于太慢。
80% 的阈值也是同理。100% 太严——有些维度(比如回复的自然程度)天生就不可能 100% 一致。50% 太松——跟抛硬币没区别。80% 意味着"绝大多数时候能做对,偶尔会抖",这是一个合理的成功标准。
这个策略的本质是:放弃"每次都对",追求"大概率对"。 这是对非确定性的第一层接纳——你不再追求确定性测试里的"通过/不通过"二元判断,而是用统计方法描述质量。
第二个策略:按维度区分——以及一个落地的反直觉发现
5 次 × 80% 这个标准对所有用例一刀切,有问题。
有些维度是高度确定性的——比如"记没记住""记了几条""有没有建立关联"。这些是查数据库的代码断言,只要 Agent 确实执行了存储操作,结果就是确定的。这些维度跑 5 次是浪费——跑 1 次就够了。
真正需要跑 5 次的,是那些 LLM 深度参与的维度——比如"回复质量好不好""内容准不准确""有没有被误触发"。这些维度每次都可能有波动,必须用多次运行来平滑。
所以更精细的策略是按维度区分运行次数:
高度确定的维度(数据库操作、触发判断的代码逻辑)
→ 跑 1-3 次,100% 通过算过
LLM 深度参与的维度(回复质量、内容准确性、意图理解)
→ 跑 5 次,80% 通过算过但分层这个想法,我光在脑子里转过是不算数的。代码断言维度该跑几次、裁判维度该跑几次、阈值定多少——这些都得拿真实波动数据去验,否则就是另一条"听着有道理、从没被验证过"的经验。于是我找了一个现成的功能做载体,专门跑了个稳定性实验。
把分层跑实
那个功能是这样的:给 Agent 一组结构化数据,让它生成一段说明文字。这种功能天然是两层评判——结构化的部分(数据有没有用对)是确定的,可以用代码断言;叙事的部分(说得对不对、有没有瞎编)是概率性的,得靠 LLM 裁判。两层叠在一个用例里,正好是验证分层的理想载体。
我把 eval runner 加了一个统计化的打分入口:对于 LLM 裁判维度,跑 N 次、统计通过率,N 和阈值都能在用例里配。代码断言维度照旧跑一次。这就是分层——确定维度不浪费算力,概率维度用多次换可信。
代码写完,我用它做了一个稳定性实验:让裁判对同一段叙事,连续打 5 次分,看打分波动多大。
实验里我准备了三类用例——按 eval 的预期结论来分,而不是按"质量好坏"来分:
- 正例:输入本身就该被认可,预期裁判判 PASS
- 反例:输入本身就该被拒绝,预期裁判判 FAIL
- 边界例:处在灰色地带,模棱两可
为什么要用"正/反/边界"而不是"好/中/差"?因为"好/差"是裁判的评价结果,而我们要观察的恰恰是裁判会不会判错——用结果去命名输入,会把话说拧了。正例和反例是 eval 用例的两种基本形态(上一章讲 eval 设计时讲过"正反平衡"那条原则),用它来描述才准确。
实验结果如下,每类用例裁判跑 5 次:
| 用例类型 | 5 次打分 | 均值 | 标准差 | 通过率(阈0.8) |
|---|---|---|---|---|
| 正例 | [1.0, 1.0, 0.3, 1.0, 1.0] | 0.86 | 0.31 | 80% |
| 边界例 | [0.7, 0.7, 0.67, 0.5, 0.67] | 0.65 | 0.08 | 0% |
| 反例 | [0.0, 0.0, 0.0, 0.0, 0.0] | 0.00 | 0.00 | 0% |
三件事,一件比一件反直觉。
第一,裁判确实飘。 正例的第 3 次,突然打了 0.3,其他四次都是 1.0。同一份输入,同一个裁判,就因为它是第 3 次跑,结论从"通过"跳到"不及格"。换句话说,一条本该被判 PASS 的正例,被误判成了反例。本章开篇讲的"裁判自己摇摆",这里用数据看到了。
第二,飘得最厉害的不是反例,而是正例。 我原本以为,反例裁判应该吵得最凶——毕竟错得离谱嘛。结果完全相反:反例 5 次全是 0.0,标准差 0,铁板一块,毫无争议;正例反而飘得最厉害,标准差 0.31。
为什么?因为反例的"错"是确定性的错——它就是错了,没什么可争的,裁判每次都一眼判定 FAIL。而正例处在一片灰色的"基本正确"区域——它确实对,但裁判偶尔会"过度挑刺",在某一次突然抠出一个莫须有的毛病,把它误判成反例。有争议的从来不是黑白分明的两端,而是中间那片灰色地带。
第三,5 次 × 80% 这个阈值,是被这个数据反向验证的。 正例的通过率正好是 80%(4/5)。如果当时图省事只跑一次,恰好抽到那个 0.3,这条本该通过的正例就被误杀了。80% 阈值让"大概率对"的过,正是当初设计这个策略的本意——只是这一次,我是用真实波动数据看到了它生效,而不是靠拍脑袋。
分层还要再细一层:按"输入可判定性"
这个实验顺带改了我对分层的理解。
我原本的分层是按"维度"分——代码断言维度少跑,裁判维度多跑。但实验显示,即便都是裁判维度,稳定性也跟输入本身有关:反例(结论明确)裁判完全不飘,跑 1 次就够;正例和边界例(处于灰色地带)裁判会偶尔挑刺,得多跑。
所以分层还得再细一层,按输入的可判定性来:
- 结论明确、黑白分明的输入(典型的正例或典型的反例)——裁判稳定,少跑
- 处于灰色地带、需要裁判"品味"的输入(边界例,以及偶尔会被过度挑刺的正例)——裁判会飘,多跑
这其实是个比"按维度分"更难把握的标准——因为写用例的时候,你不一定知道这条输入处在灰色地带。但至少,当你发现某个用例的裁判打分忽高忽低时,你知道这不是裁判坏了,是这个用例本身就处在那个"裁判会纠结"的区域,它需要多跑几次来摊平。
一个反复出现的模式
"按维度分"还是太粗——实践里很多原则,落到代码都需要再拆一层。这不是这一章独有的现象,后面讲鲁棒性、讲 CI 的时候还会撞见同一件事:经验法则往往是"统一原则",但落地时几乎都要分层。 记住这个模式,后面会反复印证。
第三个策略:跟裁判的不确定性共处
LLM 裁判的不确定性,比 Agent 本身的不确定性更隐蔽,也更危险。
Agent 输出不稳定,你能直接看到——回复变了、行为变了。但裁判不稳定,你看不到——它每次都给你一个干净的 {"pass": true/false, "reason": "..."},看起来很权威。你很容易忘了裁判自己也是个概率模型。
上面那个"正例第 3 次突然被打 0.3"的例子,就是最典型的表现。同一份输入,它前四次都判通过,第三次突然判不及格。如果你只跑一次,恰好赶上那一次,你就被它骗了。
怎么应对?几个实践经验:
第一,裁判和 Agent 用不同的模型。 如果裁判和 Agent 用同一个模型,它们会有相同的偏好和盲区——Agent 容易犯的错,裁判也容易忽略。用不同模型的裁判,至少能交叉覆盖一部分盲区。我在那个项目里就是这么做的:Agent 用一个模型,裁判换另一个模型,写成可配置的,免得以后想换。
第二,裁判的 prompt 要有锚点。 别只说"判断这条回复好不好",要说"好的标准是:1)引用了数据 2)结论和数据的方向一致 3)包含风险提示 4)没有编造数字"。标准越具体,裁判越稳定。模糊的标准会让裁判每次"理解"不一样——而模糊标准碰上灰色输入,就是飘得最凶的组合。
第三,跑多次,并用通过率代替单次判定。 这是这一章一直在讲的事。单次裁判不可信,但"5 次里 4 次说好"是可信的。统计化不是为了显得高级,是为了对抗裁判那一次莫须有的挑刺。
第四,定期人工校准。 从裁判判过的结果里抽样一批,人工复核。如果发现裁判和人的判断吻合度下降,说明裁判飘了,需要调整 prompt 或者换模型。这一步不能省——它是你信任裁判的基础。
第五,接受"裁判也会错"这个事实。 别把裁判的判断当圣经。它是一个"不太稳定的同事帮你审稿"——大部分时候有用,偶尔会犯傻。你的 eval 体系要能容忍裁判偶尔判错,而不是被它绑架。
一个对照:确定性系统怎么测
讲了这么多"概率性测试"的方法,容易让人以为 Agent 项目里所有东西都是不确定的。其实不是。
我有些项目里会有一块纯计算的功能——大量数值运算和数据处理。这些东西是确定性的,跟 LLM 没有关系。同样的输入永远得到同样的输出。
这些部分的测试方式和 Agent 部分完全不同。测试目录里全是传统单元测试:
def test_indicator_calculation():
# 固定随机种子,消除不确定性
np.random.seed(42)
data = np.random.randn(100)
result = calculate_indicator(data, period=14)
assert result[-1] == pytest.approx(58.3, abs=0.1)你看,np.random.seed(42) 一行就把随机性钉死了。测试跑一次就够了,通过就是通过,不需要跑 5 次看通过率。这是确定性系统的测试方式——传统方法论在这里完全适用。
两套测试哲学,对应两类系统:
| 确定性系统(算法/计算) | 概率性系统(Agent/LLM) | |
|---|---|---|
| 输出是否可复现 | 是,固定种子后完全可复现 | 否,每次可能不同 |
| 测试方式 | 单元测试,跑一次断言 | eval,跑多次看通过率 |
| 判定标准 | assertEqual(精确匹配) | 通过率阈值(如 80%) |
| 回归检测 | 输出变了就是退化 | 通过率下降才是退化 |
| 随机性处理 | 固定种子消除 | 跑多次平滑 |
一个产品里往往两种系统都有。一个典型的 Agent 产品,底层可能有确定性的计算模块(用固定种子测),上层有 LLM 驱动的交互模块(用 eval 测)。两套测试哲学并存,各管各的部分。
有意思的是,我后来意识到一个曾经的测试盲区:我有一个项目,确定性计算那一层测得很细——各种边界、精度、随机种子都钉死了;但它上层接的 Agent 部分,一个行为测试都没有。确定性部分测得很好,概率性部分完全没测。 这种失衡很常见,因为确定性部分好测、有成就感,概率性部分难测、容易回避。但没测的那一层,恰恰是用户直接打交道、最容易出问题的一层。上一章讲 eval 驱动开发,正是在补这块——先把"概率性部分该怎么测"这件事立起来。
这一章的工具:非确定性测试检查清单
🔧 非确定性测试检查清单
如果你的 Agent 测试结果不稳定,按这个清单排查:
是不是真的不稳定?
- [ ] 同一用例、同一代码版本,连跑 3 次,结果一致吗?
- [ ] 如果不一致——是 Agent 行为变了,还是裁判判错了?(先排查裁判)
- [ ] eval 环境是干净的吗?上一个用例的残留有没有污染?(数据库、缓存、全局状态——后面会讲怎么系统排查)
应对策略
- [ ] 你的 eval 是跑一次就判生死,还是跑多次看通过率?
- [ ] 通过率阈值设了多少?合理吗?(太严会假阳性,太松会假阴性)
- [ ] 不同维度有没有区分运行次数?(确定性维度少跑,概率性维度多跑)
- [ ] 概率维度里,处在"灰色地带"的用例(裁判容易纠结的)有没有多跑几次?
裁判可靠性
- [ ] 裁判和 Agent 是不是用了不同的模型?
- [ ] 裁判的 prompt 有没有明确的、具体的评判标准(锚点)?
- [ ] 裁判打分是单次判定,还是跑多次取通过率?
- [ ] 最近有没有人工抽查校准过裁判的判断?
- [ ] 你心里清楚"裁判也会判错"这件事吗?
全局
- [ ] 你的项目里,哪些部分是确定性的,哪些是概率性的?
- [ ] 确定性部分用传统测试,概率性部分用 eval——两边都覆盖了吗?
- [ ] 有没有"确定性部分测得很好,概率性部分完全没测"的盲区?
小结
这一章的核心是一个认知转变:Agent 测试的标准,从"每次都对"变成"大概率对"。
这不是降低标准,而是承认现实。LLM 的输出天生是概率性的,你没法用传统测试的确定性框架去套它。你能做的是——跑多次、看通过率、按维度区分、跟裁判的不确定性共处。
这里想补一句这一章反复印证的话:一条经验法则,只在脑子里转过和真拿代码验过,差得很远。 这一章的分层策略,最初也只是个直觉——"确定性维度少跑、概率性维度多跑",听着没毛病。真去做稳定性实验,才看到直觉抓不到的东西:飘得最厉害的是正例,反例反而稳如磐石。直觉告诉你"概率维度要多次",实验告诉你"同是概率维度,处在灰色地带的才需要多次"。所以当你有一条经验法则还没被代码验过的时候,对它保留一点谦虚——它大概率比你以为的要粗糙,而粗糙的那部分,只有跑起来才看得见。
最危险的情况不是"Agent 不稳定",而是"你以为它稳定"。单次跑通就以为没问题,裁判说 PASS 就以为真 PASS——这种虚假的确定感,比不确定本身可怕得多。
到这里,第三部分"怎么知道 Agent 能用"就讲完了。第 6 章讲怎么定义"成功长什么样"(eval-driven),第 7 章讲怎么跟"成功本身不确定"这个现实共处。两章合起来,就是 Agent 时代的测试方法论。
下一部分,我们进入一个更进阶的话题:当一个 Agent 不够用的时候,怎么让多个 Agent 协作。
下一章
第 8 章 · 多 Agent 不是银弹 —— 什么时候该引入多 Agent,什么时候不该。