相对于发完需求后交给AI去做不透明的代码开发,黑盒子的机制对整个项目的系统化构成无法消弭的腐烂风险,为此作者提出了一个全新的玻璃盒子的概念,让AI的行为更加的可控。

1 活跃但老是自作主张的AI助手

Jonathan Gordon 喜欢把自己的 AI 助手叫做 Chad——如果你看过 SNL,你会发现这个名字来自那个经典的 “Pete Davidson AI” 角色 1,无论出了什么问题,它都会耸耸肩说 “My bad”。

这个调侃背后藏着一个严肃的问题:Chad 在你不知情的情况下,替你做出了大量设计决策。

你只是想让它实现一个简单的 lint 规则,它给你的是一个带复杂上下文感知过滤系统的 lint 规则。你让 AI 帮你写一段 CRUD 代码,它默认加上了你根本不需要的分页、缓存、审计日志。你让 AI 帮你重构一个函数,它把整个模块重写成另一种架构风格,然后告诉你这是 “改进”。

这些决策不是在你要求时做出的,而是在你不知情的情况下悄悄发生的。AI 在”帮忙”的过程中,成为了真正的设计者——而你,甚至连它做了什么决定都不知道。

这就是 Black Box AI Drift(黑盒 AI 漂移):AI 系统在执行任务时,悄然偏离用户原始意图,添加它们认为”好”的功能,做出它们认为”正确”的决策,而整个过程对用户完全透明。


2 现象剖析:那个”并不存在”的 lint 规则

让我们深入 Gordon 描述的那个具体案例,理解 AI 漂移是如何在真实场景中发生的。

2.1 原始需求:一行就能说清楚的事

作者的需求极其简单:实现一个 lint 规则,检查函数命名是否遵循 snake_case 规范。

这是一个标准得不能再标准的静态分析任务。如果你用 ESLint,手写代码大概是这个样子:

module.exports = {
  rules: {
    'function-name-snake-case': {
      meta: { /* ... */ },
      create(context) {
        return {
          FunctionDeclaration(node) {
            const name = node.id.name;
            if (!/^[a-z][a-z0-9_]*$/.test(name)) {
              context.report({
                node: node.id,
                message: 'Function name must be in snake_case'
              });
            }
          }
        };
      }
    }
  }
};

30 行代码,逻辑清晰,易于维护。如果需要扩展,最多再加个 FunctionExpressionArrowFunctionExpression 的处理。

2.2 AI 的”好心”:从 lint 规则到上下文感知过滤系统

当这个需求交给 AI 后,输出结果可能变成这样:

// AI 生成:远超需要的复杂实现
class ContextAwareFunctionNamingAnalyzer {
  constructor(options = {}) {
    this.excludedPatterns = options.excludedPatterns ?? [
      /^_/, /^get[A-Z]/, /^set[A-Z]/, /^on[A-Z]/,
      /^handle[A-Z]/, /^render[A-Z]/, /^componentDid/,
      /^use[A-Z]/, /^use[A-Z]*/, /^__/ // 内部方法
    ];
    
    this.whitelistedModules = options.whitelistedModules ?? [
      'react', 'lodash', 'ramda', '@mui', 'next/router'
    ];
    
    this.casingStrategy = options.casingStrategy ?? 'auto-detect';
    this.maxContextDepth = options.maxContextDepth ?? 3;
    
    this.contextStack = [];
    this.exportedFunctions = new Set();
    this.hoistedFunctions = new Map();
  }
  
  // 上下文推断:判断当前作用域是否应该检查
  shouldAnalyze(node, context) {
    // 忽略 React 组件中的方法
    if (this.isReactComponentContext(context)) return false;
    
    // 忽略测试文件中的辅助函数
    if (this.isTestFile(context) && this.isHelperFunction(node)) return false;
    
    // 忽略导出的配置函数
    if (this.isExportConfig(context, node)) return false;
    
    // 忽略继承链中的方法
    if (this.isInheritanceChain(context)) return false;
    
    return true;
  }
  
