引言
早上九点,你打开测试报告。上千条用例全绿,你端起咖啡,觉得今天又是太平的一天。
直到实车冬测。车在极寒环境过减速带,预警系统哑火了。
AI生成的用例通过率九十九点二,有效缺陷发现率三点七。高通过率不是勋章,是坟头草。
99%通过率的陷阱:一个传感器标定异常的启示
某ADAS团队的真实数据:AI产出一千二百个用例,绿得刺眼。实车冬季测试,零下三十五度叠加复合振动,传感器标定漂移二点三个百分点,碰撞预警漏报。
AI不是没测低温。它测了零下四十度单温度。它也不是没测振动。它测了两千赫兹单频振动。但它从未生成过"低温加复合振动"的组合边界。更离谱的是,即便输入凑巧撞上了边界,断言也只有一句`结果不为空`。
验证了个寂寞。
传感器标定函数的边界清单其实很清楚:温度零下四十到八十五度、振动五到两千赫兹、老化零到一百二十个月、安装角度公差极限正负两度。单变量边界都有,组合边界才是杀机。AI生成的测试像一张只涂了半边的防弹衣——正面看着挺厚,侧面漏风。
AI生成测试用例:怎么工作的,为什么看起来很美
LLM生成单元测试,本质上是代码补全的变种。你给一段被测函数,模型在训练时见过的大量"函数-测试对"里找规律,猜出最可能的测试代码。
这个过程有三个结构性特征,每一个都在把边界条件往外推。
特征一:模式匹配,不是规格推导。
模型不读你的需求文档。它看到`calculate_calibration_offset(temp, vibration)`,联想到训练数据里见过的类似函数。如果那些函数的测试都是`assert result > 0`,它就照抄。它不关心这个函数在汽车安全链里处于什么位置。
特征二:贪心解码的局部最优。
生成每个token时,模型选当前概率最高的选项。就像自助餐只拿中间那排菜,边缘的看都不看。
这里有个参数叫Temperature。说人话就是控制模型"随机程度"的旋钮。但不管是调高调低,模型的选择空间仍然受限于训练分布的概率密度。corner case输入——比如零下四十度加两千赫兹——在训练数据里的共现频率趋近于零。零附近的任何微调,结果还是零。
更深层看,模型给所有选项打分后,会做一次softmax归一化。说人话就是强行让概率总和为一。这步进一步抹平了极端值的概率质量,模型更倾向于选最安全的中间选项。
特征三:幻觉断言。
微软研究院二零二四年的研究给这个现象命了名:断言幻觉。说人话:测试看起来在验证,实际上检查的是"门把手在不在",而不是"门能不能防住撞击"。
来看一组真实数据:
行覆盖七十八,看起来不错。但行覆盖不衡量边界,MC/DC才衡量决策独立性。边界分支覆盖率直接被砍半,这才是真相。
来看代码对比。AI生成的典型测试长这样:
# AI生成(典型)def test_calibration_normal(): result = calculate_calibration_offset(25.0, 100.0) assert result is not None而手写的边界测试长这样:
# 手写边界测试def test_calibration_temp_boundary_low(): result = calculate_calibration_offset(-40.0, 0.0) expected = lookup_expected_calibration(-40.0, 0.0) assert abs(result - expected) < 0.005 * FULL_SCALE, \ f"低温标定超差: {result}, 允许范围: ±{0.005 * FULL_SCALE}"一个是`不为空`,一个是`误差小于千分之五`。你品,你细品。
◆ 核心概念:Corner case的系统性盲区
Corner case不是"比较难想到的测试用例"。它是系统行为发生质变的参数空间边界区域。在物理系统里,往往对应多个环境变量同时逼近极限的耦合状态。
AI对这个区域的覆盖存在系统性盲区。不是偶尔漏掉几个,是结构性、可预期的、大规模遗漏。
第一层:神经网络的平滑性本质
大语言模型本质上是训练数据分布的平滑插值器。像Excel自动补全折线图,数据点多的地方画得准,两头全靠猜。
它在高维嵌入空间里,对样本密集区域估计准确,对稀疏区域——也就是分布边缘——估计偏差极大。Corner case恰好位于这个"稀疏边缘"。训练数据里本来就少,模型学到的是"这种情况不太常见",生成时自然回避。
更底层看,Transformer的自注意力机制会做softmax归一化。这种归一化进一步"抹平"了极端值的概率质量,让模型偏好"最安全"的中间选项。
第二层:组合爆炸的不可解性
单个变量的边界容易想到。温度极限、振动极限。但corner case的危险性来自多变量耦合。假设一个传感器有五个关键环境变量,每个变量有三个边界区域,完整的二阶组合边界就有九十种。三阶组合两百七十种。
五个变量的边界组合,比五位数的密码锁还多一格。
而LLM的贪心解码——每步只选当下看起来最稳妥的选项,从不做全局规划。每次只考虑单步最优,没有系统性组合策略。LLM没有领域知识告诉它"温度和振动是耦合的",它不会主动设计联合应力测试。
第三层:断言幻觉
即使LLM碰巧生成了一个触及边界的输入,它生成的断言往往也是"幻觉"——验证的是显而易见的性质,而非真正的边界不变式。比如测试温度补偿函数,LLM可能assert `result != null`,而正确的边界断言应该是`abs(result - expected) < 0.01 * full_scale @ T=-40°C`。
这就像保安巡逻只看门锁亮不亮,窗户被人撬了都不知道。
微软研究院把它命名为"断言幻觉"。测试通过了,但什么也没验证。
设计意图的错位
LLM被设计成"像大多数开发者那样写代码"。它的优化目标是最小化预测误差,与"发现系统缺陷"存在根本张力。大多数开发者本来就不擅长写边界测试(开源数据里占比不到百分之五),模型学了这个分布,自然复现了这个盲区。
这是feature——只不过是对"写得快"这个需求的feature,不是对"写得安全"的feature。
为什么这不是技术迭代能解决的
训练数据本身有结构性偏差。喂再多数据,如果高质量边界测试在开源世界里本身就是稀有物种,模型学到的还是"凑合测测就行"。
和模糊测试的对比
传统模糊测试也有随机性,但它的设计目标是最大化代码路径覆盖,通过覆盖率反馈指导变异。LLM生成测试没有这种反馈闭环——生成即结束,不测量实际覆盖了哪些边界分支。把LLM和覆盖率引导的模糊测试结合,才是当前研究的前沿。
混合模式:人定边界,AI填代码
务实做法不是全人工也不是全AI,而是明确分工:
# 人类定义:这些边界条件必须被覆盖BOUNDARY_CONDITIONS = [ {"temp": -40, "vibration": 2000, "description": "低温高频振动耦合"}, {"temp": 85, "humidity": 95, "age_months": 120, "description": "高温高湿老化极限"}, {"temp": 25, "mount_angle": 2.1, "description": "安装角度上限+公差"},]# AI生成:针对每个条件生成具体测试代码def generate_test_for_boundary(condition): prompt = f""" 为碰撞传感器标定函数生成单元测试。 环境条件:温度{condition['temp']}°C,振动{condition['vibration']}Hz 必须验证:标定输出误差 < 0.5% full scale 必须包含:异常时抛出 CalibrationOutOfBounds """ return llm.generate(prompt)人给骨架,AI填肉。但顺序不能颠倒——后文会讲为什么。
ISO 26262的拷问:什么样的覆盖才算"完备"
现在把问题放到功能安全的显微镜下。
ISO 26262-6:2018的表九和表十是汽车软件测试的宪法。表十列出了软件单元测试方法:
ASIL-D是汽车功能安全的最严苛等级,功能失效可能导致人员死亡,厂商得负法律责任。`++`表示"高度推荐"——对ASIL B-D是强制性实践。
但这里有个更狠的角色:MC/DC。全称 Modified Condition/Decision Coverage。说人话:你得证明每个if条件里的每个子条件,单独改一下就能让整个判断翻盘。
比如决策逻辑`if (decel > THRESHOLD && jerk > JERK_THRESHOLD || crash_zone == FRONT && !seatbelt_buckled)`,MC/DC要求你证明:减速度单独变化能改变结果、加加速度单独变化能改变结果、碰撞区域单独变化能改变结果、安全带状态单独变化能改变结果。
这意味着测试用例的设计必须基于对决策逻辑的结构分析,而非统计采样。
AI生成与标准的方法论错配
统计采样——说人话就是抽一部分数据试试,靠概率估整体。而功能安全要的是逻辑完备:每一个关键条件都必须被独立验证。
工具鉴定的结构性盲区
ISO 26262-8的工具鉴定流程通过"工具影响(TI)"和"工具检测(TD)"评估置信度。TI衡量工具引入错误的能力,TD衡量检测工具自身故障的能力。
但当一个AI测试生成器产出百分之九十九通过率的同义反复测试——断言被测代码的输出等于它自己——它既没有"引入错误",也没有"工具自身故障"。它只是制造了大规模的系统性漏报。工具鉴定流程对这类结构性假阴性完全没有评估维度。一套AI生成的测试套件可以完美通过TCL1鉴定,同时让一个ASIL-D级别的碰撞检测缺陷悄然上线。
MC/DC是错误的靶子?
行业讨论AI测试局限时,习惯性地拿MC/DC当黄金标准——"AI做不到MC/DC,所以不行"。但这个框架本身有问题。
MC/DC衡量的是代码结构的测试充分性,但AI测试的根本问题不在这里。AI可以覆盖一个`if (decel > THRESHOLD)`分支,甚至用不同输入覆盖条件独立性——但其断言可能是`assert called == True`。
真正需要关注的不是"代码路径是否被执行了",而是"代码路径被验证了什么"。我称之为规格断言密度:安全需求文档中每一个可验证的不变式,有多少在测试套件中以可证伪的断言形式存在。在典型的AI生成测试套件中,这个密度可能不到百分之五。
来看MC/DC分析示例:
// 碰撞检测决策逻辑(简化)bool should_trigger_airbag(float decel, float jerk, bool seatbelt_buckled, uint8_t crash_zone) { return (decel > DECEL_THRESHOLD && jerk > JERK_THRESHOLD) || (crash_zone == FRONT_IMPACT && !seatbelt_buckled);}// MC/DC要求的测试集(最小):// C1: decel=T, jerk=T, seatbelt=X, crash_zone=X → 结果=T// C2: decel=F, jerk=T, seatbelt=X, crash_zone=X → 结果=F (证明decel的独立影响)// C3: decel=T, jerk=F, seatbelt=X, crash_zone=X → 结果=F (证明jerk的独立影响)// C4: decel=F, jerk=F, seatbelt=F, crash_zone=FRONT → 结果=T// C5: decel=F, jerk=F, seatbelt=T, crash_zone=FRONT → 结果=F (证明seatbelt的独立影响)// AI生成的问题:它不会主动分析'哪些条件需要独立验证'// 它可能生成十个随机组合,但遗漏C3或C5AI不会主动分析"哪个条件需要独立验证"。这是结构性缺陷。
统计采样 vs 逻辑完备:两种世界观的碰撞
Dijkstra一九七二年有句名言:"测试只能证明存在bug,不能证明不存在bug。"
但ISO 26262的要求比这更强:它不是要"证明没bug",而是要"证明风险已降至可接受水平"——这是一个需要结构化论证的工程目标,而非统计估计。
两种世界观的根本差异
AI测试生成的优化目标是最大化通过率。这个目标的隐含假设是:测试用例之间独立,且总体分布平稳。但功能安全的关键问题恰恰是分布不平稳:corner case不在训练分布内,危险场景是结构性稀疏的。用统计方法处理结构性稀疏问题,是范畴错误。
历史视角:这不是新争论
一九九六年Ariane 5火箭首飞爆炸,原因是惯性导航系统软件复用了Ariane 4的数据转换例程,未处理Ariane 5更高的水平加速度值——一个边界条件未被重新验证的经典案例。当时没有AI生成测试,但已有"基于统计的可靠性增长模型"与"基于形式化方法的完备性验证"之争。
今天的AI测试生成,只是这场古老争论的新演员。
AI测试的"香蕉枯萎病"
更隐蔽的风险是相关性盲区。如果Toyota的制动团队和Honda的制动团队都用GPT-4/Copilot生成单元测试,训练数据里缺少"零下四十度低温加两千赫兹高频振动加一百二十个月老化"的三重组合——两个团队会同时漏掉这个corner case。
手写测试时代不存在这个问题,因为不同团队的工程师有不同的经验盲区,错误是独立的。AI模型制造了一种"验证均匀性":所有用户的盲区趋同。ISO 26262从未考虑过这种相关性故障模式,它假设独立开发的系统产生独立的缺陷分布。
代价与取舍
形式化方法不是银弹。过去二十年,模型检验和定理证明因为成本过高未被广泛采用。但AI测试生成正在制造隐性的成本转移:写测试的成本低了,审查测试质量加承担漏报后果的成本高了。
当漏报的严重性(ASIL C/D的碰撞检测缺陷)叠加漏报的系统性(全行业趋同的盲区),形式化方法的性价比拐点可能已经到来——不是因为形式化变便宜了,而是因为"看起来跑通了但不安全"变贵了。
SOTIF的补刀
大多数人盯着ISO 26262,忽略了ISO 21448(SOTIF,Safety of the Intended Functionality)。说人话:它管的是"系统没坏,但照样危险"的场景——恰恰包括AI测试生成器"按设计运行但仍然漏报"的情况。
当前没有任何AI测试生成工具公开过SOTIF合规论证。
落地清单:在现有条件下能做什么
接受现实:LLM不会自动帮你通过ISO 26262认证。拒绝现实:手写所有测试在大型项目中不经济。
关键是顺序。
"AI生成骨架,人工填充边界"是现在的流行说法。但这个共识掩盖了一个残酷认知:语义审查不可规模化。人类审查一个测试用例是否验证了正确的安全不变式,需要三十到一百二十秒的深度思考。一千个用例就是八到三十三小时。审查最终会退化为"检查测试是否通过"——而这恰恰是AI已经做过的。
正确的人机分工不是"AI先写,人后审",而是"人先定义可证伪的不变式,AI负责代码生成和参数填充"。顺序不能颠倒。
措施一:边界条件清单作为约束输入
不要给AI裸代码。给它结构化约束:
【被测函数】calculate_calibration_offset【必须覆盖的边界条件】- 温度:-40°C, -30°C, 25°C, 75°C, 85°C- 振动:0Hz, 5Hz, 100Hz, 2000Hz- 组合要求:(-40°C, 2000Hz), (85°C, 5Hz) 必须出现【断言要求】- 必须验证标定值精度:abs(result - expected) < 0.5% full_scale- 必须验证异常处理:超出补偿范围时抛出 CalibrationOutOfBounds措施二:LLM + 基于属性的测试(PBT)混合
用Hypothesis定义不变式,让机器自动在边界上"搞破坏":
from hypothesis import given, strategies as st, settings@given( temp=st.floats(min_value=-50, max_value=95), vibration=st.floats(min_value=0, max_value=2500))@settings(max_examples=1000, deadline=None)def test_calibration_invariant(temp, vibration): result = calculate_calibration_offset(temp, vibration) # 不变式:标定值必须在物理合理范围内 assert 0 <= result <= FULL_SCALE # 不变式:正常范围内精度必须满足 if -40 <= temp <= 85 and 5 <= vibration <= 2000: expected = reference_model(temp, vibration) assert abs(result - expected) < 0.005 * FULL_SCALE措施三:覆盖率反馈闭环
AI生成测试后,必须运行并测量语句覆盖、分支覆盖、MC/DC覆盖(对ASIL C/D)。未覆盖的条件组合,人工分析后补充。不要让AI"生成即结束"。
措施四:安全关键模块的人工审查清单
三个问题,逐一回答
为什么九十九点二通过率是危险信号?因为通过率测的是"测试跑没跑完",不是"测试验没验到点子上"。断言幻觉让bug都懒得躲。
为什么ISO 26262的工具鉴定没保护我们?因为它鉴定的是工具"会不会出错",不是工具"会不会系统性地不发现错误"。这是结构性盲区,不是意外。
谁该承担最终责任?如果工具提供方不需要为安全后果负责,OEM又无法审查千级用例——责任真空只能由明确的人机分工协议填补:人对不变式负责,AI只对代码生成负责。出了事,找不到AI头上,只能找签字的人。
"测试通过"是最廉价的安慰剂,"边界被验证"才是最贵的工程债。
你用过AI生成测试工具吗?你检查过它生成的断言到底在验证什么?聊聊你见过的最离谱的"幻觉断言"。
本文仅代表 ZAI攻城狮 运营者个人观点,不构成任何投资建议或商业决策依据。文中涉及的技术分析基于公开资料,如有疏漏欢迎指正。
夜雨聆风