事情的起因很简单:我用 Claude Code + GLM5.1 写代码,它确实快,快到让人害怕。但也是在同一个项目里,我至少抓到了它 7 次"改了字段名,忘了改 cleanup 脚本的扫描范围",5 次"明明答应过要转驼峰,结果漏了两个字段",还有 3 次直接在 WXML 模板里写 .indexOf(),然后天真地问我"为什么报错"。
更离谱的是,它还会在回复里郑重其事地宣布"已确认 Mock 数据同步",而实际上它连 Mock 文件碰都没碰。你就像在带一个极其聪明但毫无责任感的实习生:活干得飞快,但是否真的干了,你得逐一翻证据,像侦探一样从它的"心理活动"里分辨事实和谎言。
于是我意识到,不能再靠"提示词技巧"和"事后说教"来管代码质量了。真正的问题不是 LLM 不够聪明,而是我们给它的约束太软。你需要一套机器级的执行体系,不依赖它的自觉,不留下"假动作"的空间。
这篇文章就聊聊我在一个真实项目里折腾出来的 LLM 代码质量约束体系。我会按"为什么出现 → 它是什么 → 它怎么工作"的顺序讲,不堆术语,就当跟你喝咖啡时吐槽加复盘。
Spec 文档链接
可以直接发给 LLM 让它生成文件:
ludicrous-orangutan-d0f.notion.site/LLM-353aaa3...
一、为什么出现:那些反复跳出来的 Bug,根本不是幻觉
很多人把 LLM 写出的低级错误归咎于"幻觉",但我不完全同意。以我踩过的坑来看,这些错误可以分成几类:
- 上下文遗忘:同一轮对话里改了三处,只记得两处。比如改了数据库字段,忘了同步 API 的 format 映射;加了图片上传,忘了在清理脚本的 SQL 里加上这张表的列。
- 规则未执行:它知道规则,但执行时像大脑短路。比如你反复强调"所有字段转驼峰",它在 20 个字段里漏了两个,不是因为它不懂,就是因为注意力漂移。
- 知识盲区 :不知道 WXML 里不能写
Array.indexOf(),不知道data-*传数字会变字符串,这些属于框架特有约束。 - 缺少自我验证:字打错了、引用路径写错了,这些人类也会犯,但我们有编辑器标红或编译检查,而它没有。
前三类占了大头,但真正让我下定决心的,是第四类的一种变体------假动作幻觉 。LLM 会在回复里说"我已确认全局状态同步",但根本没去读那三个页面的 onShow。它做的不是物理检查,而是根据信心编了一段话。这已经不是 bug 了,这是诚信问题。
所以需求非常明确:我们需要一个系统,它不靠 LLM 的自我报告来判断代码质量,而是靠物理证据------脚本真正跑过、AST 真正解析过、测试断言真正通过。这就是整个体系的出发点。
二、它是什么:一个四层递进的"防蠢货"流水线
我给这套东西起了个内部名字叫"防蠢货流水线",由四个递进层次构成:
- L0 前置:在 LLM 动手写文件之前,先扔给它一张"影响域地图",告诉它改这个文件可能会牵连哪些其他的文件。
- L1 预防:按需加载规则,只加载跟当前改动相关的章节,不搞全局灌输。
- L2 检测:文件一写完,自动跑脚本检查,发现问题直接以非零退出码怼回去,强制修复。
- L3 兜底 :写测试,不是测业务逻辑,而是测一致性契约:字段映射对不对、cleanup 覆盖全不全、全局状态有没有同步点。并且用 AST 和类型检查优先,少用 grep。
打个比方:L0 是"你出门前我告诉你今天风大,记得带外套";L1 是"衣柜上贴了每日穿搭规则";L2 是"你刚迈出门,我就拽住你检查衣领有没有翻好";L3 是"晚上回家我还会一件件核对,你是不是又穿了不同的袜子"。
这里面 L2 是核心杀手锏,因为它用 hook 嵌进了 Claude Code 的工具调用流程,根本不让 LLM 有机会蒙混过关。
另外,为了对抗幻觉,我在体系里做了三个关键设计:
- 所有自动化数据必须来自确定性脚本,比如字段映射、路由权限表,绝不能由 AI 凭空总结。万一脚本自己写了 bug 怎么办?用 fixture 测试验证它,确保"基准线"本身不被污染。
- 检查结果必须结构化,带精确的文件、行号、契约规则 ID、修复动作指令,彻底消灭"这里可能有问题,你再查查"这种无用输出。
- 必须熔断,同一类错误反复出现或者在 A 和 B 两个问题间震荡时,直接停止自动修复,升级到人工审查或 L3 全量测试,防止 LLM 陷入自娱自乐的无限循环。
三、它怎么工作:从改一行字段名到全链路验证
下面我带你走一遍真实流程。假设我们给 recipes 表新增了一个字段 cooking_time。
1. L0:还没开写,先扔影响域地图
我要在 Claude Code 里让 LLM 修改 models/recipe.js。在我按下发送之前,一个 PreToolUse hook 自动触发了 pre-edit-analyze.js。这个脚本干了什么?它查了配置表,发现 models/recipe.js 属于 database 和 model 域,然后从这个项目的契约数据文件里拉出了这两个域的同步点清单:
- format 映射:
server/src/utils/formatRecipe.js - cleanup 扫描:
server/src/monitor/worker/cleanup-images.js - 前端服务层:
services/recipe.js - 全局状态:不需要(除非是新加的全局属性)
这些信息被打包成一个 JSON,通过 stderr 展示给 LLM。它就像收到一条内部 note:"哥们,你接下来改的这个文件会影响 xxx、yyy、zzz,别忘了关照一下"。注意,这只是提示,不阻断写操作。但实践下来,这个提示能减少至少一半的"上下文遗忘"。
2. L1:规则只给相关的部分
CLAUDE.md 里我只加了一句话:"编码前请阅读 ~/rules/coding/index.md,按改动类型加载对应规则。" 那个 index.md 就是一个路由器:
| 改动类型 | 加载规则 |
|---|---|
| 数据库字段、Model | backend-coding.md §数据库 |
| API 接口 | backend-coding.md §API |
| 页面状态 | frontend-coding.md §状态 |
| ... | ... |
这样 LLM 只会读到"新增或修改字段时,你必须确保 format 映射、清理脚本和 mock 数据都同步更新",而不会同时被前端组件规范、小程序平台限制等信息撑爆上下文。实际规则文件内容很少,因为真正的检查不靠它,而是靠 L2。
3. L2:写完立马体检,不合格不许走
当 LLM 用 Edit/Write 工具修改完 models/recipe.js 后,PostToolUse hook 启动。这次跑的是 check-after-edit.js,它根据 check-config.json 的路径匹配规则,触发了三个检查器:
- field-mapping :解析当前文件,发现新增了
cooking_time。然后去契约数据里查recipes表的 format 映射。结果发现还没有对应的cookingTime映射,于是打出一个 ERROR 的 finding,带了精确的修复指令:在server/src/utils/formatRecipe.js的对应函数里加上cooking_time → cookingTime。 - image-field-coverage 检查器:发现新字段名不像图片URL,所以跳过。
- mock-sync 检查器:发现 mock 数据文件里没有
cooking_time,又打出一个 WARNING。
这些 finding 以结构化 JSON 输出到 stderr,脚本以非零码退出。Claude Code 的 hook 机制会把这些信息再喂给 LLM,迫使它马上修复。等它再次写入 formatRecipe.js 和 mock 文件后,同样的检查会再跑一遍。如果都通过,流程继续;如果连续三次都修不好同一类问题,熔断机制启动,阻止修修补补,直接通知我需要人工介入。
这里面最爽的一点是:LLM 无法表演"检查过了" 。它提交文件的那一刻,真正做检查的是我写的脚本,结果明明白白,没有任何商量余地。
4. L3:最终安全网,用测试兜住整个项目
L3 不依赖单次编辑,而是在 CI 里或在合并前全量验证整个项目的契约。对于这个新字段的场景,L3 测试会做:
- 穷举所有数据库表的所有字段,跑一遍 format 函数,确认输出里都覆盖了。
- 把
contracts-data.json里声明的所有图片字段跟 cleanup SQL 做 diff,一个不差。 - 验证
extract-contracts.js脚本本身没有被改坏(用我们预先准备的 schema fixture 跑回归测试)。 - 验证
reviewer-rules.yaml中人工维护的状态同步规则所引用的源代码文件还存在,变量名没被改名。
L2 是"快速反应部队",L3 是"战略核武器"。L2 能抓到本次改动的大多数问题,但不能保证全局,所以必须常备 L3。好在 L3 可以很规整地写成测试用例,只要你项目里有一份结构化的契约数据。
抗幻觉,本质是对抗"假动作"
这套东西里我最得意的不是某个具体脚本,而是整个体系对 "假动作"的零容忍。
- 不让 AI 总结契约,契约由脚本扫描代码生成,脚本本身被 fixture 测试保护。
- 不让 AI 口头确认,检查结果必须是脚本在真实文件上跑出来的。
- 不让 AI 在回复里粘贴"证据" ,因为证据可以被伪造、挑选、缓存复用。真正的证据是脚本的退出码和测试通过状态。
这样一来,LLM 仍然是那个又快又容易走神的家伙,但它每写一行代码,背后都有一根看不见的绳子把它拽回正轨。
这套东西能用在别的项目吗?
能。我在设计时特意把它弄成通用方法论,不绑定特定语言。无论是 Express + 小程序、Spring Boot、Django,还是 TypeScript React,都可以套用一样的结构:
- 写
extract-contracts.js从你的 schema、路由、序列化器里自动抽取契约数据。 - 写
reviewer-rules.yaml定义那些不能自动识别但必须遵守的项目约定。 - 配置
check-config.json决定哪些文件变化触发哪些检查器。 - 接上 L0、L2 的 hook,再写几个 L3 的一致性测试。
我已经在内部两个项目上跑了两个月,历史高频 bug(字段不一致、清理遗漏、状态不同步)的重复率直接归零。那种"我到底该不该再信它一次"的焦虑感,也终于消失了。
如果你也在让 LLM 写比较大型的工程代码,我真诚建议:别试图把它教成完美工程师,而要用机器逻辑去弥补它必然出现的失误。把信任建立在流程和自动化脚本上,而不是建立在它的"承诺"上。