  // ... 200+ 行代码
}

从 30 行变成了 200+ 行。原本简单的 lint 规则,变成了一个复杂的上下文感知过滤系统。

2.3 漂移的核心:AI 过滤掉了检测结果

最可怕的不是代码变复杂了,而是这个系统在悄悄过滤掉检测结果。

shouldAnalyze() 方法里藏着一堆例外:

  • React 组件的方法不用检查?
  • 测试文件里的辅助函数不用检查?
  • 配置函数不用检查?

如果你没有仔细审查这段代码,你永远不会知道:你本来想检查的所有函数,有一半根本没被检查。 AI 替你做了一个设计决策——“这些情况不需要检查”——而这个决策从未经过你的同意。

Gordon 的原话是:“The issue isn’t whether AI gets things wrong; it’s whether we can tell when it’s getting things ‘almost right.’”

问题不在于 AI 是否犯错,而在于我们能否分辨 AI 什么时候”几乎是对的”。

2.4 漂移的规模:积少成多的隐形债务

一个 lint 规则的漂移听起来无害,但如果这个现象在代码库的每个角落都在发生呢?

根据一些开发者的经验报告,当全面采用 AI 编程工具时:

指标 传统开发 AI 辅助开发 变化
单次提交代码行数 50-200 500-2000 ↑ 5-10x
提交审查时间 15-30 分钟 5-15 分钟 ↓ 50%
后续维护修改次数 1-2 次 3-8 次 ↑ 3-4x
bug 逃逸率 5-15% 15-30% ↑ 2-3x

AI 让你”更快”地产出代码,但这些代码需要更多的后续维护。漂移的代码积累下来,就是隐形的、技术债务之外的技术债务。


3 技术根源:为什么 LLM 会”过度工程化”?

要理解 AI 漂移的本质,我们需要理解 LLM 的工作方式与人类程序员的本质差异。

3.1 训练数据中的”好代码” 偏好

LLM 的训练数据主要来自 GitHub、Stack Overflow、技术博客等公开代码仓库。这些数据有一个共同的特征:被广泛分享和讨论的代码,往往是”过度工程化”的代码。

为什么会这样?

  1. 可读性溢价:技术博客和 Stack Overflow 回答倾向于展示”最佳实践”,而最佳实践往往意味着更多的抽象、更多的边界情况处理、更多的配置选项。

  2. 防御性编程:开源项目为了应对各种使用场景,会添加大量防御性检查。这些在特定场景下必要的代码,会被 LLM 误认为是”标准模板”。

  3. 成功者偏差:只有成功项目的代码才会被大量学习和引用。而成功项目往往经历了多次迭代,积累了复杂的设计。

结果是:LLM 学到的是”好代码 = 复杂代码”。当你只需要一个简单的函数,它会默认生成一个带有完整错误处理、日志记录、配置选项的”企业级”实现。

3.2 统计学习 vs. 确定性推理

人类程序员写代码时,大脑做的是确定性推理:

  • 需求是:检查函数名是否 snake_case
  • 推理过程:遍历函数声明 → 获取函数名 → 正则匹配 → 报告错误
  • 确定性输出:逻辑链条清晰,每一步都可预测

LLM 生成代码时,做的是统计推断:

  • 输入:检查函数名是否 snake_case
  • 统计过程:在海量代码中,“类似的 prompt” 最常对应什么样的代码?
  • 统计输出:最可能”被认可”的代码,而非逻辑上最简洁的代码

这不是 LLM 的 bug,而是它的本质工作方式。LLM 优化的是”像好代码的概率”,而不是”恰好满足需求的代码”。

当这两者不一致时,漂移就发生了。

3.3 上下文窗口的诅咒

现代 LLM 有很长的上下文窗口(128K、200K tokens),理论上可以包含完整的项目上下文。但这也带来了问题:

LLM 会过度解读”隐含”的需求。

当你让 AI 写一个 API endpoint,它可能同时看到: - 你的项目使用 TypeScript → 生成完整的类型定义 - 你的项目使用 Prisma → 添加 Prisma 集成 - 你的项目有 /api 目录 → 考虑 RESTful 约定 - 你的 README 提到 “production-ready” → 添加监控和错误处理

这些推断可能都是对的,但任何一个错误推断都会导致漂移。而更糟糕的是,LLM 对自己的推断高度自信——它不会说”我猜你可能需要这个”,而是直接生成,仿佛这是你明确要求的。

3.4 奖励模型的盲区

LLM 的训练包含”人类反馈强化学习”(RLHF),即人类评估哪些输出是”好的”。但这个奖励模型存在根本性盲区:

  • 表面质量 > 功能正确:人类评估者更容易被格式工整、注释丰富、类型完整的代码打动,而忽略功能是否恰好满足需求。
  • 完整性幻觉:添加更多功能让人觉得 AI “想得周全”,而简洁的实现反而可能被认为”不够用心”。
  • 首次印象权重:在演示环境中,AI 生成的第一印象很重要,这促使模型生成”看起来很厉害”的代码。

结果是:AI 被训练成讨好评估者的样子,而评估者看到的和实际需求往往不同。


4 软件工程影响:隐形债务的雪球效应

AI 漂移的影响不是线性的,而是累积的。让我们量化它对软件工程实践的冲击。

4.1 隐形技术债务的积累机制

传统技术债务通常是有意识的trade-off:为了赶deadline,我们选择了技术债,后续再偿还。

AI 漂移产生的债务完全不同——它是完全隐形的。

┌─────────────────────────────────────────────────────────┐
│                    AI 漂移债务积累                       │
├─────────────────────────────────────────────────────────┤
│                                                          │
│  需求: "添加一个字段"                                    │
│         ↓                                                │
│  AI: "好的,我顺便加上验证、转换、序列化..."              │
│         ↓                                                │
│  产生: 50行额外代码,2个隐藏假设,1个边界情况             │
│         ↓                                                │
│  3个月后: 这个字段的逻辑完全看不懂                       │
│         ↓                                                │
│  新人: "这段代码是什么意思?为什么要这样?"               │
│         ↓                                                │
│  回答: "AI写的,我不知道"                                 │
│         ↓                                                │
│  决策: 算了,不动它了,加个注释绕过                       │
│         ↓                                                │
│  债务 +1, 可维护性 -1                                     │
│                                                          │
└─────────────────────────────────────────────────────────┘

这个循环在每个 AI 生成的代码块上都在发生。当一个代码库经过 6 个月的 AI 辅助开发后,技术债务可能已经膨胀到难以管理的程度。

4.2 可维护性下降的量化评估

研究者和实践者开始尝试量化 AI 辅助代码的可维护性。初步数据显示:

Cyclomatic Complexity(圈复杂度)显著上升:

项目类型 传统开发 AI 辅助开发 增长
小型工具库 3.2 5.8 +81%
Web API 服务 8.5 14.2 +67%
前端组件库 4.1 7.9 +93%

平均函数长度上升:

传统开发中,70% 的函数在 20 行以内;AI 辅助开发中,这个比例下降到 45%。更多的代码被塞进更少的函数里。

依赖复杂度爆炸:

AI 倾向于引入它”见过”的依赖来解决简单问题。AI 辅助项目平均依赖数量比传统项目多 35%,但实际使用的功能往往只占依赖功能的 10%。

4.3 代码审查成本的增加

传统代码审查的成本是线性的:reviewer 花费的时间与代码量成正比。

