news 2026/10/15 12:31:56

AI写代码,人审代码:四条清单与两次打回机制

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
AI写代码,人审代码:四条清单与两次打回机制

1. 为什么我让AI写代码,却把审查权死死攥在自己手里

让AI写代码这件事,现在几乎成了日常。Claude Code、各种Agent工具、本地模型接入VS Code,写个函数、补个测试、重构一段逻辑,几秒钟就能给你吐出一大段。但我自己踩过一个很典型的坑:有一次让AI帮我改一个权限校验模块,它给出的代码逻辑上完全正确,跑起来也没报错,但它在处理边界条件时,把一个本该拒绝的请求放行了。原因很简单——它只看了我给的上下文,没看到系统里另一处更早的拦截逻辑。

这件事之后我就定了个规矩:AI可以写,但审查权必须在我手里,而且审查要有清单、有流程、有打回机制。标题里说的"四条清单,两次打回",就是我这套流程的核心。四条清单分别覆盖功能正确性、边界与异常、安全与权限、可维护性;两次打回指的是——第一次打回是逻辑层面的问题,第二次打回是安全或权限层面的问题,两次都过不了,这段代码就不进主干。

这套流程不是拍脑袋想出来的,是我在真实项目里反复调整后沉淀下来的。它适合所有用AI辅助写代码的开发者,不管你是用Claude Code、还是本地模型接入编辑器,只要AI参与了代码生成,这套审查框架都能直接用。下面我把四条清单拆开讲,每一条都配上我实际打回过的案例和判断依据。

1.1 审查权为什么不能交给AI自己

很多人会想,既然AI能写,那让它自己审不就行了?我试过,结论是:AI自审的盲区恰好是最危险的地方。它审自己的代码时,倾向于确认"我写的逻辑是自洽的",而不是"这段代码放进整个系统里是否安全"。比如它写了一个文件删除功能,自审时会检查路径拼接对不对、异常捕获全不全,但它不会主动去想"这个路径有没有可能被外部输入污染"。

更关键的是,AI对权限和边界的理解是"声明式"的,你告诉它"这个接口需要管理员权限",它就会加上校验,但它不会去验证"这个校验能不能被绕过"。而人类审查者,尤其是写过安全相关代码的人,会本能地去想绕过路径。这就是审查权必须留在人手里的根本原因。

我现在的做法是:AI生成代码后,我先不看逻辑,先看它动了哪些文件、涉及哪些权限、有没有引入新的外部依赖。这一步叫"影响面扫描",是四条清单里安全清单的前置动作。扫描完再逐条过清单,效率比直接读代码高很多。

1.2 四条清单的定位与分工

四条清单不是并列关系,而是有优先级的。功能正确性清单排第一,因为逻辑错了后面都白搭;边界与异常清单排第二,因为大部分线上事故都出在边界;安全与权限清单排第三,但它有一票否决权;可维护性清单排第四,属于"可以打回但不阻塞"的级别。

清单核心问题打回级别典型触发场景
功能正确性逻辑是否自洽、是否符合需求第一次打回条件判断写反、循环边界错
边界与异常空值、极值、并发、超时第一次打回空数组未处理、超时未捕获
安全与权限越权、注入、敏感数据泄露第二次打回(一票否决)权限校验可绕过、日志打印密钥
可维护性命名、注释、耦合度建议修改函数过长、魔法数字

这个优先级顺序很重要。我见过有人先纠结命名规范,结果功能逻辑是错的,白白浪费一轮审查。正确的顺序是:先确认功能对,再看边界全不全,然后查安全有没有洞,最后才看代码漂不漂亮。

2. 功能正确性清单:AI最容易在"条件判断"上翻车

功能正确性清单是我用得最频繁的一条,因为AI写代码出错,十次里有六次是逻辑层面的。它不像人类程序员会犯"忘了写某个分支"这种低级错误,它犯的错更隐蔽——条件判断的语义偏移、循环边界的off-by-one、状态更新的顺序问题。

2.1 条件判断的语义偏移

AI在把自然语言需求翻译成代码时,经常会把"且"和"或"搞混,或者把"非"的作用范围弄错。我打回过一个典型案例:需求是"当用户不是管理员且不是资源所有者时,拒绝访问",AI写出来的条件是:

if not user.is_admin or not user.is_owner: deny_access()

看起来没问题?实际上这个条件在"用户是管理员但不是所有者"时也会拒绝访问,因为not user.is_admin为False,但not user.is_owner为True,整体为True。正确的写法应该是:

if not (user.is_admin or user.is_owner): deny_access()

