基于LLM Agent的轻量级CLI代码评审工具:open-code-review实战
2026/9/20 8:39:06 网站建设 项目流程

1. 为什么我要自己搭一套 open-code-review

团队里代码评审这件事,说多了都是泪。早些年我们靠人肉盯,谁提交了 MR 就拉个群吼一声,然后大家抽空去看。结果就是:小改动没人愿意看,大改动看不过来,关键逻辑全靠某一个人把关,他一休假整个流程就瘫了。后来也试过一些现成的代码评审工具,要么太重、要么太贵、要么把代码传到别人服务器上心里不踏实。

open-code-review这个项目,就是在这个背景下我自己动手攒出来的一套东西。一句话概括:它是一个跑在本地或内网、以命令行方式驱动、把 Git 变更喂给大模型做初审、再由人来拍板的轻量代码评审流水线。核心关键词就三个——code reviewCLILLM Agent,底座是Git

它能干什么?你写完代码git commit之后,敲一行命令,它自动把这次改动(或者某个分支相对主干的全部改动)抽出来,按文件、按 hunk 切好,交给配置好的大模型 Agent 逐块分析,输出一份带严重程度分级、带行号定位、带修改建议的评审报告。你拿着这份报告,五分钟就能过一遍,把明显的问题先筛掉,剩下的再人工细看。

适合谁参考?三类人最合适。第一类是中小团队的技术负责人,想给团队加一道自动化防线,又不想引入重型平台;第二类是独立开发者或小作坊,没人帮你 review,让模型先当个陪练;第三类是想搞明白 LLM Agent 到底怎么落地到工程流程里的人,这个项目麻雀虽小,从 Git 取数、上下文裁剪、Prompt 组装到结果解析,整条链路都齐了,拿来当学习样本非常合适。

我下面会把整套东西拆开讲:整体设计怎么想的、核心环节怎么实现的、实操怎么跑起来、踩过哪些坑。你照着抄作业,基本能复现一个七八成的东西出来。

2. 整体设计与思路拆解

2.1 为什么是 CLI 而不是 Web 服务

一开始我也想过做个 Web 界面,左边 diff 右边评论,看着多体面。但真动手之前我盘了一下使用场景,发现大部分评审动作发生在两个地方:本地终端CI 流水线。这两个地方,CLI 都是最自然的入口。

本地终端里,开发者刚写完代码,git status一看,顺手就敲ocr review,不用切窗口、不用登录、不用等页面加载。CI 流水线里更是如此,一个 shell 步骤就能调起来,失败了直接卡住合并,比调 HTTP 接口简单太多。Web 服务反而带来一堆额外负担:要部署、要鉴权、要维护前端、要考虑并发,收益却不成正比。

所以定下来的第一原则就是:CLI 优先,输出纯文本加结构化 JSON 双通道。人看的走彩色终端输出,机器消费的走 JSON,方便后续接飞书、接 CI 评论、接任何你想接的地方。

提示:CLI 工具一定要把"人读"和"机读"两种输出分开设计。人读的要好看、要能定位到行;机读的要稳定、字段要固定。混在一起后期必翻车。

2.2 为什么用 LLM Agent 而不是传统静态扫描

传统静态扫描工具(各种 linter、各种 SAST)我用了很多年,它们的强项是规则明确的问题:空指针、未使用变量、明显的注入模式。但代码评审里真正值钱的那部分——命名是否达意、抽象层次是否合理、这段逻辑是不是重复造轮子、边界条件有没有漏——这些规则写不出来,或者说写出来维护成本高到离谱。

大模型恰好补的就是这块。它能读懂语义,能结合上下文判断"这个函数名和它实际干的事对不上",能指出"这里少考虑了一个空数组的情况"。所以我把它定位成初审员而不是终审法官:它负责把可疑的地方标出来,人来决定改不改。

