AI 辅助开发的隐性成本:Token、Review 返工与技术债的治理实践

2 阅读1分钟

AI 辅助开发的隐性成本:Token、Review 返工与技术债的治理实践

我们团队在一次 Sprint Review 里翻了车。测试同学抓出一个权限校验 bug——用户通过 group 继承的权限没有校验 action 字段,意味着只要你属于某个有权限的组,不管什么操作都能通过。提交代码的同事很冤枉:"AI 生成的啊,我看着逻辑也没问题。"

问题出在哪?

这件事让我开始算一笔账。AI 写代码的速度快了 3 倍,但 Review 这段代码花了原来 5 倍的时间。Token 花了钱,Review 多了成本,还留下了一个差点上线的权限漏洞。三件事叠在一起,才是 AI 辅助开发的真实账单。

Code Review 返工率:AI 代码的"看起来对"陷阱

人写的 bug 和 AI 写的 bug 长得不一样

人写代码犯错,通常有迹可循:变量名拼错、边界条件漏了、异步没处理好。这类 bug 在 Review 时相对容易发现,因为它们"看起来就不对"。

AI 写的 bug 不一样。它的代码语法完美、命名规范、结构清晰,但逻辑上有微妙的偏差。举个例子,AI 生成了一个 useDebounce Hook:

import { useEffect, useState } from 'react';

function useDebounce<T>(value: T, delay: number): T {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    const timer = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    return () => {
      clearTimeout(timer);
    };
  }, [value, delay]); // ← 问题在这里

  return debouncedValue;
}

代码完全符合 React 的最佳实践,ESLint 也不报警。问题在哪?它把 delay 放进了 useEffect 的依赖数组。如果 delay 是通过 props 传入的,而父组件频繁渲染导致引用变化,防抖计时器每次都会被重置——防抖就完全失效了。ESLint 甚至会提示你 delay 确实应该放进依赖数组。语法层面完全正确,语义层面才有问题。正确的做法是用 useRef 缓存 delay,把它从依赖数组中移除:

useEffect(() => {
  const timer = setTimeout(() => {
    setDebouncedValue(value);
  }, delayRef.current);
  return () => clearTimeout(timer);
}, [value]); // delay 不再作为依赖

Review 成本的量化

我统计了团队三个月的 MR 数据,对比了人工编写代码和 AI 辅助代码的 Review 效率:人工编写代码的平均 Review 时间是 22 分钟,AI 辅助代码是 34 分钟;首次通过率前者 68%,后者只有 51%;平均修改轮数前者 1.4 轮,后者 2.1 轮;每个 MR 的 Review 评论数前者 3.2 条,后者 5.8 条。AI 辅助的代码,Review 时间反而增加了 55%。这带来一个很讽刺的局面——AI 帮你省的编码时间,在 Review 环节又还回去了,还加了利息。

Review 返工的三种典型模式

过度工程化是第一种,你只需要校验一个手机号格式,一行正则就搞定。AI 给你造了一套可插拔的验证规则引擎:

type ValidationRule = {
  name: string;
  priority: number;
  validate: (value: string) => boolean;
  message: string;
};

class ValidationEngine {
  private rules: ValidationRule[] = [];
  private middlewares: Array<(ctx: ValidationContext) => void> = [];

  use(middleware: (ctx: ValidationContext) => void) {
    this.middlewares.push(middleware);
    return this;
  }

  addRule(rule: ValidationRule) {
    this.rules.push(rule);
    this.rules.sort((a, b) => a.priority - b.priority);
    return this;
  }

  validate(value: string): ValidationResult {
    const ctx = { value, errors: [], stopped: false };
    for (const mw of this.middlewares) {
      mw(ctx);
      if (ctx.stopped) break;
    }
    if (!ctx.stopped) {
      for (const rule of this.rules) {
        if (!rule.validate(value)) {
          ctx.errors.push({ rule: rule.name, message: rule.message });
        }
      }
    }
    return { valid: ctx.errors.length === 0, errors: ctx.errors };
  }
}

// 实际需求只需要这一行:
const isValidPhone = (v: string) => /^1[3-9]\d{9}$/.test(v);

ValidationEngine 类、中间件机制、优先级排序,足足八十多行。这种代码在 Review 时让人哭笑不得——功能是对的,但复杂度完全不匹配需求。

