你需要测试一个函数,它的正确输出事先无法知道。路由优化器、情感分类器、物理模拟都可以。你读过 metamorphic testing:找到输入和输出之间必须成立的关系,然后测试这些关系而不是精确值。
问题在于如何想出这些关系。你盯着函数签名,脑子里一片空白。
于是你去问大语言模型。它在几秒内就抛出了十条 metamorphic relations。大多数看起来合理,少数明显错误,有一两条确实聪明。大语言模型是个有用的头脑风暴伙伴,但不能替代领域知识。不经检查就把这些关系贴进测试套件,你最终发布的测试会在错误代码上通过,或在正确代码上失败。
大语言模型实际给你的是什么
当你让大语言模型为某个函数提议 metamorphic relations 时,它实际上是在训练数据中进行高级模式匹配。它见过成千上万种排序算法、几何库和机器学习流水线的实现。它识别出”加上一个常数应该让均值平移相同常数”是一个常见性质,于是建议了它。
这不是理解,而是统计关联。大语言模型不知道你的函数是否处理空列表、NaN 值或整数溢出。它提出这条关系,只是因为它以前见过类似的。
典型的提示和回复看起来是这样的。你把函数喂给大语言模型,让它给出关系:
# Your function under test
def percentile(values: list[float], p: float) -> float:
"""Return the p-th percentile using linear interpolation."""
if not values:
raise ValueError("empty list")
if not 0.0 <= p <= 1.0:
raise ValueError("p must be in [0, 1]")
sorted_vals = sorted(values)
n = len(sorted_vals)
idx = p * (n - 1)
lower = int(idx)
upper = lower + 1
if upper >= n:
return sorted_vals[-1]
frac = idx - lower
return sorted_vals[lower] * (1 - frac) + sorted_vals[upper] * frac
Prompt: Propose 5 metamorphic relations for this percentile function.
Response:
1. Monotonicity: if p1 < p2, then percentile(values, p1) <= percentile(values, p2)
2. Scaling: percentile([v * k for v in values], p) == percentile(values, p) * k
3. Translation: percentile([v + c for v in values], p) == percentile(values, p) + c
4. Permutation invariance: percentile(values, p) == percentile(shuffled(values), p)
5. Boundary: percentile(values, 0.0) == min(values), percentile(values, 1.0) == max(values)
其中三条正确且有用。一条 subtly 错了。一条 trivially 为真,但太松散,几乎抓不到 bug。
单调性关系是正确的,不过你需要处理重复值。缩放关系在 k <= 0 时会失败,因为排序顺序会反转。平移关系是可靠的。排列不变性主要测试你是否记得排序。边界关系只有当你的插值把第 0 和第 100 百分位数当作最小值和最大值时才正确。
大语言模型不会警告你任何这些。它以同等的信心呈现全部五条。
如何筛选大语言模型生成的关系
有用的工作流不是”问大语言模型、复制输出、去吃午饭”,而是”问大语言模型、把输出当作候选列表、然后用推理和测试验证每个候选”。
第一步是按类型对每条提议的关系分类。结构性关系,如排列不变性或幂等性,往往更安全,因为它们较少依赖领域语义。算术关系,如缩放或平移,在成立时很强大,但经常在大语言模型没考虑到的边缘情况上失败:负乘数、空集合、浮点舍入。
第二步是寻找反例。对每条提议的关系,尝试找到一个让它失败的输入。这是发现大语言模型幻觉最快的方式。
def test_percentile_scaling_counterexample():
"""The LLM proposed scaling. It fails for negative k."""
values = [1.0, 2.0, 3.0, 4.0]
original = percentile(values, 0.5) # 2.5
k = -1.0
scaled_values = [v * k for v in values]
scaled_result = percentile(scaled_values, 0.5) # -2.5
# The relation holds here by accident. For nearest-rank,
# multiplying by a negative flips sort order and breaks it.
assert abs(scaled_result - original * k) < 1e-9
缩放关系碰巧对线性插值成立,但你只有测试了才知道。大语言模型不知道。它只是基于模式匹配猜测。换一种百分位算法,缩放就会以明显的方式失效。
第三步是 mutation testing。一旦你把一条关系实现为测试,就运行 mutation testing 工具。如果 mutants 存活,你的关系太弱;如果正确代码被杀死,你的关系是错的。
大语言模型的长项与短板
大语言模型在成熟领域发现关系时确实很有用。它们知道图像分类器应该对水平翻转保持不变性,排序算法应该是幂等的,矩阵乘法应该对加法满足分配律。大语言模型能瞬间回忆出这些经典关系。
在包含训练数据中不存在的隐式约束的领域,它们就没那么有用了。如果你在测试一个带有区域折扣和促销码交互业务规则的自定义定价引擎,大语言模型完全不知道。它会提议忽略业务逻辑的通用算术关系,或者更糟,建议与业务逻辑矛盾的关系。
失败模式是可预测的:
过于通用的关系。 大语言模型对一个返回概率的函数建议”输出应该为正”。这是一个弱的健全性检查,不是 metamorphic relation。它能捕获崩溃,但捕获不了逻辑 bug。
假设连续性的关系。 大语言模型提议小的输入变化产生小的输出变化。这对阈值函数和离散分类器不成立。
忽略类型约束的关系。 大语言模型建议按某个字段对 dataclass 列表排序,然后检查第一个元素的字段是最小值。它忘了有些字段可能是 optional 的,或者该类型的比较运算符可能没有定义。
数学上错误的关系。 大语言模型曾建议拼接列表的中位数等于子列表中位数的平均值。这是不对的。但大语言模型以与排列不变性同等的信心呈现了它。
实用工作流
不要让大语言模型替代你的大脑。让它加速你盯着空白页面发呆的那部分。
先写一段关于你的函数的描述,包括类型、约束和已知的边缘情况。你给的上下文越多,大语言模型产生幻觉的情况就越少。
要求按类型分类的关系:invariance relations、monotonicity relations、additive relations 和 structural relations。这种框架帮助大语言模型组织其模式匹配,产生更一致的输出。
对每条提议的关系,过一遍以下清单:
- 它对空输入成立吗?
- 它对单元素输入成立吗?
- 它对负值、零、NaN 或无穷大成立吗?
- 当输入已经有序时成立吗?逆序时呢?
- 我能写出一个 mutation test,只有在这条关系成立时才能杀死 mutants 吗?
保留通过全部五项检查的关系。丢弃其余的,并记录原因。
下面是我们内部使用的提示模板:
SYSTEM_PROMPT = """
You are a testing assistant. Given a Python function, propose metamorphic relations.
For each relation:
1. State the relation clearly
2. Identify the type: invariance, monotonicity, additive, or structural
3. List edge cases where it might fail
4. Rate confidence as HIGH, MEDIUM, or LOW
"""
def generate_relations(source_code: str) -> list[dict]:
response = openai.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": f"Propose relations:\n\n{source_code}"}
],
temperature=0.3,
)
return parse_relation_candidates(response.choices[0].message.content)
在温度 0.3 下,大语言模型创造力降低但一致性提高。对于 metamorphic relations,一致性胜过创造力。你想要的是无聊但正确的关系,而不是聪明但错误的关系。
真正的瓶颈仍然是你
大语言模型可以加速发现,但不能替代验证。一条你没有亲自验证过的 metamorphic relation 不是测试,而是一个伪装成断言的猜测。
对于”大语言模型能帮我发现 test oracles 吗?“这个问题,诚实的答案是:部分可以。它能发现候选、唤起你的记忆、建议边缘情况。但它无法告诉你,在你特定的领域、特定的约束下,哪些关系对你的具体实现是正确的。
这部分仍然需要一个理解代码的人。大语言模型是头脑风暴伙伴,不是 oracles 的 oracle。
如果你从零开始,选一个 oracle 薄弱的函数,向大语言模型要五条关系,然后花二十分钟尝试打破每一条。存活下来的关系就是你的种子集。那些被打破的关系教给你的东西,比大语言模型本身能教的还要多。