这里要澄清几个常被搞混的概念,因为热词里问的人特别多。AI 模型是底层能力,比如 DeepSeek、GPT 系列、Claude 系列,它们本质是"输入文本、输出文本"的函数。LLM就是 Large Language Model,大语言模型,是 AI 模型里专门处理语言的那一类,DeepSeek 就属于 LLM。Agent是在 LLM 之上加了一层"能自己决定下一步做什么"的调度逻辑——它会调工具、会多轮思考、会根据结果调整策略。Embedding是另一回事,它把文本变成向量,用于相似度检索,跟生成不是一条路。搞清这几个词,你才知道自己在搭什么。

open-code-review里,LLM 是引擎,Agent 是驾驶员,Git 是油箱,CLI 是方向盘。

2.3 上下文怎么裁剪才不爆 token

这是整个项目最核心的工程问题。一个中等规模的功能分支,diff 动辄几千行,全塞给模型,token 直接爆炸,而且模型注意力会被稀释,评审质量反而下降。

我的策略是三层裁剪

第一层,按文件过滤。锁文件、自动生成的代码、纯资源文件、.min.js这类,直接跳过。这些文件评审没意义,纯浪费 token。配置文件里维护一个 glob 列表就行。

第二层,按 hunk 切分。Git diff 本身就是按 hunk(变更块)组织的,每个 hunk 自带上下文行。我以 hunk 为最小评审单元,而不是整个文件。这样每个评审请求的输入是可控的,通常几百行以内。

第三层,按相关性排序。如果 hunk 数量太多,超出预算,就按"改动行数 + 文件重要性"排序,优先评审核心业务代码,把测试文件、文档往后放。这个排序逻辑我放在配置里,可以按项目调。

注意:裁剪不是越狠越好。上下文太少,模型会误判。比如一个变量在 hunk 外被重新赋值,你只给它 hunk 内的代码,它就会说"这个变量没被修改过"。所以每个 hunk 我至少保留前后各 3 行上下文,这是实测下来比较稳的值。

2.4 结果怎么结构化,才能接下游

模型输出的是自然语言,直接给人看没问题,但要接 CI、接飞书、接评论系统,就必须结构化。我的做法是强制模型输出 JSON,schema 固定:

{ "file": "src/order/service.py", "line": 142, "severity": "high", "category": "logic", "message": "这里对空列表没有做判断,后续 reduce 会抛异常", "suggestion": "在进入 reduce 前加 if not items: return 0" }

severity分三档:high(必须改,逻辑错误、安全问题)、medium(建议改,可维护性)、low(可选,风格)。categorylogicsecurityperformancestylemaintainability。有了这两个字段,下游就能做各种过滤和聚合。

模型有时候不听话,会输出带 markdown 代码块的 JSON,或者字段名写错。所以解析层必须做容错:先尝试直接 parse,失败就剥掉代码块标记再 parse,再失败就用正则兜底提取。这个容错逻辑我单独抽了一个模块,后面讲实现时会细说。

3. 核心细节解析与实操要点

3.1 Git 取数的几种姿势

open-code-review要评审的"变更"有几种来源,对应不同的 Git 命令,这块必须搞清楚,不然取错数据后面全白搭。

场景一:评审最近一次提交。git show HEAD或者git diff HEAD~1 HEAD。前者带提交信息,后者更纯粹。我一般用git diff HEAD~1 HEAD --unified=3--unified=3控制上下文行数。

场景二:评审当前工作区未提交的改动。git diff(工作区 vs 暂存区)加git diff --cached(暂存区 vs HEAD)。两个都要,因为有人习惯git add之后再改。

场景三:评审整个分支相对主干的改动。git diff main...HEAD。注意这里是三个点,表示"从共同祖先到 HEAD",比两个点更准确,能排除主干上别人提交的干扰。

场景四:评审某个 MR/PR。如果平台支持,直接拉平台的 diff;不支持就用git diff base...head

这里有个坑,热词里也有人问:git -c diff.mnemonicprefix=false -c core.quotepath=false --no-optional-locks这一长串是干嘛的。diff.mnemonicprefix=false是让 diff 里的前缀统一成a/b/,方便脚本解析;core.quotepath=false是让中文文件名不被转义成八进制;--no-optional-locks是避免 Git 去抢索引锁,在并发场景下很有用。我在脚本里默认带上这几个参数,省得后面处理各种奇葩输出。