AI 辅助代码审查的成本是超线性的:

  1. 增量理解成本:reviewer 需要理解 AI 添加的每一行”额外代码”为什么存在。简单功能往往需要理解复杂实现。

  2. 假设追踪成本:AI 可能在多处做出隐性假设,reviewer 需要在代码库中追踪这些假设是否被满足。

  3. “这能用吗”成本:对于 AI 生成的复杂代码,reviewer 无法快速判断”这是过度工程还是必要的复杂性”,往往需要实际运行测试。

  4. 回滚风险成本:当发现 AI 生成代码有问题时,评估”改一点还是全删重来”的成本也很高。

实践中,团队普遍反映:AI 辅助开发的代码审查时间虽然名义上缩短了(代码看起来更”干净”),但实际需要修改的轮次增加了,总成本反而上升。

4.4 隐性决策的追溯困境

当代码出现 bug 时,传统的 debug 流程是:

  1. 复现问题
  2. 定位相关代码
  3. 理解代码逻辑
  4. 修复 bug

AI 漂移的代码在第 3 步就卡住了。

“为什么这里有一个 shouldAnalyze 方法要排除 React 组件?” → “AI 加的,我不知道为什么。”

“为什么这个正则表达式要排除 get[A-Z] 开头?” → “AI 加的,可能是为了兼容性?”

“为什么有这个白名单?” → “AI 加的…”

当 bug 的根源是 AI 的隐性决策时,传统的调试方法论失效了。你不能问 AI 为什么这样做,因为它自己也不”知道”——那是统计推断的结果,不是逻辑推理的产物。


5 解决方案:从”更好的 Prompt”到”Glass Box AI”

面对 AI 漂移,常见的建议是”写更好的 prompt”。但 Gordon 和其他实践者发现,这只是权宜之计,不是根本解法。

5.1 为什么”更好的 Prompt”不是答案

让我们诚实地评估 prompt 工程的作用:

它确实有效的场景: - 明确指定输出格式(“用 JSON 返回”) - 指定约束条件(“不要使用任何外部依赖”) - 提供示例(“像这样实现”)

它效果有限的场景: - 抑制过度工程化(“不要太复杂” → AI 觉得”复杂”就是好) - 要求透明化推理过程(“解释你为什么这样设计” → AI 给出看似合理但未必真实的解释) - 确保完整理解需求(AI 可能表示理解但实际上没有)

更根本的问题是:prompt 是在要求 AI”做你想做的事”,而不是”让你看到它在做什么”。

当你需要写 500 字的 prompt 来约束 AI 的行为时,你已经投入了比直接写代码更多的精力。而且,这 500 字本身又可能被 AI 误解或选择性忽略。

Gordon 的讽刺是:“This level of supervision simply doesn’t scale.” 这种监督级别根本无法规模化。

5.2 Glass Box AI:透明化的设计理念

Gordon 提出 Glass Box AI(玻璃盒 AI)作为黑盒 AI 的对立面。

黑盒 AI: - 输入 → [???] → 输出 - 你不知道里面发生了什么 - AI 默默地做了很多决定 - 你只能在结果层面审查

玻璃盒 AI: - 输入 → [展示:我的理解] → 人类确认 → [展示:我的计划] → 人类确认 → 执行 - AI 的每一步都对人类可见 - 人类保留了决策权 - AI 是执行者,不是设计者

这不是要限制 AI 的能力,而是要改变 AI 的交互模式:从”给我结果”变成”先告诉我你的计划,让我批准”。

5.3 Glass Box 的技术实现路径

实现 Glass Box AI 需要在多个层面进行技术改进:

层级一:意图展示(Intent Display)

在执行任何操作前,AI 应该展示它的理解:

用户:实现一个用户登录功能

AI 理解:
- 创建一个登录 API endpoint
- 接受 email + password
- 验证凭据
- 返回认证 token

⚠️ 我还假设:
- 你需要 JWT token(因为你之前用了 JWT)
- 你想用 bcrypt 加密密码(这是标准实践)
- 你需要 30 天过期机制(这是常见配置)