这就是典型的德摩根定律应用错误。AI不是不懂德摩根定律,它在生成时倾向于逐词翻译,把"不是A且不是B"直接翻成not A or not B,而正确的逻辑等价是not (A or B)。我的审查习惯是:凡是看到not和and/or混用的条件,一律手动展开真值表验证。

2.2 循环边界与状态更新顺序

另一个高频翻车点是循环。AI写循环时,边界条件经常差一。比如需求是"处理列表里除最后一个元素外的所有元素",它可能写成:

for i in range(len(items)): if i == len(items) - 1: continue process(items[i])

逻辑上没错,但更简洁且不易错的写法是for item in items[:-1]。我打回这类代码的理由不是"能跑就行",而是边界表达越显式,后续修改时越不容易引入新bug。当有人后来把"除最后一个"改成"除最后两个"时,items[:-1]改成items[:-2]一目了然,而循环里的continue条件就要重新推敲。

状态更新顺序的问题更隐蔽。AI在写多步状态变更时,有时会先更新派生状态再更新源状态,导致中间态不一致。我审查时会特别关注:有没有在同一个函数里既改了A又改了依赖A的B,且顺序可能影响结果。如果有,就要求拆成两步或者加注释说明顺序依赖。

2.3 功能清单的实操检查项

我把功能正确性清单固化成了几个必查项,每次审查AI代码时逐条过:

  • 所有条件判断的真值表是否手动验证过,尤其是含not的复合条件
  • 循环边界是否用切片或显式范围表达,而非依赖continue/break跳过
  • 多步状态更新是否有顺序依赖,依赖是否被注释或测试覆盖
  • 函数返回值在所有分支上是否一致(有没有分支漏了return)
  • 需求里的"且/或/非"是否与代码里的逻辑运算符一一对应

这几项过完,功能层面的问题基本能拦住。我统计过,AI生成的代码里,功能正确性打回率大概在30%左右,其中条件判断问题占了一半以上。

3. 边界与异常清单:线上事故的重灾区

功能对了不代表能上线。我经历过一次线上故障,AI写的代码在正常输入下跑了三个月没问题,结果某天上游传了一个空列表进来,直接空指针崩溃。事后复盘,那段代码的功能逻辑完全正确,但它没处理空输入。这就是边界与异常清单存在的意义。

3.1 空值、极值与类型边界

AI对"正常路径"的处理通常很好,但对边界值的敏感度不够。我审查时会强制检查四类边界:空值(null/None/空字符串/空列表)、极值(最大最小值、零、负数)、类型边界(整数溢出、浮点精度、字符串编码)、以及"看起来正常但语义特殊"的值(比如空对象、全零ID)。

一个具体案例:AI写了一个分页函数,逻辑是offset = (page - 1) * page_size。功能正确,但当page=0时,offset变成负数,数据库查询直接报错。我打回后要求加上page = max(1, page)的防御。AI当时还"辩解"说调用方应该保证page从1开始——这就是AI的典型思维:它假设调用方守规矩。但真实系统里,调用方永远可能不守规矩。

审查AI代码时,永远假设调用方会传最离谱的参数进来。AI默认"输入是合法的",人类审查者要默认"输入是恶意的或错误的"。

3.2 异常捕获的粒度与吞异常问题

AI写异常处理有两个极端:要么不写,要么写一个巨大的try...except Exception把什么都吞掉。后者更危险,因为它会让错误静默消失。我打回过一段代码,AI在文件读取外面包了except Exception: pass,理由是"防止程序崩溃"。但这样做的后果是,文件不存在、权限不足、磁盘满,全都无声无息,上层完全不知道出了问题。

我的审查标准是:异常捕获必须明确捕获类型,且捕获后必须有日志或向上传递。except Exception: pass这种写法,见到一次打回一次。如果确实需要兜底捕获,也必须记录日志并标记为"未知错误",而不是静默忽略。

3.3 并发与超时的隐藏边界

并发问题AI很难自己发现,因为它生成的代码通常是单线程视角的。我审查时会特别关注:有没有共享状态被多个请求同时修改、有没有"先检查后执行"的竞态窗口、有没有对外部调用设置超时。

一个真实打回案例:AI写了一个"检查文件是否存在,不存在则创建"的逻辑,代码是if not os.path.exists(path): create_file(path)。单线程没问题,但两个请求同时进来,都判断不存在,都去创建,第二个就会报错或覆盖。正确做法是用open(path, 'x')这种原子操作,或者加文件锁。AI不会主动想到这一层,因为它没有被要求考虑并发。

