我做了个 AI 代码 review 工具,踩的坑能写一本书

33 阅读24分钟

先交代背景,不然后面的吐槽你看不懂。

我自己做了个 VSCode 插件,叫 ai-code-review,干的事很简单:输入一个人的 git 账号加一段时间,它把这段时间的提交全拉出来,结合公司的 PRD、接口文档、原型,帮你做代码 review。

为什么是"git 账号 + 时间段"这个输入?这是被真实需求逬出来的。我们平时常常要 review 别人的代码,但现实是:好几个人的提交全混在同一个项目、同一个分支里,你想单独看某个人这段时间改了什么,手工扭着 git log 翻半天也翻不清楚。所以我干脆把"按提交人 + 时间段精准圈出一批改动"做成了工具的第一步--不管代码是谁写的、混得多乱,输入个名字和一段时间,它就只把这个人的活挑出来给你看。

为什么不用现成的?CodeRabbit、Greptile 那些我都看过,包括现在一堆 AI review 的 skill。它们有个我受不了的毛病--只会看代码本身写得好不好,看不到公司内部的需求文档和接口。可真实工作里,代码翻车最多的地方压根不是语法,而是:字段名跟后端接口对不上、功能跟 PRD 有出入、交互跟原型不一致。这些通用工具一个都查不出来,因为它们进不来你们公司的飞书。

所以我自己撸了一个。一句话概括它跟别人的区别:别的工具审"代码写得好不好",我这个审"代码做得对不对"。

这玩意儿我断断续续做了好些天,改了一版又一版。这一路上踩的坑能写一本书,但有一个坑是绕不开的、也是这个项目最核心的一仗--切片(chunking) 。它折腾了我好几天,AI 在这上面把我坑得最惨。我先讲这段,再讲后面某一天集中爆发的四个坑。


最硬的一仗:为了不超字数,我把代码切碎了喂给 AI,结果它开始满嘴跑火车

这段是整个项目里我最想分享的,因为它几乎是所有用 AI 处理"长内容"的人都会撞上的墙。

先说为什么要切。大模型一次能读的东西是有上限的(就是所谓的上下文窗口)。一个人一周的提交,diff 动不动几千行,直接整坨喂进去,要么超长被截断、要么 AI 读到后面忘了前面,输出的 JSON 都是坏的。所以我做了个看起来很自然的决定:把大改动切成一块一块(chunk),一块一块分开喂给 AI,最后再把结果拼起来。

听起来很合理对吧?我也这么以为。然后灾难开始了。

AI 开始疯狂误报 bug。

切碎之后,每一块代码 AI 都是"孤立"看的--它看不到这块以外的东西。于是各种啼笑皆非的误报冒出来:

  • 它在 A 块里看到一个函数被调用,但函数定义在 B 块,它没看到定义,就报"这个函数没定义";
  • 它看到某个字段被用,但这个字段在别处早有了空值兜底处理,它在这一小块里看不到,就报"这里可能空指针";
  • 最离谱的是"先犯后修"那种:我在这段时间里先写错了、后来又自己改对了,但切片只截到"改之前"那一刀,AI 看到的是错的版本,于是把一个我早就修好的问题,当成现成的 bug 报给我。

这个"先犯后修"的坑特别真实,我把当时的细节贴出来:有一次 AI 报"某个参数硬编码成了 0.7,建议抽成常量"。我一查 git,傻眼了--我那天 18:17 那次提交确实写了 0.7,但 20:28 那次提交我已经把它改成命名常量了。两次提交都在 review 的时间范围里,但切片把这两次改动切到了不同的块里,报警那块只看到"写了 0.7",没看到"后来改好了",就这么报了。

说白了,切片解决了"喂不进去"的问题,却制造了"只见树木不见森林"的新问题。AI 不是变笨了,是我亲手把它的眼睛蒙上了一半。

