AI 纪元 追踪人工智能最新进展,深度解读前沿技术

别让 Cursor 的漂亮 diff 直接进仓库

AI 生成的代码最难防的,不是语法错误,而是看似合理却破坏项目旧约束的修改。更稳妥的做法,是把版本边界、敏感目录和变更证据写进项目规则,再交给 CI 做最终拦截。

0 次阅读 约 3666 字 7 分钟 AI 生成
A clean code diff on a screen with a hidden trap symbol
A clean code diff on a screen with a hidden trap symbol

一个 Agent 重构支付回调,把 try-catch 里的 finally 当成冗余代码删了。diff 很干净,测试也通过了,但异常发生后,数据库连接没有按原逻辑释放。

这类修改比凭空生成函数名更难发现。它符合语法,甚至更符合常见的代码风格,只是不符合眼前这个项目。

生成式 AI 进入开发流程后,代码生成越来越快。可代码审查并没有因此变得轻松。Reddit 的 r/webdev 讨论中,有开发者提醒:如果长期让 LLM 包办实现,工程师很难继续培养系统设计和代码审查能力。问题不只在于人会不会审,还在于项目里的很多约束从未被正式记录。

版本限制可能藏在部署脚本里,异常处理可能源于一次旧事故,某个 null 判断也许是在兜底历史脏数据。Agent 看不到这些背景,只能按通用经验改代码。

最危险的修改,通常看起来很合理

AI 编程中的“幻觉”不一定是一眼可见的胡写。在遗留项目里,更常见的是三种过度合理的修改。

调用当前版本并不存在的接口。 Agent 按新版文档或常见写法补代码,却没有核对项目锁定的依赖版本。代码在编辑器里没有明显异常,到了特定运行环境才暴露问题。

删除防御性代码。 if (x == null) return;、重复的异常记录、显得啰嗦的资源释放逻辑,都容易被当成清理对象。但在旧系统中,这些代码往往对应真实发生过的边界问题。

调整隐含的执行顺序。 把初始化逻辑从 init() 移到构造函数,或者把 callback 改成 Promise,单看局部可能更整洁,实际执行时序却可能已经变化。

lint 很难拦住这些问题。单元测试也只能覆盖已经写进用例的行为。真正需要守住的是项目自己的边界:运行版本、敏感目录、异常处理、并发顺序、数据库迁移和公共接口。

先把项目的旧规矩写下来

规则文件不该只写“保持代码质量”或“遵循最佳实践”。这样的要求无法验证,Agent 也很难据此判断一行代码能不能改。

有效规则至少要回答四个问题:

  • 项目实际运行在哪些语言、框架和数据库版本上;
  • 哪些文件或目录不能由普通 Agent 修改;
  • 哪些代码结构不能擅自删除或替换;
  • 哪些变更必须附带 issue、测试或人工说明。

下面是一份面向遗留支付项目的规则示意。不同 Cursor 版本对规则文件格式和能力的支持可能不同,落地前应按实际版本核对;重点是规则内容,而不是照抄字段。

# 项目约束示意
project_context: |
  本项目运行在 PHP 7.4 + Laravel 6 LTS,不可使用 PHP 8.0+ 特性。
  数据库为 MySQL 5.7,不可使用窗口函数和 CTE。

protected_code:
  - path: "app/Payment/*"
    rules:
      - "不得删除 catch 或 finally 中的处理逻辑"
      - "不得修改数据库连接释放逻辑,除非附带人工说明"

  - path: "database/migrations/*"
    rules:
      - "仅允许专门的迁移任务修改"
      - "修改前必须核对已有 migration"

change_requirements:
  - "修改函数签名时必须说明调用方影响"
  - "新增依赖前必须核对当前版本和依赖白名单"
  - "调整并发原语时必须提交对应测试"

这里最重要的不是“禁止修改”四个字,而是把规则缩小到路径、行为和证据。范围越明确,误拦截越少,人工审查也越容易找到重点。

不要把规则文件当成安全边界