超时也是同理。AI调用外部接口时,默认不设超时,因为它假设对方会及时响应。但真实网络里,不设超时等于把控制权交给对方。我要求所有外部调用必须有显式超时,且超时时间要写进代码而不是靠默认值。

3.4 边界清单的检查表

边界类型检查项打回示例
空值参数、返回值、集合是否可能为空空列表未判空直接取[0]
极值零、负数、最大最小值page=0导致offset为负
类型溢出、精度、编码大整数转int32截断
并发共享状态、竞态窗口检查后创建非原子
超时外部调用是否设超时HTTP请求无timeout
异常捕获类型是否明确、是否吞异常except Exception: pass

这张表我基本是背下来的,审查时逐项过,五分钟能扫完一个中等规模的改动。

4. 安全与权限清单:一票否决的那条线

安全与权限清单是我唯一设置"一票否决"的清单。功能有问题可以改,边界漏了可以补,但安全漏洞一旦上线,可能就是数据泄露或者越权访问。AI在安全方面的表现,说实话,比功能层面差很多,因为它对"攻击者视角"没有概念。

4.1 权限校验的可绕过性

AI写权限校验时,倾向于在"入口处"加一个检查,然后假设后面的代码都安全。但真实系统里,权限校验必须在"每个敏感操作前"都做,因为代码路径可能被绕过。我打回过一个典型案例:AI在API入口加了管理员校验,但内部的一个工具函数也能删除数据,且没有校验。如果这个工具函数被其他路径调用,就绕过了入口检查。

我的审查原则是:权限校验要贴着敏感操作写,而不是贴着入口写。删除数据的函数自己要有权限参数,而不是依赖调用方已经检查过。这叫"防御性权限设计",AI默认不会这么做,需要审查时强制要求。

4.2 敏感数据在日志和错误信息中的泄露

这是AI最容易犯的安全错误,没有之一。它写日志时,会把整个请求对象打印出来,里面可能包含密码、token、身份证号。它写错误信息时,会把数据库查询语句和参数一起返回给前端,泄露表结构。

我打回过一段代码,AI在登录失败时返回了"用户不存在"和"密码错误"两种不同的错误信息。功能上这很友好,但安全上这是漏洞——攻击者可以借此枚举出哪些用户名是存在的。正确做法是统一返回"用户名或密码错误"。AI不理解这种"友好即危险"的逻辑,需要审查者点出来。

审查AI代码时,搜一遍所有日志语句和错误返回,确认没有打印或返回:密码、token、密钥、身份证号、手机号、数据库语句、内部路径。

4.3 注入类问题的AI盲区

SQL注入、命令注入、路径穿越,这些经典漏洞AI理论上都"知道",但它在生成代码时,如果上下文里没有明确要求"用参数化查询",它可能会图省事用字符串拼接。我见过AI写"SELECT * FROM users WHERE name = '" + name + "'",理由是"这样更直观"。

审查时我的做法是:凡是看到SQL、shell命令、文件路径的拼接,一律打回要求改成参数化或白名单校验。不看它有没有实际漏洞,只看它有没有用危险模式。因为危险模式今天没被利用,不代表明天不会被利用。

4.4 权限清单的强制项

  • 每个敏感操作(增删改、导出、权限变更)前是否有独立校验
  • 校验逻辑是否可被绕过(内部函数、其他调用路径)
  • 日志和错误信息是否泄露敏感数据
  • SQL/命令/路径是否使用参数化或白名单
  • 敏感操作是否有审计记录(谁在什么时候做了什么)

这五项里任何一项不过,直接第二次打回,不进入下一轮。我宁可多花半小时改权限,也不愿意事后花三天处理安全事件。

5. 可维护性清单:不阻塞但必须记录

可维护性清单是四条里唯一"可以打回但不阻塞"的。意思是,如果功能、边界、安全都过了,但代码写得很难维护,我会记录问题但允许合并,同时开一个技术债任务后续处理。为什么不阻塞?因为可维护性是长期问题,而功能和安全是短期风险,优先级不同。

5.1 命名与注释的AI通病

AI写命名有两个倾向:要么过于通用(data、result、temp),要么过于冗长(userAuthenticationTokenValidationResult)。前者让读者不知道变量是什么,后者让代码行超长。我审查时会要求:变量名要能回答"这是什么",函数名要能回答"它做什么"。

注释方面,AI喜欢写"复述代码"的注释,比如# 将i加1对应i += 1。这种注释没有价值。我要求注释只写"为什么",不写"是什么"。比如# 这里用倒序遍历是因为删除元素会影响后续索引,这种注释才有意义。

