open-code-review:基于CLI与LLM Agent的Git代码审查自动化实践
代码审查这件事做过的都懂——它重要但真到了日复一日的开发节奏里很容易变成走过场。要么是 reviewer 扫两眼就点了 approve要么是 PR 堆了三天没人看要么是 review 意见全是这里加个空格变量名改一下这种鸡毛蒜皮。我见过太多团队在 code review 上投入了时间但收效甚微问题不在于人不够认真而在于流程本身缺少一个稳定的、不依赖人状态的第一道过滤网。open-code-review这个项目本质上就是在解决这个问题把 code review 的第一轮筛查交给一个跑在命令行里的 LLM Agent让它先过一遍 diff把明显的问题、风格偏差、潜在 bug 标出来人再做第二轮的深度判断。关键词里出现的 CLI、LLM Agent、Git 三个词恰好勾勒出了它的技术轮廓——一个基于命令行的、由大模型驱动的、深度集成 Git 工作流的自动化审查工具。这篇文章我会从它的设计动机讲起拆解它怎么跟 Git 打交道、怎么组织 LLM 的审查逻辑、怎么在 CLI 里把结果呈现得让人愿意看最后聊聊实际落地时会踩的那些坑。不管你是想直接用这类工具还是想自己搭一个类似的 Agent这里面的思路和细节应该都能对上号。1. 为什么 code review 需要一个 CLI 形态的 LLM Agent1.1 人工 review 的三个结构性缺陷先说清楚问题才能理解方案。人工 code review 有三个绕不开的缺陷跟团队水平无关是结构性的。第一个是注意力衰减。一个 reviewer 上午看第三个 PR 的时候专注度跟第一个完全不是一个量级。研究数据我就不引了你自己回想一下周五下午 review 代码的状态就明白了。第二个是覆盖不均。人天然会优先看自己熟悉的模块对不熟的代码要么跳过要么浅看导致某些角落长期没人认真审。第三个是反馈延迟。PR 提出来到拿到第一条有意义的评论中间可能隔几个小时甚至一天作者的上下文已经凉了改起来效率极低。这三个缺陷叠加起来结果就是review 变成了一种仪式感真正被拦下来的问题比例低得可怜。而 LLM 恰好在这三件事上都有优势——它不会累、不会挑模块、响应是秒级的。所以把 LLM 引入 review 流程不是要替代人而是要让它承担那个永远在线、永远认真、永远第一时间响应的第一轮筛查角色。1.2 为什么是 CLI 而不是 Web 平台或 IDE 插件那为什么形态选 CLI现在 IDE 插件、Web 平台都能做类似的事CLI 的优势在哪核心在于工作流的贴合度。开发者的代码操作——commit、diff、branch、merge——绝大部分发生在终端里。如果一个审查工具需要你切到浏览器、登录、找到对应的 PR、等页面加载那它的使用频率一定会掉。而 CLI 工具可以直接嵌进你已有的 Git 工作流git diff之后管道接一个审查命令或者 pre-commit hook 里挂一下甚至 CI 里跑一遍。它不改变你的习惯只是在你已有的动作上加了一层。另一个原因是可组合性。CLI 工具的输出是纯文本可以被 grep、被重定向、被其他脚本消费。你可以把审查结果写进文件、发给同事、存进日志。Web 平台做不到这种自由度。还有一点是本地上下文——CLI 跑在本地能直接读到完整的仓库状态、未提交的改动、本地分支的差异这些是云端平台拿不到的。提示CLI 形态的代价是学习曲线。你得熟悉命令行、得配环境、得理解参数。但对于已经习惯终端工作流的开发者来说这个成本几乎为零。1.3 LLM Agent 和普通 LLM 调用的区别在哪这里要澄清一个概念。热词里有人问agent 和 llm 和 ai 模型有什么区别放到这个项目里特别关键。普通的 LLM 调用是一问一答你给它一段代码它给你一段评论结束。而 Agent 是有目标、有工具、有循环的。open-code-review作为一个 Agent它需要自己决定去看哪些文件工具调用、自己判断 diff 的上下文够不够可能需要读完整文件、自己决定要不要追问比如发现一个函数被改了但没看到调用方、最后汇总成一份结构化的报告。这个感知-决策-行动的循环才是 Agent 的本质。打个比方普通 LLM 调用像你拿着一张照片问朋友这照片拍得咋样Agent 像你请了个摄影师朋友来家里他自己会翻你的相册、看你的相机设置、问你拍摄意图最后给你一份完整的改进建议。区别不在于模型本身多强而在于它有没有自主使用工具、自主推进任务的能力。2. 拆解 open-code-review 的核心工作链路2.1 从 Git diff 到审查输入数据是怎么流动的整个工具的第一步是把 Git 的变更变成 LLM 能理解的输入。这一步看着简单其实坑最多。最直接的做法是git diff拿原始 diff。但原始 diff 有几个问题一是上下文不足diff 只显示改动行前后各三行LLM 看不到完整的函数逻辑二是格式噪音diff 里的、-、这些标记对 LLM 来说是干扰三是大 diff 爆炸一个重构 PR 可能几千行 diff直接塞给 LLM 会超上下文窗口。所以一个成熟的实现通常会做几层处理。第一层是解析 diff 结构把每个 hunk 拆出来识别出改动的文件、函数、行号范围。第二层是按需补充上下文对于改动所在的函数把完整函数体读出来附上对于被调用的接口可能还要去读定义。第三层是分块和优先级排序把大 diff 切成若干块按改动的重要程度比如核心逻辑 vs 注释排序必要时分批送审。# 典型的 diff 获取命令注意 --unified 控制上下文行数 git diff --unified10 HEAD~1 HEAD -- *.py *.js # 只看暂存区的改动 git diff --cached # 对比两个分支 git diff main...feature-branch这里有个实操细节--unified10比默认的 3 行上下文要好能让 LLM 看到更多周边逻辑但也不能太大否则 token 消耗飙升。我一般用 8 到 15 之间具体看代码风格。2.2 审查提示词的设计怎么让 LLM 说人话拿到输入之后核心就是提示词。这块决定了审查质量的上限。一个常见的错误是提示词写得太泛请审查以下代码找出问题。 这种提示词 LLM 会给你一堆正确的废话。好的提示词需要明确审查维度、给出判断标准、约束输出格式。审查维度一般分几类正确性逻辑 bug、边界条件、空指针、安全性注入、越权、敏感信息泄露、性能不必要的循环、N1 查询、可维护性命名、复杂度、重复代码、一致性是否符合项目规范。每个维度都要给出具体的判断标准比如边界条件要明确说检查数组越界、空集合、除零、整数溢出。输出格式的约束同样重要。我倾向于让 LLM 输出结构化的 JSON每个问题包含文件路径、行号、严重级别、问题描述、修改建议。这样后续可以程序化处理渲染成表格或者写进 CI 报告。{ file: src/auth/login.py, line: 42, severity: high, category: security, issue: 密码比较使用了 而非恒定时间比较存在时序攻击风险, suggestion: 使用 hmac.compare_digest() 替代 }注意让 LLM 输出 JSON 时一定要在提示词里给一个完整的示例并且明确说只输出 JSON不要有任何其他文字。否则它经常会在 JSON 前后加一段好的以下是我的审查结果之类的废话导致解析失败。2.3 多轮审查与自我校验减少误报的关键单轮 LLM 审查最大的问题是误报。它会因为看不到完整上下文而误判会因为过度谨慎而把正常代码标成问题。减少误报的核心手段是多轮校验。第一轮让 LLM 自由发挥把所有可疑点都列出来。第二轮针对每个可疑点让 LLM 重新审视这个改动是否真的有问题请给出支持你判断的具体代码证据。 第三轮做去重和合并把同一处的多个评论合并成一条。还有一种做法是角色分离一个 Agent 负责找问题另一个 Agent 负责反驳。找问题的说这里有空指针风险反驳的说上游已经做了非空校验见第 30 行。两个 Agent 辩论几轮最后留下的才是高置信度的问题。这个思路在学术上叫 self-consistency 或者 debate实测下来对降低误报确实有效。2.4 结果呈现CLI 里怎么把审查报告做得让人愿意看CLI 的输出体验经常被忽视但它直接决定了工具会不会被持续使用。如果输出是一大坨纯文本没人愿意看。好的 CLI 输出应该做到几点颜色区分严重级别红黄绿、按文件分组、行号可点击在支持的终端里、摘要在前详情在后。比如开头先给一个总览本次审查发现 3 个高危、5 个中危、2 个低危问题涉及 4 个文件。 然后按文件展开每个问题一行格式是[严重级别] 文件:行号 - 问题描述。# 输出示例伪代码示意 open-code-review --diff HEAD~1 # 输出 # Review Summary: 3 high, 5 medium, 2 low across 4 files # # src/auth/login.py # [HIGH] L42 密码比较存在时序攻击风险 # [MED] L58 错误信息泄露了用户是否存在 # # src/api/user.py # [HIGH] L120 未校验用户权限直接返回数据如果终端支持还可以用 ANSI 转义码做颜色用 OSC 8 做超链接点击直接跳到编辑器对应行。这些细节看着小但用起来体验差别很大。3. 把 open-code-review 接进真实 Git 工作流3.1 本地 pre-commit 钩子提交前的最后一道关最轻量的接入方式是 pre-commit 钩子。在git commit之前自动跑一遍审查有问题就拦下来。# .git/hooks/pre-commit #!/bin/bash open-code-review --staged --fail-on high if [ $? -ne 0 ]; then echo 发现高危问题提交已阻止。使用 git commit --no-verify 可跳过。 exit 1 fi这里的关键是--fail-on high参数只有高危问题才阻止提交中低危只提示不拦截。否则开发者会被烦死最后所有人都加--no-verify钩子形同虚设。提示pre-commit 钩子默认不会被 clone 到别人机器上需要用 husky、pre-commit 框架或者手动脚本分发。团队协作时这点要注意。3.2 CI 集成PR 自动审查的完整配置CI 里跑审查是更正式的用法。每次 PR 提交自动跑一遍把结果作为评论贴到 PR 上。# .github/workflows/review.yml name: Code Review on: [pull_request] jobs: review: runs-on: ubuntu-latest steps: - uses: actions/checkoutv4 with: fetch-depth: 0 - name: Run open-code-review run: | open-code-review --base origin/main --head HEAD --format markdown review.md - name: Post comment uses: actions/github-scriptv7 with: script: | const fs require(fs); const body fs.readFileSync(review.md, utf8); github.rest.issues.createComment({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, body: body });fetch-depth: 0很关键默认的浅克隆拿不到完整历史diff 会算错。这个坑我踩过排查了半天才发现是 checkout 的问题。3.3 和 git worktree 配合多分支并行审查热词里出现了git worktree这个跟 code review 配合起来有奇效。git worktree允许你在同一个仓库里同时检出多个分支到不同目录互不干扰。# 为待审查的分支创建一个独立工作区 git worktree add ../review-feature feature-branch cd ../review-feature open-code-review --base main --head feature-branch这样做的好处是审查过程不会污染你当前的工作目录你可以在主目录继续开发审查在另一个目录跑。对于需要频繁切换分支审查的场景这个组合能省不少事。3.4 审查结果的持久化与追踪审查结果如果只是打印出来就丢了价值有限。更好的做法是持久化形成可追踪的记录。可以按 commit hash 存成文件比如.review/commit-hash.json然后写个脚本统计哪些文件被反复标记问题、哪类问题出现频率最高、修复的平均时长是多少。这些数据积累起来能反过来指导团队的规范制定——如果某个类型的 bug 反复出现说明需要在编码规范或者 lint 规则里加一条。4. 实测中暴露的问题与应对策略4.1 误报率LLM 审查绕不开的坎误报是这个方案最大的痛点。实测下来不加任何约束的 LLM 审查误报率能到 30% 以上。常见的误报类型有几种。一是上下文缺失导致的误判。LLM 看到user.getName()没做空校验就报警但实际上上游已经保证了 user 非空。二是过度谨慎。把正常的try-catch标成吞异常把合理的魔法数字标成硬编码。三是风格偏好。LLM 有自己的编码审美会把不符合它偏好的写法标成问题。应对策略前面提过多轮校验这里补充几个实操技巧。给 LLM 喂项目规范把团队的编码规范文档作为系统提示词的一部分让它按你的标准审而不是按它的默认标准。提供反例在提示词里明确说以下情况不要报警上游已校验、框架已处理、测试代码。设置置信度阈值让 LLM 给每个问题打一个置信度分低于阈值的直接丢弃。4.2 大仓库的性能问题token 和时间的双重压力大仓库的 diff 可能非常大直接送审会超 token 限制分批送审又慢。实测一个几千行的重构 PR完整审查要几分钟token 消耗也很可观。优化思路有几个。只审改动行不审整个文件这是最基本的。按文件类型过滤锁文件、生成代码、测试快照这些直接跳过。增量审查只审自上次审查以来新增的改动而不是每次都全量。并行处理多个文件同时送审用并发换时间。# 并行审查的简化示意 from concurrent.futures import ThreadPoolExecutor def review_file(file_diff): return llm_review(file_diff) with ThreadPoolExecutor(max_workers5) as executor: results list(executor.map(review_file, file_diffs))并发数不能太高否则会触发 API 的速率限制。5 到 10 之间比较稳妥。4.3 敏感信息泄露本地审查 vs 云端 API 的取舍这是个必须严肃对待的问题。代码送审意味着代码要离开你的机器如果用的是云端 LLM API代码就上传到了第三方。对于有保密要求的项目这是红线。应对方案有几种。用本地模型比如在本地跑一个开源模型代码不出机器代价是审查质量可能不如云端大模型。脱敏处理送审前把敏感信息密钥、内部域名、用户数据替换成占位符。只送审 diff 的抽象描述不送原始代码但这会损失很多细节。自建审查服务把模型部署在自己的服务器上走内网调用。注意无论用哪种方案都要在团队里明确规则——哪些代码可以送审、哪些绝对不行。这个规则最好写进工具配置里用白名单/黑名单强制约束而不是靠人自觉。4.4 开发者信任怎么让人愿意用而不是绕过最后一个坑是人的问题。如果开发者觉得这个工具烦不准拖慢我提交他们会想尽办法绕过——加--no-verify、把钩子删了、在 CI 里加 skip 标记。建立信任的关键是先证明价值。初期不要设拦截只做提示让开发者自己看到哦这个工具确实抓到了我没注意的问题。等信任建立起来再逐步加拦截规则。允许反馈每个审查结果旁边给个误报按钮收集反馈持续优化提示词。控制频率不要每次提交都跑全量审查只在 PR 或者关键分支上跑。我自己的经验是工具上线第一个月只做提示不拦截第二个月开始对高危问题拦截第三个月才全面铺开。这个节奏比一上来就强推要顺得多。5. 从 open-code-review 延伸出的几个思考5.1 LLM Agent 做 code review 的能力边界在哪用下来我的判断是LLM Agent 擅长的是模式识别类的问题——明显的 bug、常见的安全漏洞、风格不一致、重复代码。它不擅长的是架构判断——这个抽象是否合理、这个模块划分是否恰当、这个设计是否能支撑未来的扩展。前者是看得见的问题后者需要理解业务背景和长期演进LLM 目前还差得远。所以正确的定位是让 LLM 做第一轮筛查把机械性的、模式化的问题过滤掉人专注于架构和业务逻辑的审查。这样人的时间花在刀刃上整体 review 质量反而会提升。5.2 和传统 lint 工具的关系互补而非替代有人会问这跟 ESLint、Pylint 这些传统 lint 工具什么关系答案是互补。Lint 工具基于规则确定性强、零误报规则写对了的话、速度快但只能查规则覆盖到的问题。LLM 基于理解能查规则覆盖不到的、需要语义理解的问题但误报率高、速度慢、成本高。合理的组合是lint 先跑把机械问题拦掉LLM 再跑查语义层面的问题人最后审看架构和业务。5.3 未来可能的演进方向从技术趋势看这类工具可能会往几个方向走。一是更强的仓库级理解不只是看 diff而是理解整个代码库的结构和依赖关系做出更准确的判断。二是个性化学习团队的 review 习惯和历史评论逐渐贴合团队的风格。三是修复建议的自动化不只是指出问题还能直接生成 patch人 review 后一键应用。四是多 Agent 协作安全 Agent、性能 Agent、风格 Agent 各司其职最后汇总。不过这些都是后话。眼下最实在的还是把第一轮筛查做扎实把误报率压下去让开发者真正觉得这东西有用。工具的价值不在于多先进而在于能不能稳定地解决一个真实的问题。我个人在实际落地这类工具时的体会是技术实现只占三成剩下七成是流程设计和人的磨合。一个误报率 20% 但团队愿意用的工具价值远大于一个误报率 5% 但没人用的工具。先把人搞定再谈技术优化。