更磨人的是,它不是一个能"一把修好"的 bug,而是一连串。我前前后后折腾了好几个版本才慢慢把它摁住,每一步都是被新的误报逼出来的:

  • 先是给每一块都附上一份"全局清单"--这次一共改了哪些文件、整体在干嘛,让 AI 至少知道自己看的是哪头大象的哪条腿;
  • 然后让块跟块之间能"互相通气":我把前面几块审出来的关键结论,作为"上下文补偿"传给后面的块,让它们别重复报、别打架;
  • 接着专门治"先犯后修"--干脆改成只看这段时间的"净改动"(你先写错后改对的,净下来就是对的),AI 看到的直接就是最终正确的样子,中间那个错的版本根本不会出现在它眼前;
  • 再后来发现 AI 老对"被删掉的旧代码"报问题--它分不清 diff 里那个减号开头的行是"已经删了的",还当成现役代码在审。这又是单独一类误报,单独写规则治;
  • 还有更隐蔽的:接口文档里一张大字段表,被我切片的逻辑从中间一刀切断了,AI 只看到半张表,于是对那些明明文档里写了的字段反复报"这字段没对齐/存疑"。后来我专门改成"字段表整张保留、绝不从中间切"。

这一长串下来,误报率才真正降到能用。但代价是:切片这个看着一行就能说清的决定,背后牵出来的复杂度,比我最初想象的大了一个量级。

如果你打算用 AI 处理任何"它一口吃不下"的长东西--长文档、大代码库、整本资料--这个教训请你一定记住:

切片不是免费的。你切掉的不只是长度,还有上下文。而 AI 一旦失去上下文,它不会跟你说"我看不全",它会自信地基于残缺的信息给你编一个结论。

后来我甚至专门为"误报"做了一整套闭环:审完之后能复查、能把"这条是误报"的结论一键存下来,下次同类问题就不再报了。这都是被切片那场仗逼出来的。

讲完这场最硬的仗,再补几个前期同样有意思的坑。


切片之外,前期还踩了这些坑(每个都不一样)

切片是最硬的一仗,但绝不是唯一。在那之前的开发里,我还接二连三踩了几个性质完全不同的坑,挑三个最有代表性的说说--它们分别关于「钱」「资料」和「错觉」。

坑一:本来是省钱的缓存,反而让我多花钱

为了省成本,我给 AI 调用上了「提示词缓存」--简单说,就是每次 review 都要带的那一大坨固定资料(PRD、接口、原型),第一次发过去缓存住,后面几十个代码块复用,就不用反复花钱重发了。理论上是「写一次、读几十次」,越读越省。

结果有天我盯着账单一看,傻眼了:缓存「写」的量是「读」的十几倍。这完全反了--本该读远大于写,怎么成了拼命写、几乎不读?

排查下来,是个特别隐蔽的细节:提示词缓存是「从头开始按前缀匹配」的,前面只要有一个字不一样,后面整段缓存就作废。而我把一段「每个代码块都不同」的检索内容,不小心放在了那坨固定资料的末尾。这下好了,每个代码块走到这儿前缀就断了,几十个块几乎各写各的缓存,谁也读不到谁的--缓存等于白建,钱照烧。

修法很简单:把那段「每块都不同」的内容从固定资料里挪出来,放到后面的可变区。固定的归固定、可变的归可变,缓存前缀就不会被打断了。

这个坑的教训是:缓存这种「优化」用错了地方,比不优化还糟--它会安安静静地多花你的钱,而且不报错。 上了缓存一定要回头看命中率,别想当然以为开了就省了。

坑二:AI 反复说「这个字段不存在」,其实是我喂给它的资料缺了一半

有一阵子 AI 老报一类问题:说代码里用的某某字段「接口文档里没有、对不上、疑似遗漏」。我一开始还真信了,去翻文档--结果文档里明明白白写着那个字段。AI 在睁眼说瞎话?

不是。问题出在「喂资料」这一步。接口文档里有一张很大的字段表,我的程序为了控制长度,会把长文档切成小段再按需挑给 AI。偏巧这张大表被从中间一刀切断了,AI 只拿到了半张表,那些被切到后半截的字段,它当然「没看到」,于是反复报「不存在」。

说白了,不是 AI 答错了,是我递给它的卷子本身就缺了页。 它基于残缺的资料,给了一个「忠于残缺资料」的错误结论。