5.2 函数长度与耦合度

AI生成的函数经常偏长,因为它倾向于把所有逻辑写在一个函数里。我审查时会看:这个函数能不能用一句话说清楚它做什么?如果不能,就说明它做了太多事,需要拆。拆的标准不是行数,而是"职责数量"。

耦合度方面,AI喜欢在函数内部直接访问全局变量或外部状态。我要求所有依赖通过参数传入,这样函数才是可测试的。一个需要访问三个全局变量才能运行的函数,测试起来非常痛苦。

5.3 可维护性问题的记录方式

因为不阻塞,我会把可维护性问题记在一个单独的文件里,格式是:文件路径、行号、问题描述、建议改法。每周花半小时集中处理。这样既不阻塞当前开发,又不让技术债无限累积。

问题类型记录格式处理优先级
命名不清文件:行号 - 变量x含义不明低
函数过长文件:行号 - 函数f职责过多中
魔法数字文件:行号 - 数字86400含义不明低
全局依赖文件:行号 - 依赖全局config高

6. 两次打回的实操流程与判断标准

前面讲了四条清单,现在讲"两次打回"具体怎么执行。这不是简单的"审两遍",而是两次审查的侧重点和判断标准完全不同。

6.1 第一次打回:逻辑与边界

第一次打回发生在功能正确性和边界与异常清单的审查之后。判断标准是:这段代码在正常输入和边界输入下,行为是否符合需求。如果不符合,打回,要求AI修改,修改后重新走第一次审查。

第一次打回的特点是"可修复"。大部分逻辑和边界问题,只要指出具体哪里不对,AI都能改对。我打回时会给出具体的反例输入和期望输出,而不是笼统地说"逻辑有问题"。比如"当page=0时期望返回第一页,实际返回错误",这样AI能精准定位。

第一次打回的常见原因分布,我大致统计过:

  • 条件判断语义错误:约40%
  • 边界未处理:约30%
  • 异常处理不当:约20%
  • 其他逻辑问题:约10%

6.2 第二次打回:安全与权限

第二次打回发生在安全与权限清单审查之后,且只在第一次审查通过后才进行。判断标准是:这段代码是否存在可被利用的安全风险或权限绕过。如果有,打回,且这次打回是"一票否决"——不修复不允许合并。

第二次打回的特点是"不可妥协"。安全问题上没有"差不多就行",要么修好,要么不用。我打回时会说明具体的攻击场景,比如"攻击者可以通过构造name参数执行任意SQL",让AI理解为什么必须改。

第二次打回比第一次少很多,因为大部分安全问题在第一次审查时就能顺带发现。但一旦触发,处理时间通常更长,因为安全修复往往涉及架构调整,不是改几行代码能解决的。

6.3 两次都过之后的动作

两次审查都通过后,代码可以合并,但还有两个动作:一是把这次审查中发现的问题模式记录下来,用于优化后续的AI提示词;二是如果可维护性清单有问题,开技术债任务。

记录问题模式这件事很有价值。比如我发现AI经常在"且/或"上出错,就在提示词里加一句"注意德摩根定律,复合条件请展开验证"。这样下次AI生成时,这类错误会明显减少。审查不只是拦问题,也是训练AI的过程。

7. 把清单变成习惯:我的日常审查节奏

讲了这么多清单和流程,最后说说怎么把它变成日常习惯。一开始我也觉得四条清单太繁琐,但坚持两周后就形成了条件反射,现在审查一个中等改动大概十分钟,比事后修bug快得多。

7.1 审查节奏与时间分配

我的节奏是:AI生成代码后,先花一分钟做影响面扫描(动了哪些文件、涉及哪些权限),然后按清单顺序过。功能清单三分钟,边界清单三分钟,安全清单两分钟,可维护性清单一分钟。总共十分钟左右。

这个时间分配是有依据的。功能和安全是重点,所以时间多;可维护性不阻塞,所以快速扫过。如果某个清单发现问题,时间会延长,但那是必要的投入。

7.2 常见误判与调整

审查久了会遇到误判。比如有一次我打回了一段代码,认为它有并发问题,后来发现那个函数只在单线程环境调用。这提醒我:审查时要确认代码的运行环境,不能默认所有代码都是并发的。

另一个误判是过度要求边界处理。有些内部工具函数,调用方是受控的,不需要处理所有边界。我现在的做法是:先看函数的可见性(public还是private),public的严格要求,private的适度放宽。