风格不一致是第二种,AI 不知道你们团队的约定。你们用 camelCase,它可能给你 snake_case;你们的错误处理统一用 Result 类型,它给你 try-catch;你们用 zustand 管理状态,它给你一套 useContext + useReducer。每次 Review 都要花时间指出这些规范问题,改来改去。

第三种最危险:看似正确但缺少边界处理。AI 生成了一个列表去重函数:

function deduplicateUsers(users: User[]): User[] {
  const seen = new Set<string>();
  return users.filter((user) => {
    if (seen.has(user.id)) {
      return false;
    }
    seen.add(user.id);
    return true;
  });
}

代码简洁优雅,但业务场景里 user.id 可能是 undefined(草稿状态的用户),所有草稿用户会被 Set 当成同一个 key,只保留第一个;两个接口返回的同 id 用户 updatedAt 不同,应该保留最新的而不是第一个;后台导出场景下数组可能有几万条,filter 会创建新数组,内存直接翻倍。这些边界条件全部来自业务上下文,AI 不可能知道。

治理策略:不是限制 AI,而是限制 AI 的使用方式

分层使用策略

我们把 AI 辅助分成了三个层级。绿区可以放心用,Review 走常规流程,包括单元测试生成、类型定义和 interface、CSS 样式代码、有明确输入输出的工具函数、README 和文档。这些场景的共同特点是:产出容易验证,出错成本低。黄区可以用但 Review 需要额外关注,包括业务组件、API 调用层、状态管理、表单验证逻辑。红区 AI 只能提供参考,不能直接提交,包括:权限与鉴权逻辑、支付和金额计算流程、数据迁移脚本、加密与安全相关模块、核心业务规则引擎(如风控策略、计费规则)、涉及用户隐私数据的处理逻辑。

落地方式很简单:MR 模板里加一个字段,要求提交者标注 AI 辅助的层级。标注了黄区或红区的,Reviewer 投入更多时间。

Prompt 规范化

我们搞了一个团队级别的 Prompt 模板库,不多,就十来个高频场景。拿 React 业务组件的模板来说,它会预设好技术栈版本(React 18 + TypeScript 5.x + Ant Design 5.x + zustand)、编码规范(函数式写法、禁止 any、使用项目内的 useRequest hook、错误处理用 Result<T, E> 类型、样式用 CSS Modules),然后留出需求描述和数据结构两个填空区域。

这个模板做了两件事:一是约束了 AI 的技术选型范围——你必须用我们项目的技术栈,不要自作主张引入别的方案;二是减少了多轮对话——一次性把上下文给够,AI 不用猜。

Review 检查清单

针对 AI 生成代码的 Review,我们额外加了一份检查清单,重点关注五个维度:类型安全(有没有 any / unknown 滥用,泛型约束够不够)、团队规范(命名、目录结构、状态管理方式是否符合团队约定)、重复检测(这段逻辑在项目里是否已经存在,是否应该复用已有模块)、边界条件(空数组、undefined、并发、大数据量有没有处理)、理解验证。

最后一条是杀手锏。我们在 Review 会上会随机挑一段 AI 辅助生成的代码,让提交者当场解释核心逻辑。说不清楚的,不是否定这个人,而是说明这段代码还不能合入——你需要先理解它再提交。

这条规则执行一个月后,变化很明显:AI 辅助代码的 Review 首次通过率从 51% 回升到 64%,因为大家知道要被抽查,提交前会自己先读一遍 AI 的产出,主动过滤掉明显不合理的部分。更重要的是,"AI 生成的我也不太懂"这种说法基本消失了——团队形成了一个共识:不管代码谁写的,提交者就是责任人。有个同事说得很好:"以前是 AI 写完直接提 MR,现在是 AI 写完我再重写 30%,但这 30% 恰好是最关键的部分。"

技术债的定期偿还机制

自动化检测

我们在 CI 里配了几条规则专门针对 AI 代码的常见问题,每周跑一次,不阻断流水线,只生成报告。核心配置如下:

# .github/workflows/ai-debt-scan.yml
name: AI Code Debt Scan
on:
  schedule:
    - cron: '0 2 * * 1' # 每周一凌晨 2 点
