一个 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:
- 运行严格模式的 lint 和类型检查;
- 检查是否新增、替换了依赖;
- 识别函数签名、数据库迁移和公共 API 变化;
- 对敏感变更检查 issue 或审批记录;
- 执行与改动相关的测试。
下面的 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,但它不知道项目为什么会长成今天这样。代码审查要守住的,正是这些没有写进语法、却决定系统能否稳定运行的旧约束。