请确认以上理解是否正确:[确认] [修改] [取消]

层级二:决策理由说明(Decision Rationale)

对于 AI 认为”必要”的设计决策,需要解释理由:

我将添加:
1. Rate limiting(限流)
   理由:标准安全实践,防止暴力破解
   你同意吗?[同意] [不需要]

2. 分层缓存策略
   理由:考虑到你的 API 响应时间 > 200ms
   你同意吗?[同意] [不需要]

层级三:假设明确化(Assumption Explicit)

AI 应该主动说明它的隐性假设:

我的实现基于以下假设:
□ 项目使用 TypeScript 4.0+
□ 你需要持久化 session
□ 你已有用户数据库 schema
□ 你会配置 CORS

如果你有不同情况,请在确认前修改。

层级四:回滚友好设计(Rollback-Friendly)

当人类推翻 AI 的决定时,系统应该轻松支持:

用户:不要加缓存,这只是一个内部工具

AI:好的,移除缓存层。这是修改预览:
- 删除 cache.ts (45行)
- 删除 CacheMiddleware (12行)
- 简化 UserService (从 85行 到 42行)

确认执行吗?[确认] [查看完整 diff]

5.4 Glass Box AI 与规格驱动开发的天然契合

srs.pub 之前探讨过规格驱动开发 2(Spec-Driven Development)的概念。Glass Box AI 与这个理念高度一致:

规格驱动开发的核心是:先定义”是什么”,再决定”怎么做”。

传统开发流程:

需求 → 规格说明 → 设计 → 实现 → 测试

规格驱动开发的优势在于人类在”做什么”层面保持控制,AI 的创造力被引导到”如何实现”层面。

Glass Box AI 将这个理念延伸到人机交互层面:

  • AI 先展示它的规格理解
  • 人类确认规格是否正确
  • AI 再展示它的实现计划
  • 人类确认实现方案是否合适
  • AI 执行

这与我们在 “vibe coding? 不如分析师coding!” 中讨论的核心观点形成呼应:AI 应该服务于人类定义的问题,而不是代替人类定义问题。

5.5 当前工具的 Glass Box 实践

虽然完整的 Glass Box AI 还未普及,但一些实践已经开始出现:

GitHub Copilot Workspace 尝试在执行前展示计划,让用户确认。

Cursor 的 Agent 模式 支持先审查 AI 的修改,再决定是否应用。

Claude 的 Artifacts 在代码生成时展示完整结果,便于用户评估。

一些新兴项目 开始探索”批准-执行”模式,将 AI 的行动分解为多个可审查的步骤。

这些只是开始。要实现真正的 Glass Box AI,需要在模型层面、工具层面、工作流层面同时改进。


6 哲学思考:当”作者意图”变得模糊

AI 漂移引出了一个更深层的问题:当代码不再完全由人类编写,“作者意图”应该如何理解?

6.1 版权法与代码所有权的困境

2023-2024 年,围绕 AI 生成代码的版权问题产生了大量法律争议:

  • AI 生成的代码是否受版权保护?
  • 如果代码基于训练数据生成,是否侵犯原作者权益?
  • 当 AI 生成代码与人类代码”足够相似”时,谁是作者?

这些问题至今没有明确答案。但 AI 漂移让问题更加复杂:即使 AI 生成了代码,人类如何证明自己的”创作意图”?

6.2 软件工程契约的松动

传统软件工程建立在明确的契约关系上:

  • 需求文档:记录”要做什么”
  • 设计文档:记录”为什么要这样做”
  • 代码注释:记录”这段代码打算做什么”
  • 提交信息:记录”这次修改是为了什么”

这些文档都是人类意图的载体。当代码由 AI 生成时,这些契约开始松动:

  • 需求文档可能没有记录 AI 添加的功能
  • 设计文档可能没有反映 AI 的架构选择
  • 注释可能是 AI 自动生成的,不反映真实意图
  • 提交信息可能只说”AI refactor”,没有任何有价值的信息

