Menu

代码审查提示词:如何用 AI 审查代码

好的代码审查提示词会说明要查什么,要求每个问题都标出严重程度,给出一个具体的触发输入和一个 diff,并告诉模型跳过代码风格。然后你要逐条核实,因为其中总会有错的。

下面的每条提示词都可以编辑:改好后,在 ChatGPT、Claude 或其他 AI 应用中打开。

代码审查提示词应该告诉模型要找什么、每个问题有多严重,以及要给出什么证据。“审查一下这段代码”得到的是一份友好的建议清单,一个安全漏洞夹在一条文档字符串建议和一条命名偏好之间。带有检查清单、严重程度和 diff 要求的审查提示词,得到的是一份可以逐条处理的报告。

从“审查一下”到带严重程度的检查清单

两个标签页中的代码都有两个真正的 bug。比较一下每条回复是怎么呈现它们的。

你能审查一下这段代码吗? def get_user(conn, username): cur = conn.cursor() cur.execute(f"SELECT id, email FROM users WHERE name = '{username}'") row = cur.fetchone() return {"id": row[0], "email": row[1]}
Try it
Example replyReplies vary between models and runs.

总体来说,这个函数简短、易懂。有几点建议:

  • 加上类型注解,比如 username: str,让函数签名更清楚。
  • 加一个文档字符串,说明函数返回什么。
  • 使用参数化查询。 用 f-string 拼接 SQL 可能导致 SQL 注入。
  • 处理用户不存在的情况。 fetchone() 可能返回 None。
  • 命名: fetch_user 也许能更好地表达数据库访问。

第一条回复两个 bug 都找到了,而这正是只凭提到了什么来评判审查的问题所在。注入漏洞排在五条中的第三条,被淡化成“可能导致”,没有给出修复,旁边还是一条文档字符串建议。快速浏览的读者会加上类型注解,然后就此打住。

第二个提示词改了四处:

  • 范围。“只检查 bug、安全和错误处理,跳过代码风格”去掉了噪声。代码风格交给 linter 和格式化工具,它们更快,而且从不自相矛盾。
  • 严重程度。 它迫使模型排序,而排好序的清单能告诉你从哪里开始。
  • 触发输入。“x' OR '1'='1”把一个模糊的风险变成了一个可以运行的演示。它也是过滤误报最好的工具,最后一节会讲到。
  • diff。 每个问题一个小 diff,可以单独应用或拒绝。重写后的函数会把每个修复和没人要求的改动混在一起。

“如果某个严重程度下没有问题,不要编造”这句话比看上去更重要。被要求审查时,模型即使没什么可找,也倾向于给出一些问题,而最站不住脚的那些只是在凑数。明确允许它在某个级别下报告“没有问题”,就能去掉这些凑数的内容。

审查者需要的背景

人类审查者知道代码是做什么的、输入从哪里来。模型只知道你粘贴的内容。“username 直接来自登录表单”这句话,让 SQL 注入成为一个高严重程度的问题,而不只是理论上的风险。对审查有用的背景包括:

  • 输入从哪里来: 用户、另一个内部服务,还是一个由你控制的配置文件。
  • 什么在调用这段代码,以及它期望得到什么返回值。
  • 运行环境: 语言版本、框架、数据库驱动。上面注入问题的修复用的是 ?,因为 sqlite3 用这个占位符;其他一些驱动用的是 %s。
  • 这次修改你最关心什么: 正确性、安全性、性能,还是三者都要。

一个可复用的审查提示词

这个代码块把检查清单变成了一个模板。修改语言、重点和代码,保留规则。关掉限制部分,可以快速看到没有它时会冒出多少噪声。

代码审查模板
Fill in
Parts
你是一位资深的 JavaScript 代码审查者。你只报告真正的问题,别的什么都不说。
审查下面的代码,重点关注bug 和错误处理。
这段代码在浏览器中运行。`ids` 是一个来自我们自己 API 的数字 id 数组。
async function loadAll(ids) { const results = []; ids.forEach(async (id) => { const res = await fetch(`/api/items/${id}`); results.push(await res.json()); }); return results; }
对每个问题给出:严重程度(高、中或低)、所在行、一个能触发它的输入或场景,以及一个最小的 diff。最严重的放在最前面。
不要评论代码风格、命名或格式。如果你不确定某个问题是否真实存在,就直说,并说明什么能证实它。如果什么都没找到,就说“没有发现问题”。
Try it
Example replyReplies vary between models and runs.