3.2 怎么把 diff 解析成结构化数据

Git diff 是文本,要变成程序能处理的结构,得解析。格式大概长这样:

diff --git a/src/a.py b/src/a.py index 1234567..89abcde 100644 --- a/src/a.py +++ b/src/a.py @@ -10,6 +10,8 @@ def foo(): context line -removed line +added line +another added line context line

解析的关键是识别diff --git开头的文件块,和@@开头的 hunk 头。hunk 头里的-10,6 +10,8表示旧文件从第 10 行开始 6 行,新文件从第 10 行开始 8 行。这个信息很重要,因为模型报的行号是相对 hunk 的,要换算成文件绝对行号才能定位。

我踩过一个坑:二进制文件的 diff 和重命名文件的 diff 格式不一样。二进制文件只有Binary files ... differ一行,重命名文件有rename from/rename to。解析器必须能识别并跳过这些,不然会崩。所以解析逻辑里我加了类型判断,遇到非文本变更直接标记为"跳过评审"。

3.3 Prompt 怎么设计才让模型说人话

Prompt 是灵魂。我前后改了十几版,总结出几条硬经验。

第一,角色要具体。不要写"你是一个代码评审助手",太泛。我写的是"你是一位有十年经验的资深工程师,正在评审同事的代码,你的目标是找出真正会导致 bug 或维护困难的问题,而不是挑风格毛病"。角色越具体,输出越聚焦。

第二,输出格式要死板。我会在 prompt 里明确给出 JSON schema,并且强调"只输出 JSON,不要任何解释文字,不要 markdown 代码块"。虽然模型偶尔还是会加代码块,但明确要求能大幅降低概率。

第三,给例子。Few-shot 效果非常明显。我在 prompt 里塞两三个正例,展示什么样的输出是合格的。比如一个空指针的例子、一个命名不清的例子。模型看到例子,格式和粒度都会对齐。

第四,明确"不要报什么"。这条最容易被忽略。我会写"不要报纯格式问题(缩进、空格),不要报 import 顺序,不要报你无法确定的问题"。不写这些,模型会给你报一堆鸡毛蒜皮,淹没真正重要的。

提示:prompt 里的"不要做什么"往往比"要做什么"更重要。模型天生倾向于多报,你得给它划边界。

3.4 严重程度怎么定级

定级标准必须写死在 prompt 里,否则模型每次的尺度都不一样。我的标准:

级别判定标准典型例子
high会导致运行时错误、数据错误、安全问题空指针、越界、SQL 拼接、竞态
medium不影响正确性但影响可维护性、性能重复代码、过长函数、N+1 查询
low风格、命名、注释类变量名不达意、缺注释

这个表我直接放进 prompt,让模型对照着判。实测下来,定级一致性比不给标准时高很多。当然还是会有偏差,所以我在解析层加了一个后处理:如果模型把明显的空指针判成 medium,我会根据关键词(比如 message 里出现"空"、"null"、"越界")自动升到 high。这是兜底,不是主力。

4. 实操过程与核心环节实现

4.1 环境准备:Git 和 CLI 基础

先把地基打好。Git 的安装和配置是绕不开的,热词里问的人特别多,我快速过一遍关键点。

Windows 上装 Git,去官网下安装包,一路下一步就行,但有两个选项要注意:默认编辑器建议选你顺手的(我选 VS Code),PATH 环境变量选"Git from the command line and also from 3rd-party software",这样终端里才能直接用git。装完在终端敲git --version,能出版本号就成。

配置这块,最少要设两样:

git config --global user.name "你的名字" git config --global user.email "你的邮箱"

如果要连 Gitee 或 GitHub,还得配 SSH 密钥。ssh-keygen -t ed25519 -C "你的邮箱"生成密钥,然后把~/.ssh/id_ed25519.pub的内容贴到平台的 SSH 设置里。测试连通性用ssh -T git@gitee.com

open-code-review本身是个 CLI 工具,我用 Python 写的,因为生态成熟、上手快。依赖就几个:click做命令行解析,rich做终端美化,openai或对应厂商的 SDK 做模型调用,pygit2或者直接 subprocess 调 git 命令。我选的是 subprocess,因为不想引入 libgit2 的编译依赖,直接调系统 git 更省事。

