Coding Agent 太会写怎么办:用四层工程约束控制 AI 编程风险

作者:袖梨 2026-09-21

当代码生成的成本快速下降,团队面对的新难题已不再是 AI 能不能写,而是它会不会在缺少边界时写得过多、改得过头。过度设计、贴合实现的单测和缺乏证据的重构,都可能让看似完整的产出埋下风险。要改善这种状况,需要把工程判断变成 Agent 能执行和验证的约束。

AI 写代码最大的风险不是不会写,而是太能写:我给 Coding Agent 加的 4 层工程约束

过去大半年,几乎每个使用 Coding Agent(Claude Code、Cursor、Windsurf 等)的工程师都会经历同一种心路历程:

从最初惊叹于它几秒钟写出几十行代码,到后来对着它生成的 PR 陷入深思——AI 很少因为“语法不会”而写错,它绝大多数时候失控,是因为它在错误的优化目标下,把代码写得“太像那么回事了”。

你让它加一个轻量级的 Token 缓存,它顺手给你塞了一套工厂、接口、策略模式加单例;
你让它补单测,它顺手把私有方法的内部实现全 mock 了一遍,测了个寂寞;
你让它做 Code Review,它先夸你一句“架构清晰、逻辑优雅”,然后优雅地放过了最致命的并发竞争;
你让它解释一个老模块,它耐心地读完 30 个文件,然后把你本就能看到的文件列表原封不动地念了一遍。

这些问题,没有一个是语法问题,全都是“工程判断”的问题。

很多人的第一反应是继续往 system_prompt.cursorrules 里加规矩:“不要过度设计”、“多测边界条件”、“认真找 Bug”。但现实是残酷的:规则越长,模型注意力越稀释,最后什么都没约束住。

传统的软件工程规范(Lint、CI、Code Review)是为了对抗人类的惰性与遗忘
但面对 Agent 时,工程体系必须用来对抗模型的顺从、过度生成与过拟合

在实际项目踩坑后,我把这些隐性的工程判断抽离出来,写成了 6 个具备明确输入、边界、验证脚本与退出条件的 Agent Skill。它们本质上不是 6 个 Prompt,而是加在 Agent 自主决策链路上的 4 层工程约束环


