☰
AI代码评审实战:当大模型开始逐行审视你的代码
2026/10/16 2:35:56 网站建设 项目流程

代码评审这门手艺,以前靠人,现在靠AI。至少在我所在的团队,这个变化来得比预想中快得多。

我的第一个合并请求被AI点名批评,是在一个周一早晨。评论机器人不慌不忙地贴上三行意见:第42行open()打开的句柄没有关闭,建议用上下文管理器;第56行的变量名a/b/c无法表达业务语义;第73行缺少对文件不存在场景的处理。我看完这几行字,往椅背上一靠,后背微微发麻。

那是我写的代码,写的时候觉得自己干净利落。AI却说,这是一个刚入行的新人也有可能写出来的东西。

后来的事情更让人没想到。这场AI代码评审实践,慢慢变成了一场让全组资深程序员集体陷入“代码羞耻”的内部运动。有人被AI指出十年前的老代码有问题,有人对着评审意见沉默了一下午,有人连续几天把旧代码翻出来让AI“找茬”——不是为了证明AI不够聪明,而是想看看自己到底还有多少老底没被揭穿。

这篇文章不打算写成教程,更像一份亲历记录:AI代码评审究竟怎么工作、它凭什么让老程序员破防、破防之后我们又得到了什么。

1. 没那么高级:把评审从“人盯人”换成“模型盯人”

1.1 为什么一个写了十年代码的人,会主动要求AI来review

先交代一下团队背景。我们组负责一个内部数据平台,代码库有几年历史了,说不上多老,但“祖传代码”的味道很重。业务迭代快、排期紧,代码评审这件事早就流于形式:合并请求发出来,同事看一眼改动范围,回复一个“LGTM”就算通过。大家都知道有问题,但谁也不想当那个在别人忙得脚不沾地时突然跳出来说“你这里设计不对”的人。

问题的真正爆发是一次线上事故。某个统计模块在大促流量下把内存打满,服务直接重启。排查下来,根因是一个长期存在的共享状态没有做并发控制,而这个文件恰恰是最近半年被多人改过、却从未被认真评审过的“无人区”。

从那天起,我们几个核心成员开始认真考虑一件事情:能不能在每个合并请求上,多放一双不知疲倦的眼睛?这双眼睛不能是同事,因为同事也会疲惫、也会碍于面子放水。那段时间正好团队内部在做大模型应用试点,有人提议:把代码评审交给AI试试。

说实话,我当时心里是有点不屑的。写代码写了这么些年,见过太多所谓“AI编程神器”只能补全几行样板代码,离真正的代码评审差远了。但既然团队决定试,我就没有反对。后来的结果说明,我低估了它。

1.2 接入方式:一个不聪明的流水线步骤,决定闭嘴听意见

接入过程其实没什么魔法。我们没有引入什么神秘的商业系统,而是自己搭了一个评估服务:把部署在内网的代码大模型封装成一个HTTP接口,让它接收合并请求的diff和上下文,输出结构化评审意见,再通过机器人回帖到合并请求的评论区。

我还记得当时画出的简化流程,大概长这样:

# 简化示意:在合并请求流水线里新增一个检查步骤 ai-review \ --diff-file "${MR_DIFF_FILE}" \ --endpoint "http://internal-code-review.internal" \ --output-format markdown

流水线跑完测试后,会多出一个环节:把这次的代码改动发给评审服务,等待回帖。整个过程多花一两分钟,换来的是每个合并请求都被从头到尾扫一遍。

决定用内网私有化部署而非外部云API,是出于代码安全的考虑。核心工程代码连出内网都不允许,更不可能裸传给外部服务。这个选择后来被证明极其关键——很多团队在引入AI评审时,第一批被叫停的项目就是因为数据出境问题。

真正让我们花心思的不是部署,而是评审维度。我们让模型围绕命名、可读性、异常处理、并发安全、重复代码、测试覆盖等十几个维度出意见,同时要求每条意见必须标注严重级别和对应的代码位置,宁缺毋滥。跑了一周测试后,效果比预期猛得多,于是正式对所有合并请求开启。

2. 上线第一周,团队发生的三件小事

