我们把 AI 写代码「关进了笼子」:一个短剧后台的 AI Harness 工程化实践
不是再教 AI 怎么写代码,而是给 AI 套上一套可被审计、可被拦截、能自我修复的工程纪律。 本文全部来自一个真实 monorepo(vue-vben-admin v5 改造的短剧后台)的落地过程,有结构、有踩坑、有数字。
一、为什么我们要给 AI 上「笼子」
AI 写代码很爽,但「爽」背后是三种你迟早会踩的坑:
- 漂得快,也烂得快:今天它按 PRD 写了页面,明天你加个字段,它顺着「理解」改了三处本不该碰的文件,还顺手删了一行看起来「没用」的死代码。
- 规则是纸老虎:你把编码规范写进
AGENTS.md、写进.cursorrules,AI 读的时候「懂了」,写的时候「忘了」。没有强制,等于没有。 - 死了也没人知道:文档里一句
详见 harness/agents/README.md,文件其实叫README.mdc——这种死链不会让构建失败,但会在三个月后某个深夜咬你一口。
我们是个典型的业务后台:Vue 3.5 + Vite 8 + Pinia + Element Plus 的 pnpm monorepo,三块业务(媒资 / 运营 / 分发),几十个页面。AI 是主力生产力,但「AI 自由发挥」的代价我们付过。
于是我们做了一套 Harness(工程化约束体系),核心目标只有一句话:
让 AI 在复杂工程里稳定、规范、可被审计地交付。Harness 是工程纪律,不是一组工具。
二、Harness 是什么:一张图看懂闭环
很多人以为「AI 工程化」就是多写几个 prompt。错。我们把它当成一条必须闭合的环:
flowchart LR
A[约束<br/>Rule / Skill / Agent] --> B[执行<br/>门禁拦截]
B --> C[反馈<br/>度量 / 伤疤]
C --> D[进化<br/>SCARS 沉淀]
D --> E[自愈<br/>doctor 自校验]
E --> A
缺任何一截,体系都会慢慢「馊掉」:
- 只有约束没有门禁 → 规则是纸老虎;
- 只有门禁没有反馈 → 不知道在变好还是变坏;
- 只有反馈没有进化 → 同一个坑反复踩;
- 进化了但不自检 → 死链和漂移会悄悄回来。
我们的资产按 六层 组织,外加 四块拼图(SPEC.md 里的铁律):
| 六层资产 | 作用 | 本工程状态 |
|---|---|---|
| Rule | 常驻 / 文件作用域的硬规范 | ✅ 5 个 |
| Skill | 按需触发的能力包(触发词命中才加载) | ✅ 16 个 |
| Sub Agent | 专职角色(总控 / 需求 / 方案 / 开发 / 审查) | ✅ 6 个 |
| Workflow | 阶段化接力流程 | ✅ project.mdc 定义 |
| Scripts | 门禁 / 同步 / 自校验脚本 | ✅ 7 个 |
| MCP | 工具直连(发现类操作) | ⏸ 暂未接(刻意取舍) |
四块拼图:约束与流程 / 反馈 / 知识库 / 进化——缺一块都会失控。
三、把流程写成「可执行的纪律」:5 阶段 + 3 道人工关卡
光有零散规则不够。我们用 rules/project.mdc 把一次交付切成五阶段,并在关键节点插入三道人工关卡——AI 可以飞快跑完编码,但必须在关卡处等真人点头:
flowchart LR
P0[PRD 输入] --> S1[阶段1 需求理解]
S1 --> S2[阶段2 任务拆解]
S2 --> G1{{人工关卡①<br/>需求确认}}
G1 --> S3[阶段3 实现方案]
S3 --> G2{{人工关卡②<br/>方案确认}}
G2 --> S4[阶段4 编码实现]
S4 --> S5[阶段5 自测交付]
S5 --> G3{{人工关卡③<br/>交付前 Review}}
G3 --> DONE[推送 / 合并]
对应的门禁与产物映射:
| 阶段 | 主导 Agent | 产物 | 关卡 | 门禁 |
|---|---|---|---|---|
| 1 需求理解 | 需求 Agent | 结构化需求 + 待澄清清单 | — | — |
| 2 任务拆解 | 需求 Agent | Task 列表 | ① 需求确认 | — |
| 3 实现方案 | 方案 Agent | 实现方案文档 | ② 方案确认 | — |
| 4 编码实现 | 开发 Agent | 代码 + 自测证据 | — | lint / typecheck / unit |
| 5 自测交付 | 开发 Agent | 改动快照 | ③ 交付前 Review | baseline 伤疤扫描 |
最狠的一条纪律写在 SPEC 铁律里:
下游不可改上游产物,只能提阻塞项,由总控打回重做。
也就是说,开发 Agent 不能为了「图省事」去改需求文档或方案文档;它觉得方案有坑,只能把阻塞项丢回给上游。这条纪律让「责任链」清晰到可以审计——每次返工都能追到是谁、在哪一步、为什么。
四、单一可信源:规则只改一处,镜像自动分层
早期我们踩过一个典型坑:规则既在 harness/ 里,又在 .cursor/rules 里各放一份,改了一边忘了另一边,三个月后两份开始「分叉」,谁也不敢动。
现在定下死规矩——harness/ 是唯一可编辑源:
.cursor/rules与.cursor/agents由pnpm harness:sync生成,禁止手改;- 所有编辑只在
harness/; - 改完跑一次 sync 自动镜像过去。
但 sync 脚本有两块硬骨头,是这次重写的重点:
骨头一:手改保护(manifest 清单追踪)
旧版 sync 的写法是「先 rmSync 整个 .cursor/rules,再全量复制」——这意味着你在 Cursor 里手调的任何规则,下次 sync 直接被抹掉。
新版用 manifest 清单记录「这一轮本应生成哪些文件」,同步时只删除「本轮不再生成」的残留,其余一律保留:
// 手改文件:不在本轮生成清单里,但磁盘上存在 → 保留,不删
for (const f of existing) {
if (!manifest.has(f)) {
if (isHandEdit(f)) continue; // 手改保护:绝不误删
rmSync(f);
}
}
我们实测过:往 .cursor/rules 里塞一个 MY-HAND-EDIT.mdc,重跑 sync,它被原样保留。
骨头二:常驻 / 按需 分层镜像
旧版把 16 个 Skill 和 5 个 Rule 平铺在同一个目录,Cursor 里「每次对话都加载」和「按触发词加载」的层级被吃掉了。
新版镜像分层:
.cursor/rules/ ← 常驻规则(alwaysApply: true),每次对话生效
├── coding.mdc
├── project.mdc
├── typescript.mdc
├── vue.mdc
└── git-commit.mdc
.cursor/rules/skills/ ← 按需 Skill(alwaysApply: false + 触发词)
├── prd-to-task.mdc
├── ui-guidelines.mdc
└── ... (16 个)
顺带清掉了旧版平铺在 rules/ 根的 13 个陈旧 Skill 残留——它们早就被挪进 skills/ 了,只是没人清理。
五、门禁不是建议,是强制:本地就拦你
SPEC 里最重要的一条铁律:
能用脚本判定的(lint / type / test / 快照),就别让 AI 解释执行。
我们有一键门禁 run-gates.mjs,把硬门禁和软门禁分开:
| 类型 | 门禁 | 失败后果 |
|---|---|---|
| 硬门禁 | oxlint 类型感知 lint | 进程退出码 1,禁止声明完成 / 推送 |
| 硬门禁 | check:type 全量类型检查 | 同上 |
| 硬门禁 | test:unit 单元测试 | 同上 |
| 软门禁 | check-baseline 改动快照 / 伤疤 | 仅告警,不阻断 |
并通过 lefthook 挂到 Git 钩子上,与 AI 是否「自觉」无关:
| 钩子 | 串行 / 并行 | 执行内容 |
|---|---|---|
| pre-commit | 串行 | oxlint(自动修复回填)→ oxfmt → stylelint → check:type |
| pre-push | 串行 | unitTest → baselineSnapshot → harnessDoctor |
紧急跳过出口也留了:HARNESS_SKIP_TEST=1 git push 可跳过单测(但基线快照与 doctor 仍在)。
六、让错误长记性:SCARS 伤疤 + doctor 自校验
这是整套体系里「让人眼前一亮」的设计。
SCARS:把反复出现的问题,焊成规则
evolution/SCARS.md(Scar 伤疤机制)记录「反复踩的坑」。某个 lint 问题或 Review 阻塞项出现第二次,就不再是一次性修复,而是回流为一条规则或 Skill——下次 AI 直接带着这条记忆上场。
doctor:给 Harness 自己装一道门禁
我们自己也承认:人类维护规则时,照样会写出死链、会改了 skill 忘了更新 README 清单。所以 harness:doctor 这个脚本,专门校验 Harness 自身的一致性,并作为 pre-push 门禁运行。它做四件事:
- frontmatter 合法:所有
.mdc必须有非空description+ 布尔alwaysApply; - 交叉引用不死链:扫描
.mdc内的文件引用,多基准模糊解析,目标不存在即报错(命令串、模板占位符、省略号自动排除); - Agent→Skill 真实存在:核对 Agent 文档里引用的 Skill / Rule 名字,在
skills/、rules/确有文件; - README 清单与实际一致:解析
harness/README.md的清单行,与实际文件比对,发现漂移即报错。
它真的抓到过东西。 开发期首次运行,doctor 一次性报出 7 处问题,其中两个是货真价实的死链:
agents/orchestrator.mdc写着harness/agents/README.md,文件其实叫README.mdc;skills/api-diff.mdc引用14-sync-backend-api.mdc,正确名称是sync-backend-api.mdc(那个14-是误植)。
两处都已修回。从那以后,这类漂移在 pre-push 时就会被当场拦下,不会流到三个月后。
七、结果:我们用数字说话
不堆形容词,只列可核验的事实:
- 35 个 Harness 资产被
doctor持续守护(rules 5 / skills 16 / agents 6,外加 knowledge / evolution / scripts); - 清理了 13 个陈旧 Skill 残留(旧 sync 平铺在镜像根目录的垃圾);
doctor开发期抓出 7 处问题,修口径后确认 2 处为真死链,已修复;- 文档侧做了一次大重组:
product/拆出design/子目录(5 个非 PRD 文档归位,AI 在「需求阶段」读 product 不再误吞技术方案)、v1.0验收纪要归位、tasks/里混入的技术沉淀移入新建的knowledge/,并重写了严重滞后的 README 索引。
一次完整闭环长这样:
PRD ──(需求 Agent)──▶ 结构化需求 ──[关卡①]──▶ Task 列表
──(方案 Agent)──▶ 实现方案 ──[关卡②]──▶ 代码 + 自测证据
──(开发 Agent)──▶ [lint/type/unit 全绿] ──[关卡③]──▶ 推送
每一棒都有产物、每一关都有真人、每一次提交都被门禁咬过一口。
八、诚实的边界:我们没做什么
一篇靠谱的工程文,不该只报喜。以下是我们刻意或尚未来得及做的,亮点是——我们敢把它写清楚:
- MCP 没接。所有「发现类」操作(读库、跑命令)目前仍依赖 AI 自发,没有工具约束。这是独立大工程,留给下一轮。
- 远程 CI 门禁还没上。SPEC 规划里
.github/workflows/ci.yml要串联 lint/type/test/secret 扫描,目前仓库只有bundle-size.yml,本地 hook 虽强但可被--no-verify跳过,远程无强制。这是我们下一步最高杠杆的事。 - Agent 真实调度待验证。5 个 Agent 在 Cursor 下按
@agent设计,但在 CodeBuddy 里是否真被调度,我们没 100% 确认——要么是真编排,要么明确「本 harness 仅作规则参考,编排靠人」。模糊状态本身就需要消除。
把边界画清楚,比假装「全链路自动化」更有价值——因为它告诉你:这套东西你现在就能抄,而且知道抄完还差哪几步。
九、如果明天你要抄一套:5 步清单
小团队也能落地,不必一步到位:
- 先写 SPEC,再写规则:纪律先对齐,团队对齐全链路产研流程再动手(我们 SPEC 第一条铁律)。
- 定死单一可信源:规则只在
harness/改,镜像自动生成,禁止手改镜像。 - 把流程切成阶段 + 人工关卡:AI 可以快,但关键节点必须等真人。
- 门禁本地强制:
lefthook挂pre-commit/pre-push,硬门禁失败即禁止推送。 - 给体系自己装门禁:写个
doctor之类的自校验,防死链和清单漂移——这是最容易被忽略、却最值钱的一步。
结语
AI 工程化不是「让 AI 更聪明」,而是把人的工程纪律,变成 AI 绕不过去的墙。
我们这套 Harness 没有炫技:没有自研大模型,没有复杂编排框架,只是把「约束 → 执行 → 反馈 → 进化 → 自愈」这条环老老实实闭上了。它最贵的地方,是那句写在 SPEC 里、并真的被门禁执行的话:
能写脚本判定的,就别让 AI 解释执行。
当你发现 AI 改坏的东西,会在 pre-push 被一行 doctor 报错拦下来时——你就知道,笼子起作用了。
本文所涉结构、脚本、数字均来自 tt-mini-admin 仓库的真实落地;MCP 与远程 CI 为已规划未落地项,已在文中如实标注。