1. 高:函数在任何请求完成之前就返回了(第 3 行) forEach 不会等待异步回调。loadAll 会立刻返回空的 results 数组,push 操作在之后才发生。任何调用方拿到的都是 []。

2. 中:失败的请求被当作数据处理(第 5 行) 在 fetch 中,404 或 500 响应不算异常。JSON 格式的错误响应体会被解析并当作条目 push 进去,而非 JSON 的响应体会让 res.json() 在一个没人 await 的回调里抛出异常。

两个问题的修复方法如下,它还能让结果保持和 ids 相同的顺序:

 async function loadAll(ids) {
-  const results = [];
-  ids.forEach(async (id) => {
-    const res = await fetch(`/api/items/${id}`);
-    results.push(await res.json());
-  });
-  return results;
+  return Promise.all(
+    ids.map(async (id) => {
+      const res = await fetch(`/api/items/${id}`);
+      if (!res.ok) throw new Error(`Item ${id}: HTTP ${res.status}`);
+      return res.json();
+    })
+  );
 }

角色这一行设定了一个标准(“只报告真正的问题”),这正是角色提示最能发挥作用的地方。如果审查结果要由程序读取,比如一个把问题作为 pull request 评论发布的脚本,就要求返回字段固定的 JSON,而不是文字;结构化输出介绍了具体做法。

处理误报

有些问题会是错的。模型看不到校验输入的调用方,看不到让某种情况不可能发生的数据库约束,也不知道某行看起来奇怪的代码是有意为之。它还可能弄错某个库的行为;AI 幻觉解释了为什么一个自信的说法并不等于一个经过核实的说法。

用每个问题的触发输入去检验它。如果你在自己的系统中无法构造出能触发问题的输入,就补上缺失的背景来反驳,而不是直接接受修复:

质疑一个问题
你标出了这个函数,因为 `cents` 可能是 undefined 或字符串: function formatPrice(cents) { return `$${(cents / 100).toFixed(2)}`; } 它只会被 `renderCart` 调用,而 `renderCart` 在 `Number.isInteger(cents)` 不为 true 时会提前返回。这还是一个问题吗?如果不是,请撤回这个问题。
Try it
Example replyReplies vary between models and runs.

有了这个检查,就不是问题了:formatPrice 只会收到整数,所以这个问题不成立,我撤回它。

不过这个检查没有覆盖一种情况:负整数也能通过 Number.isInteger,而 formatPrice(-500) 会返回 "$-5.00"。如果退款或折扣可能传到这个函数,你也许想要的是 "-$5.00"。如果不会,就没有需要修改的地方。

一个看到新证据的模型,要么应该撤回问题,要么应该在考虑这个证据的前提下解释为什么问题仍然存在。两种都有用。一个对每次反驳都简单附和的模型并没有在审查,所以要根据回复给出的理由来评判,而不是看它是否同意了你。对于模型自己写的代码,要在一个新对话中进行审查,这样审查就不会沿着产生这段代码的同一套推理来看。范围、背景和测试这些习惯,从一开始就能让代码写得更好;见用于写代码的提示词。

常见问题

什么样的代码审查提示词比较好?

说明要审查什么(bug、安全、错误处理),要求每个问题都标出严重程度,并要求给出一个能触发问题的具体输入和一个修复它的 diff。告诉模型除非被要求,否则跳过格式和命名。给它一位人类审查者会掌握的背景:这段代码是做什么的、由什么调用它。

AI 能取代人工代码审查吗?

不能,但它是一个有用的第一道检查。它速度快,擅长发现常见的 bug 模式,比如未处理的 None、漏写的 await 和用字符串拼接的 SQL。它不了解你产品的规则、代码库的其余部分,也不知道某个决定背后的原因,而且它发现的问题中总有一些是错的。在人工审查之前用它,而不是用它代替人工审查。

为什么 AI 代码审查会报告并不存在的问题?

模型只能看到你粘贴的代码。如果某个值已经由调用方校验过,或者某种情况在你的系统中根本不可能发生,模型无从得知,所以还是会把风险标出来。要求给出一个具体的触发输入,可以过滤掉其中很多:一个找不到现实触发输入的问题,通常就是误报。

审查时应该让 AI 重写我的代码吗?

要求它给出小的 diff,每个问题一个,而不是一个重写后的文件。整体重写会把修复和没人要求的改动混在一起,你还得再审查一遍重写的代码。diff 让你可以逐个接受或拒绝每个问题的修复。

Coddy 编程语言插图

和 Coddy 一起学编程

开始