2.1 它连缩进都管:AI评审的颗粒度让我们集体沉默

第一周最直观的感受是:AI的意见颗粒度细到了让人发指的程度。

举一个真实的例子。组里一位后端老哥提了一个合并请求,功能很正常,跑测试也全绿。AI在评论区贴了将近十条意见,其中一条让人哭笑不得:“第88行到第92行,缩进风格与文件其余部分不一致,建议统一为4空格。缩进不会影响运行结果,但会显著影响后续维护者的阅读效率。”

缩进这种事,平时人审根本不会提。不是大家没看到,而是觉得“提这种意见太吹毛求疵,显得自己没别的可说”。AI不管这套,它把缩进、命名、魔法数字、重复的子字符串都当成问题列了出来。

那天下午,团队群里第一次出现了一种奇特的沉默。没有人像以前一样在合并请求下面热烈讨论,因为大家都不知道该说什么。有人默默去改了代码,有人开始在本地对着自己的旧代码发呆——我自己就是把一个写了两年的函数翻出来丢给AI看,然后被它一次性挑出四个问题。

这种沉默不是抵触,更像是一种震动:原来我们习以为常的代码习惯,在一个没有情绪的评审者面前,问题全被摊开了。

2.2 从反驳到认错:团队群里开始晒“破防截图”

第二天开始,群里出现了一些刺耳的声音。同事A的意见很直接:“这个AI错报率太高了,它根本不理解我们的业务背景,很多意见都是教条式的。”

确实,AI不是万能的,后面我会专门讲到它的盲区。但让人意外的是,那个星期内,大家的态度发生了微妙的变化。起因是有人把AI的几条意见贴出来,逐条验证后,发现虽然语气犀利,但没有一条是真正“错”的——它那些看起来教条的批评,背后都对应着某种真实的可维护性风险。

最戏剧性的时刻发生在周五。那位后端老哥在群里发了一张截图,是他入职第一年写的一段老代码,被AI找到了一个并发隐患。“这代码是我自己写的,”他打了几个字,“我已经好几年没回头看过它了。”

然后,事情开始变得失控。群里一个接一个地把自己尘封多年的老模块翻出来,丢给AI做“体检”,然后把截图发到群里。有人晒出“AI说这里存在未处理的边界条件”,配文“原来我一直在这里裸奔”;有人晒出“AI建议将128行函数拆成三个方法”,配文“这是我三年前最引以为豪的‘一屏到底’”。

我给这个现象起了个名字:破防截图运动。

它靠的不是AI的智能,而是它那种不讲情面、不给人台阶下的输出方式,恰好击中了资深程序员最不愿意面对的部分——你的代码,比你以为的要脆弱得多。

3. 破防不是比喻:AI把我十年代码习惯摁在地上摩擦

3.1 我翻出一段“祖传函数”,它一次性批了四个问题

破防这件事,听别人说是一回事,亲身体会又是另一回事。我自己的经典案例是一个日志解析函数,写于多年以前,当时自我感觉极好,觉得“三行代码解决一个功能,挺优雅的”。

代码大概长这样:

# 这是我多年前的习惯写法,问题非常多,不要模仿 def parse_log(path): lines = open(path).readlines() res = [] for line in lines: parts = line.split() if len(parts) > 2: res.append({ "a": parts[0], "b": parts[1], "c": parts[2:] }) return res

AI给出的意见是四条,一条比一条扎心:

第一,open()打开的文件句柄没有关闭,也没有用with保护。在代码库里,这可能造成“句柄泄漏”,尤其在长时间运行的进程里,文件描述符会一点一点耗尽。我以前从未想过这个问题。

第二,readlines()会把整个文件一次性读进内存。如果文件很大,这行代码就是内存爆炸的引信。我当时写的日志文件都不算大,所以从来没炸过——但“没炸”不代表“没问题”。

第三,变量名a/b/c完全没有信息量。后来我回看这段代码时,也认不出b到底代表什么。在我看来这是“顺手缩写”,在维护者眼里这是无字天书。

第四,没有任何异常处理。如果文件不存在、权限不够、某一行字段数量不匹配,程序会直接崩溃。我把这称为“反正生产环境不可能出问题”,AI把这称为“未处理的IOError风险”。