jobs:
  scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: 重复代码扫描(阈值降到 6 行)
        run: npx jscpd src/ --min-lines 6 --reporters json --output reports/

      - name: any 类型统计
        run: |
          echo "## any 类型使用统计" > reports/any-usage.md
          grep -rn ': any' src/ --include='*.ts' --include='*.tsx' | wc -l >> reports/any-usage.md
          grep -rn ': any' src/ --include='*.ts' --include='*.tsx' >> reports/any-usage.md

      - name: 函数复杂度检查
        run: npx eslint src/ --rule '{"complexity": ["warn", 10]}' -f json -o reports/complexity.json

      - name: 未使用导出检测
        run: npx ts-prune src/ > reports/unused-exports.txt

      - name: AI 标记文件密度统计
        run: grep -rl '@ai-assisted' src/ | awk -F/ '{print $1"/"$2"/"$3}' | sort | uniq -c | sort -rn > reports/ai-density.txt

重复代码扫描用 jscpd,阈值从默认 10 行降到 6 行——因为 AI 生成的重复代码通常比较短但很分散。any 类型统计用 grep 简单粗暴地数,趋势比绝对值重要。函数复杂度检查用 ESLint 的 complexity 规则,阈值设 10,AI 喜欢写深嵌套的复杂函数,这条规则能把它们揪出来。未使用导出检测用 ts-prune,AI 经常生成"以防万一"的导出函数,实际没人调用。

这套检测不追求精确覆盖每一个问题,而是提供趋势感知。某个模块的复杂度本周突然飙高了,很可能是新合入的 AI 代码没有经过充分重构。

给 AI 代码留下标记

一个工程化小技巧:在 AI 辅助生成的代码文件头部打上 @ai-assisted 标记,附带模型版本、生成日期、Review 人。不是为了"声讨"AI 代码,而是为了在后续重构时快速定位可能有问题的区域。

三个月后做大规模重构,先扫描所有带 @ai-assisted 标记的文件,重点检查。我们还写了个脚本统计这些标记在各目录下的分布密度。如果某个目录下 AI 辅助代码特别集中——比如 src/components/form 下有 12 个标记文件——大概率那块地方的技术债也比较高,重构优先级就往前排。

从博弈到均衡:一个可持续的 AI 辅助开发模型

说了这么多"坑",不是要否定 AI 辅助开发。我们团队现在依然重度使用 AI,但用法已经和半年前完全不同了。核心转变是从"让 AI 写代码"变成"让 AI 做脏活"。高价值且适合 AI 的场景包括:写单测(尤其是边界用例的枚举,AI 能不厌其烦地列举各种 edge case)、写类型定义和接口文档、代码翻译(JS 转 TS、Options API 转 Composition API)、正则表达式和复杂类型体操、生成 Mock 数据和 Fixture、Commit message 和 PR description。这些场景的共同特点是输出格式确定、验证成本低、业务上下文依赖弱。

低价值且不适合 AI 的场景包括:核心业务逻辑(AI 不懂你的业务上下文)、架构决策(AI 不知道团队的技术债历史)、性能优化(需要 profiling 数据,AI 只能猜)、跨模块重构(AI 看不到全局依赖关系)。这些场景要么需要深度业务理解,要么需要全局视角,恰好是当前 AI 最弱的地方。

策略调整后的数据对比很能说明问题:

指标调整前调整后变化
Token 月均花费$1100$680降 38%
Review 首次通过率51%71%升 20pp
代码重复率8.7%5.1%降 3.6pp
线上 bug 率(每千行)2.31.4降 39%
开发速度(vs 纯人工)+40%+25%降 15pp速度从 +40% 降到 +25%,纸面上看效率下降了。

治理 AI 辅助开发的隐性成本,本质上和治理任何工程问题一样:不是追求最快,而是追求可持续的快。Token 是显性成本,量化清楚就能管住;Review 返工是流程成本,靠规范、模板和检查清单控制;技术债是长期成本,靠文化(理解验证)和机制(双周审计、自动化扫描)偿还。这三者之间的平衡点每个团队不同,但找到平衡点的方法是一样的——量化、可视化、持续调整。别让 AI 帮你写代码写爽了,半年后发现自己在给 AI 还债。