Harness 工程:给 AI Coding 套上缰绳的第一次实践

Harness 工程:给 AI Coding 套上缰绳的第一次实践

让 AI 写代码不难,让 AI 写对代码才难。这篇文章记录我在业务项目上第一次把 Harness 工程跑通的全过程------从"AI 乱猜"到"AI 照着规矩来"。


目录


你一定遇到过这个场景

你用 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.mdservice-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 就猜;你写下来让它读,它就不猜了。

推荐从这几个文件开始:

  1. 一份 GUARDRAILS.md,明确哪些能改、哪些要确认
  2. 一份 contracts/,定义接口返回格式、分页约定、错误码
  3. 一份 DONE.md,列出完成标准

不用一次性写完,从最痛的点开始------接口不统一先写 contracts,AI 乱改配置先写 GUARDRAILS。慢慢补,用几次就知道还需要什么了。

第一次跑 9 步确实繁琐,但跑过一遍很多步骤就变成肌肉记忆。而且 AI 记性比你好------你写进 harness 的规矩,它每次都会读、每次都会遵守。


相关推荐
Agilex松灵机器人1 小时前
UMR × NAVIS:移动机器人行业级通用一体化底盘与3D导航解决方案
人工智能·3d·机器人·ros·slam·vla·松灵机器人
在水一缸1 小时前
OpenCV 5 深度解析:计算机视觉领域的“五年之变”与跨越式升级
人工智能·opencv·计算机视觉·开源项目·技术升级·opencv 5
2501_915716721 小时前
Models-学习指南_小白版
人工智能
Java小白笔记1 小时前
Codex 桌面应用设置功能完全指南
人工智能·opencv·目标检测·机器学习·计算机视觉·数据挖掘·语音识别
EterNity_TiMe_1 小时前
别让告警变成噪音:用Prometheus和Alertmanager搭一套可远程查看的监控系统
数据库·人工智能·ai·postgresql·prometheus·cpolar
知行产研1 小时前
厦工股份年报:主业亏损7345万,矿卡业务1.14亿当年盈利——传统机械上市公司的跨界样本
人工智能·科技
世界很奇妙塔2 小时前
大模型轻量化技术攻坚:从千亿参数到端侧部署,重构AI落地底层路径
人工智能·重构
z小猫不吃鱼2 小时前
模型剪枝经典论文精读:Rethinking the Value of Network Pruning
人工智能·深度学习·计算机视觉
阿尔泰科技官方2 小时前
阿尔泰科技——NVH/振动/声学测试专用解决方案
人工智能·信号采集·仪器仪表·数据采集卡·采样率