pip install click rich openai

4.2 核心命令设计

CLI 的命令设计要符合直觉。我定了这么几个子命令:

ocr review # 评审当前工作区改动 ocr review --staged # 只评审暂存区 ocr review --commit HEAD~1 # 评审指定提交 ocr review --branch main # 评审当前分支相对 main ocr config # 查看/设置配置 ocr report --format json # 输出 JSON 报告

主命令review的流程是:取 diff → 解析 → 过滤 → 切 hunk → 逐个调模型 → 聚合结果 → 输出。每一步都可以通过参数控制,比如--skip-tests跳过测试文件,--max-hunks 20限制评审块数。

配置我放在~/.ocr/config.toml,内容包括模型 API 地址、密钥、模型名、过滤规则、prompt 模板路径。密钥不写死在代码里,从环境变量读,这是基本安全习惯。

4.3 取 diff 和解析的实现

取 diff 这块,核心就是一个 subprocess 调用:

import subprocess def get_diff(mode="worktree", base=None): args = ["git", "-c", "diff.mnemonicprefix=false", "-c", "core.quotepath=false", "--no-optional-locks", "diff"] if mode == "staged": args.append("--cached") elif mode == "commit": args.append(f"{base}~1") args.append(base) elif mode == "branch": args.append(f"{base}...HEAD") args.append("--unified=3") result = subprocess.run(args, capture_output=True, text=True) return result.stdout

解析器我写成一个状态机,逐行扫描。遇到diff --git开新文件,遇到@@开新 hunk,遇到+记新增行,遇到-记删除行。这里要注意+++---是文件头不是内容行,得排除。还有\ No newline at end of file这种特殊行,也要处理。

解析出来的结构大概是这样:

{ "file": "src/a.py", "hunks": [ { "old_start": 10, "old_lines": 6, "new_start": 10, "new_lines": 8, "lines": [{"type": "add", "content": "...", "new_lineno": 12}, ...] } ] }

有了new_lineno,模型报的行号就能直接对上文件里的真实位置,输出报告时能精确指到行。

4.4 调模型和解析结果

调模型这块,我用的是标准的 chat completions 接口。关键是把 prompt 组装好:

def build_prompt(hunk, file_path): template = load_template("review_prompt.txt") return template.format( file=file_path, diff=render_hunk(hunk), schema=JSON_SCHEMA )

调用的时候设置temperature=0.2,低温度让输出更稳定。max_tokens给足,因为 JSON 输出可能比较长。超时设 60 秒,网络不好时重试两次。