这四条意见没有一条是高深的算法思想,全是基础得不能再基础的问题。一个写了十几年代码的人,被AI用最基础的知识点教育了一顿。我最难受的地方在于:它没说错。

改进后的版本大概是这样:

from pathlib import Path import logging logger = logging.getLogger(__name__) def parse_log_lines(log_path: Path) -> list[dict[str, object]]: parsed_entries = [] with log_path.open("r", encoding="utf-8") as f: for line_num, line in enumerate(f, start=1): fields = line.rstrip("\n").split("\t") if len(fields) < 3: logger.warning("跳过第 %s 行:字段数不足", line_num) continue try: parsed_entries.append({ "timestamp": fields[0], "log_level": fields[1], "message": fields[2:], }) except ValueError as exc: logger.warning("跳过第 %s 行:%s", line_num, exc) return parsed_entries

字段名变清楚了,文件流按行迭代不再一次性读入,句柄用with管理,异常和脏数据被显式处理。这段代码比原来长一倍,但任何一个接手的人都能在十秒内看懂它在干什么。这大概就是AI给我上的第一课:代码质量不是看谁写得短,而是看谁让下一个人少掉头发。

3.2 异常处理为零,AI告诉我什么叫“埋雷十年”

另一个让我印象更深刻的案例,是一个负责读取第三方配置文件并解析的服务模块。那是一个运行了将近十年的核心服务,经历过好几轮迁移,谁都不敢轻易动它。我出于好奇把它丢给AI做“体检”,结果AI在十分钟内指出了三个异常处理上的致命问题。

最典型的一段是:

try: data = load_config() do_something(data) except Exception: pass

这段代码是典型的“裸except + 吞掉异常”。无论底层发生什么,程序都不会报错,也不会留下任何日志。我当时写这段代码的理由是:“这个配置读取失败不应该影响主流程,吞掉就行。”但这个决定等于把所有的排查线索都掐断了——如果配置加载出问题,程序只会有一种状态:静默地坏,而且谁都找不到原因。

AI给的建议非常朴素但很有价值:要么让异常向外传递,由调用方决定怎么处理;要么至少记录一条明确日志,把异常类型和上下文留下来。我后来把这一段改成:

try: data = load_config() do_something(data) except FileNotFoundError as exc: logger.warning("配置文件未找到,使用默认配置: %s", exc) data = DEFAULT_CONFIG except PermissionError as exc: logger.error("配置文件无读取权限,服务停止启动: %s", exc) raise BootError() from exc

不是所有异常都要被兜底,也不是所有异常都要让系统崩溃。关键是每个异常路径都要有意为之,而不是靠“反正不会出错”糊弄过去。

看到这份体检报告以后,很长一段时间我都在反思:过去的那些年,我到底埋了多少颗雷?以前总觉得“代码能跑就行,有问题再说”,但“再说”往往意味着永远不会有人回头。AI倒是愿意回头,而且回头看一次只要几分钟。

3.3 并发共享状态被点名,那一晚我没睡好

如果说前两个案例是“被基础教育”,那第三个案例就是真正的“专业打脸”。

我们平台有个流量统计模块,职责是把每次请求的响应时间记下来,并维护一个最近100条记录的窗口。我最初的设计里,用一个列表和一个累计变量保存状态:

class TrafficStats: def __init__(self): self._buffer = [] self.total = 0 def record(self, value): self._buffer.append(value) self.total += sum(self._buffer[-100:])

写这段代码的时候,我的内心活动是:列表就是用来存数据的,直接append没什么问题,累计值存着方便随时读。

AI在评审意见上写道:“record()是多线程环境下被调用的,_buffer和total的读写没有加锁,两个线程同时执行会导致统计偏差。另外,sum(self._buffer[-100:])每次都会复制子列表并重新求和,高频请求下性能开销远高于预期。建议只维护一个累计值,并控制窗口长度,或直接用线程安全的结构。”

看到“多线程同时执行”这几个字的时候,我整个人愣了几秒。这个模块确实在线程池里被调用,而我写它的时候根本没往并发方向想过。在我的潜意识里,这个模块就是“一个简单的计数器”,完全没有意识到它跑在什么环境里。那一晚我基本没睡好,反复在想一个词:自以为是。

