我们把 AI 写代码「关进了笼子」:一个短剧后台的 AI Harness 工程化实践

3 阅读10分钟

我们把 AI 写代码「关进了笼子」:一个短剧后台的 AI Harness 工程化实践

不是再教 AI 怎么写代码,而是给 AI 套上一套可被审计、可被拦截、能自我修复的工程纪律。 本文全部来自一个真实 monorepo(vue-vben-admin v5 改造的短剧后台)的落地过程,有结构、有踩坑、有数字。


一、为什么我们要给 AI 上「笼子」

AI 写代码很爽,但「爽」背后是三种你迟早会踩的坑:

  1. 漂得快,也烂得快:今天它按 PRD 写了页面,明天你加个字段,它顺着「理解」改了三处本不该碰的文件,还顺手删了一行看起来「没用」的死代码。
  2. 规则是纸老虎:你把编码规范写进 AGENTS.md、写进 .cursorrules,AI 读的时候「懂了」,写的时候「忘了」。没有强制,等于没有。
  3. 死了也没人知道:文档里一句 详见 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 任务拆解需求 AgentTask 列表① 需求确认
3 实现方案方案 Agent实现方案文档② 方案确认
4 编码实现开发 Agent代码 + 自测证据lint / typecheck / unit
5 自测交付开发 Agent改动快照③ 交付前 Reviewbaseline 伤疤扫描

最狠的一条纪律写在 SPEC 铁律里:

下游不可改上游产物,只能提阻塞项,由总控打回重做。

也就是说,开发 Agent 不能为了「图省事」去改需求文档或方案文档;它觉得方案有坑,只能把阻塞项丢回给上游。这条纪律让「责任链」清晰到可以审计——每次返工都能追到是谁、在哪一步、为什么。


四、单一可信源:规则只改一处,镜像自动分层

早期我们踩过一个典型坑:规则既在 harness/ 里,又在 .cursor/rules 里各放一份,改了一边忘了另一边,三个月后两份开始「分叉」,谁也不敢动。

现在定下死规矩——harness/ 是唯一可编辑源

  • .cursor/rules.cursor/agentspnpm 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 门禁运行。它做四件事:

  1. frontmatter 合法:所有 .mdc 必须有非空 description + 布尔 alwaysApply
  2. 交叉引用不死链:扫描 .mdc 内的文件引用,多基准模糊解析,目标不存在即报错(命令串、模板占位符、省略号自动排除);
  3. Agent→Skill 真实存在:核对 Agent 文档里引用的 Skill / Rule 名字,在 skills/rules/ 确有文件;
  4. 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 全绿] ──[关卡③]──▶ 推送

每一棒都有产物、每一关都有真人、每一次提交都被门禁咬过一口。


八、诚实的边界:我们没做什么

一篇靠谱的工程文,不该只报喜。以下是我们刻意或尚未来得及做的,亮点是——我们敢把它写清楚:

  1. MCP 没接。所有「发现类」操作(读库、跑命令)目前仍依赖 AI 自发,没有工具约束。这是独立大工程,留给下一轮。
  2. 远程 CI 门禁还没上。SPEC 规划里 .github/workflows/ci.yml 要串联 lint/type/test/secret 扫描,目前仓库只有 bundle-size.yml,本地 hook 虽强但可被 --no-verify 跳过,远程无强制。这是我们下一步最高杠杆的事。
  3. Agent 真实调度待验证。5 个 Agent 在 Cursor 下按 @agent 设计,但在 CodeBuddy 里是否真被调度,我们没 100% 确认——要么是真编排,要么明确「本 harness 仅作规则参考,编排靠人」。模糊状态本身就需要消除。

把边界画清楚,比假装「全链路自动化」更有价值——因为它告诉你:这套东西你现在就能抄,而且知道抄完还差哪几步


九、如果明天你要抄一套:5 步清单

小团队也能落地,不必一步到位:

  1. 先写 SPEC,再写规则:纪律先对齐,团队对齐全链路产研流程再动手(我们 SPEC 第一条铁律)。
  2. 定死单一可信源:规则只在 harness/ 改,镜像自动生成,禁止手改镜像。
  3. 把流程切成阶段 + 人工关卡:AI 可以快,但关键节点必须等真人。
  4. 门禁本地强制lefthookpre-commit / pre-push,硬门禁失败即禁止推送。
  5. 给体系自己装门禁:写个 doctor 之类的自校验,防死链和清单漂移——这是最容易被忽略、却最值钱的一步

结语

AI 工程化不是「让 AI 更聪明」,而是把人的工程纪律,变成 AI 绕不过去的墙

我们这套 Harness 没有炫技:没有自研大模型,没有复杂编排框架,只是把「约束 → 执行 → 反馈 → 进化 → 自愈」这条环老老实实闭上了。它最贵的地方,是那句写在 SPEC 里、并真的被门禁执行的话:

能写脚本判定的,就别让 AI 解释执行。

当你发现 AI 改坏的东西,会在 pre-push 被一行 doctor 报错拦下来时——你就知道,笼子起作用了。


本文所涉结构、脚本、数字均来自 tt-mini-admin 仓库的真实落地;MCP 与远程 CI 为已规划未落地项,已在文中如实标注。