【谨慎运用】拒绝AI过度工程手册-第一章【未完】
05/1836 浏览开发心得
拒绝 AI 过度工程——约束 AI 行为的提示词与沟通策略手册
Preventing AI Over-Engineering: Constraint Prompts Handbook
版本 v4.2 | 2026-05-18
================================================================================
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
前言
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
【一句话定义】
过度工程 = 代码解决的问题比你实际拥有的问题更多。
【为什么 AI(Claude)特别容易过度工程】
BUG 是显性破坏:能看见、能定位、能修复。
过度工程是隐性破坏:代码能跑,看起来"更完善",但在每次未来修改时
都向你收取复利式的认知成本。
Claude 的过度工程有三个结构性来源:
1. 训练数据偏差
互联网上的代码示例天然展示"完整方案"。Stack Overflow 上被高赞的答案
通常是通用的、防御性的。Claude 从这些数据学习,默认复现这种风格。
2. "有帮助"的错误激励
Claude 被训练为让用户满意。"多给一些"在表面上看起来比"少给一些"
更负责任。Claude 无法感知你项目的规模和生命周期,
于是默认按"大型生产系统"的标准设计一个周末项目。
3. 用复杂度对冲不确定性
当 Claude 不确定如何干净地实现某个功能时,它会通过增加抽象层来
"保留退路"。这些抽象不是为你服务的,是为 Claude 自己的不确定性
服务的。→ 详见第一章 1.13(最重要的 AI 特有模式,含完整分析)
过度工程的真实成本:
┌──────────────────────────────────────────────────────────────────┐
│ 短期:代码量翻倍,审查时间翻倍,理解成本翻倍 │
│ 中期:需求变化时,多余的抽象层成为修改障碍而非助力 │
│ 长期:项目因复杂度失控被放弃,或维护成本超过重写成本 │
│ 隐性:每次回来看自己的项目时,你需要更长时间"重新进入状态" │
└──────────────────────────────────────────────────────────────────┘
【本手册援引的核心原则】
- YAGNI(You Ain't Gonna Need It)—— Ron Jeffries,XP 方法论
- KISS(Keep It Simple, Stupid)—— 美国海军设计哲学,1960s
- Rule of Three —— Martin Fowler《重构》:第三次重复才值得抽象
- Gall's Law —— 「能运作的复杂系统都从能运作的简单系统演化而来,
跳过简单阶段直接设计复杂系统的尝试,从未成功过。」
- Second System Effect —— Fred Brooks《人月神话》:
「工程师在第二个项目中,倾向于把第一个项目里所有
"想加但没加"的东西全部塞进去。」
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
第一章:AI 过度工程的 15 种典型模式(含简化前后对比)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
本章识别"是什么"。干预句式见第四章,决策框架见第六章。
────────────────────────────────────────────────────────────────────────────────
1.1 不必要的抽象层
────────────────────────────────────────────────────────────────────────────────
触发信号:「为了更好的可扩展性…」「为了遵循 SOLID…」「为了便于替换…」
❌ Claude 给出(60 行):
interface EmailSender { send(to, subject, body): Promise<void> }
class SmtpEmailSender implements EmailSender { ... }
class EmailSenderFactory { static create(): EmailSender { ... } }
✅ 实际需要(8 行):
async function sendEmail(to, subject, body) {
await transporter.sendMail({ from, to, subject, text: body })
}
判断标准:接口只有一个实现 → 接口是多余的。
▶ 干预见第四章 4.1「打断不必要的抽象」
────────────────────────────────────────────────────────────────────────────────
1.2 为"可能的需求"设计(YAGNI 违反)
────────────────────────────────────────────────────────────────────────────────
触发信号:「考虑到未来可能…」「虽然现在不需要,但…」「为了以后方便…」
❌ Claude 给出(Python 场景):
class StorageBackend(ABC): # 「以后可能换 S3」
@abstractmethod
def save(self, key, data): ...
class LocalStorageBackend(StorageBackend):
def save(self, key, data): ...
class S3StorageBackend(StorageBackend): # 「备用,还没接入」
def save(self, key, data): ...
✅ 实际需要(你只有本地存储):
def save_file(path, data):
Path(path).write_bytes(data)
Rule of Three:同一逻辑出现第 3 次,且三处会同步修改时,才值得抽象。
▶ 干预见第四章 4.1「打断不必要的抽象」
────────────────────────────────────────────────────────────────────────────────
1.3 过度的错误处理
────────────────────────────────────────────────────────────────────────────────
触发信号:「为了健壮性…」「为了防止极端情况…」「虽然通常不会出错,但…」
❌ Claude 给出(内部函数加了公网级别的防御):
function double(n: number): number {
if (n === null || n === undefined) throw new Error('n is null')
if (typeof n !== 'number') throw new TypeError('n must be number')
if (!isFinite(n)) throw new RangeError('n must be finite')
if (n > Number.MAX_SAFE_INTEGER / 2) throw new Error('overflow risk')
return n * 2
}
✅ 实际需要(内部函数,调用方保证类型):
const double = (n: number) => n * 2
原则:只在信任边界处校验(用户输入、外部 API 响应)。
内部函数的调用方已保证合法性时,校验是冗余成本,不是"健壮性"。
▶ 干预见第四章 4.1「打断不必要的错误处理」
────────────────────────────────────────────────────────────────────────────────
1.4 过度的配置化
────────────────────────────────────────────────────────────────────────────────
❌ Claude 给出(Lua 场景):
local Config = {
maxRetries = 3, -- 你永远不会调整这个
retryDelay = 1000, -- 你永远不会调整这个
timeout = 5000, -- 你永远不会调整这个
enableLogging = true, -- 你永远不会调整这个
}
function fetchData(url, options) -- options 有 12 个可选字段
options = options or {}
...
end
✅ 实际需要:
function fetchData(url)
return http.get(url, { timeout = 5000 })
end
标准:只有「这个值在不同环境或不同时间真的会变」时才做成配置。
▶ 干预见第四章 4.1「打断过度配置化」
────────────────────────────────────────────────────────────────────────────────
1.5 不请自来的重构
────────────────────────────────────────────────────────────────────────────────
触发信号:「我顺便…」「我还改进了…」「趁这次机会优化了…」
危险:范围外的每次修改都是新 BUG 的潜在来源,且无法归因。
❌ 你要修 line 42 的一个变量,Claude 额外做了:
· 重命名了 15 个变量("更清晰")
· 提取了 3 个新函数("更可读")
· 调整了文件结构("更符合规范")
· 加了 JSDoc 注释("便于维护")
✅ 你要修 line 42,Claude 只改 line 42。
▶ 干预见第四章 4.1「打断不请自来的重构」
────────────────────────────────────────────────────────────────────────────────
1.6 过度的注释
────────────────────────────────────────────────────────────────────────────────
❌ 噪音注释(增加阅读负担,降低信噪比):
-- 将 userId 赋值给 userId 变量
local userId = user.id
-- 返回 userId
return userId
✅ 有价值的注释(解释"为什么"而非"是什么"):
-- 用 floor 而非 round:业务要求不足1元不计,避免多收费
local fee = math.floor(amount * rate)
────────────────────────────────────────────────────────────────────────────────
1.7 引入不必要的依赖
────────────────────────────────────────────────────────────────────────────────
❌ 为一行逻辑引入整个库:
import _ from 'lodash'
const copy = _.cloneDeep(obj) // 仅此一处使用 lodash
✅ 原生 API 足够时:
const copy = structuredClone(obj)
每个额外依赖 = 安全风险 + 升级负担 + 包体积 + 许可证风险。
────────────────────────────────────────────────────────────────────────────────
1.8 类型系统滥用(TypeScript)
────────────────────────────────────────────────────────────────────────────────
❌ Claude 给出(为一个只用一次的函数写了 10 行类型体操):
type UserId = string & { readonly _brand: 'UserId' }
function createUser<T extends Record<string, unknown>>(
data: Omit<T, 'id'> & Partial<Pick<T, 'createdAt'>>
): T & { id: UserId } { ... }
✅ 实际需要:
function createUser(data: { name: string; email: string }) {
return { ...data, id: crypto.randomUUID() }
}
────────────────────────────────────────────────────────────────────────────────
1.9 过度的日志框架
────────────────────────────────────────────────────────────────────────────────
❌ 个人脚本引入企业级日志:
const logger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
transports: [ new winston.transports.File({ filename: 'app.log' }) ]
})
✅ 个人脚本:
console.log('Processing:', args)
────────────────────────────────────────────────────────────────────────────────
1.10 设计模式滥用
────────────────────────────────────────────────────────────────────────────────
触发信号:「使用 XXX 模式可以…」
❌ 简单 if/else 被强行包装成策略模式(Python 场景):
class SortStrategy(ABC):
@abstractmethod
def sort(self, arr): ...
class BubbleSort(SortStrategy):
def sort(self, arr): ...
class Sorter:
def __init__(self, strategy: SortStrategy): ...
# 实际只调用一次,排序算法固定为冒泡
✅ 实际需要:
arr.sort()
────────────────────────────────────────────────────────────────────────────────
1.11 过度的安全加固
────────────────────────────────────────────────────────────────────────────────
❌ 内网 CLI 工具加了公网级防护:
app.use(helmet())
app.use(rateLimit({ windowMs: 15 * 60 * 1000, max: 100 }))
app.use(cors({ origin: process.env.ALLOWED_ORIGINS?.split(',') }))
原则:安全措施应针对真实的攻击面,而非想象的攻击面。
────────────────────────────────────────────────────────────────────────────────
1.12 架构提前复杂化(Big Design Up Front)
────────────────────────────────────────────────────────────────────────────────
Gall's Law:「能运作的复杂系统都从能运作的简单系统演化而来,
跳过简单阶段直接设计复杂系统的尝试,从来不会成功。」
❌ MVP 阶段直接设计微服务:
用户服务 / 订单服务 / 通知服务 / API 网关 / 消息队列 / 服务发现...
✅ MVP 应该是:
一个应用 + 一个数据库,跑通核心流程再演进。
(何时演进 → 见第六章 6.6 演进触发条件)
────────────────────────────────────────────────────────────────────────────────1.13 用复杂度对冲不确定性(Claude 特有模式,最核心)
────────────────────────────────────────────────────────────────────────────────
这是 Claude 独有的过度工程模式,人类工程师极少这样做。
【机制】
当 Claude 不确定如何干净地实现某个功能时,它会通过增加抽象层来
"保留退路":接口可以换实现,工厂方法可以切换策略,配置项可以在
运行时调整。这些抽象不是为你服务的,是为 Claude 自己的不确定性
服务的。
【Claude 特有的表现形式】
· 给出一个 Claude 自己也无法解释"为什么要这样"的复杂结构
· 解释听起来很合理("这样更灵活"),但追问具体场景时开始模糊
· 实现中包含多个"可选扩展点",但你从未要求过扩展性
· 方案里有一层或多层"中间件",职责是纯转发
【识别方法】
追问:「如果去掉这层抽象,会发生什么具体问题?」
如果 Claude 的回答变得模糊,说明这层抽象是不确定性的护身符。
❌ 触发信号:
「我设计了一个 Pipeline 结构,这样各步骤可以灵活组合…」
(你只需要顺序执行 3 个步骤,而这 3 个步骤从来不需要重新组合)
► 专项干预:
「你的方案是否是因为你不确定这段逻辑最终长什么样,
所以用抽象层保留了灵活性?如果是的话,请告诉我你不确定的地方,
我们先确定需求,再写代码。」
(→ 使用模板 4 挖掘不确定性根源)
────────────────────────────────────────────────────────────────────────────────
1.14 测试过度工程
────────────────────────────────────────────────────────────────────────────────
过度工程不只发生在实现代码里,测试里同样常见。
❌ Claude 倾向给出:
· Mock 一切(包括不需要 mock 的内部函数)
· 测试实现细节而非行为(断言内部变量的值,而非函数的输出)
· 为每个私有方法写单独的测试
· 100% 覆盖率的强迫症(覆盖了永远不会出错的 getter/setter)
· 设置 / 拆除代码比实际测试代码还长
✅ 好的测试:
· 测试行为(给定输入,验证输出),不测试实现
· 只 mock 外部依赖(网络、数据库、时钟)
· 测试对重构透明——你重构实现,测试不需要改
► 干预句式:
「你的测试是在测试这个函数"怎么实现的"还是"做了什么"?
如果我重构实现但保持行为不变,这些测试需要改吗?
如果需要,说明测试耦合了实现细节,请重写。」
────────────────────────────────────────────────────────────────────────────────
1.15 修复过程中的范围蔓延(Scope Creep in Repair)
────────────────────────────────────────────────────────────────────────────────
与 1.5(不请自来的重构)的区别:
1.5 是"修改时顺便重构",是风格上的扩展。
1.15 是"修复 BUG 时把修复范围越扩越大",Claude 会声称这些都是
"BUG 的根本原因"或"必要的修复",因此比 1.5 更难拒绝。
❌ 你要修一个变量名拼写错误,Claude 给出:
「问题根源在于整个模块的状态管理方式。我将:
1. 修复拼写错误(你要的)
2. 重构状态管理为 Redux 模式(「防止类似问题」)
3. 加入单元测试(「确保修复有效」)
4. 更新接口定义(「保持一致性」)」
✅ 你要修拼写错误,Claude 只改那一个拼写错误。
【关键判断】:1~3 是真的 BUG 原因?还是 Claude 趁机把待办清单里
的"改进项"塞进来?追问:「拼写错误修好之后,2/3/4 各自单独
会在什么地方出什么 BUG?」
► 干预句式:
「只修复这一个 BUG,其他所有"改进"算作独立任务,
现在不做。修复完成后,我来决定是否处理那些改进。」
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━


