AI 写代码之后,哪些工程判断需要补回来
作者结合云安全分析引擎的开发经历,讨论 AI 主导编码时容易缺失的七类工程职能。案例涉及删除大量代码、撤销子系统、先写架构文档,以及开发测试差异分类工具。
AI 深度解读
AI 能把功能写出来,却未必能替团队守住「为什么只做这些功能」。Stave 的作者在开发一套云安全推理引擎时,删掉了数万行可运行代码,还专门写文档、架构测试和差异分类工具,补回 AI 编程容易跳过的判断。
他的核心主张是:把架构边界、领域知识和行为承诺留在人的脑子里,已经不够了。它们需要变成下一次 AI 会话能读懂、构建流程能检查的约束。这篇文章有价值的地方,正是那些「测试通过,但方向错了」的具体案例。
测试全绿,为什么还要删掉整个子系统
Stave 对基础设施快照做静态分析,按照系统不变量输出发现,要求隔离运行、结果确定。作者把开发中遇到的问题,对应到 1972 至 1994 年的七项研究:模块边界、领域模型、必要复杂度、意图记录、可验证性、行为契约,以及结构完整性。
这些概念最直观的落点,是一次失败的功能开发。
作者在一天内通过 9 次提交,写出约 4,800 行目录冲突分析代码。面对包含 630 项控制规则的目录,它报出 590 个矛盾,其中 546 个涉及子资产路由字段:作用于不同子资产的规则,被误认为互相冲突。
编译、测试和文档都没发现方向问题。继续修补也可以,但作者判断,这会变成没完没了的目录专用过滤工作。最终提交涉及 21 个文件,新增 152 行、删除 5,726 行,并用 catalog-authoring-as-service.md 记录决定:目录编写应作为服务提供,不该继续塞进产品。
六周后,作者又删除了 10 个超出范围的命令,共 25,350 行代码,并在 features/scope.yaml 中加入 forbids,防止功能回流。
测试能证明实现符合已有预期,却不能证明这个预期值得实现。
类似的取舍也发生在 iam-explain:它只有 8 条规则,每条用 4-6 行 Go 闭包就能表达,却因为项目使用 CEL(Common Expression Language),引入了解析、环境初始化和类型适配。作者改回普通 Go,并把 CEL 版本留在 alt-cel/ 供比较。
另一处复杂度则不能直接删除。迁移类型时出现 risk → report → evaluation → risk 循环,最终需要建立共同依赖的叶子包 internal/core/findings/。工具可以减少实现负担,但业务概念之间的依赖,仍要有人理清。
给 AI 留上下文,要留下决策和可执行约束
作者长期维护详细提交信息,以及 DOCTRINE.md、MEMORY.md、ARCHITECTURE.md 等文件。它们记录的不只是改了什么,还有试过什么、为何回滚、以后必须尊重哪些限制。
他把这套做法称为简陋的 RAG(检索增强生成)。更准确地说,它是一份附带代码差异的决策日志。修改模块前,真正需要的问题往往是:「这里有哪些约束,哪些方案已经失败,移动这些类型会破坏什么?」
Git 历史能保存尝试、撤销、替代的先后关系。但文档只负责解释,约束还得进入工具。
Stave 的核心曾经通过中间适配器间接依赖 CLI 框架 Cobra。单独禁止 Cobra 不够,因为依赖还能从别的路径绕进来。作者引入纯数据结构 GlobalSettings,再把 depguard 从禁止某个库,改成检查架构层次。TestHexagonalDependencyDirection 则在每次构建时检查依赖方向,累计抓住了 3 次违规。
「为什么不做」也需要记录。面对 3 项超出产品职责的审计发现,作者没有补功能,而是写了 246 行 docs/architecture/boundaries.md。DOCTRINE.md 用 9 条有仓库实例支撑的原则解释取舍,TERMINOLOGY.md 则统一 CLI、文档、MCP 工具描述和文章里的词汇。
输入输出契约也有同样的问题。提取器漏填 properties.foo 时,field: properties.foo op: eq value: true 可能得到 false,系统于是报告没有违规,实际却是检查所需的数据缺失。
作者增加按资产类型划分的 schema 注册表,用 Required/Optional 声明字段,并在摄入时对必填缺失发出结构化警告。输出侧则补齐 FindingDTO 已输出、JSON Schema 却遗漏的 22 个字段,修正 first_seen_unsafe 与 first_unsafe_at 的名称漂移,对齐 Draft 2020-12,并把校验改为默认开启。
类型形状正确,还不等于下游拿到了完成判断所需的数据。
可验证的规则,怎样长成可复用的事实来源
Stave 对确定性的要求很硬:输入相同、--eval-time 相同,输出必须逐字节相同。
Go map 遍历顺序随机,曾让 CompoundFinding.AttackStages 的输出次序变化,修复只需序列化前排序的 2 行代码。另一处问题更大:--eval-time 被忽略,referenceTime 始终使用最后一份快照的 CapturedAt。修复一个文件,却改变了 325 个 golden 文件,即保存预期输出的测试基准。
为审查这些变化,作者写了 595 行的 regenerate-goldens,把差异分成 CLEAN、FINGERPRINT-ONLY、METADATA-ONLY、BEHAVIORAL、MIXED 五类。它解决的不是生成答案,而是让人知道答案为什么变了。
规则表达也沿着类似方向演进。最初,Stave 用 unsafe_predicate 声明安全条件,由进程内 Google CEL 执行:
unsafe_predicate:
all:
- field: properties.storage.access.external_account_ids
op: not_subset_of_field
value_from_param: allowed_accounts
它表达的是:检查外部账号集合是否超出允许范围。要看清 op: not_subset_of_field 和 value_from_param: allowed_accounts,比较方式与参数来源都被明确写了出来。
这套格式支持 any/all 嵌套及封闭的操作符集合,包括 eq、ne、in、missing、present、contains、gt、lt。
2026 年 4 月,复合链引擎上线。5 月 5 日,作者通过一个上午的 3 次提交、941 行代码,建立结构化中间表示 SIR,包含 ControlFact、AssetFact、IdentityFact、TemporalFacts,并增加 forbidden_state、intent_rationale。
5 月 6 日,他删除了 728 行 Python Z3 求解器后端,明确内部求值只使用 Google CEL,Z3 留作 Go 示例。边界变成:导出配置事实,由外部程序解释这些事实。
5 月 7 日,导出命令增加 --format jsonl 和 --format smt2,接入 Z3/cvc5、Clingo、PySAT、Soufflé、Prolog;次日再加入 PRISM、TLA+、Game Theory。不同工具分别回答不安全路径是否存在、影响范围多大、路径为何可达等问题。
作者把这种外置约束的方向称为 Reasoning Specs:在接口形状之外,继续描述安全不变量、行为承诺和架构限制,让它们跨会话保存,并被工具检查。
我的判断
我认同作者把「解释原因」和「机械检查」放在一起。只写文档,约束容易被忽略;只加检查,后来者又可能不知道规则为何存在。删除功能时同步留下决策记录和禁止项,尤其有参考价值。
但「AI 不会删功能」「AI 无法识别多余抽象」说得太绝对。原文没有给出模型、提示词、对照团队或失败率,这些是单个项目的经验判断,不能当成普遍能力结论。更稳妥的理解是:如果任务只奖励功能完成,这些工作很容易被遗漏。
作者对 RAG 的批评也需要限定。普通相似度检索可能丢失时间关系,但检索系统并非不能结合提交、日期和依赖信息。Git 日志同样不会自动把正确决策送到当前任务面前,仍需要检索入口和维护。
Reasoning Specs 在原文中更像方法方向,尚未展示完整规范、跨团队落地或迁移成本。OpenAPI 的类比有启发,但接口描述本身也不保证业务行为完整。
此外,「8 个推理引擎」沿用了作者分组,不能直接等同于 8 套生产级集成。原文也在 Ralph loop 的介绍中截断,无法据此判断其后续结论。
能拿来做什么
- 给 AI 修改模块前,提供相关决策记录,写清已否决方案、原因和触发重新评估的条件,比不断堆积项目介绍更有针对性。
- 在前端或 Node 仓库中,把领域层不能导入页面、框架或适配器等规则接入依赖检查,并让构建实际执行。
- 对依赖接口字段的判断,同时检查数据形状与必填完整性;字段缺失时应暴露无法判断的状态,避免默认为业务条件不成立。
- 批量更新快照前,先区分排序、元数据和行为变化,再审查基准更新,避免把数百个文件的一次性通过当成正确性的证据。