修法是让程序「认得」字段表这种结构--遇到表格就整张保留,绝不从中间切断。

这个坑和前面切片那场仗其实是一个道理的两面:AI 的结论再离谱,先别急着怪它笨,回头看看你到底喂了它什么。 很多「AI 幻觉」,根子在「人喂错了料」。

坑三:我以为它卡死了,其实它在埋头干活

还有个不算 bug、但特别影响判断的坑。早期跑大项目时,几十个代码块并发去审,界面上就干巴巴一行「正在 review」,半天不动。我好几次以为它卡死了、崩了,差点手动掐掉重来。

后来才反应过来:它没卡,是在埋头干活。一个后端大项目切几十个块、并发调用 AI,本来就要好几分钟。问题不在「慢」,在「它没告诉我它在动」--没有进度、没有预计剩余时间,黑箱一样,人就会脑补成「死了」。

更坑的是,确实也碰到过真卡住的情况:某一次请求连接挂死,因为没设超时,它能默默拖住整批任务十分钟,干等。「假卡」和「真卡」混在一起,我根本分不清。

于是我做了两件事:一是把进度显示出来--完成几个、总共几个、跑了多久、大概还要多久,让「在动」这件事看得见;二是给每个请求加了「超时墙」,超过时间就当它挂了、自动重试,不许一个死请求拖垮一整批。

这个坑的教训偏体验,但同样重要:一个不告诉你「我还活着」的长任务,跟死了没区别。 无论是给自己用还是给别人用,长流程一定要有进度反馈--不然用户(哪怕这个用户就是你自己)会在它正常干活的时候把它掐了。

说完这些早期的坑,再讲讲后面某一天,AI 用四种全新的姿势集中坑我的事。


第一次:它去改了一个不存在的文件,然后跟我说改好了

那天上午我让 AI 帮我删掉表单上一个勾选框。这勾选框是个历史遗留,早该删了,但界面上一直还挂着。

AI 二话不说就动手,去改 extension/views/FormPanel.js

问题是--这个文件根本不存在。我项目里那个文件叫 ReviewFormPanel.js,前面多俩字。它没去确认文件到底叫啥、在不在,纯靠"感觉"猜了个路径就开干了。

要是只是改错文件也就算了,离谱的在后面。它改完那个空气文件,居然一条龙服务:把插件版本号给我升了,CHANGELOG(更新日志)也写好了,白纸黑字写着"已移除该勾选框"。

你品一下这个场面:版本号涨了,更新日志信誓旦旦说修了 bug,结果源码一行没动。要是我那会儿信了它的话直接打包发出去,就是个彻头彻尾的假修复--而且日志还会骗未来的我。

我是怎么发现的?纯属习惯。每次让它改完,我都会让它自己验证一下、把改动 grep(全局搜索)出来给我看。这一搜它就露馅了--源码里压根搜不到那段勾选框的文案,它改的那个文件根本不存在,自然啥也没动。

换句话说,它信誓旦旦跟我说「改好了、版本也升了、日志也写了」,但底层是一片空白。它没有真去确认那个文件在不在、那段代码长什么样,全程是在凭想象「演」一个修复给我看。

这个坑给我最大的提醒是:AI 对文件名、路径、函数名这种"事实性"的东西,是会一本正经瞎编的。它不会跟你说"这文件我不确定在不在",它会直接当它在。所以但凡涉及"某个东西到底存不存在"的操作,先让它去搜、去列目录确认,别让它凭印象动手。


第二次:同样的代码,Claude 跑得好好的,DeepSeek 一跑就崩

这个坑技术含量高一点,但特别能说明问题,我尽量讲明白。

我这插件可以选不同的大模型来做 review。那天我发现一个怪事:同一个 review 任务,用 GPT 和 Claude 跑都没事,一换成 DeepSeek 就崩,报错 object is not iterable--翻译成人话就是"这东西没法挨个遍历"。

更气人的是,30 个代码块明明都分析完了,AI 该输出的也都输出了,偏偏在最后"把结果汇总"那一步崩掉。前面活都干完了,临门一脚摔了。