第一层:改动约束(code-ablation

约束 Agent 的重构冲动:不要删有价值的复杂度,只消融无价值的包装。

AI 编程有一个极其危险的本能:要么在写新代码时过度设计,要么在重构时把“代码简化”理解成“疯狂删代码,行数越少越好”。

code-ablation 这个 Skill 里,我写在最前面的核心约束是:

“只有一个实现只是线索,不是删除依据。”

看一个很典型的场景:

// 很多 AI 看到这里只有一个实现,会直接把它内联掉
export interface TokenStore {
  get(key: string): Promise<string | null>;
  set(key: string, value: string, ttl: number): Promise<void>;
}

export class RedisTokenStore implements TokenStore { ... }

如果只看当下,删掉 TokenStore 接口确实能省掉 4 行代码。但这个抽象可能承担着跨模块边界、测试桩替换、或者隔离第三方基础设施的责任。盲目删掉它,本质上是在用未来的架构可维护性,换取当下的行数缩减。

因此,这个约束明确框定了消融的前提条件:

  1. 不变性约束(Invariants):公共 API 不变、错误语义不变、副作用不变、非法输入行为不变、权限与幂等性边界不变。
  2. 证据驱动:必须有代码调用链或生命周期证据证明某段逻辑是死代码。测试全绿不代表没有外部使用。
  3. 接受“零删除”:如果分析完发现每层抽象都有其防御意义,允许并鼓励 Agent 输出“无需消融”。

Agent 在尝试“简化”代码时,必须先回答:这层抽象到底现在解决了什么真实问题?它如果消失,破坏的是哪个设计边界? 答不上来,就不准动。


第二层:验证与证伪(test-first + pre-mortem

防止 Agent 陷入“用自己的实现来证明自己正确”的自圆其说。

让 AI 写测试最容易出现的灾难是:它先写完代码,然后照着当前实现的每一行去写断言。 这种测试哪怕覆盖率达到 100%,对回归测试也毫无意义,因为如果明天重构内部实现,测试会全部崩溃;而如果代码逻辑本身理解错了,测试反而固化了错误。

这一层由两个呈对偶关系的 Skill 组成:

1. test-first:测试约束的是行为契约,不是实现细节

这个 Skill 强迫 Agent 遵守一套认识论:测试不仅是给机器跑的,更是给人类维护者看的验收文档。

它强制 Agent 的单测描述必须遵循“业务主语 + 行为 + 触发条件”,严禁出现 should worktest submitcalls charge once 这种面向实现的废话。

更关键的约束是:

需求意图
  ↓
定义验收场景(契约)
  ↓
写出必然失败的测试(先红)
  ↓
编写满足测试的最小实现(再绿)
  ↓
重构

如果 Agent 测到了私有变量、依赖了未公开的实现细节,必须打回。验证这套测试合不合格的标准只有一个:如果我明天把函数体用完全不同的算法重写一遍,你的测试能不能不改一行直接跑通?

2. pre-mortem:改变任务的前提,而不是改变提问的措辞

普通的 Review Prompt:“请审查这段代码是否存在 Bug。”
在模型的概率分布里,这个任务的前提是:作者写了一段看起来合理的代码,请帮他查漏补缺。 于是模型很自然地进入夸夸群模式,挑几个无关紧要的命名建议就交差了。

而在 pre-mortem Skill 里,我直接切断了这个前提,把它的目标函数强行修改为:

“假设系统已经在半年后的高并发场景下发生了 P0 级事故,现在复盘调查。不要替作者辩解,找出直接导致这次崩溃的 3 个最致命原因。”

当目标从“评价代码”变成“事故调查”时,Agent 的关注点瞬间转移到了:

  • 爆炸半径:一个节点的失败会不会级联打垮上游?
  • 静默失败:有没有吞掉异常导致状态不一致?
  • 不可逆性代码可以瞬间回滚,但被污染的数据不一定能回滚。

通过切换任务的立足点,从根源上摧毁了模型默认的“协作迎合倾向”。


第三层:知识表达分流(self-documenting-code + code-comments

让机器可以表达的留在机器里,让机器无法表达的留给人类。

很多开发者误以为“好代码少写注释”和“详尽注释”是对立的。实际上,这俩是一套严密的信息分流漏斗:

具体逻辑行为       → 由清晰代码自身承载
状态与结构约束     → 由类型系统(Type / Schema)承载
运行时边界限制     → 由断言(Runtime Assert / Guard)承载
行为契约与验收条件 → 由自动化测试(Test)承载
外部无法表达的知识 → 留给注释(Comments)

1. self-documenting-code:不要让人记住机器本来能记住的事

AI 最喜欢写这种代码:

// 错误示范:靠注释维持脆弱的结构
interface ApiResponse {
  status: 'loading' | 'success' | 'error';
  // 当 status 为 success 时 data 必然存在
  data?: UserData;
  // 当 status 为 error 时 error 必然存在
  error?: Error;
}

这就是典型的“把编译器的活丢给人脑”。只要有人漏看注释,就会产生运行时隐患。

这个 Skill 约束 Agent:必须将状态收敛到结构与类型里:

// 正确做法:用类型消除非法状态
type ApiResponse =
  | { status: 'loading' }
  | { status: 'success'; data: UserData }
  | { status: 'error'; error: Error };

如果代码和类型已经能够自闭环表达“是什么”,任何关于“是什么”的注释全部算作代码噪音,必须清除。

2. code-comments:保留代码无法表达的决策与外部现实

当代码已经被类型和结构极致表达后,剩下的注释应该写什么?

这个 Skill 规定:注释只用来记录外部世界的事实、历史妥协与决策代价。

// 合格的注释:代码无法表达的现实约束
// Safari 16.0~16.3 在页面退入后台时会重复触发 visibilitychange,
// 此处做 150ms 节流以规避主流程重复初始化。

同时设立边界:

  • 函数外注释:服务于调用者,只讲输入前置条件、返回值保证、可能抛出的异常,绝口不提内部怎么实现的。
  • 函数内注释:服务于维护者,只讲为什么选这个算法、为什么不能采用看起来更优解的替代方案(Why over What)。

第四层:交付与认知编译(context-compression

Agent 应该压缩的是人类的认知负荷,而不是简单地压缩字数。

这是日常体验中最容易被忽视、但最影响人机协作效率的一环。

当一个 Agent 在终端里排查完一个涉及 20 多个文件的老旧复杂 Bug 后,它通常有两种坏习惯:

  1. 纯流水账:从 A 文件讲到 Z 文件,每行改了什么全部倒出来;
  2. 纯结论:一句话“已经修复了并发问题”,丢下一个巨型 Git Diff 让你盲猜。

第一种直接引爆开发者的认知负荷,第二种根本无法建立信任。

context-compression 约束的本质是:要求 Agent 替人类开发者完成最后一次“认知编译”。

源码结构与零散链路
      ↓(Agent 深度分析)
完整技术结论与因果链
      ↓(上下文认知压缩)
交付给开发者的心理模型(Mental Model)

它强迫 Agent 在交付结论时,必须组织为:

  1. 核心矛盾与系统模型:问题的根因机制是什么,用最小因果链解释;
  2. 关键决策与非目标:我们选择了什么方案,主动放弃了什么,为什么;
  3. 架构变动点:关键边界、生命周期和副作用转移到了哪里;
  4. 验证证据:哪个自动化测试证明了该场景已被覆盖。

开发者不是小白,他们只是没有刚刚读过这 20 个文件的即时上下文。Agent 存在的价值,不仅是搞清楚代码,更是帮开发者用最低的认知成本,快速建立起这块代码的心理模型。


写在最后:走向 Agent 原生工程规范

回过头看这 6 个 Skill,它们贯穿了一个非常清晰的生命周期:

       【理解模块】
            ↓
   context-compression
            ↓
       【设计结构】
            ↓
  self-documenting-code
            ↓
       【实现细节】
            ↓
      code-comments
            ↓
       【验证行为】
            ↓
       test-first
            ↓
       【逆向审查】
            ↓
       pre-mortem
            ↓
       【精简收敛】
            ↓
      code-ablation

在日常使用中,你不需要一股脑把它们全扔给模型。当需要做模块重构时,挂载 code-ablation;当要准备提交关键 PR 时,调用 pre-mortem;当要让它写核心业务逻辑时,触发 test-first

AI 编码工具发展到今天,代码生成的边际成本几乎已经降到了零。

但软件工程从来不是比拼“谁能在单位时间内生产更多字符”。越是在生成容易的时代,对不必要复杂度的警惕、对独立证伪能力的坚持、以及对认知负荷的控制,反而变得比以往任何时候都更加关键。

这或许就是 AI Coding 时代正在催生的新常态:我们不再只是给人类写规范,我们正在将那些沉淀多年的工程判断,编码成 Agent 必须遵守的可执行契约。


仓库开源在 GitHub:DBAAZzz/skills ,包含各个 Skill 的完整定义、边界条件与配套 scripts,欢迎参考或提 PR 讨论。

相关文章

精彩推荐