AI 生成的东西,谁来兜底?
太长不读
过去一年,我做了好几个 AI 项目:用 AI 解析需求文档的、流式生成测试用例的、做实体关系抽取的,还有配套的 CLI 工具。每个项目都翻过车,翻到最后,解法收敛成了同一句话——别让模型直接产交付物,让它产中间语义层,交付物由脚本装配,契约由脚本校验。
整理下来,有几处体会和我原来想的不太一样:一是让模型「更可靠」基本是个伪命题,确定性只能靠架构收紧到代码里;二是看起来最笨的脚本兜底,反而是整个系统里最值得信赖的部分;三是规则系统很好用,但它会腐烂,得像代码一样治理。读完这篇,大伙对「AI 生成的东西谁来兜底」这个问题,应该能有些更清晰的感知。
矛盾本身:模型不确定,交付物必须确定
大模型最大的问题是什么?不够聪明?不是。太贵?也不是。
我的答案是:它什么都知道一点,但对你的业务一无所知。更要命的是,同一个问题问两遍,答案可能不一样。
跑通一个 demo 和做出一个能用的产品,完全是两回事。demo 里答错一次,笑笑就过去了;产品里答错一次,就是一次事故。
我一开始的思路是错的:调 temperature、堆 few-shot、换更贵的模型,想让模型「更可靠」。折腾一圈发现,这些只能降低翻车概率,不可能归零。让模型可靠是模型厂商的事,工程师能做的是另一件——在架构上把确定性收紧到代码里。
具体做法一句话:模型负责理解和生成语义,脚本负责装配和校验契约。这个分工很像编译器:
| 角色 | 对应编译器 | 干什么 | 特点 |
|---|---|---|---|
| 模型 | 前端 | 把自然语言解析成语义对象(类似 AST) | 不确定,但理解力强 |
| 脚本 | 后端 | 把语义对象装配成终态产物,并校验 | 完全确定,可 diff |
没有哪个编译器会让前端直接吐机器码。同理,也不该让模型直接吐交付物。
语义对象:模型只产中间层
拿「AI 解析需求文档」举例。最早的方案很「端到端」:prompt 里塞个模板,让模型直接输出最终文档。结果是,今天输出的字段和昨天不一样,引用的内容时不时是它自己编的,想改一个字就得全文重新生成,出了问题都不知道该怪谁。
后来把架构改了:模型不产文档,只产语义对象。
{
"facts": [
{
"kind": "requirement",
"statement": "订单支付超时 30 分钟自动取消",
"source_index": { "doc": "PRD", "block": 42 }
}
],
"findings": [
{
"category": "risk",
"statement": "超时取消与库存回滚的时序未定义"
}
]
}
三个关键设计,每一个都是踩坑踩出来的:
- 封闭枚举的 kind。facts 的 kind 只有 6 种,枚举之外直接拒收;findings 的 category 是开放字符串,因为风险点没法预先穷举。该封闭的封闭,该开放的开放。
- source_index 证据锚定。每条事实必须带指向原文第几个 block 的索引。编造的陈述拿不出证据,「幻觉」就从难发现变成了可检测。
- 字段级权威排序。同一字段多处来源冲突时,PRD > URD > wiki > prompt,按优先级取。这个逻辑写在代码里,不靠模型判断。
终态 JSON 由装配脚本组装,契约校验脚本校验,流水线上还挂着一组门禁脚本,任何一步不过,直接红。
门禁红了怎么办?红了是好事,说明这次语义对象不合格,打回去重跑或者修 prompt。真正可怕的不是门禁红,是没有门禁——错误一路绿灯流到下游,最后在用户那里炸开。
这套下来,交付物拿到三个性质:结构化、可追溯、可证伪。模型还是那个不确定的模型,但产物是确定的。
顺带一个意外收获:语义对象本身也是进 git 的。prompt 改了、模型升级了,重新跑一遍,diff 的是语义对象而不是整篇文档——哪条事实变了、哪条是新增的,一目了然。这层中间层一旦存在,很多原本没法做的事就顺理成章了。
流式协议:自己定协议,自己写修复
第二个例子是 AI 生成测试用例,前端要实时展示生成过程。
第一个坑:让模型输出 JSON,前端没法流式渲染。JSON 必须完整闭合才能 parse,模型吐到一半,前端只能干等。
解法有点「野」:不让模型输出 JSON,自己定一个文本协议。
@@目录: /登录模块/异常场景
@@用例标题: 密码错误超过 5 次锁定账号
每行一条指令,来一个 token 解析一个,前端逐 token 增量构建用例树,不用等完整 JSON。用户能看到用例一条条长出来,体验好了很多。
第二个坑紧随其后:各家模型的输出变体太多。中英文括号混用、SSE 截断出的半个标识、莫名其妙的多重花括号——每个模型都有自己的口音,换个模型上线就是一场灾难。
对策是两个小组件:一个格式修复器,把已知变体归一化成标准协议;一个流式渲染缓冲器,把未闭合的 markdown 语法先缓冲住,闭合了再渲染,防止页面闪烁。
这套方案谈不上优雅,但可控。协议是契约,契约归我管,修复变体的成本也归我管。把希望寄托在「模型下次会输出标准格式」上,才是真的不可控。
代价也要说清楚:协议是自造的,就意味着文档、解析器、修复器都得自己维护,每接一个新模型都要回归一遍变体集。这笔账在立项时就得算进去,它不是免费的。
确定性操作:不给模型留入口
第三个例子来自配套的 CLI 工具,环节是物料占位替换:用例里的占位符 ${data_xxx},导入平台前要替换成真实值。
这事模型能干,还干得不错。但规矩是:禁止模型逐条替换,必须走脚本。
理由很朴素。这是 100% 确定性的查找替换,没有智能的用武之地。模型做一万次只要错一次,就得把一万次全量校验一遍——用模型干确定性的事,等于把 O(1) 的校验问题变成 O(n) 的抽检问题。
同样思路的是导入前的校验脚本。背景有点坑:平台四种用例类型,各自读的字段互不重叠;后端对缺字段只打 warn、照常落库。也就是说,类型一旦错配,导入显示「成功」,但用例是空壳,打开啥都没有。
这种坑靠人 review 防不住,靠模型更防不住——它自己就是不确定性的来源。最后只能 CLI 兜底,CLI 是最后一道防线。本地事务同理:fcntl 锁、原子写、内容指纹漂移检测,全是确定性代码,一行模型的活都不留。
规则大于模型,但规则会腐烂
最后一个例子是技术文档的实体关系抽取。按主流思路,这活应该微调个小模型。
我们没有。全部外置成 40 多条规则配置:实体类型对映射、中英双语正则、置信度权重。原因很简单:小数据量的技术文档场景,规则比模型准,而且可解释、可 diff、可回滚,出了 badcase 改一行配置就行。
但规则有自己的坑。有一次 NER 把代码里的 submitOrder 误标成了 PERSON,一个方法名被当成了人名。模型改不了,就加了个后置修正器,本质是规则补丁。后来补丁越打越多,补丁叠补丁,那堆配置慢慢没人敢动了。
教训是:规则是对抗不确定性的好武器,但它会腐烂。补丁只增不减,配置越来越像祖传代码。治理办法和代码一样:定期合并、清理失效规则、给每条规则挂上对应的案例。别把规则当配置,把它当代码管——它本来就是代码,只是换了个文件格式。
这里还有一层更本质的边界问题:规则管的是「已知的已知」,模型管的是「未知的理解」。规则腐烂的根源,往往是拿规则去补模型该干的活——每补一条,就欠一笔债。补到第十条的时候,该停下来想想,是不是这个场景的数据量已经够训练一个像样的模型了。
为什么脚本比端到端更值得信赖
可能有人觉得,又是门禁又是脚本又是规则,看起来不够「AI Native」。
我以前也这么觉得。一年下来,结论反过来了:
| 维度 | 端到端 AI | 脚本兜底 |
|---|---|---|
| 出错了 | 只能怀疑人生 | 确切知道哪条契约挂了,可证伪 |
| 变更管理 | 行为藏在权重里,没法 review | 脚本在 git 里,可 diff |
| 复盘追责 | 「模型今天状态不好」,没法修 | 「装配脚本第 42 行错了」,能修 |
确定性代码是系统里最便宜的组件:不会累、不会变、不会编。把确定的部分交给它,把理解和生成留给模型,各司其职。
最后
回头看这一年,最大的转变是:不再试图驯服不确定性,而是给它修围墙。墙内让模型尽情发挥,墙的边界上用脚本把契约焊死。
模型会越来越聪明,但「交付物必须确定」这一点不会变。这道墙还得继续修,这大概就是工程的意思。