Rust团队如何用LLM规则保护人工代码审查?

📅 2026/8/27 5:40:29
Rust团队如何用LLM规则保护人工代码审查?
“五支 Rust 团队开始用 LLM 规则保护人工代码审查”——这句话第一次看到时我以为是又一份“AI 即将替代人”的案例。等到真去了解这类实践后我的判断反转了。这些团队做的事情方向恰好反了过来。他们不是让 LLM 自动批准或者否决代码而是让 LLM 先接走那些重复、机械、消耗注意力的审查脏活让审阅者把有限的精力集中在真正需要人判断的地方。这篇文章想聊的就是这套做法为什么会出现在 Rust 社区里以及如果你想在自己的项目里尝试该从哪里开始、会踩到哪些坑。1. 代码审查真正的问题不是“看得不够多”而是注意力被稀释先从一个很常见的场景说起。你是一个 Rust 项目的维护者PR 并不算多但每个 PR 的 diff 都牵扯到好几个文件。刚开始你会认真逐行看慢慢你会发现自己经常陷入两种状态一种是机械扫描看格式、命名、有没有unwrap()另一种是疲劳式阅读看过一堆无关紧要的修改之后重要地方反而没力气想了。这不是态度问题是认知资源的问题。人工代码审查是有明确消耗上限的而这个上限通常不是靠“更认真”就能突破的。1.1 机械观察不等于有效审查很多团队把代码审查效果不好归结为审查者不够重视但实际上真正的瓶颈是审查者的注意力被大量低信息量的内容占住了。比如一个 PR 里改了 800 行真正需要审阅者深思的只有 3 个 API 边界和 2 处错误处理。剩下的 600 行可能只是格式化、重命名、移动文件位置。如果 LLM 把每行都点评一遍审阅者是得到了“更全面的反馈”还是被噪声打得更乱答案通常是后者。所以真正要解决的问题不是“能不能让 AI 看得更多”而是“能不能把人的注意力从低价值区域里腾出来”。1.2 Rust 项目让这个问题变得更尖锐Rust 的情况比较特别。编译器已经挡掉了大量低级问题类型错误、所有权错误、生命周期的大部分问题、内存安全风险。也就是说能跑过cargo check的代码通常已经过了第一道非常严格的闸。但代码审查仍然重要只是重点变了。重点不再是“你这个类型写错了”而是这个接口设计是否能在六个月后进行演进这个错误处理路径是否会导致服务在临界状态下静默失败这个异步调用是否会悄悄阻塞 runtime 线程这个unsafe块是否真的 safe还是说“编译器过了但语义上不安全”这些问题需要语义层面的理解而 LLM 做得不好的地方恰恰是“表现得很自信”它真正可能对团队产生价值的地方是当它被约束成一个带边界的工具时。1.3 先看清主判断LLM 的角色应该是“过滤器”不是“决策者”一整篇文章的主判断在这里先定下来LLM 规则在代码审查里的目标不是替代人的判断而是保护人的判断。它把噪声、重复、机械性的检查消化掉再把少数真正值得人看的问题用清晰、可追踪的方式交出来。如果这个目标没有先想清楚后面很容易把工具做成新的噪声源。2. “LLM 规则”不是把审查交给 AI而是给 AI 套上边界很多人提到“用 LLM 做代码审查”第一反应是把 PR 甩给 GPT让它把所有问题列出来。听起来很美实际上不可控。问题在于不设边界的 LLM 审查会让输出非常随机。一次可能给 30 条意见另一次可能只给 3 条今天觉得某个unsafe写法可以明天又觉得不行。这不是模型“笨”而是你没有给它一套工作边界也就是“规则”。2.1 在常见实践里“规则”到底指什么我见过的 LLM 代码审查规则不只是一个 prompt而是一组约束通常包含四个方面审查范围只处理本次 PR 的 diff还是还可以看相关调用点只看 Rust 文件还是也要看配置。输出格式要求结构化输出比如“位置、风险类别、建议、理由”避免长篇大论。判断边界允许指出问题但不允许给出“必须合入”或“禁止合入”的结论。优先级规则哪些先看哪些可以不看比如unsafe区块必看格式化问题可以忽略。第二条很重要。如果 AI 只输出“这里有问题”人是没法快速判断的。更合理的输出是一条可定位的线索在哪个文件、哪一行、什么样的变更模式、为什么可能有问题、建议往哪个方向深挖。这也是它和“整段代码通读评语”之间的关键差异。2.2 一个最小流程长什么样从实践来看很多 Rust 团队会走这样一个最小流程PR 创建后CI 触发一次 LLM 预审。LLM 拿到 diff、PR 描述、相关模块的说明还有一套规则文件。它只输出规则允许的内容比如潜在风险列表不给“合入/不合入”结论。这些评论进入 PR 讨论区但被标记为“机器人建议”。人类审查者按照自己的节奏逐一确认可以接受也可以忽略或者回复理由。这个流程的精髓在于LLM 扮演的是“预筛员”的角色它在人类审查者进入之前先把低风险项过滤掉把高风险项集中到台面上来。2.3 三类规则决定 LLM 的工作方式如果想把规则体系清晰化可以分成三类。第一类是硬性规则只要出现就必须引起人类注意。比如新增了unsafe代码、删除了公共函数、修改了 trait 的默认行为。这类规则不需要 LLM 判断“是不是问题”它只需要标记“这里有需要进一步看的东西”。第二类是探测规则LLM 根据模式去推测可能存在风险。比如异步代码里出现了阻塞调用、错误处理分支里吞掉了原始错误、大段unwrap出现在库里。这类规则需要模型有一点推理能力但结论应该以“候选”而不是“事实”呈现。第三类是抑制规则告诉 LLM 哪些内容不必要反复提。比如命名风格、行尾空格、格式微调这些应该由rustfmt和 linter 去做不需要 LLM 重复报。如果忽略第三类规则LLM 很容易把代码审查变成“各种意见的噪音瀑布”那就完全违背了保护人工审查的本意。3. 为什么在 Rust 项目里这套打法更容易跑通技术方案不能脱离语言背景讨论。LLM 辅助审查不是只有 Rust 团队在用但 Rust 团队确实是更适合这套模式的人群之一。这不是偶然背后有几个具体原因。3.1 Rust 编译器已经挡掉了第一层噪音静态分析能力越强的语言留给 LLM 的“专业分工”就越清晰。在 Python 或 JavaScript 项目里LLM 可以帮忙找明显的空指针、未定义变量、类型错配。但在 Rust 里这些错误大部分不会进入审查环境因为编译器已经提前拒了。于是 LLM 不需要再当“低配编译器”它可以直接往语义层面走。这也让“LLM 规则”可以写得更聚焦。它不必覆盖语法级问题只需要覆盖编译器覆盖不到、现有工具难覆盖、而人需要花时间判断的那部分。3.2 语义意图才是 Rust 审查的重灾区Rust 审查中真正费神的通常不在“这行代码能不能跑”而在“这段代码想表达什么以及它有没有表达对”。举几个例子一个unsafe块安全但依赖了外部可变静态变量后续多线程改动会不会破坏不变量一个 error type 实现了From转换但调用端可能丢失底层的错误上下文日后再排查困难。一个异步函数里持有了某个MutexGuard但实现里跨了await一旦并发就会导致锁被长时间持有。这类问题没法通过规则表达式精准识别往往需要结合语义理解。这正是 LLM 相对于传统静态分析的增量价值所在。它能看到“这种模式在其他项目里通常意味着风险”然后用自然语言把风险说清楚。但注意这里说的“增量价值”并不是它一定正确只是它给审查者提供了一种更接近语义的提示方式。最终能不能成立还是要人的判断。3.3 团队文化的契合度Rust 项目的文化通常更看重确定性、可维护性和长期演进。审查讨论经常不是只看“这行代码怎么改”而是在讨论 API 设计、抽象边界、安全性约束。这种文化其实很适合做“半自动审查”的实验因为团队愿意把规则当作代码库的一部分去维护愿意给机器标定边界也愿意用充分的 review 来兜底。反过来如果团队只看重“效率提升”和“吞掉 PR”那这套模式大概率会翻车。因为 LLM 规则只是提升了输入质量并没有缩短必须的思考时间。4. 一套能运行的 Rust 审查规则可以从这四类开始如果你想自己实验最忌讳的是先把规则写得又多又全。我建议从四类低风险、高价值的规则入手。这四类在 Rust 项目里出现频率高同时 LLM 的判断相对容易验证。4.1 unsafe 与生命周期边界对大部分 Rust 项目来说unsafe是有限范围内的高风险区。规则应该要求 LLM 在执行以下任务时只标记代码中出现了新的unsafe块或者修改了已有的unsafe块。但不要让 LLM 自己证明“这是否 safe”。更好的做法是让 LLM 标记出这个块依赖了什么不变量并要求审查者去检查这些不变量是否仍然成立。比如“这里假设 addr 一定非空”“这里假设调用者已经持有锁”。这比一句“unsafe 需要谨慎”有用得多。对生命周期相关修改规则可以要求 LLM 定位泛型生命周期参数是否被不合理地放宽或紧缩。如果项目里的公共 API 从a改成了static这会直接抬升调用方的约束应当被重点标记。4.2 错误处理与可恢复性Rust 的错误处理经常是审查的深水区。规则可以让 LLM 检查三个信号是否吞掉了原始错误只抛出一个无上下文的轻量错误是否把本应可恢复的错误直接变成了 panic是否通过unwrap()来处理本可能出现失败的边界情况。但同样不能让它给出统一的修改建议。因为错误处理策略取决于当时场景有些库确实选择 panic有些服务则必须返回兜底结果。所以 LLM 的任务是标记模式而不是替团队定策略。4.3 异步阻塞与并发风险用 Rust 写异步服务时最常见的坑之一就是阻塞 runtime 线程。规则可以要求 LLM 在异步函数里找“标准库的同步阻塞调用”比如std::thread::sleep、同步文件 I/O或者大计算量的同步循环。同时可以关注MutexGuard在.await范围内是否被持有这可能造成后续任务长时间阻塞。这类规则的价值不只是找到问题还能帮助团队积累出一份“异步并发风险清单”。等清单成熟后甚至可以回写进项目文档让新成员提前避坑。4.4 API 设计与调用方影响公共 API 变更最容易出现“改了一天爆了十个调用点”的情况。规则可以让 LLM 做一件事当某个公共函数、结构体、trait 的方法签名发生变化时分析 PR 中能看到的调用点标注出哪些调用方行为会改变。不需要把所有调用方都扫完只要基于当前 diff 和相关引用给出提示即可。这类规则特别适合 Rust 项目因为 trait、泛型、生命周期约束带来的连锁影响比动态语言更隐蔽。用 LLM 做一次“潜在影响面标注”能显著减少人类审查者的上下文切换成本。4.5 你的规则文件可以从这个模板起步下面是一份偏保守的模板适合做最小实验角色Rust 代码审查预筛员。 范围只分析当前 PR 的 diff不扩大到整仓代码。 任务按规则检查变更不输出与规则无关的内容。 输出格式 - 位置: 文件路径 行号 - 风险类别: unsafe / 错误处理 / 异步并发 / API 影响 - 影响说明: 为什么这个变更值得关注 - 建议方向: 请审查者重点确认的事情 约束 1. 不给出合入或不合入的结论。 2. 不确定时使用“建议确认”而不是“一定有问题”。 3. 重复出现的同一类问题只输出最高优先级的三条。 4. 不检查格式、命名、缩进等其他工具已覆盖的问题。这份模板的优点是“可拒绝”。它给 LLM 设定了明确的行为上限避免回答过于发散。你后续要调整也简单只改规则文件不需要重写流程。5. 保护人工审查者不能只靠模型自律还要靠流程兜底很多团队把 LLM 审查跑起来后很快就遇到一个问题自动评论倒是很多但审阅者越来越不看了。为什么会这样因为流程没有兜底。LLM 不是人类它没有“在对团队负责”的压力。如果你指望它自觉做到不乱评论、不重复报那一定会失望。必须把保护人工审查变成一个流程设计目标而不是一句口号。5.1 最终决定权永远属于人在所有规则文件里我建议都要写一句类似“本工具不参与合入决策”的约束。这不是为了表明立场而是为了明确责任边界。LLM 可以提出候选风险但合入还是不合并必须由一个担责的人类决定。如果自动审查开始替代决策团队很快就会失去对工具输出的敏感度反正都是机器说了算人就不看了。那一旦 LLM 出现幻觉风险会直接进入代码库。5.2 让 LLM 只标记不表态“这个问题需要看一下”和“这段代码有问题”是两回事。前者是提示后者是结论。审查者应该收到提示然后自己去确认结论。更合理的做法是让 LLM 输出“风险模式 位置 影响说明”但不要输出“这里怎么改”的完整补丁。原因很简单LLM 给出的改动方案容易产生一种“机器已经想清楚了”的错觉这会削弱人的审查动机。事实上它经常只看到了局部信息未必理解当前项目的架构约束。5.3 每条人工可见的评论都必须是可追踪的如果 LLM 说“这里的错误处理有问题”但看不到对应代码、没有行号、没有引用内容这条评论的价值就很低。规则应该强制输出结构化信息让审阅者可以在 30 秒内定位到代码位置并知道这条评论来自哪条规则。这能显著降低核查成本也会让 LLM 的可信度反馈变成可积累的资产。这里建议给规则文件加一个“版本号”。规则变更后评论也会带上新版本号。团队可以统计每个规则版本的假阳性率从而判断哪些规则该调。5.4 一定要有反馈闭环而不是一次配置终身运行LLM 规则不是写一次就完事。它需要根据人工审查者的真实反馈持续更新。常见做法是每星期花 15 分钟把上一轮的自动评论翻一遍标记哪些被采纳、哪些被忽略、哪些被明确反驳。被反驳的规则要调整验证过的规则可以保留误报率高的规则应该降低优先级或直接移除。真正的风险不是规则不完美而是没人去修正它最终让机器人“自嗨式审查”污染了讨论区。6. 落地最容易翻车的四个位置以及排查顺序再好的方案也有失败模式。这里说四个我见过的最常见的坑以及如果出现了应该按照什么顺序排查。6.1 坑一规则太多输出变成噪声瀑布一开始只加了 4 条规则效果不错于是顺手加了十几条覆盖范围越来越大。结果不久后 LLM 在一个 PR 里输出 50 条评论人工审查者只看了前 5 条就关掉了。这其实是方向错了。规则的价值不在覆盖多而在于筛选准。宁愿只有 5 条高质量提示也不要 50 条“好像有点道理但也没什么要紧”的评论。如果遇到这个问题先不要急着改提示词先回到规则列表删掉那些在统计中被忽略率最高的规则。6.2 坑二LLM 幻觉却用了很确定的口吻LLM 在生成代码审查意见时经常会把“可能有问题”说成“这里错了”。比如它会说“这个函数会阻塞 runtime”但实际代码里并没有阻塞调用只是 LLM 根据函数命名做出联想。对策只有一个在规则里明确约束输出粒度。例如“只有你确认代码路径中存在明显风险时才使用‘问题’表述否则只能写‘建议确认’”。同时要求输出必须携带代码片段作为依据。没有代码引用的评论优先级一律降低。6.3 坑三上下文不完整导致评论基于旧代码或错误假设LLM 审查 PR 时只看到 diff不一定能看到它依赖的模块定义、环境配置和测试结果。如果某个依赖函数的行为变了LLM 可能还在按旧逻辑推断。处理办法是给 LLM 提供更完整的“现场信息”相关的Cargo.toml片段、关键模块的 doc comment、测试命令的日志。这些信息不用全部展开但至少给它一个锚点避免让它在错误假设上继续推理。如果发现问题仍然频繁先检查输入内容是否出现过期。这往往比调 prompt 更有效。6.4 坑四只关注效率指标忽略了人工是否还在认真看最后这个坑最隐蔽。团队统计时会发现PR 合入时间减少了看起来效率提升明显。但这并不能证明人工审查质量提升了也有可能只是因为审阅者相信了 LLM 的评论没有做足够深的人工判断。这个风险很难靠规则文件解决需要靠团队文化兜底。我的建议是不要以“自动评论被采纳数”作为唯一指标可以定期抽审几个高风险 PR只靠人类来判断是否过关。如果人类自己不看那 LLM 只是换了一个更隐蔽的“替你签字”的助手。6.5 如果问题出现按这个顺序排查如果你在实验阶段发现输出质量不行建议按顺序排查先看输入LLM 拿到的 diff 是否完整PR 描述是否清晰是否有过多过期信息。再看规则规则是不是太泛、太细或互相冲突。比如“检查所有 unwrap”和“忽略低优问题”同时存在就可能互相干扰。再看输出约束是否要求结构化了是否强制要求和代码位置绑定是否限制了结论性的表述再看模型不同模型的判断稳定度差异很大。如果你用的是小模型就要降低它对复杂语义问题的期待。最后看反馈闭环有没有人定期校对校对结果是否回流到规则文件里如果回流断了问题只是早晚的事。排查时最忌讳的是上来就调 prompt。在输入和反馈闭环都没稳定时调 prompt 只是把一个不稳定的系统放在另一个不稳定系统上面。7. 一份可复用的五步采用框架如果你从零开始建议按照下面五步推进。它不是为了追求立竿见影而是为了让你在“增加效率”和“保护人工判断”之间找到平衡。第 1 步限制实验范围。不要一开始就对所有 PR 启用 LLM 审查。选一个模块比如错误处理最麻烦的模块或者unsafe代码最集中的模块只在这个范围里跑。第 2 步给 LLM 制定“可拒绝”规则。明确告诉模型它只能输出候选风险不能输出合入建议。它给出的每一条评论都只是提示不是指令。第 3 步先给人工审阅者做模板再谈自动化。与其一开始就追求“自动评论”不如先让某个人用同一套规则手动跑两三次看看这套规则是否真的能帮助定位问题。如果人对这套规则没有体感自动化上来也只会更乱。第 4 步用可观察指标衡量而不是看评论数量。比如假阳性率、被忽略率、被采纳率、人工审查者平均单条判断耗时。目标不是“LLM 报得多”而是“人类在单位时间里做的有效判断更多”。第 5 步把规则当成代码仓库的一部分来维护。规则文件要有版本号变更要经过 review长期使用要积累一份“常见风险清单”。这样 LLM 审查不仅能服务当前 PR还能反哺团队的领域知识库。这里要特别提醒一句这套框架适合已经有成熟代码审查文化的团队。没有这种文化规则再细LLM 再强结果也只是把一个原本该有人的环节变成了一个看起来更自动的无人区。8. 回到核心真正被保护的是人工审查者本来就该有的判断力五支 Rust 团队采用 LLM 规则的故事真正值得思考的不是“有多少规则”也不是“用哪个模型”而是他们为什么要用“保护”这个词。一开始我也以为保护是“保护代码质量”。后来想想代码质量不会因为多一些扫描而自动变好关键还是在于让对人站在正确的位置上。被保护的对象其实是人类审查者本来就稀缺的判断力。LLM 规则把噪音减少把重点抬高把重复劳动拿走让人有时间和精力去思考那些需要架构感、经验和责任感的问题。这比“AI 自动审查全部代码”要有意义的得多。我的建议也很简单如果你所在的项目有成熟的 review 流程不妨从最小范围开始定几条保守的规则让 LLM 做那个先扫一遍的预筛员再让人类审查者做最终裁决。先跑通积累反馈再逐步放大范围。这套流程里LLM 真正扮演的角色不是一个无所不知的审查官而是一个被约束住、又足够勤奋的助手。而它有没有用最终还是取决你愿不愿意在那个终于空出来的时间里认真做一次判断。