如何给LLM代码评审打分:从缺陷检出率到幻觉控制 📅 发布时间:2026/9/4 8:12:13 👁 浏览次数: 你有没有遇到过这样一类“AI 代码评审”模型对着一段埋了雷的代码输出一整页正面评价最后结论是“LGTM可以直接合并”而代码里刚好有一个在产品并发环境下必然触发的缺陷。标题 The Review That Praised the Bug: grading three LLM code reviews against the code 说的就是这种现象——评审不仅没有发现 Bug反而把带 Bug 的实现当成良好设计表扬了一遍。本文不是想讨论“哪个大模型更强”而是想把“如何给 LLM 代码评审结果打分”这件事讲透。我们会准备一份带有种子缺陷的 Java 代码用三份风格完全不同的 LLM 评审结果做横向对比然后按同一套评分规则给它们打分。读完你会得到一套可复用的评审质量评估维度、一个最小可运行的打分脚本以及把 LLM 代码评审真正接入工程链路时需要注意的坑。1. 背景与问题定义1.1 什么是 LLM 代码评审代码评审Code Review是软件开发流程中用来保证代码质量的关键环节。传统做法是由一名或多名有经验的工程师在代码合并Merge / PR之前对变更进行检查主要关注功能正确性、代码风格、并发安全、资源释放、边界条件、潜在安全漏洞等。LLM 代码评审则是把“读代码、找问题、写意见”这件事交给大语言模型来完成。常见做法是把本次代码变更git diff 或完整文件作为文本输入在提示词中描述项目背景和本次变更意图让模型输出评审意见包括问题位置、原因、建议修改方式工程师根据模型意见进行二次确认后处理。相比传统人工评审LLM 评审的优势是响应快、覆盖面广、能同时从多个角度提意见缺点是会一本正经地生成错误结论也就是通常说的“幻觉Hallucination”。在实际项目中如果工程师对模型输出缺乏判断力这类错误结论会造成两种后果一是把真正的问题漏掉二是把不存在的“问题”当成真问题去改。1.2 “表扬 Bug”的评审到底差在哪里标题中的 “praised the Bug”准确说是“评审对带有 Bug 的代码给出了正面评价”。这种评审最大的危害不是“没说话”而是“说了错误的安全结论”。举个例子如果一段代码使用了线程不安全的SimpleDateFormat正确的评审意见应该指出“并发调用时可能产生错乱必须替换成DateTimeFormatter或ThreadLocal”。而一份“表扬 Bug”的评审会怎么说它可能会说“这里复用了SimpleDateFormat避免了重复创建格式化对象的性能开销是一种不错的优化。”这段话单看前半句并没有错复用对象确实能减少创建开销。问题在于SimpleDateFormat内部保存了可变状态不是线程安全的在多线程共用一个实例时可能出现日期错乱甚至抛出异常。评审只看到了“复用”的收益却没有评估“共享可变对象”的风险最终得出一个让代码带着隐患上线的结论。LLM 评审的另一个典型问题是“结论过于正面”。很多模型在缺少明确约束时倾向于输出礼貌、肯定、建议性的内容而不是直接说“这段代码不能合并”。当模型输出大量表扬性文字时工程师很容易放松警惕以为代码没有问题。1.3 为什么需要给“评审结果”打分我们平时会给代码质量打分给测试覆盖率打分却很少给代码评审本身打分。在引入 LLM 代码评审之后“评审质量”这件事变得非常重要因为模型输出不稳定不同提示词、不同模型、不同运行次数下结果差异都很大。给 LLM 评审打分本质上是在回答几个问题它发现了多少真实缺陷—— 缺陷检出率Recall它发现的“缺陷”里有多少是真的—— 精确率Precision它给出的修改建议能不能直接执行—— 可执行性它给合入结论时是否负责任—— 风险决策质量。只有用同一套标准去评估多次评审结果团队才能判断当前引入的模型和提示词方案是否可靠。2. 实验设计三个评审 vs 一份代码“给 LLM 代码评审打分”这个命题需要一个可控的评估环境。否则你无法判断一条评审意见是否准确也无法量化模型的表现。2.1 三层评估结构整个评估可以拆成三层第一层被测代码 包含已知种子缺陷Ground Truth ↓ 第二层LLM 评审输出 三份或多份评审结果 ↓ 第三层评审打分 用固定评分维度给每份评审结果打分很多团队只做到了前两层把代码丢给模型模型给出了意见然后凭感觉判断“这个模型行不行”。如果希望结论可复现、可对比第三层才是关键。没有地面真值Ground Truth和固定评分标准“模型表现不错”就只是一句无法验证的主观感受。三层结构中有一个容易被忽略的点被测代码的种子缺陷清单必须在评审之前冻结。评审前先确定“哪些问题算 Bug”评审后严格按照这份清单判断命中情况避免根据模型输出临时调整标准。2.2 打分维度与权重设计本文使用的评分表包含 5 个维度按重要程度加权满分 100 分。评分维度权重考察重点缺陷检出Recall40%是否识别出种子缺陷 BUG-01、BUG-02并给出准确位置评审准确率Precision20%是否存在幻觉问题即把不存在的缺陷当作真问题提出修复建议可执行性20%建议是否落到具体代码是否可复制、可验证风险结论正确性10%最终结论是“肯定放行”还是“修复后合并”是否匹配实际风险边界覆盖度10%是否覆盖并发、资源、空值、时间等边界上下文缺陷检出权重最高因为代码评审的第一使命就是找出能导致线上事故的问题。评审准确率同样重要它可以避免工程师被一堆不存在的“问题”带偏。2.3 保证评审过程可复现对 LLM 评审做评估时需要固定几个参数固定模型版本和接口参数建议将温度设为 0固定系统提示词和用户提示词避免每次提问方式不一致同一问题至少运行 3 次观察结果稳定性评审时只给模型代码本身和必要的业务契约不要事先告诉它“这里埋了 Bug”。如果模型运行多次结果差异很大说明评估得分只能代表某一次输出不能代表模型稳定水平。这也是为什么很多规范做法会把同一份代码跑三遍然后观察检出率的浮动区间。3. 被测代码与种子缺陷为了让读者能亲手复现下面准备一份完整的 Java 示例代码。业务背景是用户积分服务代码里包含两个高风险并发缺陷。3.1 示例业务背景假设有一个会员积分系统RewardService被多个 HTTP 请求线程并发调用需要满足两个业务规则多个线程可以同时对同一个用户累加积分累加不能丢失积分明细需要把奖励时间格式化成字符串进行展示多线程并发调用格式化方法时结果必须正确。代码文件如下。// 文件路径src/main/java/com/example/reward/RewardService.java package com.example.reward; import java.text.SimpleDateFormat; import java.util.Date; import java.util.HashMap; import java.util.Map; /** * 用户积分服务。 * 注意该类会被多个 HTTP 请求线程同时使用。 */ public class RewardService { private static final SimpleDateFormat DATE_FORMAT new SimpleDateFormat(yyyy-MM-dd HH:mm:ss); private final MapString, Integer pointsMap new HashMap(); /** * 给指定用户增加积分。 */ public void addPoints(String userId, int points) throws InterruptedException { if (points 0) { throw new IllegalArgumentException(points must be 0); } Integer oldPoints pointsMap.get(userId); int newPoints (oldPoints null ? 0 : oldPoints) points; // 模拟累计过程中的耗时方便观察并发问题 Thread.sleep(10); pointsMap.put(userId, newPoints); } /** * 查询用户当前积分。 */ public int getPoints(String userId) { return pointsMap.getOrDefault(userId, 0); } /** * 把奖励时间格式化为字符串并返回。 */ public String formatDate(Date date) { return DATE_FORMAT.format(date); } }这段代码表面上结构清晰方法命名也没有问题。但结合“多线程并发使用”这个前提代码里有两个明确的种子缺陷。3.2 种子缺陷清单在评审开始之前先冻结缺陷基线。编号位置缺陷类型触发场景修复思路BUG-01DATE_FORMAT字段 /formatDate()共享可变对象 线程不安全多个线程同时调用formatDate解析和格式化日期改用线程安全的DateTimeFormatter或对SimpleDateFormat使用ThreadLocalBUG-02addPoints()先读后写非原子操作 HashMap线程不安全产生丢失更新同一用户积分被并发累加时只有最后一次写入生效使用ConcurrentHashMap.compute()原子累加或加锁NOTE-01Thread.sleep(10)代码异味非功能缺陷无这是为了演示并发问题而加的耗时逻辑真实代码中应移除需要注意Thread.sleep(10)本身不是必现 Bug但它会放大 BUG-02 的竞争窗口。真实项目中数据库更新、远程调用、复杂计算都会天然制造竞争窗口所以并发累加问题并不只存在于这段模拟代码里。3.3 为什么选择这两个缺陷选择SimpleDateFormat和HashMap累加作为种子缺陷有两个原因它们在 Java 并发场景中非常典型有明确的权威结论适合做自动评估它们都是“静态代码检查工具不一定能查全、算法题训练数据里又大量出现”的问题适合测试 LLM 在上下文明确时能否准确命中。SimpleDateFormat线程不安全是 Java 官方文档明确说明的。SimpleDateFormat内部维护了Calendar等可变状态多线程共用时会出现不可预期的结果。DateTimeFormatter被设计为不可变且线程安全是替代方案。4. 三份评审输出的横向对比下面用三份不同类型的评审输出演示打分过程。这三份输出是构造的示例内容只是为了展示典型表现不代表任何具体模型的真实结论。实际评估时请使用你希望测试的模型生成结果再按同样方法打分。4.1 评审 A通篇表扬没有发现缺陷评审 A 的输出内容如下。整体看下来这段代码写得很干净结构清晰方法命名直观。 addPoints 中用 map.get 判断旧值再累加逻辑简单可靠 formatDate 使用统一的 SimpleDateFormat避免了每次创建格式化对象的开销 getPoints 提供了默认值处理调用方不需要额外判空。 唯一的小建议是方法注释可以再补全一些。 结论LGTM可以直接合并。评审 A 就是典型的“表扬 Bug”。它的结论“可以直接合并”和真实风险完全相反。我们逐句看“addPoints逻辑简单可靠”——没有考虑并发覆盖问题“使用统一的SimpleDateFormat避免了创建开销”——把线程不安全的共享对象当成合理优化“getPoints提供了默认值处理”——这句没错但掩盖了真正需要关注的高风险问题。这项评审最大的问题不是没提建议而是给出了一个误导性的放行信号。工程师如果信任这份评审代码就会带着并发缺陷进入发布流程。4.2 评审 B发现一半缺陷但存在幻觉意见评审 B 的输出内容如下。代码整体可用但发现一个问题 1. RewardService 使用了线程不安全的 SimpleDateFormat。 在并发环境中可能产生日期错乱建议每次调用时 new SimpleDateFormat(...) 或者使用 ThreadLocal 包装。 2. addPoints 方法中的读取和写入不是原子的可能存在并发覆盖风险。 不过考虑到调用方似乎是单线程写入该问题风险不大建议确认一下调用方。 3. 另外addPoints 没有对 points 0 的情况做提前返回 在积分不变化时仍然会执行 map.put建议补充提前返回逻辑减少无效写入。评审 B 比评审 A 好很多它发现了 BUG-01也对 BUG-02 表达了疑虑。但问题是对 BUG-01 的修复建议是“每次调用时new SimpleDateFormat”虽然可行但会带来不必要的对象创建开销并不是最优方案对 BUG-02 的表述是“风险不大建议确认一下调用方”而类注释已经明确说明“会被多个 HTTP 请求线程同时使用”这个疑虑本可以被直接确认“对points 0提前返回”是一条典型的幻觉意见。该方法本来就会对 0 积分做同样的写入提前返回对并发正确性没有任何帮助只会让代码多一个分支反而增加理解成本。评审 B 的问题在于把真问题和假问题混在一起输出工程师需要逐条甄别评审的辅助效率就打了折扣。4.3 评审 C缺陷定位与修复建议相对完整评审 C 的输出内容如下。代码存在两个需要修复的高风险问题。 BUG-01高风险位于 RewardService.DATE_FORMAT 字段 DATE_FORMAT 是 static final 的 SimpleDateFormat而 SimpleDateFormat 内部持有可变状态 不是线程安全的。类注释已说明该类会被多个 HTTP 请求线程使用因此并发调用 formatDate 时 可能出现日期错乱、Calendar 状态互相覆盖等异常。 修复建议改用 java.time.format.DateTimeFormatter。 DateTimeFormatter 是不可变且线程安全的可以作为 static 字段复用。 private static final DateTimeFormatter DATE_FORMAT DateTimeFormatter.ofPattern(yyyy-MM-dd HH:mm:ss); public String formatDate(LocalDateTime dateTime) { return dateTime.format(DATE_FORMAT); } BUG-02高风险位于 addPoints 方法 addPoints 执行的是“先取值、再累加、最后写回”的操作并非原子操作。 当两个线程同时为同一 userId 调用 addPoints 时两个线程可能读到相同的 oldPoints 随后各自写入最终只有一个线程的累加结果生效造成积分丢失。 修复建议将 pointsMap 改为 ConcurrentHashMap并使用 compute 原子更新。 private final MapString, Integer pointsMap new ConcurrentHashMap(); public void addPoints(String userId, int points) { pointsMap.compute(userId, (key, oldValue) - { int old (oldValue null ? 0 : oldValue); return old points; }); } 结论建议修复 BUG-01 和 BUG-02 后再合并。评审 C 的质量主要体现在三点两个种子缺陷都被定位到具体方法和字段说明不是泛泛而谈每个问题都解释了“为什么危险”以及“什么场景会触发”修复建议是可直接复制的代码并且最终合入建议与风险等级匹配。4.4 三份评审的直观对比把三份评审放在一起对比结果差异非常明显。对比项评审 A评审 B评审 C是否命中 BUG-01否是是是否命中 BUG-02否表达疑虑但未确认是是否产生幻觉意见无实质意见有points 0无修复建议质量无一般可直接执行最终合入结论直接合并修复后合并修复后合并如果只问“哪个模型更厉害”这个对比还不够严谨。真正严谨的做法是把三份评审放入固定评分表中逐项打分。5. 打分与结果分析5.1 逐维度打分过程缺陷检出维度权重 40%评审 A 没有命中任何种子缺陷得 0 分。评审 B 明确命中了 BUG-01对 BUG-02 只是“疑虑但没有确认”因此只能得到一半分数即约 18 分满分 40。评审 C 两个缺陷全部命中且定位准确得满分 40 分。评审准确率维度权重 20%评审 A 虽然没有任何正确发现但也没有输出错误结论不过在“评审准确率”这个维度上没有检出同样意味着没有给工程师提供有效保护按照从严原则记 10 分。评审 B 输出了一条明显的幻觉意见“对 points 0 提前返回”因此扣分较多记 8 分。评审 C 没有发现虚假问题记满分 20 分。修复建议可执行性维度权重 20%评审 A 没有给出任何代码级修复建议记 0 分。评审 B 的修复建议偏笼统“new 一个 SimpleDateFormat 或用 ThreadLocal”虽然可以执行但不是最优方案记 6 分。评审 C 给出了DateTimeFormatter和ConcurrentHashMap.compute()两种完整修改思路记满分 20 分。风险结论正确性维度权重 10%评审 A 的 “LGTM可以直接合并” 属于严重误判记 0 分。评审 B 虽然结论是整改后再合但对 BUG-02 做了错误的风险降级记 6 分。评审 C 对两个高风险问题都提出了“修复后合并”的正确结论记满分 10 分。边界覆盖度维度权重 10%评审 A 完全没有考虑并发边界记 2 分。评审 B 提到了并发但没有结合类注释把风险确认下来记 6 分。评审 C 覆盖了并发场景、线程安全 API 选择和在多线程下的影响记满分 10 分。5.2 最终评分汇总评分维度权重评审 A评审 B评审 C缺陷检出40%01840评审准确率20%10820修复建议可执行性20%0620风险结论正确性10%0610边界覆盖度10%2610总分100%12441005.3 从结果中得到的三个结论“没有被发现问题”不等于“没有问题”。评审 A 的满分区间里没有任何检出它的总分甚至低于只命中一半缺陷的评审 B。所以在选择代码评审模型时不能只看评审是否“看起来专业”而要看它能否命中已知缺陷。检出率与幻觉率必须同时评估。评审 B 命中了一个种子缺陷看起来不错但它同时输出了一条错误意见增加了工程师的甄别成本。如果团队只看“模型找到了多少个问题”很容易被幻觉意见带偏。评审结论必须落到合并决策上。一个评审无论写了多少条建议最终都要回答“这段代码能不能合入”。评审 A 最大的问题就是错误放行评审 C 之所以得到满分恰恰是因为它在发现风险后给出了明确且正确的合入决策。6. 搭建一个最小可复现的“评审质量”评估脚本上面的人工打分比较依赖人工判断。实际团队在做模型选型或提示词调优时可以用脚本快速量化“缺陷检出率”和“幻觉率”。下面给出一个简化版 Python 评估脚本作为思路示范。# 文件路径scripts/grade_review.py 简化版 LLM 代码评审质量评估脚本。 思路把种子缺陷与评审文本中的关键词做匹配统计检出率、精确率和幻觉次数。 注意这里的关键词匹配只是演示真实评估建议由人工或更强模型辅助判断。 GROUND_TRUTH { BUG-01: [SimpleDateFormat, 线程安全, formatDate], BUG-02: [丢失更新, 并发, addPoints], } # 常见幻觉信号不同评审可能不同 HALLUCINATION_SIGNALS [ 没有对 points 0 做提前返回, SimpleDateFormat 在 JDK 8 后已是线程安全, ] def hit_bug(review_text: str, keywords: list[str]) - bool: low_text review_text.lower() return all(keyword.lower() in low_text for keyword in keywords) def evaluate_review(review_text: str) - dict: detected [] for bug_id, keywords in GROUND_TRUTH.items(): if hit_bug(review_text, keywords): detected.append(bug_id) false_positives [] for signal in HALLUCINATION_SIGNALS: if signal.lower() in review_text.lower(): false_positives.append(signal) bug_count len(GROUND_TRUTH) recall len(detected) / bug_count if bug_count else 0.0 if detected or false_positives: precision len(detected) / (len(detected) len(false_positives)) else: precision 1.0 return { detected: detected, recall: round(recall, 3), precision: round(precision, 3), false_positives: false_positives, } if __name__ __main__: review_b 代码整体可用但发现一个问题 1. RewardService 使用了线程不安全的 SimpleDateFormat。 ... result evaluate_review(review_b) print(result)运行脚本后会输出类似下面的结果{detected: [BUG-01], recall: 0.5, precision: 0.5, false_positives: [没有对 points 0 做提前返回]}使用脚本时需要注意几点关键词匹配无法判断同义表述例如模型写的是“并发累加可能覆盖”脚本不一定能匹配到丢失更新幻觉信号的判断最好由人工维护将多次评审中发现的错误意见添加到信号列表脚本适合做“初筛”真正选择模型或调整提示词时还是要以人工抽样评审为准。如果你的团队已经把代码评审流程收口到内部平台也可以把这个脚本作为回归用例在每次调整评审提示词后自动跑一遍观察召回率和精确率的升降。7. LLM 代码评审中的常见问题与排查思路在真实业务中使用 LLM 做代码评审通常不会像示例这样干净常见的问题可以整理成一张排查表。问题现象常见原因解决思路模型把明显有 Bug 的代码评为“设计优秀”提示词缺少“必须找出问题”的约束模型倾向于输出正面信息在提示词中明确要求“逐项检查并输出每个风险的严重级别”禁止只给表扬结论模型提出不存在的缺陷缺少业务上下文、运行场景、依赖信息模型靠猜测补全把类注释、调用方、接口契约尽量补充到上下文中把建议限定在当前 diff 范围内同一代码多次评审结果不一致LLM 采样存在随机性提示词不固定温度设为 0固定 system prompt 和用户 prompt跑多次后汇总评审建议太抽象无法指导修改模型只描述了“可能有问题”没有给具体位置和修改方式要求每条意见输出四个要素文件/方法、触发条件、影响范围、修改代码示例模型对并发类软件缺陷不敏感没有明确告知该类会被多线程调用或者模型没有对并发场景做专项检查在代码上下文里显式说明运行环境提示词中增加“并发安全专项检查”评审意见太长淹没重点没有设置输出格式让模型按 Blocker / Major / Minor / Nit 四级输出并按严重程度排序在使用 LLM 代码评审时还要小心一个隐藏问题模型会复用训练数据中的相似代码片段。如果训练数据里本来就有和你业务代码相似但错误的写法模型可能会基于“这段代码看起来常见”而给出错误判断。这一点很难从提示词层面彻底解决只能通过“评审必须给出可复现的触发条件”来限制模型空口下结论。8. LLM 代码评审的工程化最佳实践8.1 用“bug-first”提示词约束评审目标要让 LLM 代码评审的结论更可靠应该尽可能让提示词面向“找 Bug”而不是面向“给评价”。下面是一个可以按团队情况调整的提示词模板。你是一名资深的 Java 代码评审专家。请对下面的代码做严格评审。 评审要求 1. 按以下顺序检查功能正确性、并发安全、资源释放、边界条件、空值处理、性能 2. 每条意见必须输出四个部分【文件或方法】【触发条件】【影响】【修复建议】 3. 修复建议给出可直接使用的代码片段 4. 如果没有发现可确认的缺陷明确写“本轮未发现可确认的缺陷” 5. 不要输出与代码风险无关的表扬性内容不要凭猜测提出不存在的 Bug。 6. 最后必须给出合入结论通过 / 修复后合并 / 不通过。 被测代码 {代码贴到这里}这个模板的核心是两点取消泛泛表扬强制结论落到合并决策。这样模型输出的内容会更容易被人工复核也更容易被评分脚本自动统计。8.2 把 LLM 定位成“初筛”而不是“终审”从这次三份评审的对比可以看到LLM 代码评审可以很优秀也可以非常不靠谱。这里建议团队把它定位成“初筛助手”而不是“终审人”。推荐的流程是静态检查工具先跑一遍处理确定性的规范问题单元测试和集成测试先跑过滤功能性回归LLM 代码评审重点处理模式化的问题如并发、空指针、资源泄漏、常见安全漏洞最后由有经验的工程师做终审重点关注业务语义和合入决策。LLM 特别适合处理“常见 Bug 模式”的初筛比如单例里持有可变的SimpleDateFormat、没有关闭的流、错误的锁顺序。但业务规则是否严谨、架构约束是否满足这些只有结合项目上下文才能判断模型很难替代人。8.3 把评审经验沉淀成可复用的规则库团队可以尝试把典型的代码评审案例整理成一个小的规则库包括种子代码、缺陷说明、正确评审意见和错误评审意见。这样做的价值在于每次更换模型或修改提示词后可以快速回归新加入团队的工程师可以通过规则库理解“什么样的评审意见是合格的”规则库可以用类似知识库的方式持续更新积累越多评审评估越准。这里要注意的是规则库里的代码片段在提交前要做脱敏处理不要把公司内部真实业务代码直接放进外部模型或公共平台涉及代码安全合规时优先选择企业内部部署的模型或对代码进行脱敏后使用。8.4 警惕“看起来很专业的幻觉”LLM 代码评审里最危险的不是不说话而是说一大堆看起来很专业的错误结论。这种幻觉可能出现在以下几个方面编造不存在的 API 或方法对代码行号和位置的错误引用把训练数据里的旧版本 API 行为当作当前版本结论对“可能存在问题”的表述过于自信不给复现场景。对应的处理手段是要求每条意见都给出“触发条件”。如果模型不能描述一个可以复现的触发条件那么这条意见很可能是在猜测。反过来说评审 C 之所以可信度高正是因为它给每个问题都提供了具体的并发触发场景。9. 总结与下一步实践回到标题 The Review That Praised the Bug这个实验说明了一个很容易被忽略的事实模型给出的评审结论并不因为语气自信而变得更可靠。真正决定评审价值的是它能否命中真实缺陷、是否误报、建议是否可执行、结论是否恰当。通过把一份已知带有种子缺陷的代码同时交给多个评审方案再使用统一的评分表打分团队就能把“哪个模型更靠谱”从主观感受变成可量化的结论。如果你正在尝试把 LLM 引入代码评审建议先从一个小仓库开始。准备两份到三份带种子缺陷的代码让候选方案各跑三遍按上面的维度打分把结果整理成表格。这样你能很快知道当前的提示词方案在并发问题上是否敏感、在幻觉控制上是否合格。然后再把评审脚本接到你的代码评审流程里持续积累案例。代码评审是一道需要长期维护的质量防线。让模型当助手、让人做终审同时用一套标准给“评审的评审”打分可能是当前最稳妥的落地方式。