【谨慎运用】拒绝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,其他所有"改进"算作独立任务,
     现在不做。修复完成后,我来决定是否处理那些改进。」
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
5
1