修改后的版本:

import threading class TrafficStats: def __init__(self): self._lock = threading.Lock() self.total = 0 self._recent = [] def record(self, value): with self._lock: self.total += value self._recent.append(value) if len(self._recent) > 100: self._recent.pop(0)

改动不大,但含义完全不同:每一次共享状态的修改都有锁保护,窗口长度被显式限制,累计值不再反复遍历整个列表。这三个问题,覆盖了并发安全、性能、可维护性的不同层面。而AI发现它们只用了一次评审。

4. 代码羞耻背后的心理底牌

4.1 代码是作品,被AI念出来就像当面念日记

为什么“代码羞耻”会成为一种集体情绪?我觉得根子在于,程序员几乎都把代码当成自己的作品。

很多老程序员写代码,不只是为了完成需求,更是在进行一种智力表达。我们会在变量命名上斟酌,会在函数结构上追求简洁,会在心里说“这一小段设计得真漂亮”。代码不只是工具,它像日记,记录了当时的思考方式、审美标准、知识边界。

当AI像批改作业一样把这段“日记”逐行念出来,说“这里字写得不好看、这句语法不通、那里逻辑跳跃太大”,听到的人很难不产生羞耻感。因为批评的不仅是代码,更是代码背后那个人的判断力。

这种羞耻感在我的团队里体现得特别具体:有人被AI提醒“函数超过200行”后,第一时间不是去重构,而是去翻代码提交记录,想找到“当初我是怎么允许自己写成这样的”。这种行为本身就是一种心理防御——我们需要知道,羞耻是从哪一刻开始埋下的。

4.2 老手的“经验直觉”被AI用穷举法打败

资深程序员最值钱的资产是什么?经验直觉。看过太多线上事故、踩过太多坑之后,老手能在代码提交的瞬间嗅出风险:这个边界条件会炸、这种写法会泄漏、这个并发场景肯定有问题。

我自己一直相信这种直觉是多年积累换来的,别人拿不走。但AI给了我一个很不舒服的对比:我靠直觉能发现的问题,它靠穷举也能发现;我靠直觉发现不了的问题,它照样可以发现。它不需要“看过”太多事故,只需要在训练数据里见过足够多被修好的坏代码,就能预测哪里可能是雷区。

这并不意味着经验贬值,而是说明经验的本质并不是玄学,而是可以被结构化、被沉淀、被复用的规则集合。过去,这些规则只存在于老程序员的脑子里,靠口口相传才能保存;现在,AI把它变成了一种可以随时调用的能力。看到这一点之后,我的羞耻感反而减轻了不少——被AI比下去的只是“规则的覆盖面”,而不是我这个人本身的价值。

4.3 没有情绪对手的公开评审,让人无处辩解

还有一个很容易被忽略的心理点:AI评审是公开的,而且你没有可以辩驳的对象。

人与人之间的代码评审,本质上是社交活动。评审者会下意识留面子,被评审者也有机会解释。你可以说“这地方我是故意写成这样的”“这块逻辑比较特殊,注释里写了原因”。就算真的被指出来问题,气氛尴尬几秒,下一句“这个改一下就好”也能轻松带过。

AI不会。它把每一条意见都贴在合并请求的公共评论区,带着精确到行的代码位置,还有一段针对性的说明。你想反驳,它的态度永远是“这是根据你代码里的逻辑推测出的风险”,它甚至不会生气。你面对的是一个没有情绪、没有社交压力、也不会给你递台阶的对手。

刚开始这会让人很不适应,后来我们反而发现这是优点:因为不存在人情,评审的重心完全回到代码本身。有的话人嘴里不好意思说出来,AI能说;有些内部潜规则导致评审走形式,AI不买账。当代码质量必须被“公开处刑”的时候,大家反而会主动把代码改得更好再提交——谁也不想在评论区丢人。

5. 破防之后,真正值钱的五条铁律

5.1 名字写清楚,胜过十条注释

AI的评审意见里,出现频率最高的永远是命名问题。a、b、data、tmp、result这些词,是代码库里最常见的路障。