结果解析是重灾区。模型输出可能是纯 JSON、带 ```json 包裹的、带前后解释文字的。我的解析函数按顺序尝试:

def parse_response(text): # 尝试直接解析 try: return json.loads(text) except json.JSONDecodeError: pass # 剥掉代码块标记 match = re.search(r"```(?:json)?\s*(.*?)```", text, re.DOTALL) if match: try: return json.loads(match.group(1)) except json.JSONDecodeError: pass # 正则兜底提取第一个 JSON 对象 match = re.search(r"\{.*\}", text, re.DOTALL) if match: try: return json.loads(match.group(0)) except json.JSONDecodeError: pass return None # 彻底失败,记录日志

解析失败的 hunk 我会记到日志里,不阻塞整体流程。评审报告里会标注"该块解析失败,建议人工查看"。

4.5 输出报告

终端输出用rich做彩色渲染,high 用红色,medium 用黄色,low 用灰色。每个问题显示文件、行号、级别、描述、建议。最后给一个汇总:本次评审 N 个文件、M 个 hunk、发现 X 个 high、Y 个 medium。

JSON 输出就是原始结构,方便接飞书机器人或者 CI 评论。接飞书的话,把 JSON 转成卡片消息格式,一个 webhook 就发出去了。这块热词里问"codex cli 接入飞书"的人不少,思路是一样的:CLI 产出结构化数据,webhook 负责投递。

5. 常见问题与排查技巧实录

5.1 模型报的行号对不上

这是最高频的问题。原因通常是模型数错了行,或者它报的是 hunk 内相对行号。我的解法是不信任模型报的行号,而是让它报"锚点文本"——也就是问题所在的那行代码原文。然后我在解析层用锚点文本去 hunk 里匹配,匹配到哪行就是哪行。这样即使模型数错行,只要它引用的代码是对的,定位就准。

如果锚点文本也匹配不到(模型改写了代码),就退回到它报的行号,并在报告里标注"行号可能不准"。

5.2 模型把没问题的地方报成问题

误报是 LLM 评审的固有毛病。缓解手段有几个:一是 prompt 里强调"只报你有把握的问题";二是加一个置信度字段,让模型自评,低于阈值的直接过滤;三是二次确认,对 high 级别的问题再调一次模型,问它"你确定吗,给出理由",两次都确认才保留。

我实测下来,二次确认能把误报率降一半左右,代价是 token 翻倍。所以只对 high 级别做,medium 和 low 不做。

5.3 token 超限和费用失控

一个分支几百个 hunk,全跑一遍费用不低。控制手段:限制单次评审的 hunk 数,超出部分提示用户分批;缓存结果,同一个 commit 的同一个 hunk 评审过就存起来,下次直接读缓存;用小模型做初筛,大模型只处理初筛有问题的块。

缓存这块我用文件系统做,key 是 hunk 内容的 hash,value 是评审结果。简单有效,不用引入 Redis。

5.4 常见问题速查表

现象可能原因排查方向
报错找不到 gitPATH 没配好终端敲git --version验证
diff 为空分支名写错或没改动手动跑git diff对比
中文文件名乱码core.quotepath=false检查 git 调用参数
模型返回非 JSONprompt 约束不够加强格式要求,加 few-shot
行号偏移模型数错行改用锚点文本匹配
费用异常高hunk 太多没限制--max-hunks参数
并发时索引锁冲突多进程同时跑 git--no-optional-locks

5.5 几个独家避坑心得

心得一:先跑通再优化。我一开始就想做多模型投票、做缓存、做并发,结果卡了两周没跑通。后来砍到最小可用——单模型、串行、无缓存——一天就跑通了,然后再逐步加。这个顺序很重要。

心得二:prompt 要版本管理。prompt 改动对结果影响巨大,必须像代码一样管理。我放在 Git 仓库里,每次改动记 commit message,出问题能回滚。

心得三:留人工兜底。无论模型多准,最终合并决策必须是人。我在 CI 里把评审结果作为"评论"而不是"阻断",除非是 high 级别且人工确认过,才阻断合并。这样既享受自动化,又不被误报卡死。

心得四:注意 Git worktree 场景。热词里有人问git worktree,如果你在 worktree 里跑评审,路径解析要注意,因为.git是个文件不是目录。我的做法是统一用git rev-parse --show-toplevel拿仓库根目录,不自己拼路径。

心得五:git commit --amend之后要重新评审。amend 会改写提交,之前的评审结果作废。我在工具里加了检测,如果 HEAD 的 hash 变了,缓存自动失效。

6. 后续可以怎么扩展

这套东西跑顺之后,能扩展的方向不少。比如接入更多模型做对比,同一个 hunk 让两个模型各评一遍,取交集作为高置信问题。比如做历史学习,把人工最终采纳和拒绝的评审结果存下来,作为 few-shot 例子动态注入 prompt,让模型越来越懂你们团队的偏好。再比如接 IDE 插件,在 VS Code 里直接显示评审结果,不用切终端。

我个人在实际操作中的体会是:这类工具的价值不在于"替代人",而在于"把人从重复劳动里解放出来"。模型帮你把 80% 的明显问题筛掉,你只需要专注那 20% 真正需要判断力的部分。这个定位想清楚了,工具就好用了,也不会因为偶尔的误报就否定它。

最后分享一个小技巧:刚开始用的时候,别急着接 CI,先在本地跑一两周,你自己手动核对模型的每一条意见,看看它的准确率和偏好。摸清脾气之后,再决定哪些级别可以自动阻断、哪些只做提示。这个磨合期省不得,省了后面一定出乱子。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询