我去刨根问底,发现是这么回事,这段是这篇里我觉得最有用的:

GPT 和 Claude 支持一个东西叫 strict schema(严格结构约束)。简单说就是你提前规定好输出长什么样,模型被强制照办,该是数组的地方就一定是数组,跑不掉。

但 DeepSeek 不支持这个。它只能用一个弱一档的模式,这模式只保证"输出是一段合法的 JSON",但不保证结构对

于是问题来了:当某个代码块确实没问题、问题列表是空的时候,DeepSeek 有时候会把这个空列表写成 {}(一个空对象),而不是 [](一个空数组)。这俩在它看来差不多,但在我代码里是天壤之别--我写的是"挨个遍历问题列表",遍历数组没问题,遍历一个对象直接就炸了。

说白了,模型偷的懒,变成了我代码里的崩溃

怎么修的?我加了道"门",专门收拾这种烂摊子:不管模型给我返回的是 {} 还是 null 还是别的什么奇形怪状,统一给它掰回成正常的数组再往下走。而且不止一个地方加,前前后后三道防线,还专门写了个测试把这个 bug 复现出来,保证以后不会再犯。

这事的教训挺朴素:别指望大模型乖乖按你的格式输出,国产模型尤其。凡是要解析 AI 输出的地方,都得先做好"它会给我返回垃圾"的心理准备,加好兜底。你的代码在 Claude 上"能跑",不代表换个模型还"能跑"。


第三次:DeepSeek Pro 不是不会审代码,是懒得审

崩溃修好之后,来了个更阴的。

我拿 DeepSeek 跑了一次真实项目的 review,23 个提交、30 个文件。结果出来--问题列表:空的。总体建议那一栏,从头到尾全是 approvedapprove,里头还有一个拼错了,写成 appoved

23 个提交、30 个文件,要说一个问题都没有,这基本不可能。这不叫"没看出问题",这叫敷衍,盖个章就交差

我不死心,把这次 review 的原始输出扒出来看。证据确凿:每个代码块它就回我一个 approve,业务对照那栏完全空着。连拼写都懒得检查的"通过",这哪是在审代码,这是在划水。

我琢磨了一下,它为啥这么懒,大概三个原因凑一块了:

一是上面说的 strict schema 它不支持,导致我要求的"必须填业务对照"从硬性规定变成了软性建议,它当然挑最省事的来;

二是 DeepSeek 这类模型(尤其便宜的档位)有股子"老好人"劲儿,倾向于默认你代码没毛病、先夸为敬;

三是这个有点黑色幽默--我 prompt 里本来写了一大段"这些情况别瞎报",是用来防止它误报的。结果 DeepSeek 理解力不够,把"宁可少报"直接理解成了"全都别报",一刀切了。

怎么治?我做了套只对国产模型生效的"紧箍咒"。一旦检测到用的是 DeepSeek、Kimi、智谱、通义、豆包这些,就自动在 prompt 里塞一段硬话:不许只写 approved、必须说清你到底检查了哪些方面、有上下文的时候业务对照不许空着。Claude 和 GPT 不加这段--人家本来就认真,不用催。

这事让我对"模型强不强"有了新认识:模型聪明,和它肯不肯好好干活,是两码事。DeepSeek Pro 的脑子真不差,但在 review 这种又要较真、又要严格按格式来的活儿上,它因为约束松、性子又软,实际表现就是差一截。所以选模型得看场景:要质量,老老实实上 Claude、GPT;要省钱跑量,国产模型也行,但你得额外给它上规矩,不然它就划水给你看。


第四次:这次怪不到 AI 头上,是我自己作的

这个坑我得老实交代,因为它是人跟 AI 搭伙干活时最容易犯的--而且犯错的是我。

那天为了图快,我让 AI 同时并行改好几处代码。心想反正它快,一起改多省事。结果接连出岔子:

改"中断功能"的时候,AI 拿它"以为"的代码长相去匹配文件,但我那文件是 Windows 的换行格式(CRLF),它按另一种格式去对,根本对不上,好几处修改悄无声息地就失败了,它还以为成了。

