最近这大半年,我明显感觉到AI编码工具已经变成了团队里的标配。每天Pull Request里都有大段大段的AI生成代码,提交速度确实快了不少。但随之而来的一个很现实的问题:代码是变多了,能让人一眼看懂的部分却没跟着变多。AI生成代码的可读性,正在成为代码审查效率的最大瓶颈,也是后面维护成本里最容易爆雷的一点。
怎么说呢,AI写代码特别像一位工作效率极高但性格莽撞的新同事:他能在两分钟内把功能写完,但他不会主动考虑读代码的人怎么想。你要是不在提示词和审查环节上做约束,他就能给你造出一座由魔法数字、深层嵌套和无意义变量名组成的抽象迷宫。这篇文章我想从实际踩坑的角度,把“让AI生成代码更好读”这件事拆开聊透——先看问题出在哪,再看怎么调教AI、怎么在审查时把关,最后聊聊维护阶段怎么让可读性不滑坡。适合正在用AI编码工具的开发者,也适合需要给团队定代码规范的负责人。
1. AI生成代码的可读性问题出在哪
1.1 典型问题模式:嵌套过深、命名敷衍、逻辑绕远路
先说最常见的三类问题,基本每个用AI写代码超过两周的人都见过。
第一类是嵌套地狱。AI特别擅长用if套if再套if,每层缩进都像是在跟你玩套娃。比如让它写个数据校验逻辑,它可能生成这样的代码:
def validate_order(order): if order is not None: if order.items: if order.items[0].price > 0: if order.customer: return True return False功能上没错,但读起来像在爬楼梯——你要看到最后一个return True,得先在心里把四层if全部解开。这种代码在改动时特别容易出错,因为你根本看不清哪一层对应哪个条件。
第二类是命名敷衍。data、temp、res、list1、result2这种名字在AI代码里出现的频率高得吓人。有一次我审查一个Python脚本,里面有个变量叫data2,它其实是订单金额的累加结果。过了三天我自己去维护这段代码,完全想不起来这个data2到底是个什么东西。AI生成代码时是按概率来选词,它并不会去理解这个变量在业务里的含义,所以倾向于给一个最“通用”的名字,而这个“通用”在代码里基本等于“无用”。
第三类是逻辑绕远路。AI在生成复杂业务逻辑时,偶尔会用一个异常复杂的表达式绕一个大圈,只为了实现一个本该非常直接的功能。比如它可能为了合并两个字典,先生成一个列表再用dict.fromkeys转一遍。这种代码运行没问题,但读代码的人会不断产生“你为什么要这么写”的困惑。
1.2 为什么AI会生成这些代码:概率生成的本质
要理解这些问题,得先想清楚AI写代码的工作方式。目前主流的代码生成模型,本质是在做“下一个token最可能是什么”的预测。它见过海量的开源代码,知道在某个上下文里出现什么样的代码段最“像样”——但它并不知道这段代码将来会被谁读、会在什么场景下被改。
这就导致了一个核心矛盾:我们人类写代码时,脑子里有一张三张图——功能图(实现什么)、结构图(代码怎么组织)、读者图(谁会来读)。AI只有前面半张,它追求的是“在这个位置生成一段合理的代码”,而不是“让这段代码最大概率被同行理解”。所以AI生成的代码经常局部合理、整体松垮,变量名不承载语义,控制流不表达意图,注释也只停留在“代码在做什么”而不是“代码为什么这么做”。
明白了这一点,你就会理解为什么靠“提醒AI写好一点”是没用的。你得把可读性的要求变成明确的约束,塞进上下文里,逼着它在生成阶段就遵循人类的阅读习惯。
2. 提升AI生成代码可读性的核心原则
2.1 命名规范:让名字自己解释一切
想让AI生成的代码可读性上去,第一条要攻的就是命名。我们的目标不是“有个名字就行”,而是“任何人看到这个名字,不用看上下文就知道它是什么、用来干嘛”。
我整理了一套给AI用的命名约束,实测下来效果相当好:
- 布尔变量用
is_、has_、can_开头,比如is_available而不是available_flag; - 函数名用动词开头,比如
fetch_invoice而不是invoice_processing; - 集合类型用复数名词,比如
orders而不是order_list; - 表示耗时或计数的变量,带上单位,比如
timeout_seconds而不是timeout; - 禁止出现
data、temp、res、list1这类无意义名字。
在提示词里,我会直接把这些规则写进去,并且提供一个对的例子。比如:
请用以下命名规范生成代码: - 布尔变量使用 is_/has_/can_ 前缀 - 函数名使用动词开头 - 集合变量使用复数名词 - 禁止使用 temp、data、res、list1 等无意义命名 示例: # 错误:def process(x): data = load(); y = calc(data) # 正确:def calculate_total_amount(invoices): return sum(invoice.amount for invoice in invoices)有读者可能觉得这样写提示词有点啰嗦,但你想,AI生成一段代码只要几秒钟,你花几十秒把规范说清楚,换来的是你后面读代码、改代码时节省的几十分钟。这笔账怎么算都划算。
2.2 结构化控制流:扁平化优先,嵌套控制在两层以内
AI生成的控制流特别容易滚成一团。要让代码好读,核心原则是“扁平化”。我在团队里定了一条硬规矩:嵌套深度超过两层就要想办法拆。
最常见的扁平化手法是“卫语句”。把异常情况、边界条件提前返回,让主流程留在后面平铺直叙地走。比如前面那个订单校验的例子,优化后是这样:
def validate_order(order: Optional[Order]) -> bool: if order is None: return False if not order.items: return False if order.items[0].price <= 0: return False return order.customer is not None每个条件都是一个独立的“关卡”,一眼扫过去就知道每个条件拦的是什么。
另一个扁平化手法是“提取函数”。当一个函数里有明显可以独立成块的逻辑,就让AI把它抽成单独的函数,用函数名表达意图。这里的判断标准是:一个函数如果超过15到20行,或者你读的时候需要停下来想两次,就该拆了。
我在提示词里是这样约束AI的:
控制流要求: - 嵌套层层级不超过2层 - 所有边界条件和异常情况,使用卫语句提前返回 - 函数体超过15行时必须拆分为多个小函数 - 避免过度使用三元运算符和 lambda,除非能显著提升可读性实际经验告诉我,加了这些约束之后,AI生成的代码虽然不会从“惊艳”变成“完美”,但至少从一个“让人皱眉头的黑盒”变成了“基本能顺着读下来的普通代码”。这正是我们需要的——可读性的目标从来不追求天才般的优雅,而是追求让读代码的人少死点脑细胞。
2.3 注释策略:告诉读者为什么,而不是重复在做什么
AI生成注释有两个极端:要么完全忘写,要么写出“废话文学”级别的注释。比如:
total = 0 for item in items: total += item.price # 累加价格这种注释就是把代码翻译了一遍,一点信息量都没有。真正有效的注释是什么?是解释为什么这么写、为什么不那样写、这里有什么坑。比如:
# 这里不能直接用 sum(items.price),因为 price 在历史数据里可能是 None # 所以手动累加,并在累加时做空值跳过 total = 0 for item in items: if item.price is not None: total += item.price你看,读完这段注释,你理解了作者面对的数据约束,下次修改时就知道该注意什么。
我给AI的注释规范是这样写的:
注释规范: - 注释解释“为什么这么写”,不解释“代码在做什么” - 如果代码本身已经很清晰,不要写注释 - 在涉及业务规则、边界条件、历史包袱的地方,必须写注释说明原因 - 禁止写“这是一个XX函数”“XX变量表示XX”这类描述性废话这个约束在多数模型上是有效果的,但有时候AI还是会生成一些“代码说明书”,这时候就需要人工审查环节去把关,把废话注释直接删掉。
3. 实操:用提示词工程让AI一次生成高质量可读代码
3.1 一份可以抄作业的提示词模板
讲了这么多原则,直接给一套我目前用得比较顺手的提示词模板,你可以根据自己的场景改。
请帮我写一个[功能描述]的[编程语言]函数/模块。 要求: 1. 命名规范: - 函数名用动词开头,布尔变量使用 is_/has_/can_ 前缀 - 集合变量用复数名词,避免使用 data、temp、res 等无意义名称 2. 结构要求: - 嵌套层级不超过2层,边界条件用卫语句提前返回 - 单个函数不超过15行,超过就拆分成多个小函数 - 不要让函数有“隐性副作用”,如果需要修改外部状态请明确说明 3. 注释要求: - 注释解释“为什么”,不解释“做什么” - 涉及业务规则时,必须在注释里说明背景和约束 4. 输出要求: - 先给出完整代码,再附加 3 条阅读提示,说明这段代码的关键设计意图你可能会好奇,第4条“阅读提示”是干什么用的。这是个很妙的小技巧:让AI在生成完代码后,额外输出它自己对这段代码的理解。这一步有两个好处,第一是逼迫AI在生成时多想一步“这段代码满足什么意图”,第二是给你审查时提供了一个快速了解代码的入口。你会发现自己读代码的速度明显变快了。
3.2 用代码格式化与重构手段兜底
哪怕提示词写得再好,AI偶尔还是会交出一些结构丑陋的代码。这时候不要手写改全部,直接让格式化工具和重构手段来兜底。
团队里可以统一启用类似ESLint、pylint这类带可读性检查的Lint工具。它们至少能帮你拦住一部分明显问题,比如过长的行、未使用的变量、过深的嵌套。代码提交前强制跑一遍format,保证风格统一,这也能让AI生成的代码和人类写的代码在格式上没有割裂感。
如果AI生成了一段烂代码,我的做法不是让它“再写一遍”,而是给它具体的修改指令。比如:
代码可读性需要改进,请按以下要求修改: 1. 把嵌套的 if 改成卫语句 2. 将变量名 data 改为 order_amount 3. 把这段逻辑拆分成两个函数:parse_order 和 calculate_discount这样做的成功率远高于“请优化这段代码的可读性”。因为“可读性”这个词太抽象,模型不知道该从哪下手;但“改掉这个嵌套,改名、拆分”是具体的操作,模型执行起来非常稳定。
4. 代码审查中如何给AI生成代码把关
4.1 审查要点清单:先看可读性,再看正确性
团队里引入AI编码工具之后,我把Code Review的检查顺序调了个个。以前是”先看功能对不对”,现在是“先看代码能不能懂”。因为一个没人能读懂的代码,就算功能对了,三个月后需要改的时候也一定会坏。
下面是我给团队整理的一份AI生成代码审查清单,直接贴出来供参考:
| 检查项 | 关注点 | AI常见问题 |
|---|---|---|
| 命名质量 | 变量/函数名是否准确表达语义 | 大量使用data、temp、x1之类无意义命名 |
| 控制流结构 | 嵌套层级是否过深,是否能扁平化 | if套if套if,把简单逻辑写成多重嵌套 |
| 注释质量 | 是否解释了“为什么”而非“是什么” | 要么没注释,要么注释是代码的逐行翻译 |
| 函数粒度 | 单个函数是否过长,是否职责单一 | 一个函数干三件事,参数列表长得吓人 |
| 业务规则 | 实现是否符合领域逻辑 | 生成通用逻辑但遗漏了重要业务约束 |
| 冗余代码 | 是否存在未被调用的分支或重复逻辑 | 生成时多写了一些防御代码,导致逻辑混乱 |
每次审查,我要求至少把“命名质量”和“控制流结构”这两项过一遍再合并。其他几项如果时间紧可以拖后,但命名和控制流是“一眼就知道bad”的项目,过了这关才算有资格进主干。
4.2 自动化审查与人工审查怎么各司其职
自动化审查能解决“风格统一”和“明显缺陷”这个层面的问题,但它替代不了人类判断。Lint工具可以告诉你“这行超过了100个字符”“这个函数有10个分支”,但它无法告诉你“这个merge_data到底是在合并什么数据,这么命名会不会误导维护者”。后者的判断需要领域知识,需要理解业务上下文,需要知道这个模块将来会怎么演化。这是人该干的部分。
不过有一个AI辅助审查的小技巧可以分享:让AI自己审一遍自己生成的代码。你可以在代码生成之后,让模型站在“一个陌生工程师的视角”评价一下这段代码的可读性,并给出修改建议。比如:
请把下面的代码当作一个首次接触这个项目的人来阅读,指出你最困惑的三处地方,并解释为什么困惑: [粘贴代码]这个做法经常能揪出我一开始没注意到的命名或结构问题。因为模型不用考虑维护者的面子,它的“困惑”其实就等于普通读者读代码时的真实体验。
人工审查时,我还有一个习惯:凡是AI生成的代码,我会多问一句“这值不值得用AI来写”。有些代码是纯粹因为AI写起来快才用AI,但如果这段代码难度不高、上下文又复杂,AI往往写得不伦不类,反而应该自己动手写个干净版本。可读性最高的代码,有时候恰恰是不请AI代劳的代码。
5. 维护阶段的长期策略:让可读性不滑坡
5.1 把代码规范沉淀成团队的“AI使用手册”
维护阶段最大的挑战不是代码本身,而是“每个人用AI的方式都不一样”。有的人会在提示词里写上完整的业务上下文,生成出来的代码质量很高;有的人让AI猜业务逻辑,生成出来的代码跑通了功能却堆满了Magic Number。所以团队要想在维护阶段不崩溃,最好的办法不是靠某个人自觉,而是把AI代码生成的规范固定成一份文档。
我们团队现在有一份《AI代码生成使用手册》,里面包括:
- 提示词模板(就是我上面分享的那套,做了团队定制)
- 禁止事项列表(比如禁止生成超过30行的函数、禁止使用魔法数字)
- 代码提交前的本地检查流程(Lint + 格式化 + 自己先读一遍)
- 代码评审中针对AI生成代码的追加要求
这张文档最大的价值,是让新人也知道“用AI写代码”不是写出来就算完,而是跟正常写代码一样的标准:要能被同事读懂才叫交付。
5.2 让AI学习你项目的“领域词汇”和“架构约定”
维护阶段还有一个更高阶的做法:如果你用的是支持项目上下文或自定义指令的编程工具,可以把项目的领域词汇表和架构约定注入进去。比如你们的项目里有order、invoice、refund这些领域实体,你希望它们在代码里保持固定命名,不希望AI突然改叫purchase_record或reversal_entry。
做法也不复杂,把下面这段内容加到你的AI配置里:
本项目的领域名词与约定: - 订单实体统一命名 Order,禁止使用 Purchase 或 Record - 金额累计逻辑统一走 AmountCalculator 类,禁止到处手写累加 - 所有配置读取必须经过 ConfigService,禁止直接读取环境变量 - 错误处理统一抛 BusinessException,禁止使用裸 raise这样做有三个明显好处:第一,AI生成的代码在一个周期内和人类代码的融合度高;第二,新同学接手时可以在代码里看到统一的领域概念,而不是在order和purchase之间来回猜;第三,代码库的架构边界被AI自动遵守了,维护时不会出现“一个模块突然绕过服务层直连数据库”这种结构性腐化。
我自己实际操作下来的体会是,想让AI生成的代码变得好读,真正的重点不在“让AI写得更好”,而在“把AI能读懂和遵守的标准建好”。这套标准花不了多少时间,但它会把编码效率的红利真正变成团队资产,而不是在代码库里留下一堆只有本人和AI能看懂的烂摊子。
最后再分享一个小技巧:每次生成完代码,你自己默读一遍,凡是需要停顿或回看的行,就是可读性需要修补的地方。这不光适用于AI生成代码,也适用于所有要交给别人的代码。读起来顺畅的代码,维护起来才顺畅。