我现在的标准是:如果半年后的自己回到这段代码,能不能在不用注释的情况下看懂这个变量装的是什么?如果答案是不确定,那这个变量名就不合格。timestamp比a好,log_level比b好,pending_order_ids比ids好。多敲几个字符的成本,远远低于后人查看代码时的心智消耗。

5.2 函数超过二十行,就开始拆

AI特别爱提示“此函数包含多个职责,建议拆分”。一开始我嫌它啰嗦,觉得把函数拆得七零八落还不如一个长函数来得连贯。拆过几个之后我发现,AI是对的:一个超过百行的函数,读起来像是被迫读完一整篇没有小标题的论文;拆成若干十到二十行的小函数后,每段逻辑都有名字,读者可以按需跳读。

拆函数有一个简单判断法:如果能在用一句话说清“这段代码在做什么”,它就可以是一个函数;如果说的时候要想几秒钟,说明它已经混进了不属于它的职责。

5.3 异常和边界要显式处理,别把锅甩给调用方

多年以来,我所谓的“简洁”,其实很多时候是偷懒——不写异常处理,是因为不知道会发生什么,所以假装不会发生什么。AI让我意识到,异常路径也是逻辑的一部分,而且往往是更重要的那部分。

现在的习惯是:可能为空的集合显式判断,可能抛异常的操作显式捕获并记录日志,禁止裸except吞异常。吞掉异常的代码也许能活过线上,但它牺牲的是整个系统的可观测性。一个没有日志、不会报错的模块,就像一辆没有仪表盘的汽车,你永远不知道它什么时候已经坏了。

5.4 每一次并发共享,都要能说清“凭什么安全”

AI对我们的教育最狠的一课是并发安全。人脑天然不擅长并行,我们写代码时默认“程序是顺序执行的”,这是一个危险的默认值。

现在每当我写一个类里有实例变量、函数需要修改共享集合时,都会停下来问自己一句:这个状态会被多个线程或协程同时访问吗?如果会,要么加锁、要么用原子结构,要么干脆把它改成不可变数据。给出“应该有锁吧”这种回答是不够的,我需要能指出具体机制的名字,并说明它为什么保证安全。这个要求让我后续写的代码几乎没有再出现过竞态问题。

5.5 代码是写给半年后的自己看的

这条铁律是AI教得最温柔的一条。回想那些让我破防的代码,它们在写下的那一刻都是“当时最优解”,却经不起时间的检验。因为写代码的时候,全副心思都花在“怎么让机器跑通”上,完全没想过“让半年后的人看懂”这件事。

我现在在提交代码前会多问一句:这个写法是在炫技,还是在帮读者省时间?复杂推导式虽然能三行搞定,但比不上一个有名字的中间变量直白;位运算技巧很酷,但半年后谁都读不顺。代码首先是给人读的,其次是给机器跑的——这句话我过去常挂嘴边,实际动手时经常忘,AI让我重新想了起来。

6. 别把AI当全能裁判:这些坑它确实靠不住

6.1 业务上下文:AI不知道“这个字段本来就该这么写”

AI代码评审的盲区非常明确,最大的一个就是业务上下文。

举个例子,我们平台有些字段名约定来自上游老系统,如cnt代表计数、uname代表用户名,后续对接方都以这些名为准。AI会非常诚恳地建议:“建议将uname改为user_name,以提高可读性。”如果照改,看似更规范,实际会破坏接口兼容,引发几十个下游的联动变更。

还有一次,一个订单状态字段在业务上约定“值为0时表示未知”,AI认为这是“魔法数字”,建议用枚举替代。这个建议本身没错,但它不知道整个行业生态都把0作为默认值,改动会造成外部系统无法对接。业务领域知识、跨团队约定、行业惯例,这些在代码diff里是看不见的。

6.2 团队约定和刻意为之,是误报的高发区

AI的另一类误报来自“刻意为之”。有些写法看着不优雅,其实是经过权衡之后的最佳选择。

我们有一个性能极敏感的请求链路,为了压低延迟,某段热点代码用手写循环替代了通用的流式写法。AI多次提示“这里可以改成stream().filter().map(),更符合惯例”。但团队成员做过基准测试,流式写法在这个场景会引入明显的分配开销,手写循环是综合性能测试后的结果。