更刺激的是,有一次并行改的两处动了同一段代码,俩改动撞一块了,把一个函数的逻辑顺序给搅乱了。

还有一回最逗,它一边在打包,我让它并行去删旧的安装包--结果新包还没打完,旧包先被删了,扑了个空。

这些坑最后怎么都被揪出来的?还是靠那个老习惯:每次大改完,我必让它干三件事--查语法、跑测试、把改动 grep 出来给我看。就这套笨办法,把上面那些暗坑全照出来了。后来我用脚本绕开了换行格式的问题,重新打了包,又用 git 把那个没改全的提交补救回来。

这次的教训特别实在:为了快而并行操作,是出事的重灾区。尤其是多个文件互相牵连的改动,老老实实一步步来,慢就是快。还有个特别容易被忽略的小东西——换行符。Windows 项目经常是 CRLF,AI 默认按另一种来,就会出现“明明改了却没生效”的鬼现象。下次你遇到这种,先去查换行符,八成是它。


最后一公里:报告里的问题喂给 AI 去修,它一口气驳回了一半

这个坑是这篇里我最想提醒做同类工具的人的,因为它差点让整个工具失去意义。

先说我设计的闭环:工具 review 出一堆问题后,每条问题旁边有个「复制修复 Prompt」的按钮。你点一下,它把这份报告整理成一段指令复制好,你直接贴给 Cursor / Claude / Codex,让另一个 AI 对照报告去把代码改了。听起来很顺:一个 AI 挑错,另一个 AI 改错,人只管点两下。

然后现实给了我一巴掌。

我把修复 Prompt 喂给 Claude / Cursor,它干的第一件事不是改,而是一条条驳回——「这个不是问题」「这个是项目有意这么写的」「这条信息不足,无法判断」……一口气把一多半的问题给打回来了。

我当时第一反应是火大,但冷静下来一想,出了一身冷汗:如果一半的问题都被下一棒的 AI 驳回,那我这个 review 工具到底还有什么意义? 挑出来的问题没人认、改不动,那它跟一个满嘴跑火车、净报些没用东西的工具有什么区别。这已经不是「某个 bug」,这是「工具立不立得住」的问题。

我硬着头皮去刨每一条被驳回的,发现原因不止一种,而且大多数还真不能怪改代码的那个 AI

  • 有些是真误报——第一棒审的时候上下文不全,挑出来的根本不是问题,第二个 AI 一较真就驳回了。这种驳回是对的。
  • 有些是「项目有意这么设计的」——比如某个看起来该抽成常量的写法、某个看着多余其实是兼容旧逻辑的判断,团队心里有数、就是要这么留着。可第二棒的 AI 不知道这些内情,只能按通用规范判它「该改」,我(或者另一个我)又得跳出来解释「这个别动」。
  • 最致命的一类——这些问题的来龙去脉,其实项目内部的说明文档里早就写清楚了,谁改的、为什么这么改、哪些是有意保留。可我这工具一开始压根不会去读这些内部说明,第二棒的 AI 自然也看不到。于是双方都在「信息缺一半」的情况下互相拉扯:一个反复报,一个反复驳,人夹在中间反复解释。

说白了,问题的根子不在「AI 不肯改」,在「该让它知道的事,没人告诉它」。两棒 AI 之间断了片,全靠人来回传话,那工具省的事又被吐回来了。

