这是AI功能测试系列的第2篇,整个系列会更60篇
同一套测试,昨天跑通过,今天跑不过------不是代码变了,是模型随机采样变了。
0. 写在前面:一个你可以自己验证的实验
与其听我讲故事,不如先跑一段 10 行代码。
用任何支持 OpenAI 兼容接口的模型(通义千问、文心一言等),执行下面的脚本:
from openai import OpenAI
client = OpenAI(api_key="<你的 API Key>")
prompt = "请简要介绍稳健型理财产品的特点"
for i in range(3):
resp = client.chat.completions.create(
model="qwen-max",
messages=[{"role": "user", "content": prompt}],
temperature=0.7,
)
print(f"第 {i+1} 次:{resp.choices[0].message.content[:50]}...")
同样的 Prompt,同样的模型,同样的 temperature=0.7,三次输出大概率不完全一样。 可能措辞不同、结构不同、甚至关键数据不同。
这不是 Bug------这是 LLM 的设计特性。自回归生成中,每一步都从概率分布中采样,temperature 参数控制采样的随机程度。温度越高,随机性越大;即使 temperature=0.3,不同模型版本之间也会有输出差异。
通义千问官方文档对 temperature 参数的说明是:"Sampling temperature, between 0 and 2. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic." 换句话说,模型厂商自己都在文档里告诉你:这个参数会让输出变随机。
这个不可确定性带来三个致命问题:
1. 概念讲解
AI系统最大的测试难题不是"怎么测",而是"同一套测试,每次跑出来的结果都不一样"。
你在测传统系统时,同一个测试用例跑 100 次,结果 100 次一样。但在 AI 系统上,同一个 Prompt 调用 100 次,可能得到 100 种不同的回答。这不是你的测试框架有问题,这是 LLM 的设计特性------自回归生成中每一步都从概率分布中采样。
这个不可确定性带来三个致命问题:
- 回归测试不可靠。 昨天跑通过的测试,今天可能跑不过。不是代码变了,是模型随机采样变了。你的 CI/CD 会频繁误报。
- 质量基线无法建立。 你不知道"合格"的标准是什么。回答 A 和回答 B 字面不同,但质量可能一样。你怎么定义"通过"?
- Bug 复现困难。 用户报告了一个错误回答,但你用同样的 Prompt 跑,可能得不到那个错误回答。Bug 复现不了,开发就没法修。
不做这个认知转换,你后面做的一切都是错的。
不可确定性不是 AI 的缺陷,而是 AI 的本质。你的测试策略必须围绕这个本质来设计,而不是试图消除它。
1.5. 精确断言 vs 统计判定:8 个维度对比
光讲概念不够直观,下面这张表格把两种判定方式的核心差异列清楚。
| 对比维度 | 精确断言(传统) | 统计判定(AI) |
|---|---|---|
| 判定逻辑 | actual <span class="wx-em-red"> expected |
mean(score) >= threshold |
| 运行次数 | 1 次定生死 | 10-20 次采样取分布 |
| 通过标准 | 100% 一致 | 均值 ≥ 0.85 + 下四分位 ≥ 0.70 |
| 失败处理 | 直接标记 FAIL | 查看分布,分析离群点 |
| 误报率 | 显著偏高(因模型随机性导致大量假阳性) | |
| 测试耗时 | 单次调用,秒级 | N 次调用,分钟级 |
| API 成本 | 1 次调用/用例 | N 次调用/用例(成本 × N) |
| 适用场景 | 格式验证、类型检查 | 质量评分、语义评估 |
关键结论: 统计判定不是"更慢的精确断言",而是完全不同的判定逻辑。你看的不是单次结果,而是质量分布。
2. 核心方法论
应对不可确定性,核心思路是从精确断言转向统计判定。具体分四步:
- 确定性模式测试。 对于需要精确验证的场景(格式验证、工具调用验证),使用
temperature=0或seed参数固定随机种子,获得可复现的输出。这不是作弊,而是分层测试的策略------硬性验证层需要确定性。 - 多次采样 + 统计判定。 对同一输入执行 N 次调用(通常 10-20 次),统计输出质量的分布。不是看单次结果,而是看平均值、下四分位数、标准差。如果平均质量达标、下四分位不低于阈值、波动在可接受范围,就算通过。
- 分层测试策略。 把测试分为三层:格式/结构验证用 temperature=0 精确断言;质量评分用多次采样统计判定;用户体验用人工抽检。不同层用不同方法。
- 黄金数据集回归。 维护一个经过人工验证的黄金测试集,每次变更(模型更新、Prompt 调整、代码修改)后自动回归。黄金数据集不要求每次输出完全一致,但要求质量分数不低于基线。
四步不是顺序执行,而是同时使用------打个比方你就明白了:
想象你在经营一家餐厅,每天要确保菜品质量:
| 步骤 | 餐厅日常 | 对应 AI测试 |
|---|---|---|
| Step 1:确定性模式 | 检查食材有没有过期、分量够不够------这是硬性标准,不过关直接打回 | 格式验证、字段检查、工具调用是否正确------对或错,没有中间地带 |
| Step 2:多次采样 | 同一道菜做 10 份,尝一尝平均水准如何、有没有哪次盐放多了------看的是整体稳定性 | 同一 Prompt 跑 10-20 次,看质量分布------不看单次,看平均和下四分位 |
| Step 3:分层测试 | 凉菜和热菜标准不同:凉菜看新鲜度,热菜看火候和口味------不同品类不同标准 | 硬性验证用 Step 1,质量度量用 Step 2,用户体验靠人工------不同层用不同方法 |
| Step 4:黄金回归 | 每道招牌菜保留一份"标准样",新做出来的跟它比------不能比上次差 | 维护黄金测试集,每次模型更新后回归------质量不低于基线 |
关键理解: 这四步是餐厅的四个质检环节,不是流水线。你不可能"今天只做 Step 1,明天再做 Step 2"------它们同时运转,各自解决不同类型的问题。 硬性验证层用 Step 1,质量度量层用 Step 2+3,每次变更后用 Step 4 做持续监控。
3. 示例场景:医疗 AI 诊断辅助系统的回归测试
以下是一个脱敏后的示例场景,用于说明方法论。数据为示意值,实际项目中请替换为你自己的测试数据。
场景: 系统基于 通义千问-Max,辅助医生进行初步诊断。测试团队维护了 500 条医疗问答测试用例。
问题: 每次模型微调后跑回归测试,通过率在 70-95% 之间波动。开发团队抱怨"测试不稳定,不知道到底修好了没有"。测试团队也困惑------有时候同一个用例第一次过、第二次不过。
根因分析: 测试团队对每条用例设置了精确的关键词匹配------回答中必须包含特定医学术语。但模型在不同温度设置下,可能用同义词代替了术语(比如用"心肌梗死"代替"急性心肌梗塞"),导致关键词匹配失败。
解决方案:
- 将 500 条用例分为三层:100 条硬性验证(必须包含诊断结论、必须给出就医建议),300 条质量度量(准确性、完整性、安全性),100 条用户体验(医生主观评价)。
- 对硬性验证层,使用 temperature=0 + 精确关键词匹配。
- 对质量度量层,使用 10 次采样 + BERTScore 评分,阈值设为 0.85(注意:这是起始参考值,需根据你的实际数据调整)。
- 对用户体验层,每月邀请 3 位医生进行盲评。
- 回归测试通过率从大幅波动收窄到稳定区间,误报明显减少(具体改善幅度取决于你的模型和场景,请以实际测试数据为准)。
4. 代码示例
下面是多次采样 + 统计判定的核心实现。完整可运行版本需要 bert-score 和 numpy 两个依赖:
import numpy as np
from typing import List, Dict
from bert_score import BERTScorer
# 初始化 BERTScore 评分器(用于语义相似度评估)
scorer = BERTScorer(lang="zh", rescale_with_baseline=True)
# AI 系统客户端(根据你的实际系统替换)
# 这里以 OpenAI 兼容接口为例
from openai import OpenAI
client = OpenAI(api_key="your-api-key")
def call_llm(prompt: str, temperature: float = 0.7) -> str:
"""调用 AI 系统,返回回答文本"""
response = client.chat.completions.create(
model="qwen-max",
messages=[{"role": "user", "content": prompt}],
temperature=temperature,
)
return response.choices[0].message.content
def bert_score_metric(text: str, reference: str) -> float:
"""用 BERTScore 计算语义相似度(0-1 之间)"""
_, _, F1 = scorer.score([text], [reference])
return F1.item()
def consistency_test(prompt: str, golden_reference: str,
n_samples: int = 20, temperature: float = 0.7) -> Dict:
"""
一致性测试:对同一输入多次采样,统计质量分布
Args:
prompt: 测试输入
golden_reference: 黄金参考答案(用于质量评分)
n_samples: 采样次数,默认20次
temperature: 模型温度,默认0.7(模拟真实使用场景)
Returns:
包含统计结果的字典
"""
scores = []
responses = [] # 记录每次的回答,用于分析离群点
for i in range(n_samples):
# 调用 AI 系统,每次可能得到不同回答
text = call_llm(prompt, temperature=temperature)
responses.append(text)
# 用 BERTScore 评估每次回答的质量
score = bert_score_metric(text, golden_reference)
scores.append(score)
# 统计判定:不仅看平均分,还要看下四分位和波动
return {
"mean_score": np.mean(scores), # 平均质量
"p25_score": np.percentile(scores, 25), # 下四分位(最差25%的质量底线)
"p5_score": np.percentile(scores, 5), # 第5百分位(极端情况)
"std_score": np.std(scores), # 标准差(波动程度)
"min_score": np.min(scores), # 最低分
"max_score": np.max(scores), # 最高分
"n_samples": n_samples,
"responses": responses, # 原始回答列表(用于分析离群点)
}
# 判定逻辑
def is_consistent_pass(results: Dict) -> bool:
"""
一致性通过标准(示例值,需根据实际场景调整):
1. 平均分 >= 0.85(整体质量达标)
2. 下四分位 >= 0.70(最差25%也不至于太烂)
3. 标准差 < 0.15(波动在可接受范围)
注意:不同场景的阈值差异很大。医疗场景可能需要 >=0.90,创意写作可能 >=0.60 就够了。
建议先用你的真实数据跑一轮,观察分数分布后再确定阈值。
"""
return (
results["mean_score"] >= 0.85 and
results["p25_score"] >= 0.70 and
results["std_score"] < 0.15
)
# 使用示例
if __name__ </span> "__main__":
prompt = "请介绍稳健型理财产品的特点"
reference = "稳健型理财产品年化收益率约3.5%,风险等级R2,适合风险承受能力较低的投资者"
results = consistency_test(prompt, reference, n_samples=20, temperature=0.7)
print(f"平均分: {results['mean_score']:.3f}")
print(f"下四分位: {results['p25_score']:.3f}")
print(f"标准差: {results['std_score']:.3f}")
print(f"通过: {is_consistent_pass(results)}")
5. 注意事项和常见坑
- 采样次数不是越多越好。 20 次是一个经验值------太少(5次)统计不稳,太多(50次)测试时间太长。CI 环境可以用 10 次,离线回归用 20 次。
- 温度参数的选择很关键。 测试时用的温度应该和线上实际使用的温度一致。如果线上 temperature=0.7,你测试用 temperature=0,测出来的结果和线上完全对不上。
- 统计判定要设多个阈值。 只看平均分会掩盖问题。平均分 0.90 看起来不错,但如果下四分位只有 0.50,说明有 25% 的回答质量很差------这 25% 就是用户投诉的来源。
- 黄金参考的质量决定了一切。 如果你的黄金参考本身就写得不好,那 BERTScore 评分再高也没意义。黄金参考必须经过领域专家审核。
- 别忽略并发调用速率限制。 20 次采样 × 500 条用例 = 10000 次 API 调用。如果串行执行,假设每次 3 秒,需要 8.3 小时。用
asyncio或concurrent.futures并发调用,可以把时间压缩到 30 分钟以内。 - 记录每次采样的原始回答。 统计结果只能告诉你"有问题",但找不到"问题在哪"。保存每次采样的原始回答,当测试失败时,逐条分析离群点(得分最低的几次),才能定位根因。
- 不同模型的不可确定性程度不同。 通义千问-Max 的稳定性远高于 通义千问-Plus,通义千问-Plus 的稳定性高于 通义千问-Turbo。如果你的模型本身就很稳定,采样次数可以适当减少;反之则需要更多采样。
5.5. 常用工具一览
| 工具 | 用途 | 适用场景 |
|---|---|---|
| bert-score | 语义相似度评分 | 快速初筛,本地计算无需 API |
| numpy | 统计计算(均值/百分位/标准差) | 采样结果的统计分析 |
| promptfoo | 开源 Prompt 测试框架 | 内置多次采样 + 统计判定功能 |
| DeepEval | LLM 测试框架 | 集成 CI/CD,支持统计判定 |
| RAGAS | RAG 系统专用评测 | 检索稳定性 + 生成稳定性联合评估 |
| pytest + hypothesis | Python 测试框架 + 属性测试 | 将统计判定集成到 pytest 测试套件 |
工具选择原则:能本地算的不用 API,能粗筛的不一上来就精评。 把贵的资源用在刀刃上。
6. 总结与思考
不可确定性不是 AI 的缺陷,而是 AI 的本质。你的测试策略必须从"精确断言"转向"统计判定"------看分布,不看单次。
【思考题】 你遇到过 AI测试中"同一用例跑两次结果不同"的情况吗?你是怎么处理的?欢迎在评论区分享你的经验。
关键词: AI功能测试、不可确定性、统计判定、BERTScore、多次采样、LLM测试、测试框架、自动化测试、CI/CD