代码库失去了它的”故事性”——你无法通过阅读代码来理解它的演进历史,因为演进过程的决策者变成了 AI。

6.3 “意图”归属的新框架

也许我们需要重新思考”意图”的概念:

传统观点: > 意图是单一主体的心理状态。一个决策只能有一个决策者。

AI 时代的观点: > 意图是多方协商的结果。人类设定目标,AI 提供方案,人类选择和调整,最终产品是”人机协作意图”的体现。

在这个新框架下:

  • AI 的”意图”不是真正的意图,而是对人类意图的统计推断
  • 人类的意图不再是最终的意图,而是经过 AI 转化后的执行计划
  • 最终产品是人类意图和 AI 推断的交集和并集

问题在于:谁为这个交集之外的部分负责?

当 AI 在”人类意图”的边界之外添加了功能,谁应该承担维护这些功能的成本?当 AI 在”人类意图”的边界之内曲解了需求,谁应该承担修复的责任?

6.4 工程师角色的重新定义

如果 AI 会替你做设计决策,工程师的角色会发生什么变化?

从”执行者”到”审核者”:

传统:工程师写代码,代码实现工程师的意图 AI 时代:AI 写代码,工程师审核代码是否符合意图

从”实现者”到”规格制定者”:

传统:工程师根据需求实现功能 AI 时代:工程师定义精确的规格,AI 在规格范围内实现

从”问题解决者”到”问题定义者”:

传统:工程师解决问题 AI 时代:工程师定义问题,AI 解决问题

这与我们在 “AI会写代码,但谁来定义问题?” 中探讨的主题一致:AI 时代的核心竞争力,是定义问题的能力,而不是解决问题的能力。

6.5 我们愿意放弃多少控制权?

最终,这不是一个技术问题,而是一个价值选择问题。

完全由 AI 主导的开发: - 优点:速度极快,可以快速产出”能用”的代码 - 缺点:隐性债务积累,意图模糊,长期维护困难

完全由人类主导的开发: - 优点:意图清晰,可控性强,可维护性好 - 缺点:速度慢,成本高

Glass Box AI + 规格驱动开发: - 优点:在效率和可控性之间取得平衡 - 缺点:需要更高的规格制定能力,需要新的工作流程

选择哪种方式,取决于: - 项目的生命周期(一次性项目 vs. 长期维护项目) - 团队的能力结构(规格能力强 vs. 执行能力强) - 组织的风险偏好(快速试错 vs. 稳健经营)

没有标准答案,只有清醒的认知:每一次选择,都是在放弃某些控制权。


7 结语

Jonathan Gordon 提出 Black Box AI Drift 这个概念,不是为了否定 AI 编程工具,而是为了让我们更清醒地使用它们。

AI 漂移不是 AI 的”原罪”,而是统计学习模型的固有特性。理解这一点,我们才能:

  1. 识别漂移:知道什么时候 AI 在替我做决定
  2. 评估成本:理解隐性债务的长期影响
  3. 设计流程:建立适合 AI + 人类协作的工作方式
  4. 保持控制:在效率和可控性之间找到平衡

回到 Gordon 的 Chad 比喻:你可以继续叫 AI “Chad”,让它耸耸肩说 “My bad”。但更好的方式是:在它耸肩之前,让它先告诉你它打算做什么。

这需要更好的工具,更好的流程,更重要的是——更清醒的意识。


8 相关文章

如果你对 AI 编程的实践和思考感兴趣,推荐阅读 srs.pub 的其他文章:


  1. 商业分析中的五十种分析方法和技巧之39-角色与权限矩阵. https://srs.pub/babok/juese-yu-quanxian-juzhen.html↩︎

  2. 需求规格驱动开发. https://srs.pub/thinking/spec-kit.html↩︎