基于LLM Agent的本地CLI代码评审工具:open-code-review实战 📅 发布时间:2026/9/20 18:01:25 👁 浏览次数: 1. 为什么我要自己动手做一个 open-code-review 工具团队里代码评审这件事说多了都是泪。项目一多人手一紧PR 堆成山谁都不想当那个“专职看 diff”的人。更麻烦的是评审质量完全看当天谁有空、谁心情好同一个空指针风险有人一眼扫过有人能揪出来写三条评论。时间一长代码规范形同虚设线上问题回溯时才发现当初那个“看起来没问题”的改动恰恰是事故源头。我想要的其实很简单一个能跑在本地、能接进 Git 工作流、能调用大模型做初步审查的命令行工具。它不需要多花哨核心就三件事——把改动抓出来、把上下文喂给模型、把有价值的意见按严重程度排好队返回给我。这就是 open-code-review 这个项目的出发点。它本质上是一个CLI 形态的代码评审助手底层靠 LLM Agent 驱动输入是 Git 仓库里的 diff输出是结构化的评审意见。适合谁来参考如果你是会写点脚本、日常用 Git 管代码、又对 LLM 调用有点概念的开发者那这篇内容基本可以照着抄。哪怕你只是刚装完 Git、还在研究git commit --amend怎么用也能看懂整体思路因为我会把每一步为什么这么做讲清楚。工具不是目的把评审这件事从“靠人肉”变成“有兜底”才是真正值钱的地方。2. 整体设计与技术选型拆解2.1 为什么是 CLI 而不是 Web 服务一开始我也想过做个网页版点一下按钮就出评审报告多直观。但真动手时发现代码评审这个动作天然发生在开发者的终端里。你刚git add完正准备git commit这时候切到浏览器、登录、粘贴 diff流程直接断掉。CLI 的好处是它能嵌进你已有的肌肉记忆git diff | ocr review或者干脆做成 Git hook提交前自动跑一遍。另一个现实原因是权限和隐私。代码是团队最敏感的东西走 Web 服务意味着要把 diff 传到别人的服务器上。CLI 跑在本地模型调用走你自己的 API Key中间不经过第三方中转心里踏实得多。这也是我坚持用本地 CLI 远程 LLM API这个组合的核心逻辑计算和编排在本地推理在远端边界清晰。2.2 LLM Agent 在这里扮演什么角色很多人把 Agent 和 LLM 混着说其实区别挺大。LLM 是那个“会说话的大脑”你给它一段 prompt它给你一段回复。Agent 则是在 LLM 外面套了一层“手脚”它能决定先做什么、再做什么能调用工具、能读文件、能根据上一步结果调整下一步。在 open-code-review 里Agent 的职责不是简单地把 diff 丢给模型而是先判断这次改动涉及哪些文件、哪些语言针对不同语言加载不同的评审规则比如 Go 要看 error 处理Python 要看可变默认参数把 diff 和必要的上下文比如被改函数的完整定义拼成合适的 prompt拿到模型回复后做结构化解析过滤掉“看起来不错”这种废话按严重程度排序输出到终端。这套流程如果只靠一次 LLM 调用效果会差很多因为模型看不到全局。Agent 的价值就在于它能把“看代码”这件事拆成可编排的步骤。至于底层用哪个模型DeepSeek、GPT 系列、Claude 系列都能接只要提供兼容 OpenAI 格式的接口就行。我实测下来DeepSeek 在中文注释和国内代码风格上表现挺稳成本也低适合日常跑。2.3 和 Git 的集成方式选择Git 提供了好几种拿 diff 的方式选哪种直接决定工具的易用性。我对比过三种方式命令适用场景缺点工作区 diffgit diff评审未暂存改动看不到已 add 的内容暂存区 diffgit diff --cached提交前评审需要先 add分支对比git diff main...HEAD评审整个 PR需要知道目标分支最后我选择让工具支持--staged、--branch、--commit三个参数默认走暂存区。原因是大多数人的习惯是git add之后、git commit之前做最后检查这个时间点最自然。另外还支持git worktree场景因为有些团队用 worktree 隔离不同任务diff 的基准目录会变工具需要能识别当前 worktree 的根路径。提示如果你的仓库用了core.quotepathfalse这类配置diff 里的中文文件名会正常显示否则会出现八进制转义。工具在解析路径时做了兼容处理但建议还是把git config --global core.quotepath false设上省得后面踩坑。3. 核心细节解析与实操要点3.1 diff 解析别小看这一步拿到git diff的输出只是开始真正难的是把它解析成结构化的“文件-块-行”模型。Git diff 的格式看着简单实际有一堆边界情况二进制文件、重命名、模式变更、行尾空格、\ No newline at end of file标记。我一开始用正则硬啃结果遇到重命名就崩了。后来改成按行状态机解析遇到diff --git开新文件遇到开新块遇到记新增行遇到-记删除行遇到空格记上下文。这样即使有重命名也能通过rename from/rename to正确识别。解析出来的结构大概长这样{ file: src/service/user.go, language: go, hunks: [ { header: -12,7 12,9 , added: [func GetUser(id int) (*User, error) {, if id 0 {], removed: [func GetUser(id int) *User {], context: [// 查询用户] } ] }为什么要这么细因为后面拼 prompt 时我需要知道哪些是新增、哪些是删除、哪些是上下文。模型对“新增了什么”和“删除了什么”的敏感度完全不同如果混在一起给它它容易把删除的代码当成现存代码来评审闹出笑话。3.2 prompt 构造决定评审质量的关键prompt 写得好不好直接决定模型是给你“这代码写得不错”还是“第 15 行缺少边界检查”。我的经验是分三层来写第一层是角色和任务说明明确告诉模型“你是一个严格的代码评审员只关注正确性、安全性和可维护性不要评论代码风格偏好”。第二层是评审规则按语言注入比如 Go 的规则里会强调 error 必须处理、goroutine 要有退出机制。第三层才是具体的 diff 内容。这里有个细节不要把整个文件都塞进去。我试过把完整文件给模型结果它开始评审没改动的代码噪音太大。正确做法是只给 diff 涉及的函数上下文一般取改动行前后各 20 行就够了。如果改动跨函数再额外把被调用函数的签名带上。注意prompt 里一定要加“如果改动没有问题返回空数组”这条。否则模型为了显得自己有用会硬凑几条无关痛痒的意见反而干扰判断。3.3 输出结构化让模型说人话也让人能读模型返回的自然是文本但工具需要的是结构化数据。我要求模型按 JSON 返回字段包括file、line、severity、message、suggestion。severity 分三档critical必须改有 bug 或安全问题、warning建议改可能有问题、info提示可选。实际跑下来模型偶尔会返回带 markdown 代码块的 JSON或者字段名拼错。所以解析层要做容错先尝试直接json.loads失败就剥掉 json 包裹再试再失败就用正则提取最外层大括号。这个容错逻辑我改了三四版才稳定早期版本经常因为模型多打一个逗号就整个崩掉。3.4 严重程度排序与去重模型返回的意见经常有重复比如同一个空指针风险它在两个 hunk 里各说一遍。去重逻辑按file line 归一化后的 message做 key保留 severity 最高的那条。排序则按 critical warning info同级按文件路径和行号排。这样终端输出时最该看的排在最前面不用翻半天。4. 实操过程与核心环节实现4.1 环境准备Git 和 Python 是基础先把 Git 装好这是前提。Windows 用户去官网下安装包一路默认就行记得勾选“Git Bash”和“Add to PATH”。装完在终端敲git --version能出版本号就成。Mac 用户brew install git更省事。配置方面至少设好用户名和邮箱git config --global user.name 你的名字 git config --global user.email 你的邮箱Python 建议 3.10 以上因为用到了match语法和一些新类型标注。依赖就三个openai调模型、rich终端输出好看点、pyyaml读配置文件。用虚拟环境装python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate pip install openai rich pyyaml4.2 配置文件设计把模型和规则解耦我不想把 API Key 和模型名硬编码在代码里所以设计了一个ocr.yaml配置文件放在项目根目录或用户主目录都行。结构大概这样model: provider: deepseek base_url: https://api.deepseek.com/v1 api_key: ${OCR_API_KEY} model_name: deepseek-chat temperature: 0.2 review: max_context_lines: 20 severity_threshold: info ignore_patterns: - *.md - vendor/* - *_test.go rules: go: - 所有 error 必须处理不能忽略 - goroutine 必须有退出机制 python: - 禁止使用可变对象作为默认参数api_key用环境变量注入避免明文写文件里。ignore_patterns用来跳过文档和第三方代码不然模型会对着 README 评头论足。temperature设 0.2 是为了让输出稳定评审这种任务不需要创造力。4.3 核心流程代码实现主流程其实不复杂按顺序走解析参数、读配置、拿 diff、解析 diff、构造 prompt、调模型、解析结果、输出。关键代码片段如下import subprocess import json from openai import OpenAI def get_diff(stagedTrue, branchNone): if branch: cmd [git, diff, f{branch}...HEAD] elif staged: cmd [git, diff, --cached] else: cmd [git, diff] result subprocess.run(cmd, capture_outputTrue, textTrue) return result.stdout def parse_diff(diff_text): files [] current_file None current_hunk None for line in diff_text.splitlines(): if line.startswith(diff --git): if current_file: files.append(current_file) current_file {file: , hunks: []} elif line.startswith( b/): current_file[file] line[6:] elif line.startswith(): current_hunk {header: line, added: [], removed: [], context: []} current_file[hunks].append(current_hunk) elif current_hunk is not None: if line.startswith() and not line.startswith(): current_hunk[added].append(line[1:]) elif line.startswith(-) and not line.startswith(---): current_hunk[removed].append(line[1:]) elif line.startswith( ): current_hunk[context].append(line[1:]) if current_file: files.append(current_file) return files调模型的部分我用 OpenAI 兼容接口这样换模型只改配置def review_with_llm(files, config): client OpenAI( api_keyconfig[model][api_key], base_urlconfig[model][base_url] ) prompt build_prompt(files, config) response client.chat.completions.create( modelconfig[model][model_name], messages[ {role: system, content: 你是一个严格的代码评审员。}, {role: user, content: prompt} ], temperatureconfig[model][temperature] ) return parse_response(response.choices[0].message.content)build_prompt里会把每个文件的 diff 和对应语言的规则拼起来parse_response负责容错解析 JSON。这两块代码加起来大概一百多行是整个工具的核心。4.4 终端输出让人愿意看输出用rich库做了颜色区分critical 红色、warning 黄色、info 蓝色。每条意见显示文件路径、行号、严重程度和具体描述。最后给一个汇总比如“本次评审发现 2 个严重问题、5 个警告”。如果没有任何问题就打印一行绿色的“未发现明显问题”让人心里舒服。实测下来一个中等规模的 PR改动 300 行左右从拿 diff 到出结果大概 15 到 30 秒主要时间花在模型推理上。如果嫌慢可以只评审 critical 和 warning跳过 info能省不少 token。5. 常见问题与排查技巧实录5.1 模型返回的不是合法 JSON这是最高频的问题。模型有时候会在 JSON 前后加解释文字或者用 markdown 代码块包起来。我的处理是三级容错先直接解析失败就找第一个{和最后一个}之间的内容再失败就用正则匹配file:\s*([^])这种字段逐个提取。实测三级容错能覆盖 95% 以上的情况。剩下 5% 基本是模型抽风直接跳过那条重试一次就行。5.2 diff 里有二进制文件导致解析异常Git diff 遇到二进制文件会输出Binary files a/xxx and b/xxx differ没有块。解析时如果发现某个文件没有 hunks直接跳过不要报错。另外图片、字体这类文件本来也不该进代码评审在ignore_patterns里加上*.png、*.woff之类的规则更省事。5.3 中文文件名显示成八进制这是 Git 的core.quotepath默认行为导致的。解决办法就一条命令git config --global core.quotepath false设完之后 diff 里的中文路径就正常了。工具层面我也做了兼容遇到\346\226\207这种转义会尝试解码但不如从源头解决来得干净。5.4 API 调用超时或限流模型接口偶尔会超时尤其是改动特别大的时候。我的做法是给每次调用设 60 秒超时失败后指数退避重试两次。如果还是失败就把这次评审标记为“未完成”提示用户稍后重试而不是直接崩溃。另外如果 diff 超过 5000 行建议分批评审按文件切分每次只发一个文件的改动避免单次请求过大。5.5 模型“幻觉”出不存在的问题有时候模型会指着一个正确的代码说“这里有空指针风险”其实它看错了上下文。这种情况没法完全避免但可以通过两个手段降低一是 prompt 里明确要求“只评审 diff 中新增或修改的行不要评审未改动的代码”二是把上下文行数控制在 20 行以内给太多上下文反而容易让模型分心。如果还是出现幻觉就在输出里标注“建议人工复核”不要直接当成结论。5.6 常见问题速查表问题现象可能原因解决办法解析 JSON 失败模型输出带额外文字启用三级容错解析二进制文件报错diff 无 hunk 块跳过无 hunk 的文件中文路径乱码quotepath 未关闭设core.quotepath falseAPI 超时改动过大或网络波动分批评审 重试机制评审意见重复同一问题跨 hunk按 filelinemessage 去重模型评审未改动代码上下文给太多限制上下文行数prompt 约束提示如果你在 Windows 上用 Git Bash路径分隔符和 Linux 不一样解析 b/时要注意反斜杠的情况。我踩过这个坑后来统一用os.path.normpath处理路径跨平台就稳了。6. 我踩过的坑和几条实在建议第一个坑是太依赖模型。早期版本我把模型返回的所有意见都原样输出结果有一次它把一个正常的类型断言标成 critical差点让同事白改半天。后来我加了 severity 阈值配置默认只显示 warning 以上info 级别的需要加--verbose才看。评审工具是辅助不是裁判这个定位得摆正。第二个坑是忽略 token 成本。一个大型 PR 动辄几千行 diff全量发给模型一次调用可能几毛钱一天跑几十次就是一笔开销。我的优化是按文件过滤vendor、node_modules、生成代码全部跳过只评审业务代码。另外把max_context_lines从 50 降到 20token 消耗直接少了三分之一效果几乎没差别。第三个坑是没做 Git hook 集成。工具做出来之后我手动跑了两周发现根本坚持不下来因为总会忘。后来写了个pre-commithook提交前自动跑一遍有问题就阻断提交。这一步之后工具才真正融入工作流。hook 脚本很简单#!/bin/sh ocr review --staged if [ $? -ne 0 ]; then echo 代码评审未通过请修复后再提交 exit 1 fi当然hook 不能太严格否则会让人烦。我的策略是只有 critical 问题才阻断提交warning 和 info 只提示不拦截。这样既起到了兜底作用又不会拖慢正常开发节奏。最后分享一个实用技巧如果你团队用 GitLab 或 Gitee 做 MR可以把 open-code-review 的输出格式化成 markdown 评论通过 API 自动贴到 MR 上。这样评审意见直接出现在代码旁边比在终端里看直观得多。我目前只做到了本地输出但接口已经预留好了后续接上应该不难。工具这东西先跑起来再慢慢打磨比一开始就追求完美强得多。