用 AI 审查自己的代码一周后,我发现最大的问题不是代码,是我自己
用 AI 审查自己的代码一周后,我发现最大的问题不是代码,是我自己
你有没有过这种经历:自己写的代码,自己 Review 三遍,上线第二天发现一个蠢到想打自己的 Bug?
我有。而且不止一次。
上个月,我在一个个人项目里犯了一个让我想消失的错误——一个分页逻辑写反了,offset 和 limit 的计算顺序搞混,结果用户点"下一页",看到的永远是第一页的内容。这个 Bug 在我本地跑了整整三天,我每次测试都用的是数据量不超过 10 条的假数据,完全没触发。
上线后第一个真实用户就发现了。
那一刻我想的不是"这个 Bug 怎么修",而是"我 Review 了三遍,为什么没发现?"
这个问题让我开始了一个为期一周的实验:在每次提 PR 之前,先让 Claude 审一遍代码,把所有反馈记录下来,看看它能发现什么我发现不了的东西。
声明一下:这不是什么工具评测,也不是广告。这是一次照镜子实验,镜子里的像有点丑。
---
一周实验数据总览
先把数字放出来,后面再逐一拆解。
| 统计项 | 数据 | | 实验周期 | 7 天 | | 提交 PR 次数 | 14 次 | | Claude 提出意见总数 | 87 条 | | 我最终采纳的意见 | 61 条(采纳率 70%) | | 我自己发现的问题 | 23 条 | | AI 发现、我自己没发现的 | 48 条 | | 被我忽略后真的出了问题的 | 3 条 |那 3 条被我忽略后真的出了问题的意见,是这次实验里最让我后怕的部分。
错误类型分布:- 命名误导 / 注释漂移:32%
- 错误处理不完整:28%
- 边界条件未覆盖:22%
- 性能隐患:11%
- 其他(代码风格、冗余逻辑):7%
这个分布本身就是结论的一部分。我以为 AI 会最擅长找性能问题或者逻辑漏洞,结果排在第一位的,是"命名和注释"——这恰恰是我最不重视的地方。
---
发现一:我的注释在骗我自己
实验第一天,Claude 给了我一条反馈,我当时觉得有点莫名其妙:
[misleading name] FunctiongetUserDataperforms cache writes and logs request metadata as side effects. The name implies a pure read operation. Consider renaming tofetchAndCacheUserDataor extracting side effects into a separate call.
我去看了一眼那个函数,确实,函数名叫 getUserData,但里面干了三件事:
1. 从数据库读取用户数据
2. 把结果写入 Redis 缓存
3. 往日志系统写一条请求记录
问题代码:// 获取用户数据
async function getUserData(userId) {
const data = await db.users.findById(userId);
// 写缓存
await redis.set(user:${userId}, JSON.stringify(data), 'EX', 3600);
// 记录访问日志
logger.info({ event: 'user_data_accessed', userId, timestamp: Date.now() });
return data;
}
修改后:
// 读取并缓存用户数据(含访问日志记录)
async function fetchAndCacheUserData(userId) {
const data = await db.users.findById(userId);
await redis.set(user:${userId}, JSON.stringify(data), 'EX', 3600);
logger.info({ event: 'user_data_accessed', userId, timestamp: Date.now() });
return data;
}
// 纯读取,不产生副作用(优先读缓存)
async function getUserData(userId) {
const cached = await redis.get(user:${userId});
if (cached) return JSON.parse(cached);
return fetchAndCacheUserData(userId);
}
我自己看这段代码,压根没觉得有问题。因为我知道这个函数在做什么——我写的时候就是这么设计的,"顺手"把缓存和日志放进去了,很自然。
但 Claude 不知道"我当时是怎么想的",它只能读代码本身。函数名说的是"获取",代码做的是"获取+写入+记录",这就是矛盾。
这个现象在一周里反复出现,占到所有问题的近三分之一。长期独自开发,会让人对自己的"命名谎言"完全免疫——因为你永远知道自己想表达什么,所以永远看不见自己实际表达了什么。
核心洞察: 注释和命名的腐化是渐进的。你每次改动都"大概知道"这个函数是干什么的,所以不会更新命名;但六个月后,你的队友(或者六个月后的你自己)看到的就是一个会撒谎的函数名。
---
发现二:我有一套重复的"懒人模式"
实验到第三天,我开始注意到一个规律——Claude 的某一类反馈开始重复出现:
[incomplete error handling] Error is caught but only logged to console. In production, this may silently fail without alerting upstream callers. Consider re-throwing or returning a structured error response.
第一次看到这条,我觉得"有道理,改"。
第二次看到,我觉得"嗯,这个场景不太一样,但也改吧"。
第三次看到,我坐在那里想了很久。
这不是三个不同的问题,这是同一个习惯。// 我的典型写法(问题版)
async function sendNotification(userId, message) {
try {
await emailService.send(userId, message);
} catch (err) {
console.log('发送失败:', err); // 然后就没有然后了
}
}
// 修改后
async function sendNotification(userId, message) {
try {
await emailService.send(userId, message);
} catch (err) {
logger.error({ event: 'notification_failed', userId, error: err.message });
throw new NotificationError(Failed to send notification to user ${userId}, { cause: err });
}
}
我用 console.log 吃掉错误,已经是一个根深蒂固的习惯了。在本地开发阶段,这没什么问题——打印出来,我能看到。但在生产环境,这意味着一个功能静默失败了,没有报警,没有重试,没有任何上游感知。
这个发现让我有点不舒服。因为这意味着我的问题不是偶发性的疏忽,而是系统性的认知盲区。
---
发现三:有些"能跑"的代码在等待时机崩溃
这是一周里最让我后怕的部分。
实验第三天,Claude 标记了一处边界条件:
[unhandled edge case]items.slice(page size, (page + 1) size)will return an empty array whenpageis negative or whenitemsis undefined. If called with invalid input, this silently returns[]rather than throwing, which may mask upstream bugs.
我当时的反应是:"这个参数是从前端传过来的,前端不可能传负数,这个 case 不会发生。"
然后我把这条意见标记为"不采纳",继续往下。
第五天,我在本地跑集成测试,有一个测试用例的 mock 数据初始化顺序写错了,items 在某个时序下是 undefined。
程序没有报错。
它静默地返回了空数组,然后下游的渲染逻辑以为"数据加载完了,就是没有数据",展示了一个空列表。
整个链路,没有一个地方抛出异常。如果不是我碰巧盯着那个测试看了一眼,这个问题可能就这样混过去了。
// 问题代码("能跑"的版本)
function paginateItems(items, page, size) {
return items.slice(page size, (page + 1) size);
}
// 修改后(防御性版本)
function paginateItems(items, page, size) {
if (!Array.isArray(items)) {
throw new TypeError(paginateItems: expected array, got ${typeof items});
}
if (page < 0 || size <= 0) {
throw new RangeError(paginateItems: invalid pagination params (page=${page}, size=${size}));
}
return items.slice(page size, (page + 1) size);
}
这个案例揭示了自审最危险的心理机制:为自己的假设辩护。
当我写 items.slice(...) 的时候,我的脑子里有一个隐含的前提:"items 一定是数组,page 一定是非负整数。"这个前提让我觉得边界检查是多余的。
但 Claude 没有这个前提。它只看代码,代码里 items 可以是任何东西,所以它标记了。
自审时,人会下意识地用"我当时是这么想的"来为自己的代码辩护。AI 没有"我当时是这么想的"这个包袱。
---
重新理解"代码自审"这件事
三个发现,表面上是三类不同的问题,但背后是同一个根本原因:
自审的最大障碍是"作者视角"。你永远知道自己想做什么,所以你永远看不见自己没做到什么。
这不是能力问题,这是认知结构问题。你写代码的时候建立了一套心智模型,你 Review 代码的时候用的还是同一套心智模型。你看到的是"我想表达的意思",而不是"代码实际表达的意思"。
AI 没有这套心智模型。它只能读代码本身。这恰恰是它的优势所在。
一套可落地的 AI 辅助 PR 自审 SOP
┌─────────────────────────────────────────────┐
│ AI 辅助 PR 自审流程 │
└─────────────────────────────────────────────┘
1. 写完代码,准备提 PR
│
▼
2. 生成 diff(git diff main...HEAD)
│
▼
3. 把 diff 丢给 Claude,附上 Prompt:
"请以资深工程师视角 Review 以下代码变更,
重点关注:命名一致性、错误处理完整性、
边界条件覆盖、潜在副作用。
请用中文输出,每条意见注明严重程度。"
│
▼
4. 人工过一遍 Claude 的意见
- 标记"采纳" / "不采纳" / "待讨论"
- 对"不采纳"的条目,写下你的理由
│
▼
5. 修改代码,提 PR
│
▼
6. 每周回顾一次"不采纳"列表
(看看有没有打脸的时刻)
关键原则: 不采纳 AI 的意见完全正常,但你必须能说出理由。如果你说不出理由,只是"感觉不需要",那大概率是你的作者视角在作祟。
这套流程的接入成本比你想象的低。我用的是 [api.884819.xyz](https://api.884819.xyz) 做的 API 中转,直接调 Claude Sonnet 4.6 做 Review,把代码 diff 丢进去就能用,不需要额外配置专门的 Code Review 工具,个人开发者用量按量计费,一周下来花了不到 5 块钱。
注册即送 50 万 token,想要更多可以通过工单联系客服申请,再手动赠送 200 万 token——对于跑这种实验来说,完全够用。
---
最后想说的
这一周实验结束之后,我对"代码自审"这件事的理解变了。
以前我觉得自审是"再看一遍,确认没问题"。现在我觉得自审是"用一个不了解你意图的视角,重新读一遍你的代码"。
这两件事,不是同一件事。
工具不是替代你的判断,而是给你提供一个你自己给不了自己的视角。就像你永远无法给自己理发一样——不是技术问题,是角度问题。
---
对了,这次实验还有一个意外收获。
>
我让 Claude 审完代码之后,顺手问了它一句:"如果你是攻击者,你会从哪里入手?"
>
它的回答让我当场关掉了一个我以为没有暴露的端口。
>
下一篇,我们聊聊用 AI 做「攻击视角代码审查」这件事——同一套工具,换一个提问角度,你会看到完全不同的东西。
---
本文由8848AI原创,转载请注明出处。关注8848AI,带你从零开始学AI。#AI编程 #CodeReview #Claude #代码审查 #8848AI #开发者工具 #AI实战 #编程技巧