先说一个场景,可能你也遇到过:周一让 AI 写了个日志解析函数,跑得好好的,周三再问同一句,它把 split(",") 改成了 split(",", 2),理由还挺自信——"这样更安全"。你问我怎么知道它变了?因为我给它写了个测试。
是的,给 AI 写代码的那天起,真正的工作量不是"让它写",而是"验证它写的是不是人话"。很多团队的 AI 落地卡壳,不是因为模型不行,而是因为没人给 AI 建一套能跑的"期望值"。于是每次上线都靠人肉点两下,靠"看起来挺对"当验收标准,靠线上报错当单元测试。
这篇不聊哪家模型更强,我们聊聊一件更被低估的事:如何给一个"会一本正经胡说八道"的系统写测试。这是 AI 工具链里最容易被跳过、但收益最高的一环。
一、先承认一个残酷事实:传统测试对 AI 基本无效
传统单测的假设是"函数是确定的,输入 X 必然输出 Y"。而大模型的输出是概率分布,同一句话问十次,可能有九次一个样,第十次给你换了个变量名。这不叫 bug,这叫它的运行机制。
所以很多团队的做法是:把断言去掉,改成"跑一下看看,人工 review"。听起来很务实,实际上是把质量保证退化成抽签。我见过一个团队上线三个月改了四十多个文件,回归全靠 QA 手动点,点到最后大家开始说"这个 bug 好像不是 AI 引入的"——这句话本身就是 bug 温床。
所以正确的思路不是"用传统测试套壳",而是给 AI 输出单独设计一套测试范式。我把它拆成三层,从便宜到贵、从确定到模糊,配套的工具链其实已经相当成熟了。
二、第一层:结构化输出——先把"胡说"变成"报错"
这是最省事、收益最大的一招:永远不要让 AI 返回自由文本给你做判断,先让它返回结构化数据。
比如你要做内容分类,别让它回"我认为这条内容属于科技类"——你拿到的是一个字符串,还得解析,甚至可能是一段客套话前缀。让它直接返回 JSON:
{
"category": "tech",
"confidence": 0.86,
"reason": "文中讨论了模型推理成本的优化策略",
"risk_flags": []
}
好处立刻就来了:第一,解析失败本身就是测试信号,说明模型没听话,程序会立刻炸,而不是把一段垃圾悄悄存进数据库;第二,你能对字段做断言,而不是对整段话做字符串匹配;第三,"confidence" 这个字段让你有机会做阈值分支,把低置信度的结果丢给人看——这才是人机协作的正确姿势:机器干 90% 确定的活,人扛那 10% 不确定的。
实操建议三条:Schema 里把枚举值写死,别用自由字符串;temperature 在结构化抽取任务上直接调到 0 或者极低,别为了"多样性"给自己找麻烦;解析要容错,但容错必须记账——把"解析失败率"当成一个核心指标挂到看板上,它比"准确率"更早告诉你模型在退化。
一个反直觉的经验:让 AI 输出 JSON 时,最常见的失败不是格式错,而是"漏字段"。它会理直气壮地少给你一个 key。Schema 必填项缺失,请当成一等公民的测试用例。
顺带吐槽一句:现在很多团队"用 AI 写测试",本质是让 AI 给自己判卷——被测代码和写断言的代码来自同一颗大脑,共享同一批盲区。所以要么换模型写测试,要么用黄金集做外部锚点,别指望它自己喊自己及格。
三、第二层:语义断言——"差不多对"怎么算对
结构化解决了"格式",但解决不了"内容对不对"。AI 写的函数可能逻辑通顺、格式完美、就是边界条件算错了。这时你需要语义断言。
我一般分三档来卡:
- 精确档:数字、金额、ID、枚举值、是否为空——这些必须完全相等,一毫不差。这类字段一旦错了就是事故。
- 包含档:关键词、必答要素、禁用词。适合做生成内容的合规检查,AI 最容易在这里翻车,比如你明确禁止它输出"作为AI",它偏偏给你加一句。
- 语义档:意思对就行,不要求逐字。判断方法有两条路——用另一个模型当裁判(LLM-as-a-Judge),或者用 embedding 相似度卡阈值。
LLM 当裁判这件事,要吐槽一句:它便宜、快、但有偏见。同模型自我评分会偏向上自己(自偏好),而且新模型普遍爱给高分——裁判模型和被测模型用不同家族会好一些,提示词里必须明确"你的任务是挑刺,不是鼓励",并且强制它先输出"扣分理由"再给分,不然它的分数会一路虚高。
更稳妥的玩法是让模型先自证,再由程序验尸:要求它把答案的关键步骤作为结构化字段输出,然后你的测试用例直接对步骤做断言。这招叫"让 AI 自己写检查单",在生成式任务里非常好用,因为它把黑盒输出变成了半白盒。
# 伪代码:一个能跑的语义测试骨架
def test_extract_struct():
raw = model.generate(EXTRACT_PROMPT, schema=Schema, temperature=0)
r = parse(raw) # 解析失败 = 直接 fail,计入 metric
assert r.category in CATEGORIES
assert 0 <= r.confidence <= 1
assert r.reason, "AI 没给出理由就交付 = 甩锅给下游"
assert contains_no(r.reason, ["作为AI", "作为一个AI语言模型"])
这段代码看着朴素,但它把"AI 质量"从玄学变成了可观测的工程量。跑一百条真实样本,出一份报告:哪些类别错最多、哪些输入触发了解析失败、平均 confidence 是多少。你就有了一份可以拿去跟模型厂商谈判的、而不是拍脑袋的评估基线。
四、第三层:黄金集与回归——别让每次换模型变成赌博
这一层是护城河。做法很土但有效:维护一个 50 到 300 条的"黄金集",每条包含输入、期望行为、评判标准。这套东西一旦建起来,你就可以安全地做很多平时不敢做的事:换模型、升版本、改提示词、调参数。
关键在于黄金集要分层,而不是随机撒网:
- 冒烟集(10~20 条):每次改动必跑,秒级反馈,红了直接回滚。
- 回归集(50~100 条):覆盖已知的历史 bug,每次改动都跑。
- 金丝雀集(200+ 条):统计性评估,看整体指标漂移,跑批处理任务用。
我见过不少团队把黄金集做成一个"越攒越大、跑一次要三小时、最后没人看结果"的巨型目录,然后它就死了。原则是:冒烟集要小到能挂在每次提交上,金丝雀集要能进定时任务并把结果推到你手机。评估不落地,它就是另一种形式的自欺欺人。
另外一个反模式:拿生产数据直接当测试集。生产数据里包含大量"模型根本不该这么回答"的脏样本,评出来的分数会误导人。测试集要人工筛选,标准是"这个输入下,一个靠谱的初级员工应该给出什么",而不是"上一次模型给了什么"。前者是标准,后者只是历史。
五、Prompt 层面的三个心法,把不稳定变成可控
测试是事后,提示词是事前。这里给三个真正管用的写法:
1. 让模型先拆解,再执行。复杂任务别指望一句话到位。先让它输出"我会怎么做"的步骤清单,人或程序确认后再执行。多花一轮,换来少一堆幻觉。
2. 自洽性采样(self-consistency)。让模型对同一个问题跑 3 到 5 次,取多数结果。在推理类任务上,这比把单次调用的温度降到 0 还要稳。代价是成本翻几倍——所以只用在高价值决策上,别拿它当锤子砸所有钉子。
3. 明确"不知道"的合法出口。很多幻觉是因为你从来没允许它拒答。在提示词里加一句"信息不足时输出 INSUFFICIENT 并列出缺失项",然后把这个出口写进测试断言。你会发现幻觉量断崖式下降——因为模型从"必须编一个答案"变成了"可以承认缺口"。
一个反直觉的经验:让 AI 输出 JSON 时,最常见的失败不是格式错,而是"漏字段"。
六、聊聊更大的图景:AI 工程正在从"调模型"变成"管质量"
把这几年行业的变化连起来看,路线挺清楚:早期是比参数和跑分,中期是比上下文长度和多模态,现在是比可靠性、可控性和可交付性。当模型的"平均能力"普遍够用了,区分度就转移到了那些不显眼的工程细节上——你怎么测它、怎么约束它、怎么在它失手的时候优雅降级。
这跟当年做后端服务是一模一样的路径:框架能力趋同后,真正拉开差距的是那些"测试、监控、灰度、降级"的脏活累活。AI 不会改变这个规律,只会再证明一遍。
而对个人开发者来说,这反而是好事。个人做不了大模型训练,但完全可以做评测集、做测试框架、做那些大厂懒得做的"质量基建"。模型能力是租来的,评测能力是自己的。
最后
回到开头那个改坏 split(",") 的例子。给 AI 写代码的正确姿势,其实跟带新人没什么两样:任务拆清楚、验收标准写下来、每次改动都跑一遍回归、做得不对就当场指出来。
区别只有一个——新人会不好意思承认自己没听懂,而 AI 会面不改色地把你带到沟里。所以我们的测试必须比"信任"更狠一点。
这套东西搭起来大概花不了一天,但从此以后每次用 AI 都稳一点。复利这东西,在工具链上照样成立。