遇到这种情况,我的处理方式是在注释里写明“为什么这么写”,再让AI评审时带着注释上下文。这么一来,误报率明显下降,AI的意见也更贴近实际。

6.3 过滤噪音的“三个问题”

AI评审意见不可能条条都被采纳,这也是很多团队在试用初期觉得“AI不行”的原因。跑了将近一个月之后,我找到了一套有效的意见过滤标准,每次收到AI的评审意见,问自己三个问题:

第一,这条建议是否违反团队的既有规范或明确约定?如果是,保持原样。第二,它是否改变代码行为?如果只是风格建议,可以慢点采纳;如果涉及并发安全、异常处理、资源释放,必须认真对待。第三,如果采纳这条建议,我的代码比原来好在哪?能否用一句具体的话说明?说不出来就不动。

这套过滤规则帮我们大幅降低“改来改去”的内耗。AI就像一个给建议很勤快的同事,你要学会分清哪些是真正的重要提醒、哪些只是它想显得自己很细心。

6.4 隐私和权限边界,决定了AI评审能走多远

我在前面提过,我们选择内网私有化部署模型,就是为了解决代码不出内网的硬性要求。如果你所在团队也想引入AI代码评审,我强烈建议先确认这件事,否则会出现比代码风格严重得多的问题。

除了模型部署位置,还要控制权限边界。AI评审服务的帐号应该只读代码diff,不应该拥有推送代码、修改分支或合并请求的权限;评审结果应当只对相关协作者可见,而不是任何内部人员都能随意翻看他人的提交记录。代码是资产,评审是辅助,权限越清晰,大家用起来越没有心理负担。

6.5 最终定位:第二读者,不是最终裁判

经过几个月的磨合,我们慢慢给AI定了一个位置:它是每个合并请求的第二读者,负责在正式评审前先扫一遍;真正的最终裁判还是人。

这个定位带来两个好处。第一,AI处理了那些需要耐心和细心的基础问题,比如命名、缩进、重复代码、明显的资源泄漏,人审可以把注意力放在架构设计、业务一致性和演进路径上。第二,当AI已经指出问题时,人审不需要唱红脸,沟通成本大幅下降,同事之间的关系反而比之前更融洽了。

我们现在的流程是:提交合并请求 → AI先给出评审意见 → 作者自己处理明显问题 → 再分配给同事做人工评审。AI先把“低垂的果实”摘完,人就不用在地里一遍遍弯腰了。

7. 尾声:每周五的“翻车清单”

最后分享一个现在还在坚持的小习惯,也算是对这场“代码羞耻运动”的产物。

每个周五下午,我会把那周所有合并请求里AI指出的、最有代表性的问题整理成一张“翻车清单”,发到团队群里。清单不长,三条五条就够了,每条附上代码片段、AI的意见、我们最终的改进方式,以及一句简短的点评。比如“这周有三位同学被AI抓到同一个问题:没有关闭所有异常路径,值得全组拉练一次”。

刚开始,这张清单的标题是“本周AI批评合集”,发出来大家都有点尴尬。后来慢慢变了味道,大家开始主动往清单里投稿,看到有意思的问题还会在群里讨论“这要是早几年被AI抓到,我能省多少排查时间”。它不再是一个让人羞耻的榜单,反而成了团队的代码质量“周报”。

我个人的体会是,面对AI代码评审,真正重要的不是和它较劲,而是把它当成一个说话不留面子的同事。它说出的那些让人脸红的问题,大部分是我们早就知道、却一直没正视的。代码羞耻这个感受并不坏——它证明你还在意自己写出来的东西,还在意别人看它时的感受。把这种羞耻感转化成改进的习惯,才是这场“运动”留给我们最值钱的东西。

如果你也想在你的团队里试试AI评审,我的建议是:先让它做一次非正式“体检”,跑一段历史代码,看看它挑出来的问题哪些是真问题、哪些是噪音。体验一次“被AI按在地上摩擦”,再决定要不要把它正式请进你们的开发流程。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询