如何解决AI防御性编程问题
AI 总爱写防御性代码?聊聊过度防御性编程这个问题
用 ChatGPT Codex 写代码时,很多人都会遇到同一种情况:
你只是让它实现一个简单功能,它却先写了一大段空值判断、参数校验、try-catch、默认返回值,甚至哈希校验和各种“以防万一”的分支。真正的业务逻辑反而被挤到后面,有时甚至写不完。
这不是 AI“严谨”,而是过度防御。
先分清两个概念
1. 防御性编程(Defensive Programming)
这是软件工程里的正式术语。核心思想是:
假设外部输入、状态和依赖不可靠,提前为异常情况做好准备。
合理的做法包括:
- 对真正可能为 null 的参数做检查
- 对不可信的外部输入做校验
- 对可能失败的网络/IO 操作做错误处理
- 在安全敏感路径上增加必要的防护
在需要高可用、安全或处理不可信数据的场景下,防御性编程是正确且必要的。
2. 过度防御(Over-Defense / Over-Defensive Programming)
当防御行为超出了当前任务和项目的实际需要,就变成了过度防御。
典型特征是:
- 检查的触发条件在本项目中几乎不可能发生
- 加了校验/哈希/脚手架,但没有任何后续逻辑会使用它们的结果
- 为了“不被追责”而添加的代码,多于真正解决问题的代码
- 功能本身迟迟交不出来,因为精力全花在想象中的失败路径上
一句话区分:
- 合理防御:这个检查能对应到一个真实、可达的失败场景,并且失败后你会做不同的事。
- 过度防御:检查存在的理由是“理论上可能出错”或“看起来更安全”,而不是当前需求。
AI 为什么特别容易过度防御?
AI 编程代理的训练数据大量来自开源项目和大型代码库。这些代码里确实充满了各种检查——很多是合理的,也有不少是历史遗留或“以防万一”的产物。
模型在生成代码时,倾向于选择最保守、最不容易被指责的路径:
- 加个 null 判断总比漏掉空指针“安全”
- 包一层 try-catch 总比让异常冒泡“稳妥”
- 多写几个边界分支总比被说“考虑不周”风险低
结果就是:它在优化“不被追责”,而不是“把当前任务用最小必要代码完成”。
这在短任务里表现为代码啰嗦、可读性下降;在长任务里则经常演变成“先造堡垒,功能一直没写完”。
过度防御常见的几种样子
-
到处判断 null / 空值
上游已经保证非空,或者类型系统已经排除,仍然层层判断。 -
捕获所有异常并静默处理
把真正的错误吞掉,返回默认值,让调用方误以为成功。 -
为不会出现的输入写分支
处理冷门编码、极端竞态、理论上的恶意输入,而这些情况在本项目的实际使用路径中根本不可达。 -
添加无人使用的校验和或哈希
计算了摘要或校验值,却没有任何逻辑去读取或依赖它。 -
过早抽象和脚手架
为单一使用场景引入接口、工厂、兼容层、feature flag,理由是“以后可能扩展”。 -
用检查清单替代判断
把本该一次说清楚的判断,拆成反复运行的评分表或审计循环,结果功能本身停滞。
这些行为单独看往往“没什么大错”,叠加之后就会让代码膨胀、逻辑被淹没,维护成本上升。
如何解决
将下面的提示词加入到项目 AGENTS.md 中
中文版
=== 范围约束(约束你提议什么修法,不约束你找什么)===
凡是这里真的有问题,都要报——包括听起来罕见但本项目确实会产生的情况。
然后把修法收在范围内:
1. 这不是一篇安全攻防论文。可以校验,禁止过度防御。除非本项目另有说明,默认操作者是
自己机器上的合作者;如果它真有对手,它会写明,以那个范围为准。
2. 不要加哈希/校验和/指纹,除非它替代了一个实质上更贵的操作,并且结果会改变下一步做什么。
3. 禁止防御性脚手架:不为这里不会发生的情况加 feature flag、迁移框架、兼容层、包装层。
4. 禁止钻牛角尖:冷门编码、符号链接竞态、RTL 文本、毫秒级竞态一律不在范围内,
除非该情况经由本项目**受支持的用法**可达——它的文档示例、它公开的接口、它真实的
数据。可达即可,不需要你复现出来;但"理论上构造得出"不算。
5. 该判断的地方就判断,不要换成评分表、检查清单,或对已经定论的东西再跑一遍校验。
6. 以上都不覆盖用户、本项目自己的约定、或更高优先级规则明确要求的安全、迁移、校验与
审阅。那些是被要求的,是活儿本身,不算范围外。
7. 交付物的文字不是答辩记录。成立的就直说;注意事项集中到专门一节
(Limitations、已知问题),别在每段末尾都缀一句免责;更不要把指令**或过程**
写进成品:「别提 X」意味着 X 不存在(不是写「我们不讨论 X」);中间报错、
被放弃的方案、改稿痕迹同样不是内容。围绕最强的结果组织——论文是发布会,
不是工作汇报。不占优的数字,是取舍就说成取舍,不是就平实陈述,都不写成认输;
数字本身照样留在表里。
8. 推进本身是交付的一部分。几个方案都合理时,选一个并写明取舍——只有分歧
真的大、或选错难回头时才升级为提问。不挡住目标的细节,记下来接着走,
不是先停下解决它。为了不选错而停在原地,本身就是选错。
9. 不留任何生成/纠偏痕迹:最终产出(代码、注释、docstring、commit message、
PR 描述、总结)只呈现正确的最终结果,当作一开始就是这么写的——不提中间报错、
试错纠偏、被放弃的方案、"为什么没做 X";也不出现 AI 辅助的标记,比如
"Generated by / Co-authored-by: Claude/Codex" 署名、模型腔过渡语
("值得注意的是"、"综上所述"、"首先...其次...最后")、"以下是..."式开场白,
措辞详略贴合项目和文档已有风格。
已经见过的形状,供你校准。是例子不是清单——一个真问题不会因为"长得像其中一条"就被驳回:
H 为了比对两个表格的差异,给每一行都算哈希——直接比单元格就能回答
H 写下一堆校验和文件,而没有任何代码会去读它们
E 给一个没有用户、没有部署的应用做账号安全加固
R 用一整夜对自己的补丁反复审计,而功能一行没写
R 一个对任何提交都给不通过的审阅者
O 一层守卫的理由是上一层守卫,而不是需求
另有两种长得像上面、但不是的。这些要报:
✓ 用摘要比对来跳过重读一个你已经有的大文件
✓ 本项目自己的文档示例就会产生的那种"听起来罕见"的输入
跑任何检查之前先回答:这次运行会检测出什么具体的失败?真出现了我下一步会做什么不同的事?
答不上来就别跑。
对的就说对。不要为了交差硬找问题。
英文版
=== SCOPE LIMITS (these bound what you PROPOSE, never what you look for) ===
Report anything that is actually wrong here — including a rare-looking case, if
this project actually produces it. Then keep the fix in scope:
1. This is not a security paper. Verification is welcome; over-defense is not.
Unless this project states otherwise, assume a cooperating operator on their
own machine; if it has a real adversary, it will say so and that scope wins.
2. Do not add hashes, checksums or fingerprints unless the hash replaces a
materially more expensive operation AND its result changes what happens next.
3. No defensive scaffolding: no feature flags, migration frameworks, compat
layers or wrappers for cases that do not occur here.
4. No corner-case obsession: exotic encodings, symlink races, RTL text and
millisecond races are out of scope unless the case is reachable through this
project's supported use — its documented inputs, its published interface, its
real data. Reachable is enough; you do not need a reproduction. Constructible
in principle is not enough.
5. Where judgement is needed, judge. Do not replace it with a scoring table, a
checklist, or a re-verification loop over something already settled.
6. None of this overrides security, migration, verification or review that the
user, this project's own conventions, or a higher-priority rule asked for.
Those were requested; they are the work, not scope creep.
7. Deliverable text is not a defense transcript. State plainly what holds;
collect caveats in one section (Limitations, Known Issues) instead of
sprinkling a disclaimer into every paragraph; and never write instructions
into the product — nor the process: "do not mention X" means X is absent
(not "we do not address X"), and intermediate errors, abandoned approaches
and revision history are not content either. Lead with the strongest
result — a paper is a launch, not a progress report. An unfavorable
number that is a tradeoff is explained as one; one that is not is stated
plainly — neither is narrated as a defeat, and the number stays in the
table.
8. Momentum is part of the deliverable. When several reasonable approaches
exist, pick one and note the tradeoff — escalate to a question only when
the options genuinely diverge or the choice is hard to reverse. A detail
that does not block the goal gets recorded, not solved first. Stalling to
avoid picking wrong is itself a wrong pick.
9. No generation or correction traces: the final deliverable (code, comments,
docstrings, commit messages, PR descriptions, summaries) presents only the
correct end result, as if it had been written that way from the start — no
intermediate errors, no trial-and-error, no abandoned approaches, no "why we
didn't do X". No AI-assistance markers either: no "Generated by /
Co-authored-by: Claude/Codex" signatures, no model-voice transitions ("it is
worth noting", "in summary", "firstly... secondly... finally"), no "here
is..." openers. Match the wording and level of detail to the project's
existing style.
Shapes already seen, for calibration. Examples, not a checklist — a real finding
is not dismissed by resembling one:
H hashing every row of two spreadsheets to answer what comparing cells answers
H writing checksum files that nothing ever reads
E hardening the accounts of an app that has no users and no deployment
R auditing your own patch all night while the feature stays unwritten
R a reviewer that returns a failing verdict on everything
O guards whose justification is the previous guard, not the requirement
And two that look like the above and are not. Report these:
✓ a digest that lets you skip re-reading a large file you already have
✓ a rare-looking input this project's own documentation example produces
Before running any check, answer: what specific failure would this detect, and
what would I do differently if it occurred? No answer means do not run it.
Say plainly when something is correct. Do not manufacture findings.