Rust团队引入LLM规则辅助代码审查:保护人类注意力而非替代
最近 Rust 社区有一则消息值得关注:五个知名的 Rust 团队开始引入“LLM 规则”来辅助代码审查,但目的并不是让 AI 替代人类 Reviewer,而是反过来——保护人类 Code Review 的时间和注意力。
这件事很容易被误读成“AI 接管代码审查”,但实际上,它背后的技术判断要克制得多:LLM 只负责处理机械性、重复性的检查,人只负责真正需要判断力的部分。这篇文章会从 Code Review 的现状痛点讲起,解释这套“LLM 规则”到底在解决什么问题,然后落到 Rust 项目里可以怎么落地实践,最后给出明确的边界建议。
如果你是 Rust 开发者,或者正在团队里推动 Code Review 流程改进,这篇文章应该能帮你省下不少沟通成本。
1. 代码审查的真实困境:不是质量问题,是注意力问题
先说一个可能有点反直觉的判断:现代软件开发里,代码审查的真正瓶颈不是“代码质量”,而是“人类评审者的注意力”。
过去几年,PR(Pull Request)的规模越来越大,依赖越来越多,CI 检查也越来越复杂。但代码审查这件事,本质上仍然依赖一个资深工程师打开 Diff 页面,逐行看代码。问题是,一个资深工程师每天能投入到 Code Review 的时间是有限的,而 PR 里真正需要人类判断的内容,往往只占很小一部分。
大部分 Review 时间消耗在哪里呢?举几个很典型的例子:
- 格式不一致、命名风格不统一;
- 明显的错误处理缺失,比如
unwrap()直接用在可能为None的场景; - 常见的安全隐患,比如不安全的
unsafe块使用; - 文档注释缺失或过时;
- 没有遵循项目里既有的模式,比如错误类型没有实现
std::error::Error; - 简单的逻辑错误,比如边界条件判断反了。
这些内容不是不重要,但它们有一个共同点:不需要“资深工程师”也能发现。可现实是,它们确实消耗了资深工程师的审查时间。等 Reviewer 把这些问题一个个挑出来、写评论、等作者修改,真正需要他投入判断力的架构问题、并发安全问题、API 设计问题,反而可能因为精力耗尽而被草草放过。
这就是为什么五个 Rust 团队会不约而同地选择“LLM 规则”这条路。他们不是在用 AI 替代人类审查,而是把人类从重复劳动里解放出来,让注意力重新回到高价值判断上。
2. 什么是“LLM 规则”?它不是你想的那个东西
很多人第一反应是:LLM 规则 = 把 PR 丢给 ChatGPT,让它直接给结论。这其实是最容易踩的误区。
从 Rust 团队的实际做法来看,LLM 规则更像是一种可编程的审查辅助层。它的核心不是“问 AI 怎么看这段代码”,而是“给 LLM 定义一套明确、可执行的检查规则,让它按规则去扫描代码,输出结构化的审查意见”。
用一句话概括:LLM 规则不是让 AI 代替人思考,而是让人把“如何检查代码”写成规则,然后让 AI 按规则去执行。
这里有一个很关键的区别:
- 传统静态分析工具(比如
clippy)依赖确定性规则,优点是稳定,缺点是只能识别模式,不能理解上下文。 - 通用 LLM 审查(比如直接把代码贴给 GPT)依赖模型的模糊能力,优点是理解力强,缺点是输出不稳定,容易漏检,也容易误报。
- LLM 规则介于两者之间:用规则约束 LLM 的检查范围和输出格式,用 LLM 理解代码上下文。
打个比方,clippy像一个拿着检查清单的保安,只会按清单打勾;通用 LLM 像一个知识渊博但容易跑题的顾问;而 LLM 规则像是一个“带了行业规范手册的审计员”——他知道规则,也知道怎么理解实际情况,但最终报告必须按规范格式输出。
从公开资料看,这些 Rust 团队的做法通常包含几个要素:
- 规则文件:用自然语言或结构化格式描述检查项,例如“检测所有直接对
Result调用unwrap()且函数签名返回Result的场景”。 - LLM 推理:将规则文件和代码片段一起提交给 LLM,要求它按规则检查。
- 结构化输出:LLM 返回的结果被解析成标准格式,比如 JSON,包含问题位置、严重级别、建议修改方式。
- 人工确认:最终审查意见必须有人类 Reviewer 确认,LLM 的输出只是辅助草稿。
这个流程的意义在于:规则是团队定义的,AI 只负责执行。如果 LLM 误报,修正的是规则,不是让团队去适应 AI 的随机发挥。
3. 为什么是 Rust 团队先跑通这条路?
很多人会好奇:为什么是 Rust 团队先大规模讨论和采用这套方案?这其实和 Rust 语言本身的特性、社区文化都有关系。
3.1 Rust 的编译器已经把一部分“人类审查”自动化了
Rust 有一个非常强大的编译器,加上cargo fmt、clippy这些工具,已经消灭了大量原本需要人工审查的问题。比如所有权、借用检查、生命周期、空指针,这些在其他语言里需要 Reviewer 重点关注的领域,在 Rust 里大部分是编译错误,根本到不了 Code Review 阶段。
这意味着什么?意味着 Rust 团队的 Code Review 可以更早地把注意力放在“人类才能判断的问题”上。但与此同时,Rust 也引入了新的复杂性问题:生命周期标注是否合理、unsafe块是否真的安全、抽象层次是否恰当、错误处理是否符合项目惯例,这些都是编译器管不了的。
换句话说,Rust 的 Code Review 比很多语言更“纯粹”,也更适合让 LLM 介入。因为 LLM 不需要处理“这段代码会不会空指针”这种低级问题,它可以直接处理“这个错误处理模式是否符合项目惯例”这种规则性问题。
3.2 Rust 社区对“工具链”的接受度极高
Rust 社区可能是所有编程语言社区里最拥抱工具链的。从cargo、clippy、rustfmt到各种 Cargo 插件,Rust 开发者习惯“用工具解决问题”。因此,把 LLM 规则纳入开发流程,对 Rust 团队来说不是一个文化冲击,而是工具链的自然延伸。
3.3 安全和正确性敏感性
Rust 经常被用于系统编程、嵌入式、网络协议、区块链等对安全和正确性要求极高的场景。这类团队的 Code Review 压力更大,也更愿意尝试能在“不降低标准”的前提下提高效率的方案。LLM 规则正好符合这个诉求——它不改变审查标准,只是把重复性检查自动化。
这里有一个值得注意的技术背景:Rust 生态里有一个非常流行的工具cargo-deny,用于检查依赖许可证和安全漏洞;还有cargo-geiger,用于检测unsafe代码的使用比例。这些工具证明了 Rust 团队很习惯“把安全审查自动化”。LLM 规则是这条路线的一个延伸,只不过检查的对象从依赖变成了代码本身。
4. LLM 规则和传统 Code Review 工具的区别
要理解 LLM 规则的价值,必须把它和现有的工具放在一起对比。很多团队会问:我已经用了clippy、cargo fmt、SonarQube,为什么还需要 LLM 规则?
下面这张表可以比较直观地说明问题:
| 维度 | 传统静态分析工具 | 通用 LLM 审查 | LLM 规则辅助审查 |
|---|---|---|---|
| 规则确定性 | 高,同一代码永远同一结果 | 低,输出不稳定 | 中高,规则约束输出格式 |
| 上下文理解 | 弱,只能识别模式 | 强,能理解语义 | 较强,能够按规则结合上下文 |
| 误报率 | 中,模式匹配容易误报 | 中高,容易过度解读 | 中,可通过规则调优 |
| 是否可团队定制 | 有限,依赖插件生态 | 弱,Prompt 不可控 | 强,规则文件由团队维护 |
| 是否替代人类 | 否,只辅助 | 容易造成依赖 | 明确不替代,结果需人工确认 |
| 适合检查的内容 | 格式、模式、已知问题 | 开放性问题 | 团队定义的规则性问题 |
从这个对比可以看到,LLM 规则的定位非常清楚:它填补的是“传统工具查不了、通用 LLM 不敢信”的中间地带。
举个例子,一个团队可能有一条内部规则:所有公开 API 的文档注释必须包含# Panics部分,说明可能的 panic 场景。clippy查不了这个,因为它需要理解“这个 API 是否可能在哪些场景 panic”;通用 LLM 可以查,但结果不稳定;LLM 规则可以用一段明确的话定义这条规则,然后让 LLM 逐函数检查。
5. 在 Rust 项目中落地 LLM 规则的完整流程
聊完了理念,下面进入实操部分。我们以一个真实的 Rust 项目为例,演示如何把 LLM 规则集成到 Code Review 流程中。
5.1 明确你要检查什么
这是最重要的一步,也是最容易被跳过的。很多团队拿到 LLM 就直接说“帮我看看代码有什么问题”,结果得到一堆泛泛而谈的建议,毫无价值。
在 Rust 项目里,比较适合用 LLM 规则检查的场景包括:
- 错误处理模式:是否所有可能失败的函数都返回了
Result?是否存在应该使用?却手动match的场景? unsafe块审查:每个unsafe块是否都有注释说明为什么安全?是否被限制在最小范围内?- 公开 API 文档:所有
pub函数是否都有# Panics、# Errors部分? - 生命周期标注:是否存在不必要的生命周期参数?
- 特性(Trait)一致性:相同概念是否使用了不同的 trait 命名?
- 并发模式:是否存在不必要的
Mutex使用?是否应该用Arc的场景漏掉了?
注意,这些规则必须写成能让 LLM 理解的形式。规则不是“检查错误处理”,而是类似这样:
检查每个返回 Result<T, E> 的公开函数,确认错误类型 E 是否实现了 std::error::Error。 如果没有实现,报告为 WARNING 级别问题。5.2 设计规则文件
我们可以在项目根目录创建一个llm-review-rules.md文件,作为团队的审查规则文档。这样规则本身也可以被 Code Review(meta review),后续修改也有迹可循。
示例内容如下:
# Code Review LLM 规则 本文件定义 LLM 辅助代码审查时需要执行的规则。 LLM 的输出仅作为 Reviewer 的参考,不直接作为合并依据。 ## R1: 错误类型规范 - 所有公开函数返回的 Result 错误类型必须实现 std::error::Error。 - 如果错误类型是自定义 enum,必须实现 Display 和 Error。 ## R2: unsafe 块安全性 - 每个 unsafe 块上方必须有一行注释,解释为什么这里的 unsafe 操作是安全的。 - unsafe 块内不得包含与该操作无关的代码。 ## R3: 文档完整性 - 所有 pub 函数必须包含文档注释。 - 如果函数可能 panic,文档必须包含 # Panics 部分。 - 如果函数返回 Result,文档必须包含 # Errors 部分。 ## R4: 生命周期参数 - 如果生命周期参数在函数签名中只出现一次,视为多余,应改用'_。这个文件的价值在于:它让 LLM 的检查行为变得可预期、可审计、可改进。如果 LLM 在某个规则上反复误报,团队可以直接修改规则描述,而不是靠运气调试提示词。
5.3 编写一个最小可用脚本
为了避免把 LLM 的 API 调用逻辑散落在 CI 配置里,更推荐写一个独立的 Python 脚本,负责:
- 获取 PR 变更的代码文件;
- 读取规则文件;
- 将规则和代码片段发送给 LLM;
- 解析 LLM 的响应,输出结构化的审查意见。
这里给出一个简化示例,重点演示流程,生产环境需要根据实际 LLM API 调整。
# 文件路径:tools/llm_review.py import json import os import subprocess import sys from openai import OpenAI RULES_FILE = "llm-review-rules.md" MODEL = os.getenv("LLM_REVIEW_MODEL", "gpt-4o-mini") def get_changed_rust_files(): """获取当前分支相对 main 分支变更的 Rust 文件列表""" result = subprocess.run( ["git", "diff", "--name-only", "origin/main..."], capture_output=True, text=True, check=True, ) files = [ line.strip() for line in result.stdout.splitlines() if line.strip().endswith(".rs") ] return files def read_rules(): with open(RULES_FILE, "r", encoding="utf-8") as f: return f.read() def review_file(client, rules, filepath): with open(filepath, "r", encoding="utf-8") as f: code = f.read() prompt = f"""你是 Rust 代码审查助手。请严格按照以下规则审查代码。 规则: {rules} 需要审查的文件:{filepath} 代码: ```rust {code}请以 JSON 数组格式输出审查结果,每个元素包含:
- "file": 文件名
- "line": 行号(如果可判断)
- "rule": 违反的规则编号
- "severity": "ERROR" 或 "WARNING"
- "message": 具体问题描述
- "suggestion": 修改建议
如果没有任何问题,输出空数组 []。 """
response = client.chat.completions.create( model=MODEL, messages=[{"role": "user", "content": prompt}], temperature=0, ) content = response.choices[0].message.content.strip() # 清理可能的 markdown 代码块标记 if content.startswith("```"): content = content.split("```")[1] if content.startswith("json"): content = content[4:] return json.loads(content)def main(): api_key = os.getenv("LLM_API_KEY") if not api_key: print("错误:请设置 LLM_API_KEY 环境变量", file=sys.stderr) sys.exit(1)
client = OpenAI(api_key=api_key) rules = read_rules() files = get_changed_rust_files() if not files: print("没有变更的 Rust 文件,跳过 LLM 审查。") return all_findings = [] for filepath in files: print(f"正在审查:{filepath}") try: findings = review_file(client, rules, filepath) all_findings.extend(findings) except Exception as e: print(f"审查 {filepath} 失败:{e}", file=sys.stderr) print(json.dumps(all_findings, indent=2, ensure_ascii=False))ifname== "main": main()
这段代码的核心逻辑是:**先读取团队规则,再逐文件调用 LLM,最后统一输出结构化结果。** 有几个细节值得解释: - `temperature=0` 是为了尽可能让 LLM 输出稳定,减少随机性。 - 要求输出 JSON 数组,是为了让结果可以直接被后续流程消费,比如自动评论到 PR 上。 - 用 `git diff` 只获取变更文件,而不是全仓库扫描,是为了控制成本和保证反馈速度。 - 对 LLM 的异常做了捕获,避免单个文件失败导致整个流程崩溃。 ### 5.4 集成到 GitHub Actions 有了脚本之后,可以把它接到 CI 流程里。下面是一个 GitHub Actions 配置示例: ```yaml # 文件路径:.github/workflows/llm-review.yml name: LLM Code Review on: pull_request: types: [opened, synchronize] jobs: llm-review: runs-on: ubuntu-latest steps: - name: 检出代码 uses: actions/checkout@v4 with: fetch-depth: 0 - name: 设置 Python uses: actions/setup-python@v5 with: python-version: "3.11" - name: 安装依赖 run: pip install openai - name: 运行 LLM 审查 env: LLM_API_KEY: ${{ secrets.LLM_API_KEY }} LLM_REVIEW_MODEL: ${{ vars.LLM_REVIEW_MODEL }} run: python tools/llm_review.py注意一个隐藏的坑:uses: actions/checkout@v4默认只检出单个提交,git diff origin/main...会失败。因此必须设置fetch-depth: 0,否则脚本拿不到完整的 Git 历史。
5.5 把结果写回 PR 评论
脚本输出 JSON 数组后,还需要一个步骤把结果发布到 PR 评论。上面actions/github-script做得比较干净:
- name: 发布审查结果到 PR uses: actions/github-script@v7 with: script: | const fs = require('fs'); const findings = JSON.parse(fs.readFileSync('llm_findings.json', 'utf8')); if (findings.length === 0) { console.log('LLM 审查未发现问题'); return; } let body = '## 🤖 LLM 辅助审查结果\n\n'; body += '> 以下结果由 LLM 根据团队规则自动生成,仅供 Reviewer 参考,不直接作为合并依据。\n\n'; for (const f of findings) { const icon = f.severity === 'ERROR' ? '🔴' : '🟡'; body += `${icon} **${f.file}:${f.line || '?'}** [${f.rule}] ${f.message}\n`; if (f.suggestion) { body += `> 建议:${f.suggestion}\n\n`; } } github.rest.issues.createComment({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, body: body });这里需要把 Python 脚本的输出保存到文件,而不是只打印到控制台。可以在main()的最后一行做一个小修改:
with open("llm_findings.json", "w", encoding="utf-8") as f: json.dump(all_findings, f, indent=2, ensure_ascii=False)这一步的意义在于:LLM 审查只是 Code Review 的第一个 Pass,而不是最后一个。把结果写回 PR 后,人类 Reviewer 仍然需要逐条确认,区分“真问题”和“误报”。
6. 运行结果与效果验证
脚本运行成功后,你会在控制台或 JSON 文件里看到类似下面的输出:
[ { "file": "src/storage/engine.rs", "line": 42, "rule": "R1", "severity": "ERROR", "message": "错误类型 StorageError 未实现 std::error::Error", "suggestion": "为 StorageError 实现 std::error::Error,可使用 thiserror 宏" }, { "file": "src/network/server.rs", "line": 87, "rule": "R2", "severity": "WARNING", "message": "unsafe 块缺少安全说明注释", "suggestion": "在 unsafe 块上方补充注释,解释为什么该操作是安全的" }, { "file": "src/api/client.rs", "line": 15, "rule": "R3", "severity": "WARNING", "message": "公开函数 connect 的文档缺少 # Errors 部分", "suggestion": "补充 # Errors 部分,说明可能返回的错误类型" } ]如何判断这套流程运行成功?可以从三个维度来看:
- 格式正确:LLM 返回了合法的 JSON,并且能被后续流程解析。
- 规则响应:对明显违反团队规则的新代码,LLM 给出了有效提示。比如你故意在一个
pub fn上缺失# Errors文档,规则 R3 应当能捕获。 - 误报可控:对符合规范的代码,LLM 没有输出大量无关建议。如果误报太多,需要回到规则文件,把描述写得更精确。
如果运行失败,第一步应该检查:
LLM_API_KEY是否设置正确;- 规则文件是否存在、格式是否正确;
git diff origin/main...是否能获取到变更文件;- LLM 返回的内容能否被
json.loads解析。如果模型返回了多余的文本,需要增强清理逻辑。
7. 常见问题与排查思路
在实际使用中,团队可能会遇到下面这些典型问题。我把它们整理成一张排查表:
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| LLM 返回的内容不是合法 JSON | 模型输出带有多余文本或 markdown 代码块 | 打印原始响应,检查清理逻辑 | 增强清理逻辑,或在 prompt 中强调“只输出 JSON” |
| 审查结果经常重复同一类误报 | 规则描述太模糊,模型理解偏差 | 对比误报案例,查看规则文本 | 把规则改得更具体,补充正面和反面示例 |
| 审查时间太长,CI 超时 | 变更文件太多,或模型选择过重 | 查看 CI 日志,统计耗时 | 限制文件数量、使用更快更小的模型、增加并发调用 |
| 漏报明显问题 | 规则覆盖不全,或 LLM 上下文窗口被截断 | 检查是否只审查了 diff 文件 | 完善规则清单;对超大文件做分段审查 |
| API 调用失败 | Key 过期、额度不足、网络问题 | 查看错误日志 | 配置重试机制,或接入备用模型 |
| 人类 Reviewer 不信任 LLM 结果 | LLM 评论与人工判断不一致 | 统计一段时间内的采纳率 | 在评论中标注“由 LLM 生成”,并逐步调整规则减少噪音 |
在这里要特别强调:LLM 规则的维护成本是真实的,不是一次配置就能一劳永逸。规则文件需要随着项目的演化持续调整。新引入的 crate、新的架构模式、新的团队约定,都需要映射到规则文件里。这个维护成本,本质上是在建设团队自己的“审查知识库”。
8. 最佳实践与工程建议
8.1 规则先行,工具其次
这是最重要的一条建议。不要先接 LLM,再想规则。先让团队坐下来,列出 Code Review 中最常见、最机械、最消耗时间的问题清单,然后写成规则文档。规则越具体,LLM 的效果越好。
一个好的规则应该具备三个特征:可判定、可操作、可举例。比如“检查错误处理是否正确”是一个不可判定的规则;而“所有公开函数返回的 Result 错误类型必须实现std::error::Error”就是一个可判定的规则,因为它有明确的是非标准。
8.2 LLM 只做第一遍,人类始终有否决权
无论 LLM 的输出看起来多专业,都要保持一个原则:LLM 是提议方,人类是决策方。PR 的合入权限永远不能交给 AI。在 CI 配置里,LLM 审查结果可以输出为 annotation,但不能设置为required check,更不能让它直接 approve PR。
8.3 控制成本和延迟
对大型项目,全量扫描成本很高。推荐的做法是:
- 只审查
git diff中的变更文件; - 按文件大小拆分,超过 500 行的文件分段审查;
- 选择延迟和成本合适的模型,小问题用轻量模型,复杂问题用强模型;
- 设置并发上限,避免触发 API 限流。
8.4 把规则文件当作代码来维护
llm-review-rules.md和普通代码一样需要版本管理、变更记录和 Review。建议放在仓库根目录,任何人修改规则都要触发一次 Code Review。规则变更后,可以拿历史 PR 做一次回归测试,看看新规则会不会引入大量误报。
8.5 建立反馈闭环
如果 LLM 的某个检查结果被人工 Reviewer 否决了,建议记录下来。一段时间后统计分析:哪些规则采纳率高、哪些规则经常误报、是否有重要的规律被遗漏。这些数据反过来驱动规则优化,形成一个可持续改进的闭环。
8.6 不要忽略隐私和数据安全
把代码发送给外部 LLM API 之前,需要确认代码是否包含敏感信息。涉及商业机密、安全密钥、未公开协议的项目,应该选择私有部署的模型,或者在发送前做脱敏处理。这一步在接入初期就要评估,不要等技术债积累后再补救。
9. Rust 生态里可配套使用的工具
LLM 规则并不是孤立存在的,它可以和 Rust 生态里现有的工具链组合使用,形成一套更完整的检查体系:
| 工具 | 解决的问题 | 与 LLM 规则的关系 |
|---|---|---|
cargo fmt | 代码格式统一 | 格式化问题交给工具,LLM 不需要管 |
clippy | 常见代码模式和潜在 bug | LLM 规则可以检查 clippy 覆盖不到的“团队约定” |
cargo-deny | 依赖许可证和安全漏洞 | 依赖问题交给工具,LLM 聚焦代码本身 |
cargo-geiger | 统计unsafe使用情况 | 定位有unsafe的文件后,用 LLM 规则检查安全性注释 |
sccache | 编译缓存 | 加速 CI,间接提升 LLM 审查流程的整体效率 |
这个组合思路的核心是:确定性工具负责确定性问题,LLM 负责需要理解的规则性问题。两者不是竞品,而是互补。如果一个规则能被clippy实现,就应该去写clippylint,而不是用 LLM;只有当规则需要理解代码语义和团队上下文时,才值得使用 LLM。
10. 对 Rust 开发者的现实建议
回到开头的问题:这套方案适合你的团队吗?
从 Rust 团队的实践来看,适合采用 LLM 规则的是那些已有明确工程规范、Code Review 压力巨大、且团队成员具备工具链改造能力的项目。如果团队还没有基础的行为规范,直接引入 LLM 规则只会增加噪音。
对个人开发者来说,这套思路同样有参考价值:你可以为自己的开源项目写一套 LLM 审查规则,每次提交 PR 前先跑一遍,让 LLM 扮演“第一轮 Reviewer”。这比直接让 AI 从头到尾审查整个项目要可靠得多,因为它有明确的目标和边界。
对 Rust 社区来说,这则消息真正的启示不是“AI 取代了人类审查”,而是**“人类对 AI 的使用方式正在变得更成熟”**。我们不再追求让 AI 做所有事,而是让 AI 做它擅长的事,同时把人类的精力留给真正需要人类判断的领域。
如果你打算在团队里尝试,建议从一个小范围开始:选一个模块,定三条规则,跑一个月,看看采纳率和误报率,再决定是否推广。这个循序渐进的过程,本身也是对 LLM 规则方案最理性的验证方式。
