上个月,我们负责的内部知识问答系统走完了最后一轮测试,项目组在验收单上签了字。但我清楚,所谓"测试收官",只是把可以穷举的部分全部穷举完了,剩下那些测不完、说不清、偶尔还翻车的部分,才刚刚进入长期观察期。
如果你用传统软件测试的思路来测一个基于大语言模型的AI系统,头两周你会觉得"这不就是接口测试加了一点规则吗",第三周心态就会崩——同一个问题,上午和下午的回答不一样;加了一个"偏移词",整个回答的立场就变了;线上跑了三千条用例全过,用户随口一句"你女朋友呢"就能把系统带进沟里。这篇文章就把我们这一趟AI测试的完整旅程写下来:怎么重新定义测试对象,怎么设计五层验证体系,踩了哪些让团队集体沉默的坑,以及最后凭什么判断"可以结束了"。如果你是第一次负责AI系统的测试,或者已经在AI测试里被badcase折磨得怀疑人生,这篇应该能给你一些可以直接用的东西。
1. 当被测对象从"确定性代码"变成"概率性行为"
1.1 传统测试的第一性假设为什么崩塌
传统软件测试的核心假设是:给定同样的输入,经过同样的逻辑,必然产生同样的输出。基于这个假设,我们才能谈通过/失败、谈回归、谈自动化。但基于大模型的AI系统不是这样,模型推理时的采样过程本身带随机性,同样的输入在不同时间、不同采样参数下可能给出不同回答。
我印象最深的一次争论是,项目组质疑我们:"你们是不是没等模型加载完就跑了?为什么同一个问题两次结果不一样?"我只能打开日志,把两次输出的token分布、采样seed、温度参数摆出来。这不是测试执行的问题,而是被测对象的属性变了——从确定性逻辑变成了分布性行为。你没法用"断言一个精确字符串"的方式去验证一个概率模型,就像你不能要求一个真人每次都用一模一样的措辞回答你。
这个认识是后面所有工作的起点。我们跟研发达成了一个共识:AI测试的本质不是"检验一次结果是否正确",而是"评估大量样本下行为的整体可信度"。传统测试追求的是"零缺陷",AI测试追求的是"缺陷在可控范围内且可被持续观测"。
1.2 测试模型的重构:从"判对错"到"量偏差"
既然输出是概率性的,那测试就不能只回答"对不对",而应该回答"偏不偏""偏了多少""能不能接受"。我们做了一件事:把原先的测试用例分类从"功能点"改成"行为维度"。
具体拆成了五类:语义一致性(用户意图是否被正确理解)、观点立场一致性(回答是否越界、偏航、被带偏)、内容安全合规(是否触碰禁区)、任务成功率(是否能完成知识查询、信息抽取等具体任务)、极端输入容忍度(乱序、错别字、恶意引导时是否崩溃)。每个维度都定义了可量化的观测指标,而不是笼统地说"回答质量差"。
比如"任务成功率"我们定义成:给定一百个标准知识检索请求,系统能在规定时间内返回且答案与知识库原文的语义相似度超过阈值的比例。"极端输入容忍度"则用一组包含错别字、口语噪音、中英混排的对抗样本来评估,看系统答非所问的比例是否超过5%。这些指标虽然不完美,但至少让团队有了一把共同的尺子。
1.3 用例设计方式的变化
原来写用例是"输入 → 期望输出 → 断言",现在写用例是"输入 → 基准回答 → 容忍区间 → 多轮采样"。比如同一个用户问题,我们会让系统回答5次,统计语义相似度分布,低于某个阈值才标记为异常。
这个问题听起来简单,实际上一开始我们的脚本写得很粗糙,循环调用接口五次然后直接比对字符串,结果误报率高得吓人。后来我们把"多轮采样"设计成三个层级:同一参数下采样、不同温度参数下采样、不同prompt模板下采样。只有同一参数下结果出现显著漂移,才判定是模型不稳定;不同参数下的差异属于预期行为,只做记录不做判断。这个区分帮我们省掉了大量无效排查。
2. 五层验证体系:把AI系统拆成看得见的地方
2.1 输入解析层:考验的是鲁棒性不是正确性
整个系统不是一个裸模型直接对外提供服务,它前面有意图识别、实体抽取、检索召回、规则拦截等一堆环节。我们按调用链路拆了五层,每一层分别设计测试策略。
第一层是输入解析层,包括ASR文本转写(如果有语音入口)、错别字纠正、中英文混排处理等。这一层的测试重点不是"解析得对不对",而是"面对各种畸形输入还能不能继续往下走"。我们造了一批离谱的输入,比如全角半角混用、emoji夹杂、连续多个标点、拼音和汉字混写,观察系统是报错、乱答还是给出合理兜底。
有一个很典型的badcase:用户把"帮我查一下明天的天气"写成"帮我查一下明天滴天气",系统在前端纠错环节把"滴"正常识别为"的",但到了下游意图识别时却把整句判定为闲聊,直接返回了一句俏皮话。你说它错了吧,它确实识别出了意图;说它对了吧,任务完全没完成。这类问题的根子不在模型,而在管道环节的置信度互传机制,只有分层测试才能暴露出来。
2.2 知识召回层:向量检索返回的是候选,不是答案
第二层是知识召回层,系统会先把用户问题转成向量,在知识库做相似度检索,再决定从哪几篇资料里提取答案。这里最容易踩的坑是:把召回结果直接当成答案来断言。
我们的做法是单独评估"召回准确率"和"顶k命中率"。也就是说,故意构造一批只与知识库某一条记录语义相关、但措辞完全不同的提问,看系统能否从海量文档里捞出正确的那篇。这一层不需要看最终回答是否漂亮,只看候选列表里有没有正确答案。有一次我们查一个"为什么我的申请被驳回"的问题,系统捞回来的文档全是关于"驳回申诉流程"的,跟用户想要的"驳回原因分类"完全不沾边,最后生成阶段再努力也答不到点上。这种问题在端到端测试里很难定位,但在分层测试里一目了然。
2.3 生成策略层:温度参数不是随便调着玩的
第三层是生成策略层,也就是把召回结果和用户问题组装成最终回复的那一步。这一层的变量最多,因为prompt模板、温度参数、最大token数、停止词都会影响输出形态。
我们专门做了一组"参数敏感性测试":固定所有输入,只调温度参数从0.1到0.9,观察输出的语义漂移程度。结果发现,温度超过0.7之后,回答开始出现大量口语化填充词,虽然读起来更"自然",但知识性内容开始漏;温度低于0.2,回答又变得极其机械,甚至出现复读机现象。最后我们把业务场景做了拆分——知识问答类强制用低温,闲聊寒暄类可以适当提高温度。这个经验后来被写进了系统配置规范,每个接入的业务线都按这个原则去调参。
2.4 多轮对话状态层:上下文记忆的过期与串味
第四层是多轮对话状态层,也是最容易出隐蔽问题的地方。系统需要记住用户前几轮说了什么,才能给出连贯回答。但与人类对话一样,记忆是有期限和边界的。
我们发现两个典型故障模式。一是"上下文过期":用户在第3轮修正了需求,系统到第5轮还在按老需求回答,因为对话状态管理器没有正确更新槽位。二是"上下文串味":两段完全不相干的对话被粘在同一个session里,模型把前一段话题的信息错误地引用到后一段。比如用户先问"推荐几部科幻电影",又突然问"明天有什么会议安排",系统会把电影名当成会议议题来回答。这类问题用单轮接口测试永远发现不了,必须设计一系列多轮场景,而且要在中间故意变更用户意图,验证系统能否"遗忘"掉已经过时的信息。
2.5 业务规则层:对齐约束是否真实生效
第五层是业务规则层,包括内容安全拦截、敏感话题回避、品牌黑话管理等规则。这一层测试起来有点像传统的规则引擎测试,但难点在于"规则与模型的优先级"。
我们遇到过一种情况:安全规则库明明拦截了包含某个敏感词的问题,但模型在回答时还是顺着语境输出了一段擦边内容。原因是安全模块只做了关键词匹配,而模型生成阶段没有接入相同的约束。后来我们调整了验证策略,不再只看"规则有没有生效",而是看"规则在每一条生成结果里是否都生效"。做法是把安全规则做成了生成阶段的post-check和pre-check双重校验,测试也分两步走:先验证入口拦截,再验证生成结果端的合规性。
这五层不是割裂的,一个端到端问题往往同时在两层里都有影子。但把它们拆开之后,至少每次出问题我们都能快速定位,而不是对着一个黑盒瞎猜。
3. 那些让测试团队集体沉默的AI异常现场
3.1 同一条问题,上午测通过、下午测翻车
我要说一件让团队集体沉默的事。我们有一组核心回归用例在早上跑全绿,下午发布新版本后跑了同样的用例,居然有十几个直接失败。研发第一反应是:"新版本引入回归了,赶紧回滚。"但仔细看了日志之后发现,失败的原因不是模型权重变了,而是我们把测试环境的并发数提到二十之后,部分请求走了不同的GPU节点,节点的量化精度设置不一样,导致输出分布发生偏移。
这不是偶发个案。大模型服务通常部署在多节点集群上,不同节点的推理引擎版本、量化方式、显存状态都可能不一样,于是同一版本也会出现"节点差异"。从那以后,我们把"多节点一致性"列入了回归项,每次跑用例之前先记录请求落在哪个节点上,如果结果异常,先做节点维度聚类分析,而不是武断地归因到模型更新。
3.2 换个称呼就全盘失守的口令漂移
我们的系统里设置了一个"终止话题"的功能,用户说"换个话题"或者"不聊这个了"时,系统应该礼貌地结束当前话题。测试用例里的"换个话题"、"聊点别的"都通过了,结果上线当天就被打脸——用户说的是"咱们换个话题吧,这个没意思",系统完全没识别出来,还在继续输出上一个话题的内容。
这就是典型的"口令漂移":同一个意图有无数种表达方式,测试样本只覆盖了其中一小部分。我们后来做了一套"语义等价扩写"工具,用大模型本身把每一条测试集中的口令类用例扩写成五十个不同说法,再拿扩写后的集合去跑测试。实测下来,召回率从原来的68%提到了89%,但仍然没有到100%。这件事让我彻底明白了一个道理:AI测试的样本覆盖率永远追不上真实世界的表达多样性,能做的是不断用线上badcase回灌测试集,把这个覆盖率的缺口越缩越小。
3.3 "一本正经胡说八道"的置信度陷阱
我们的系统会在回答后面附带一个置信度分数,设计初衷是让用户自己判断结果可信不可信。测试时我们发现,模型对某些冷门知识点的回答明明已经错到离谱了,置信度居然还显示0.95。研发解释说,这个置信度来自"解码概率"的归一化,只能反映模型对生成内容的"内在确定性",不能反映事实正确性。
这个发现非常要命。如果用户看到高置信度就自然信任了,错误答案的传播影响会被放大。所以我们调整了产品交互:不再展示一个孤立的数字,而是把置信度拆成"三个标志"——是否有知识库依据、检索结果与问题的相关性、生成内容的语义连贯性。只有三项都满足才显示"推荐采纳",否则直接标注"仅供参考,请核查来源"。这个改动本质上是用工程手段对抗模型的内在不确定性,也是测试逼出来的产品改良。
3.4 复现不了的问题要怎么记录
AI测试最让人抓狂的是badcase不可复现。我们跨部门联合排查一个多轮对话异常,业务方反馈用户遇到的僵硬回答,我们拿着日志里的原始输入去复现,结果连续三次都是正常回答,直到第四十八次才终于出现一次预期内的错误。
后来我们形成了一套"现场快照"的规范:但凡发现异常行为,第一时间抓取session ID、请求体、响应体、模型版本、参数配置、时间戳、节点编号,并强制保存一个"上下文切片"——特别是多轮对话前几轮的完整记录。没有这些信息,badcase基本等同于不存在。另外,我们不再追求"每次都能复现",而是用"在什么分布条件下出现概率更高"来描述问题,比如"当轮次超过5轮且用户连续两次修改了槽位时,出错率约12%"。这种描述比"偶发故障"有价值得多,研发也更容易定位到真正的根因。
4. "测试终点"的真实含义:通了不等于用得住
4.1 用三个硬指标定义"可以收官"
项目上线前,产品负责人连着问了三次"到底测完没有"。这个问题让我想了很久,最后我们定下了三个硬指标,全部满足才允许签署测试通过结论。
第一个是场景覆盖率:核心业务场景的用例覆盖率达到95%以上,且这些用例由业务方、测试方、研发方三方共同评审确认。第二个是语义漂移容忍度:在固定样本集上连续运行一周,每日输出与基准版本的语义相似度波动不超过设定阈值,这个阈值按业务场景不同而不同,知识问答类控制在3%以内,开放闲聊类控制在8%以内。第三个是badcase闭环率:近两个迭代周期内发现的所有高优badcase,必须全部有明确的处置结论,要么修复验证通过,要么确认在已知风险清单里并有对应降级方案。
有意思的是,这三个指标全部满足之后,我反而觉得"测试结束"这个说法不准确。准确的说法是"这一阶段的测试目标已经达成,可以进入运维期的持续监控"。所谓终点,其实是一个阶段性的验收点,不是一个可以一劳永逸的句号。
4.2 灰度流量与线上哨兵策略
测试通过了并不意味着就可以把所有监控都撤掉。我们的做法是让测试体系在线上以另一种形态继续活着:对真实流量做抽样的自动标注、自动比对、自动告警。
具体来说,我们在线上搭建了一个"哨兵"通道,对进到系统的请求按1%的比例做无侵入的副作用记录,然后把模型输出发给一个独立的评测模型打标签,标签维度包括回应相关性、安全性、任务完成度。一旦某类标签的异常率超过历史基线两倍,就自动触发告警并把样本回灌到下个版本的测试集里。这套机制上线后帮我们抓到了好几个线下测不出来的问题,其中最典型的是"长尾用户问法"导致的回答质量劣化——只有真实流量大了,这些才会浮现。
4.3 环境清理与长期观测机制的接管
在宣布测试收官之前,我们还做了一件很琐碎但必须做的事:把测试环境与生产环境彻底隔离,测试数据不再参与线上模型的任何训练或评测流程。听起来简单,实际执行中很容易出差错,因为团队的调试脚本偶尔还会指到旧环境。
清完之后,我们把长期观测责任正式交给了运维侧,测试团队只保留每个迭代的波动提醒。线上监控面板上挂了三个核心指标:用户反馈差评率、回答超时率、内容安全拦截率。这三个指标任何一个出现连续下跌,值班人员就会拉一个临时测试专项。从那一刻开始,项目的测试阶段才算真正画上句号。
5. 走完这趟旅程后,我想分享的五条实战经验
5.1 测试数据集要当成产品来维护
这是我这趟旅程最大的教训。最初测试集是开发临时攒的,按各自理解随手丢进去几十条,结果不同版本的测试结果之间没法对比——不是模型变好了,而是样本被换掉了。后来我们专门建了一套测试集管理流程,所有人提的新用例必须过评审,每个用例标注业务来源、优先级、创建时间、最近一次有效时间。每个月做一次"样本体检",去掉过时或重复的用例,补充新发现的badcase。只有把测试集当成一个独立的交付物,测试结论才是可信的。
5.2 badcase报告要带"上下文切片"
初期的badcase描述都是"用户问了一个关于A的问题,系统回答了很多关于B的内容"。研发拿到这种问题描述,根本无法定位是意图识别错了、检索错了还是生成错了。后来我们统一了badcase报告模板,必须包含四个部分:原始输入的完整上下文(特别是多轮历史)、实际输出、期望行为说明、问题发生的链路层推断。人话说就是:不仅要说"发生了什么",还要说"是在哪一步开始错的"。这个改变把badcase的处理效率提升了不止一倍。
5.3 用大模型生成对抗样本,是一把最锋利的矛
研发最烦测试说"这里可能有问题,但我想不出来怎么构造这个输入"。我们的解决方案是,让测试人员和研发用大模型自己做"语义等价扩写"和"对抗输入生成"。把原生用例喂给模型,让它改写成不同语气、不同句式、不同干扰程度的变体,再人工筛选后加入测试集。这样做的好处非常明显:人工设计用例时受思维定势影响,写来写去都是"正常问法",模型生成的变体则能覆盖很多让人意想不到的表达。当然,生成出来的样本一定要人工复核,否则容易混进去本身标注错误的脏数据。
5.4 人机协同回归,比全自动回归更可靠
我们一开始追求"全自动回归",脚本跑完直接出红绿报告。后来发现全自动脚本在AI测试里有一个致命缺陷:它只能检查机器能定义的规则,比如超时时间、关键词、长度上限,而真正的语义质量它判断不了。于是我们把回归改成两段式:第一段全自动跑出所有结果,第二段由测试人员抽检10%到20%的样本做人工语义审核,同时用评测模型给所有样本打预标签,人工只复核评测模型标出的"疑似异常"。这套模式兼顾了速度和准确度,也让人工从海量重复劳动里解放出来。
5.5 AI测试工程师的核心能力是"翻译"
最后一条经验可能有点务虚,但我觉得是最重要的。做AI测试,技术手段只占一半,另一半是沟通能力。你要能把用户的模糊反馈翻译成测试工程师能执行的验证场景,把测试发现的问题翻译成研发能定位的技术语言,把测试风险翻译成产品能理解的业务语言。很多时候三方讨论陷入僵局,不是问题本身多复杂,而是各说各话。测试人员一旦能把这些翻译工作做好,你会发现排查问题的效率能再上一个台阶。
现在每当我被问到"AI测试什么时候能结束",我的回答依然不是"测完就结束",而是"你需要在哪些指标上建立稳定信心,在哪些风险上保留可监督的容忍空间"。这些想清楚了,终点自然就到了。