Harness 工程:给 AI Coding 套上缰绳的第一次实践
让 AI 写代码不难,让 AI 写对代码才难。这篇文章记录我在业务项目上第一次把 Harness 工程跑通的全过程------从"AI 乱猜"到"AI 照着规矩来"。
目录
- 你一定遇到过这个场景
- L1/L2/L3:三层约束模型
- [harness 长什么样](#harness 长什么样)
- [9 步完整流程实操](#9 步完整流程实操)
- 踩坑实录
- [harness 在每一步的作用](#harness 在每一步的作用)
- 个人思考:什么时候该用,什么时候别用
- 写在最后
你一定遇到过这个场景
你用 Claude Code(后面简称 CC)给运维管理平台加功能------问题记录模块支持视图切换,描述字段不截断。
你打开 CC,把需求一丢,等着它出活。
结果它上来就写代码------字段猜的、返回格式自己造的、分页组件从别的模块复制过来参数还对不上。
改了三轮,勉强能跑,但浑身别扭------字段少了一个,返回格式跟其他接口不统一,分页传参方式跟项目约定不一样。
这不是 CC 笨。它有能力写对代码,但不知道这个项目的规矩。接口约定、状态码、鉴权方式散落在代码各处,有的甚至只在老员工脑子里。CC 只能反推,推不准是常态。
这就是 Harness 工程要解决的问题:给 AI 一套它能读的「项目规矩」,让它从「猜」变成「读」。
Harness 本身不含业务代码,是一套存规则和约束的文档体系------你项目的「指挥层」。AI 开工前先读这些文档,再在约束下写代码。
我第一次接触这个概念,觉得又是那种"多写文档就好了"的万金油建议。但实际跑了一遍 9 步流程之后,发现它确实改变了 AI 生成代码的质量。下面从原理到实操,一步步讲。
L1/L2/L3:三层约束模型
Harness 的核心是一条自上而下的约束链,把系统知识拆成三层:
┌─ L1 架构层 ───────────────────────────────────────────────────┐
│ 系统边界 · 核心模块 · 数据流向 · 服务职责 · 外部依赖 │
│ 回答:这个系统到底是什么 │
└─────────────────────────────────────────────────────────────────┘
│ 指导
▼
┌─ L2 模式与抽象层 ─────────────────────────────────────────────┐
│ 接口定义 · 组件/服务/仓储/Schema · 状态管理分层 · 跨服务契约 │
│ 回答:系统应该按什么方式生长 │
└─────────────────────────────────────────────────────────────────┘
│ 约束
▼
┌─ L3 文件级代码层 ─────────────────────────────────────────────┐
│ 函数/类 · 页面/组件 · 配置 · 测试 · 各种具体实现 │
│ 回答:具体怎么实现 │
└─────────────────────────────────────────────────────────────────┘
L1 指导 L2,L2 约束 L3。下层在上层的夹缝里生长,不能反向定义上层。
| 层 | 典型内容 | 变化频率 | 谁维护 |
|---|---|---|---|
| L1 架构层 | 系统边界、服务地图、部署形态、安全/性能约束 | 极低,动它等于动系统定义 | 架构师/TL |
| L2 模式与抽象层 | API 契约、分页约定、错误码、认证方式、模块内部约定 | 低,动它等于改契约 | 前后端共同 |
| L3 代码层 | 函数实现、页面组件、具体配置、测试用例 | 高,每个任务都在改 | 开发者/AI |
关键决策:Harness 只存 L1 和 L2,L3 永远留在代码工程里。
为什么?因为 L3 太容易过时了。你今天在 harness 里写「问题列表页用
ProblemList.vue」,明天重命名一下组件,harness 就过期了。文档和代码一旦不同步,比没有文档还糟糕------AI 读了过时的文档,会自信地写出错误的代码。
判断口诀很好记:一条信息如果改一行代码就可能过时,它就是 L3,不写进 harness。
举个例子。「所有接口返回必须用 AjaxResult 包装」------这是 L2 契约,写进 harness。「ProblemController.list() 方法调用了 ProblemService.selectList()」------这是 L3 实现细节,不写。前者几个月不变,后者可能每周都在改。
harness 长什么样
理论讲完,看看实际目录结构:
harness-engineering-starter/
├── AGENTS.md ← AI 入口:每轮任务第一站
├── LAYERS.md ← 三层模型原理说明
├── SKILL.md ← 防腐化工具:精简 AGENTS.md
├── docs/
│ ├── GUARDRAILS.md ← 改动护栏:红区/灰区/绿区
│ ├── DONE.md ← 完成标准(DoD)
│ ├── GLOSSARY.md ← 领域术语
│ ├── architecture/ ← L1:系统边界、服务地图、约束、部署
│ ├── contracts/ ← L2:API 响应、分页、错误码、认证
│ ├── flows/ ← 跨服务业务流程
│ └── decisions/ ← 决策记录(ADR)
└── services/
├── _template/ ← 新增服务的起点
├── seal-frontend/ ← 前端服务事实层
└── seal-backend/ ← 后端服务事实层
文档按两个维度分区:全局层 (docs/)存跨服务共享的架构、契约、流程;单服务层 (services/<svc>/)存各服务内部约定,不上提到全局。
三个关键防护文件:
GUARDRAILS.md--- 改动护栏,把代码区分为红/灰/绿三区。红区(生产配置、schema、状态机、鉴权)改前必须问人,绿区(页面实现、样式)AI 可自改,灰区改了要说明影响面。DONE.md--- 完成标准(DoD),任一项不满足任务就算没完成。包括:是否落在安全区、编译是否通过、契约是否被悄悄改动。SKILL.md--- 防腐化工具。AGENTS.md 每轮全量加载,必须精简。路径、函数名、目录清单这些容易过时的东西,一律不写进去。
9 步完整流程实操
Harness 解决了"AI 不知道规矩"的问题,但得有流程把它串起来。我在业务项目上走了一遍 9 步流程:
#mermaid-svg-Bz1Zy2a3v2bEDfDN{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Bz1Zy2a3v2bEDfDN .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Bz1Zy2a3v2bEDfDN .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Bz1Zy2a3v2bEDfDN .error-icon{fill:#552222;}#mermaid-svg-Bz1Zy2a3v2bEDfDN .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Bz1Zy2a3v2bEDfDN .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Bz1Zy2a3v2bEDfDN .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Bz1Zy2a3v2bEDfDN .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Bz1Zy2a3v2bEDfDN .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Bz1Zy2a3v2bEDfDN .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Bz1Zy2a3v2bEDfDN .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Bz1Zy2a3v2bEDfDN .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Bz1Zy2a3v2bEDfDN .marker.cross{stroke:#333333;}#mermaid-svg-Bz1Zy2a3v2bEDfDN svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Bz1Zy2a3v2bEDfDN p{margin:0;}#mermaid-svg-Bz1Zy2a3v2bEDfDN .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Bz1Zy2a3v2bEDfDN .cluster-label text{fill:#333;}#mermaid-svg-Bz1Zy2a3v2bEDfDN .cluster-label span{color:#333;}#mermaid-svg-Bz1Zy2a3v2bEDfDN .cluster-label span p{background-color:transparent;}#mermaid-svg-Bz1Zy2a3v2bEDfDN .label text,#mermaid-svg-Bz1Zy2a3v2bEDfDN span{fill:#333;color:#333;}#mermaid-svg-Bz1Zy2a3v2bEDfDN .node rect,#mermaid-svg-Bz1Zy2a3v2bEDfDN .node circle,#mermaid-svg-Bz1Zy2a3v2bEDfDN .node ellipse,#mermaid-svg-Bz1Zy2a3v2bEDfDN .node polygon,#mermaid-svg-Bz1Zy2a3v2bEDfDN .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Bz1Zy2a3v2bEDfDN .rough-node .label text,#mermaid-svg-Bz1Zy2a3v2bEDfDN .node .label text,#mermaid-svg-Bz1Zy2a3v2bEDfDN .image-shape .label,#mermaid-svg-Bz1Zy2a3v2bEDfDN .icon-shape .label{text-anchor:middle;}#mermaid-svg-Bz1Zy2a3v2bEDfDN .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Bz1Zy2a3v2bEDfDN .rough-node .label,#mermaid-svg-Bz1Zy2a3v2bEDfDN .node .label,#mermaid-svg-Bz1Zy2a3v2bEDfDN .image-shape .label,#mermaid-svg-Bz1Zy2a3v2bEDfDN .icon-shape .label{text-align:center;}#mermaid-svg-Bz1Zy2a3v2bEDfDN .node.clickable{cursor:pointer;}#mermaid-svg-Bz1Zy2a3v2bEDfDN .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Bz1Zy2a3v2bEDfDN .arrowheadPath{fill:#333333;}#mermaid-svg-Bz1Zy2a3v2bEDfDN .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Bz1Zy2a3v2bEDfDN .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Bz1Zy2a3v2bEDfDN .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Bz1Zy2a3v2bEDfDN .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Bz1Zy2a3v2bEDfDN .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Bz1Zy2a3v2bEDfDN .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Bz1Zy2a3v2bEDfDN .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Bz1Zy2a3v2bEDfDN .cluster text{fill:#333;}#mermaid-svg-Bz1Zy2a3v2bEDfDN .cluster span{color:#333;}#mermaid-svg-Bz1Zy2a3v2bEDfDN div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Bz1Zy2a3v2bEDfDN .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Bz1Zy2a3v2bEDfDN rect.text{fill:none;stroke-width:0;}#mermaid-svg-Bz1Zy2a3v2bEDfDN .icon-shape,#mermaid-svg-Bz1Zy2a3v2bEDfDN .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Bz1Zy2a3v2bEDfDN .icon-shape p,#mermaid-svg-Bz1Zy2a3v2bEDfDN .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Bz1Zy2a3v2bEDfDN .icon-shape .label rect,#mermaid-svg-Bz1Zy2a3v2bEDfDN .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Bz1Zy2a3v2bEDfDN .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Bz1Zy2a3v2bEDfDN .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Bz1Zy2a3v2bEDfDN :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 通过
需修改
第 0 步:准备
第 1 步:探索
第 2 步:提案
第 3 步:人工审核
第 4 步:拆任务
第 5 步:TDD 实现
第 6 步:集成验证
第 7 步:代码审查
第 8 步:需求验收
第 9 步:归档沉淀
9 步拆成三个阶段,每阶段三步。
规划三步曲:准备 → 探索 → 提案
这三步的目标是想清楚再动手。
第 0 步:准备 。让 CC 先读 GUARDRAILS.md 和 service-map.md,确认改动属于红区、灰区还是绿区。CC 读完分析:灰区(后端 Service)+ 绿区(前端页面),可以继续。
我:先读 GUARDRAILS.md 和 service-map.md,告诉我这次改动属于哪个区域。
CC:分析出灰区+绿区,涉及 ruoyi-business 和前端页面。
我:确认没问题,继续。brainstorm 探索方案。
第 1 步:探索 。不写代码,先 brainstorm。CC 提了三种方案:前端切换视图(改动最小)、后端新增接口(职责清晰)、后端接口加参数(一个接口两种模式)。我选了第三种------复用现有接口加 groupBy 参数。
第 2 步:提案 。用 /opsx:propose 生成正式的功能规格文档。提案里必须标注 GUARDRAILS 区域、涉及模块、验收标准、测试策略。这一步是把口头讨论变成可执行的规格。
实现三步曲:审核 → 拆任务 → TDD
第 3 步:人工审核。唯一的硬性人工卡点。逐条看 proposal 的设计、验收标准、边界情况,有问题打回去,通过才继续。
第 4 步:拆任务。proposal 拆成 TDD 任务,每个 task 标注区域和契约约束。
第 5 步:TDD 实现 。逐 task 走红绿灯:先写测试(红灯,预期失败),再写实现(绿灯,测试通过),再重构。这步最能体现 harness 的价值------CC 写代码时要遵守 constraints.md 的技术约束和 contracts/data.md 的数据约定,不会自己发明一套。
我:按 task TDD 实现,遵守 constraints.md 和 contracts/data.md。
CC:先写测试(断言接口返回格式符合 AjaxResult 包装),红灯。再实现 Controller/Service/Mapper,绿灯。
我:过。跑集成验证。
收口三步曲:集成验证 → 验收 → 归档
第 6 步:集成验证 。编译 + 测试 + arch-guard 架构规则检查,手动验证关键路径。
第 7 步:代码审查 。按 DONE.md 逐条自查:安全区、契约完整性、诚实声明。
第 8 步:需求验收 + 归档 。openspec verify 检查契约一致性。验收通过后,判断有没有新知识需要回写 harness(决策→decisions/、术语→GLOSSARY.md、契约→contracts/),然后归档提交。
9 步走下来,最大的体感变化:AI 不再「自由发挥」了。 每一步有边界、有检查,不是限制它,是给它确定的跑道。
踩坑实录
流程看起来很顺畅,但实际跑的时候,CC 经常"抄近道"。以下是我遇到的几个典型坑。
坑 1:跳过 GUARDRAILS 就开工。 有一次 CC 直接改了灰区的 Service 查询逻辑,没说明影响面。发现方式:输出里没有区域分析。纠正:"先读 GUARDRAILS.md,告诉我属于哪个区域,再继续。"
坑 2:没写测试就开始写实现。 CC 写完实现再补测试,测试自然是绿的------按自己实现写的测试没有验证价值。纠正:"停。按 TDD 来:先写测试(红灯),再写实现(绿灯)。回退刚才的实现。"
坑 3:brainstorm 阶段就写代码。 探索阶段聊着聊着就开始贴代码。纠正:"现在是 brainstorm 阶段,不要写代码。先探索方案,对比优缺点。"
坑 4:改了红区文件。 CC 改了鉴权注解(红区),改完甚至没提。纠正:"你改了鉴权注解,这是高危区。停下来,我需要确认这个改动是否必要。"
这些坑的共同特点:CC 默认走捷径。 不是故意违规,是训练数据里没有"先读护栏再开工"这种模式。harness 把它变成显式指令------你不提醒它就跳过,写进文档它大概率遵守。
harness 在每一步的作用
有人会问:harness 不就是文档吗?
区别在于:普通文档写完扔那,harness 每一步都被 AI 主动读取------不是参考资料,是执行指令。
| 步骤 | harness 文件 | 作用 |
|---|---|---|
| 准备 | GUARDRAILS + service-map | 确认边界:能不能动、涉及哪些模块 |
| 探索 | GUARDRAILS | brainstorm 时不会提出越界方案 |
| 提案 | GUARDRAILS + DONE | proposal 里自带区域标注和验收标准 |
| 拆任务 | GUARDRAILS + contracts | 每个 task 标注边界和契约约束 |
| TDD 实现 | constraints + contracts | 写代码时遵守约定,不会自己发明一套 |
| 集成验证 | arch-rules.yaml | 自动检查架构规则 |
| 代码审查 | DONE + contracts | 逐条核对完成标准 |
| 需求验收 | contracts | 检查是否符合契约 |
| 归档 | 全部 | 判断要不要回写,知识沉淀 |
每一步都有对应的 harness 文件做决策锚点------该读什么、遵守什么、输出什么,都是明确的。
本质区别:文档是被动的,harness 是主动的。 它是 AI 工作流的一部分,不是存档材料。
个人思考:什么时候该用,什么时候别用
跑完一遍 9 步流程后,一些感受分享。
适合用 harness 的场景
多人协作的业务项目。 两三个 AI 同时改一个项目,没有约束层它们的"理解"会互相冲突。harness 让所有 AI 读同一套规矩。
模块边界模糊的老项目。 知识散落在代码注释、wiki 和老员工脑子里。harness 迫使你把隐性知识显式化,对 AI 和新人都有好处。
AI 生成代码占比高的项目。 偶尔补一两个函数不需要 harness,但 AI 承担 50% 以上编码时,没有约束层就是赌运气。
不适合用 harness 的场景
小脚本、一次性工具。 处理个 CSV 搞什么 harness,直接让 AI 写。
探索性原型。 还不知道要做什么的时候,约束反而拖慢探索速度。先试,方向明确了再补。
个人小项目。 上下文都在你脑子里,不需要传递就没必要搞 harness。
成本 vs 收益
说实话,前期投入不小。读文档、理解三层模型、整理自己项目的 GUARDRAILS 和 contracts,花了不少时间。
但收益是持续的。最大的变化是:AI 不再反复踩同一个坑了。 以前没有 contracts,AI 每次写接口都自己发明返回格式,改了五次五次不一样。有了 contracts,它第一次就写对了。
还有人审代码变轻松了。以前得逐行核对字段和约定,现在 CC 按 DONE.md 自己先过一遍,我审的是它的自查结果。
跟裸 AI Coding 的体感对比
一句话:从「猜」到「读」。
裸 AI Coding 的体感:每次对话都像带一个聪明但什么都不知道的新人,你说什么它做什么,但理解总有偏差,大量时间花在纠正上。
有 harness 的体感:每次对话都带一个读过项目文档的同事,知道规矩和边界。纠正时间少了,讨论方案的时间多了。
前者是你在教 AI 做事,后者是你在跟 AI 合作做事。
写在最后
Harness 工程的本质就一句话:让 AI 从「猜你的项目」变成「读你的项目」。
它不解决 AI 的能力问题,解决的是上下文问题------项目规矩、边界、验收标准。你不说,AI 就猜;你写下来让它读,它就不猜了。
推荐从这几个文件开始:
- 一份
GUARDRAILS.md,明确哪些能改、哪些要确认 - 一份
contracts/,定义接口返回格式、分页约定、错误码 - 一份
DONE.md,列出完成标准
不用一次性写完,从最痛的点开始------接口不统一先写 contracts,AI 乱改配置先写 GUARDRAILS。慢慢补,用几次就知道还需要什么了。
第一次跑 9 步确实繁琐,但跑过一遍很多步骤就变成肌肉记忆。而且 AI 记性比你好------你写进 harness 的规矩,它每次都会读、每次都会遵守。