想明白这点,我就照着「让信息流起来」这个思路,做了一整套东西,把这一公里补上:

  • 第一步,把修复 Prompt 本身做“严”。 它不再是简单一句“照着报告改”,而是逼着第二棒 AI 走固定流程:逐条先打开真实代码二次核实(明确告诉它“AI Review 也会出错,别无脑信”),再把每条分成三类——确实是问题就改、是误报就跳过并说明、信息不够就列出来等人确认。光这一步,就把“无脑乱改”和“无脑全驳”两个极端都摁住了。
  • 第二步,让“为什么不改”能被记下来、传下去。 第二棒 AI 改完之后,要求它额外写一份“复查备注”:哪条是误报、哪条是项目有意保留、哪条在别的文件里其实已经修了、哪条要等人确认,都结构化地写清楚。下次我再让工具去复查“到底修好没有”时,这份备注会被一并喂进去——这样复查的 AI 就不会再把“有意保留的设计”硬判成“你没改”,也不会把“已经在别处修好的”当成“还没修”。
  • 第三步,也是最治本的——把内部说明“沉淀”进项目上下文。 我加了个一键功能:把每次复查里确认下来的“误报 / 有意设计”,自动追加到项目里一份固定的说明文件中。下一次再 review 这个项目时,工具会先读这份文件——于是同一类误报,第二次就不再报了。报得越久,这个项目越“懂事”。

这三步下来,被驳回的比例才真正降到能接受。回头看,这一仗治的其实不是“AI 不听话”,而是“两个 AI 加一个人之间,信息怎么不丢”。

这个坑给做工具的人最大的提醒是:一个挑错的工具,如果挑出来的错没人认、改不动,它的价值就是零,甚至是负的——因为它还浪费你时间去驳。 想让“AI 挑错 + AI 改错”这条流水线真的转起来,光让两头都聪明没用,你得保证“该让它知道的事”能一路传到底。让信息流起来,比让某一个 AI 更强,重要得多。


那这一天到底干成了啥

吐槽归吐槽,AI 的产出是真实的,这点我不昧着良心。就这一天,插件实打实落地了一堆东西:

每条问题加了句"说人话"的解释,不再满屏黑话--这是同事提的,说 AI 报的问题里"竞态条件""幂等性"这种词他们看不懂,确实,普通前端谁天天念叨这个。

修了 DeepSeek 那个崩溃。给国产模型上了"紧箍咒"治划水。还给每条问题加了"自信度打分",并且逼模型在报问题之前先自己站到反方想一句"万一这不是问题,会是因为啥"--它要是反驳自己反驳得挺有道理,那这条多半是误报,就别报了。这招其实有个正经名字叫 LLM-as-a-Judge,让模型自己当自己的审稿人。

另外还加了几个顺手的:review 跑一半发现选错人选错时间,可以随时中断了,已经发出去的请求也会被取消;界面上直接显示插件版本号,省得我每次猜"我装的到底是不是新版"(被这个坑过好几回);还有说明文档支持点按钮从项目里直接选,不用再手动复制粘贴路径。


写在最后

一天干完过去差不多一周的活,这是真的。同一天被 AI 用四种不同姿势坑到,这也是真的。这俩并不矛盾。

我现在越来越觉得,用 AI 写代码,真正拉开差距的不是"你多信任它",而是"你多了解它会在哪儿翻车"。它会瞎编文件名,会被你换的模型坑,会划水盖章,会在你图快的时候跟着你一起翻。你得心里有数,得有一套接得住它犯错的习惯--对我来说就是那句最土的"改完必须自己验证一遍"。

这篇里每一个坑,都是我那天真真切切踩过、又一个个填上的。要是能帮你以后少摔几跤,就值了。

你看,连写一篇"AI 会犯错"的文章,AI 都能在里面再犯一次同样的错。所以那句话我再说一遍:它的东西,永远得你自己过一遍眼。 写了这么多,如果你也在用 AI 写代码、或者正打算做点自己的小工具,欢迎交流。

这个 review 工具是我一个人断断续续做出来的,从最早一次性把代码丢给 AI、到现在切片 + 缓存 + 误报闭环这一整套,全是被真实的坑一脚一脚踩出来的。过程里的很多取舍(比如该不该切片、国产模型怎么调教、误报怎么治)我都还在迭代。

对这个工具本身感兴趣的,也可以私信我。它现在能跑、我自己天天在用,也在考虑拿给更多人用——你可以跟我说说你的场景(什么语言、想 review 谁的代码、要不要对照需求文档),我看看合不合适、能不能发你试试。

如果这篇对你有帮助,点个赞让更多人看到就行。

有想聊的——AI 编程的坑、代码 review 的思路、或者你自己工具上的问题——评论区或私信都可以,看到都会回。