背景
随着 AI 在编码领域的能力不断提升,开发生产级软件的工作越来越多地交给 Agent,而不是人类工程师。这一现状意味着软件工程的范式转变,而它带来的真正的问题不在于 agent 能不能写出代码,而在于:人类工程师的注意力应该放在哪些层面?agent 留下的技术债务,又能用什么机制提前预防或减轻?
带着这两个问题,以及希望学习与实践Harness Engineering的工程哲学的出发点,我们开发了基于 Kimi Code CLI 的桌面应用,用来支持本地多个 agent 并行开发 GitHub 仓库、审阅与合并。整个项目从一个空的 git 仓库开始,先按递归式地图原则做了目录治理,再陆续设计一组工具和约束法则——CI 脚本、AST 分析器、定期清理 dead code 的工具、仓库一致性检查工具。
在这个过程中,我们注意到 OpenAI 团队 Dex 的一段演讲。他引用了一份对 10 万开发者的调查:使用 AI 做软件工程时大部分时间都在返工和 code churn;复杂任务和成套代码库(brownfield codebase)效果不佳;产出虽然增加了,但很多产出只是在修上周匆忙发布的代码。这和我们撞到的问题对得上——说明我们看到的不是个例。
下面这些经验,是我们在这个项目里反复撞到的几堵墙,以及由此长出来的判断。
关键挑战与讨论
Agent 的固有失败模式
在 kimi-code-swarm 的开发中,我们发现 Agent 在编码上犯的错,相当一部分不是随机失误,而是可复现的固有模式——不是"这次没做好",而是"每次都会这样"。这类问题源自训练分布里长出来的惯性,无法通过简单的提示词"再努力一点"根治。
对于典型的问题,我们进行了复盘,并声称这是绝大部分编码能力相当优秀,但实际工程能力比较欠缺的 Agent 存在的通病。包括但不限于不考虑程序健壮性,模糊需求不对齐,自作主张加护栏等情况。一种是想当然——把"主流程跑通"当成"功能完成",在有测试功能的情况下默认不加以验证;另一种是自作主张——在你没要求的地方擅自加操作、加"护栏",而你完全无法预知它会引发什么。这些情况都会导致结果偏离我们的预期。
具体几个场景:
清理只做了一半,运行时直接崩溃。它删掉一个"看起来没用"的兜底机制时,只删了入口,没检查还有没有别的环节依赖它——像拆承重墙只拆了外表,里面的梁柱还在撑着。结果一轮对话收尾时 App 直接报错卡死。
兜底被当冗余删掉,异常时用户一片空白。CLI 输出偶尔格式异常、结构化解析失败,本应至少把原始输出原样显示出来。但那段兜底显示逻辑同样被它当成"没用的东西"删了——解析一失败,前端只剩一条错误日志,用户一个字都看不到。它把"异常时的安全带"当成了"多余的重量"。
只顾后端逻辑,前端的基础体验整段缺失。它把注意力全压在后端,没有验证输入与渲染的约束:输入框不支持换行、Shift+Enter 直接触发发送,用户根本没法分段表达;长消息气泡也不折行,横向无限延伸到屏幕之外。
还有一类失败更难处理:agent 自己根本不知道问题出在哪里。Kimi 在长链路 bug 上尤其明显——它会反复猜、反复改、反复回复"修好了",但每一轮其实在原地打转,每次"修复"只是换了个症状继续走错。等我们意识到它已经卡住时,已经多走了好几个错误的 commit。
鉴于这样的问题,可靠的做法,是把这些已知的失败模式前置成机械约束:代码变更时须同步更新文档和测试,不能破坏现有测试,不能引入一些冗余的设计或明确提及的错误原则的实践等。在这个意义上,Harness Engineering 实际就是执行良好的约束,遵循软件基础设计的准则。**变化的不是原则,而是受众——从工程师换到了 agent。**过去靠"工程师自律"撑住的约束,现在必须更彻底地机械化、留下可核对的证据。
漂移机制与约束粒度
Agent 在生成代码时会出现设计漂移,问题核心在于人类的注意力不能总是和 Agent 执行的粒度对齐。在一个数十万行的商业项目里,我们撞到过一次非常典型的——而且这次漂移不是 agent 违背了文档,而是 agent 严格按文档做,结果仍然偏离了我们的预期。
对于该项目,我们采用典型的前后端分离架构。一个按钮被点击,前端把信息序列化后请求后端,调用 Controller 的方法,再交由 Service 层处理业务逻辑。文档里写了一条约束,本意是堵住"控制器层重写或复制业务逻辑"那个口子,原话是:后端已有可用功能模块时,Server 层采用复用的设计原则,只做"能力暴露与适配",不重写也不复制业务逻辑。
当让 agent 写一个新功能模块,要求严格遵循这条约束。它非常严格地遵循要求;在 Server 层加了一大段适配逻辑,因为文档上明明白白写着"Server 层只做能力暴露与适配"——"适配"既然写在这一行,那就放在这一层。结果 Server 类一下子变得臃肿、职责模糊、脆弱。
当然这不是我们的真实设计意图。Server 应该只做序列化,而微调和适配下放到后端能力层。当初写"不要重写业务逻辑"是想堵上层重写下层逻辑这个口子,并没有意识到自己同时把"适配"这两个字放在了 Server 头上。约束的字面意思和真实意图之间留了一截缝——agent 严格按字面照做,就漂到了缝的那一头。
这件事让我们对几个相关学科的分工有了更具体的体感:
- Prompt Engineering 关心怎么让 AI 听懂指令;
- Context Engineering 关心怎么在上下文不够、信息不足的长链路场景里把信息凑齐;
- Harness Engineering 关心的是:当指令的字面意思与真实设计意图之间留有歧义时,agent 必然会在歧义里漂移——这时候靠什么样的系统约束把这种漂移挡在外面。
漂移在项目复杂度低的时候不致命,看上去甚至像"风格差异";但项目越大、文档越不严格、系统设计本身越有歧义,漂移就被成倍放大,最后体现为持续累积的技术债。
诚实地说,我们对漂移的现行应对相当原始——在问题一出现的早期就打断、回退、重对齐,"把债务扼杀在摇篮里"。这种打法防住了债务累积,但代价是注意力消耗极大,实际产出效率并不高。怎么把这一层从人工守门变成可落地和通用的约束,是我们承认的一块未完成。
这其实指向一个开放问题:文档究竟应该抽象到什么程度,才能让收益最大化?约束写得太细,会把 agent 的手脚捆住——后面那条"明确目标,模糊细节"就是冲着这一头去的;但约束写得太松、太抽象,又会留出歧义空间让漂移钻进来,像我们刚撞到的这次一样。
skill 如何在 Harness Engineering 语境下起到良好作用
继续前文的问题,要让 Agent 输出收益最大化,最接近我们心目中的预期,可行的办法就是控制输入。
在文档约束的问题层面,输入就是文档这类的指令,一个优化思路是让 Agent 在最终输出前先与我们对齐,将一个需求的输入变成多轮的信息,现在已经有很多 skill 能做到这一点。然而尽管 skill 被定义为可复用的指令模版,我们更推荐自定义每个重要的 skill,因为 skill 真正强大的特点是高度灵活与可定制化,同时能够控制抽象层次——例如我们可以在 skill 里提到 Agent 执行任务时参考的哪些文档输入在他看来是不合寻常的。
举一个我们自己做的具体例子,把它叫任务接入门控:Agent 接到任何涉及改动代码 / 文件 / 配置的任务,第一件事就是加载它,铁律只有一句——未经用户明确批准,不许写任何实现代码。门控分四个阶段,前三个阶段一行代码都不写:
- 需求澄清与文档清单 —— 用自己的话复述需求,把假设逐条列出来,有歧义当场提问。
- 场景与预期结果 —— 把任务拆成具体场景,必须覆盖正常路径、失败与边界路径、用户视角。只写 happy path,等于这一步没做完。
- 对齐门控 —— 把前两阶段完整摆给用户,等一个明确的批准;用户没点头,第四阶段免谈。
- 实现与验证 —— 过了门才动手,且全程受一组不变量约束,验证时把原始输出贴出来。
这里有一个我们认为最关键的设计取舍:第四阶段写的是"不变量",不是"禁令"。禁令("不要 mock""不要乱删")读完照样能绕过去,因为没人检查;而不变量是一个能被检出的性质——"删除前先 grep 引用并列出来"会产出一份引用清单,"贴原始验证输出"会产出一段日志,"列出所有场景"会产出一张场景表。把要求写成会留下证据的不变量,门控才真正有牙齿,而不是又一条会被礼貌性忽略的约定。
额外的经验教训
目录级别上下文窗口
"地图即边界" 这一法则在项目根目录的 AGENTS.md 上运行良好,但当文档层级深一级时,问题开始浮现。
例如在某次 issue 修复里,agent 自动命名文档为 agent-engine-spawn-windows.md。这个名字对新来的 agent 而言毫无意义——它无法从文件名判断这是一份关键 bug 的复盘记录,还是一份引擎启动方案,从而不得不打开阅读,浪费宝贵的上下文窗口。
对于这样的文档级别上下文窗口优化,我们采用两个思路:
思路一:子目录级 AGENTS.md
在 docs/exec-plans/ 下放一个极简的 AGENTS.md,只列出目录内各文件的主题与适用场景。agent 到达该目录时先读索引,再按需深入,实现地图式访问。
思路二:自解释文件名
直接把关键文档改名为 critical-bug-solved.md,原则是文件名即摘要。
这两个思路都能做成可复用的 skill,核心仍然是针对"agent 的注意力是有限资源"这一前提,做目录级别的信息压缩。agent 的效率瓶颈本质是信息论问题——输入决定输出,注意力是稀缺约束。
Rethinking 是必要的,不能外包你的思考
在开发 kimi-code-swarm 的过程中,我们撞上了一连串生产环境的问题:装好安装包、启动 App,每次新建 agent 都失败,连报错的 toast 都弹不出来。
第一反应是让 agent 去修,它也确实在修——一个 commit 加上 node.exe 的路径探测(用户的 Node 装在 nvm 目录、不在系统 PATH 里),下一个 commit 修打包遗漏、补一个启动健康检查……每个 commit 都精准地解决了一个症状,但 App 还是起不来。三个 commit 之后我们才意识到:问题不在任何一个症状上,而在架构本身。
这个桌面 App 在生产环境里竟然是靠 tsx 在运行时现场转译 TypeScript 的——它的启动链路要求"用户机器有 Node + 打包进去的 esbuild 二进制 + 源码"三样东西同时成立。在这条链路上打补丁,永远是在堵一根本来就不该存在的管子上的漏点。真正的解法不是第四个补丁,而是退一层把链路换掉:build 时用 tsc 预编译成 JS,生产环境直接跑 dist/index.js,把"运行时转译"这一整段删掉。一个架构决策,抵得过之前所有补丁的总和。
问题出在让 agent 不停修修补补,看起来一直在推进,本质上是低效的——它在症状层面打转,而你误以为自己在解决问题。Agent 时代,编码可以外包,但当系统产出效率出现瓶颈,第一时间识别并下判断的能力不可替代。编码可以外包,思考不能外包。
黄金法则
对于踩过的坑以及一些其他先验的规则,我们总结了仓库里几条黄金法则:
地图即边界 —— Agent 只读
AGENTS.md,细节去docs/按需加载。来自目录级上下文窗口那一节的事故。机械化约束优先 —— 代码必须过 CI:类型 → Linter → AST → 构建。规则要能被脚本检出,不能只靠人盯。
仓库是唯一事实源 —— Slack / 口头约定对 Agent 等于不存在。
执行即更新文档与状态 —— 代码变更后同步更新文档,并在
docs/STATUS.md标记状态(✅ / ⚡ / 🚧 / ❌);被阻断时回顾本次查阅的文档。约束即代码 —— 所有规则必须机械可执行;不能自动检查的约定等于不存在。
反复 bug 或根因不明 → 加日志定位并留痕 —— 修复后在代码注释或 commit 中记录根因。
代码改动必验证 —— 修改后必须 build、测试、lint 全绿才允许合入。
但反过来说,如果把 agent 约束到每个细节,反而会让它束手束脚、甚至无法工作。所以另有一条不在 7 条里、但贯穿整套做法的原则:明确目标,模糊细节。对长期规划,工程师在更高抽象层操作,只要方向对,agent 在大体约束下自由发挥反而更出活。这非常像与能力卓越的同事共事,你只希望方向正确,设定一个目标,而不做过多行为上的约束,大家会被目标激励,想办法做到最好。
收尾
软件工程范式的改变是生产力的跃进,也带来了全新的问题。Harness Engineering 要解决的核心问题只有一个:怎么最大化 Coding agent 的收益,而不让人类的注意力成为瓶颈。这件事我们还在路上。