7.3 给不同基础读者的建议

如果你是刚接触AI辅助开发的新手,建议先从功能正确性清单开始,只查条件判断和边界。这两项能拦住大部分问题。等你熟悉了,再加安全清单。

如果你已经用AI写了很久代码,但没系统审查过,建议从安全清单入手。因为功能问题通常会在测试时暴露,但安全问题往往要到出事才发现。先把安全底线守住,再补其他清单。

如果你在团队里推广这套流程,建议先从"两次打回"的机制开始,让团队习惯"AI写的代码必须审"这件事,再逐步细化清单。机制比清单更重要,因为清单可以调整,机制是底线。

7.4 一个具体的审查实例

最后给一个完整的审查实例。AI生成了一个"根据用户ID删除评论"的函数:

def delete_comment(comment_id): comment = db.query("SELECT * FROM comments WHERE id = " + comment_id) if comment: db.execute("DELETE FROM comments WHERE id = " + comment_id) log.info(f"Deleted comment: {comment}") return True return False

我按清单过一遍:

  • 功能正确性:逻辑基本对,但没检查调用者是否有权删除
  • 边界与异常:comment_id为空或非数字时,SQL会报错;没处理数据库异常
  • 安全与权限:SQL拼接有注入风险;日志打印了完整comment对象,可能含敏感信息;没有权限校验
  • 可维护性:函数名可以,但缺少类型标注

第一次打回:要求加参数校验和异常处理。第二次打回:要求改参数化查询、加权限校验、日志只打ID不打内容。修改后:

def delete_comment(comment_id: int, operator_id: int) -> bool: if not isinstance(comment_id, int) or comment_id <= 0: raise ValueError("invalid comment_id") if not has_permission(operator_id, "delete_comment"): raise PermissionError("no permission") try: affected = db.execute( "DELETE FROM comments WHERE id = %s", (comment_id,) ) if affected: log.info(f"Deleted comment id={comment_id} by operator={operator_id}") return True return False except DatabaseError as e: log.error(f"Delete comment failed id={comment_id}: {e}") raise

这段改完,四条清单全过。整个过程花了大概十五分钟,但避免了一个注入漏洞、一个越权漏洞和一个日志泄露。这十五分钟,比事后处理安全事件划算太多。

审查AI代码这件事,说到底就是把"信任但验证"落到实处。AI是个很好的执行者,但它不是好的审查者。四条清单是我的验证框架,两次打回是我的执行机制。你可以根据自己的项目特点调整清单内容,但"AI写、人审、有清单、有打回"这个核心结构,我建议不要省。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/15 12:31:47

微服务依赖版本治理:四层防线实战体系

1. 项目概述&#xff1a;为什么微服务里的版本号会变成“定时炸弹”在某次线上故障复盘会上&#xff0c;我亲眼看着一个跨三个团队协作的微服务系统&#xff0c;因为订单服务升级了某个公共工具包到 2.3.0 版本&#xff0c;而支付服务还卡在 2.1.4&#xff0c;结果调用链里一个…

作者头像 李华
网站建设 2026/10/15 12:31:07

Flutter GraphQL在鸿蒙平台的适配与API治理实践

1. 项目背景与核心价值作为一名长期深耕移动端开发的工程师&#xff0c;我最近在将Flutter应用迁移到鸿蒙平台时遇到了GraphQL集成难题。经过两周的实战摸索&#xff0c;终于成功将angel3_graphql这个强大的Flutter GraphQL客户端库适配到鸿蒙环境。这个过程中积累的经验&#…

作者头像 李华
网站建设 2026/10/15 12:29:37

自动抢票脚本实战:从导包到接口直连的完整指南

简介&#xff1a;面向演出、赛事、展览等热门票务场景的票星球自动抢票脚本&#xff0c;是一款用Python编写的轻量自动化工具&#xff0c;帮助用户代替繁琐的手动刷新与点击&#xff0c;减少因手速或网络延迟而错失抢票机会。资源以ZIP压缩包形式提供&#xff0c;内部仅含1个.p…

作者头像 李华
网站建设 2026/10/15 12:29:10

软件定义汽车时代,企业级可信AI体系如何落地?

汽车研发圈最近几年有个高频词&#xff0c;叫“软件定义汽车”&#xff0c;听着挺宏大&#xff0c;但落到我们这些做研发、做测试的人身上&#xff0c;其实就一句话&#xff1a;车好不好&#xff0c;越来越不取决于底盘和发动机&#xff0c;而是取决于代码写得好不好、模型跑得…

作者头像 李华