项目规则只能影响 Agent 如何生成代码,不能替代仓库里的强制检查。模型可能忽略规则,也可能正确理解要求却给出错误实现。

真正的拦截应放进提交前检查或 CI:

  1. 运行严格模式的 lint 和类型检查;
  2. 检查是否新增、替换了依赖;
  3. 识别函数签名、数据库迁移和公共 API 变化;
  4. 对敏感变更检查 issue 或审批记录;
  5. 执行与改动相关的测试。

下面的 cursor.json 仅用于说明钩子应覆盖哪些环节,不应被视为所有 Cursor 版本均可直接运行的官方配置。若当前版本不支持对应钩子,可以把同样的命令放进 Git hooks 或 CI。

{
  "hooks": {
    "postEdit": [
      {
        "name": "static-analysis",
        "command": "pnpm run lint:strict",
        "blockOnFailure": true
      },
      {
        "name": "type-check",
        "command": "pnpm run typecheck",
        "blockOnFailure": true
      },
      {
        "name": "dependency-audit",
        "command": "node scripts/check-deps.js",
        "blockOnFailure": true
      },
      {
        "name": "contract-validation",
        "command": "node scripts/validate-contracts.js",
        "blockOnFailure": true
      },
      {
        "name": "test",
        "command": "pnpm test",
        "blockOnFailure": true
      }
    ]
  }
}

依赖审计可以从一份很小的白名单开始:

{
  "allowAdd": ["lodash", "date-fns", "zod"],
  "forbidReplace": {
    "request": ["ky", "axios"],
    "orm": ["prisma", "drizzle-orm"]
  }
}

这份配置表达的不是“这些库永远不能用”,而是 Agent 不能顺手替换基础依赖。真要替换,应单独开任务,由人检查兼容性、调用范围和回滚方案。

敏感改动还可以要求留下引用记录:

{
  "requireIssueRefWhen": [
    "functionSignatureChange",
    "databaseMigration",
    "concurrencyPrimitiveChange",
    "publicApiDeprecation"
  ],
  "blockWithoutRef": true
}

它解决不了设计问题,但能阻止一类常见情况:Agent 改了公共接口或数据库结构,PR 里却没人解释为什么要改。

审 diff 时,盯住三类高风险变化

配置完成后,人仍然要审代码,只是注意力不必浪费在格式和基础类型错误上。

并发代码是否真的等价

Agent 很容易把 mutex 与手动 unlock 改成更现代的写法,也会把 callback 重写为 Promise。局部代码可能更简洁,但加锁顺序、释放时机和异常路径未必等价。

只要 diff 涉及锁、线程、任务队列或共享状态,就不应按普通重构处理。至少要说明原来的执行顺序,并补上能覆盖异常路径的测试。

错误处理是否丢了现场

多层 try-catch 看起来啰嗦,实际上可能分别负责记录上下文、转换错误类型和释放资源。把它们合并成一层,可能仍会抛出异常,却丢掉定位故障所需的信息。

审查时不要只问“错误有没有被捕获”,还要看日志字段、错误类型和资源释放是否保留。

数据库迁移是否被顺手补全

Agent 发现模型缺字段时,可能直接生成 migration。问题在于,它未必知道仓库中是否已有后续迁移,也不知道线上数据库的真实状态。

更稳妥的做法是锁住 database/migrations/ 等目录,把 schema 变化拆成独立任务。普通重构只修改业务代码,不同时改数据库结构。

自动化负责拦截,人负责判断

规则引擎的价值,不是证明 AI 生成的代码可靠,而是把高风险变化显式标出来。

版本边界由规则约束,语法和类型交给工具,依赖变化由脚本检查,数据库迁移和公共接口变化必须留下记录。到了人工审查阶段,reviewer 才能集中判断设计意图:为什么改、影响谁、失败后如何恢复。

AI 可以快速生成一个漂亮的 diff,但它不知道项目为什么会长成今天这样。代码审查要守住的,正是这些没有写进语法、却决定系统能否稳定运行的旧约束。

相关阅读