跳到主要内容
返回写作

这篇记录更新了项目判断 / Stop That Shit

AI Agent 的防御性工程,是怎么一步步长出来的?

兼容层、全量测试、迁移框架和免责声明并不天然多余。问题是 AI Agent 怎样把未来风险提前做成当前工作,以及我们怎样用可达证据判断去留。

本文索引06
  1. 01防御不是错,提前实现才需要证据
  2. 02Agent 为什么特别容易把未来风险搬到现在?
  3. 03用“可达证据”把判断拉回当前系统
  4. 04“只做最小修改”也可能是错的
  5. 05把授权模式写在任务入口,而不是写成长篇声明
  6. 06好的防御性工程,应该能说出它在防什么

一个小任务变大,通常不是因为 Agent 突然决定重写系统。

它更像这样:先加一个“保险”的校验,再补一个兼容入口,然后发现新入口也要测试,于是跑完整矩阵。最后为了说明这些边界,又多写一份文档和一组配置。

回头看,每一步都有理由。可用户原本只要一个能工作的结果。

这就是防御性工程最容易失控的地方:单个动作往往合理,组合后的工作量却没有当前消费者。

防御不是错,提前实现才需要证据

真实工程当然需要防御。

公开 API 有多个调用方,改签名时必须处理兼容。生产环境已经有旧数据,升级时必须迁移。发布流程要求 checksum,构建就应该生成。一个共享模块触达多个包,跨组件测试也不能因为“改动很小”就跳过。

这些工作有共同点:当前系统里存在一个能被指出来的依赖关系。

你可以找到读取旧字段的代码,可以数出受影响的 caller,可以看到 release verifier 消费摘要,也可以说明哪条验收在缺少这一步时会失败。

防御性工程开始变成过度工程,通常是因为这个依赖关系只存在于想象里:

  • 目前没有旧数据,但先建迁移框架;
  • 目前没有第二个实现,但先抽象 Adapter;
  • 目前没有外部调用方,但先保留兼容入口;
  • 当前变更没有跨模块契约,却默认跑完整测试矩阵;
  • 没有一个用户决策需要风险说明,却把内部顾虑写成一排 UI 免责声明。

它们不是永远没用。它们只是还没有证据证明现在就要做。

Agent 为什么特别容易把未来风险搬到现在?

Coding Agent 收到的是一个开放世界。它读到一个函数,不知道是否还有隐藏调用方;看到一个文件,不知道是否会被外部流程消费;修一个错误,也可能担心没跑到的测试。

在这种不确定性里,继续加保护是一个很自然的局部选择。多一层抽象、多一次校验、多跑一轮测试,似乎都能降低“漏掉什么”的风险。

但局部降低风险,不等于全局完成任务。

额外层会产生新的维护面,额外配置会增加状态组合,额外测试会拉长反馈时间,额外说明会让真正需要用户判断的边界更难看见。更关键的是,每一项新增工作都会成为下一项工作的理由。

兼容层需要测试,测试矩阵需要 CI 配置,CI 配置又需要文档。任务不是一次被做大,而是前一项“保险”不断为后一项“保险”提供证据。

用“可达证据”把判断拉回当前系统

我在 Stop That Shit 里使用的 Stop Ladder,有一个容易被忽略的词:可达

哪段可达的代码、数据、用户决策、部署状态或验收条件证明这一步有必要?

可达代码不是“也许有别的模块在用”,而是能沿 import、调用或协议找到的真实消费者。可达数据不是“以后可能有旧记录”,而是当前环境里已经存在、必须处理的数据形态。可达验收也不是“更完整会更好”,而是少做以后某一条明确条件会失败。

这个标准可以直接用在几类常见争议上:

Agent 想做的事当前证据判断
生成 SHA-256后续没有命令读取停止
生成 release checksumverifier 会读取并拒绝不匹配产物保留
加兼容 Adapter只有一个实现,没有外部调用方暂不实现
修共享接口 caller仓库内有三个可达调用方一起修改
跑完整测试矩阵变更只触达独立 CSS,无共享契约跑受影响检查
扩大回归测试修改了多个消费者共用的序列化格式扩大验证

这张表没有把动作分成“高级”和“低级”,只看当前系统有没有消费者。

“只做最小修改”也可能是错的

限制过度工程时,另一个常见误区是把目标改成最小 diff。

如果一个共享类型变化后有三个 caller,漏改两个 caller 的 diff 确实更小,但结果是坏的。如果修复改变了序列化契约,不补 fixture 和回归测试也许只改一行,却无法证明兼容性。

所以 Stop That Shit 的边界 不是“文件越少越好”,而是“请求的结果和必要后果都要完成,其他工作停止”。

必要后果包括真正受影响的 caller、fixture、测试、无障碍、安全、迁移和兼容工作。它们不需要被用户逐条点名,只要可达证据说明省掉就会让当前任务失败。

这也解释了为什么四个问题里既有“用户要求了吗”,也有“它是否必要”。只有第一问,会漏掉合理后果;只有第二问,Agent 又可以把所有未来可能性解释成必要。

把授权模式写在任务入口,而不是写成长篇声明

实际使用时,我更喜欢先声明任务模式。

$stop-that-shit review -- Review 这个 diff,只报告问题,不要修改。
$stop-that-shit change -- 修复失败的配置测试。

review 把写文件排除在当前授权之外;change 允许完成修复所需的修改。边界特别清楚时,再加依赖、hash、文件或 subagent 预算:

$stop-that-shit change deps=allow -- 添加我要求的解析器依赖。
$stop-that-shit change hash=allow -- 生成发布流程要求的 checksum。
$stop-that-shit change agents=1 -- 使用一个独立测试 subagent。

不知道全部受影响文件时,不要为了显得精确而硬写 files=。先让 Agent 沿真实调用链检查,再把必要修改收进任务。

模式的作用不是替代工程判断,而是让授权边界在动作发生前就能被检查。

好的防御性工程,应该能说出它在防什么

下次 Agent 提议加兼容层、迁移框架、全量测试或一组 guard,可以先不讨论它“专不专业”。

让它指出当前消费者、被替代的昂贵操作、改变的下一步,以及省略后会失败的验收。

如果答案存在,就把防御做好。如果答案只指向一个没有发生的未来,先把它记录成候选,而不是偷偷塞进当前实现。

防御性工程的价值不在于层数,而在于它阻止了一个真实、可达的失败。