ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

基于LLM Agent的轻量级CLI代码评审工具:open-code-review实战

基于LLM Agent的轻量级CLI代码评审工具:open-code-review实战 1. 为什么我要自己搭一套 open-code-review团队里代码评审这件事说多了都是泪。早些年我们靠人肉盯谁提交了 MR 就拉个群吼一声然后大家抽空去看。结果就是小改动没人愿意看大改动看不过来关键逻辑全靠某一个人把关他一休假整个流程就瘫了。后来也试过一些现成的代码评审工具要么太重、要么太贵、要么把代码传到别人服务器上心里不踏实。open-code-review这个项目就是在这个背景下我自己动手攒出来的一套东西。一句话概括它是一个跑在本地或内网、以命令行方式驱动、把 Git 变更喂给大模型做初审、再由人来拍板的轻量代码评审流水线。核心关键词就三个——code review、CLI、LLM 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、接飞书、接评论系统就必须结构化。我的做法是强制模型输出 JSONschema 固定{ file: src/order/service.py, line: 142, severity: high, category: logic, message: 这里对空列表没有做判断后续 reduce 会抛异常, suggestion: 在进入 reduce 前加 if not items: return 0 }severity分三档high必须改逻辑错误、安全问题、medium建议改可维护性、low可选风格。category分logic、security、performance、style、maintainability。有了这两个字段下游就能做各种过滤和聚合。模型有时候不听话会输出带 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 --unified3--unified3控制上下文行数。场景二评审当前工作区未提交的改动。用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.mnemonicprefixfalse -c core.quotepathfalse --no-optional-locks这一长串是干嘛的。diff.mnemonicprefixfalse是让 diff 里的前缀统一成a/b/方便脚本解析core.quotepathfalse是让中文文件名不被转义成八进制--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不影响正确性但影响可维护性、性能重复代码、过长函数、N1 查询low风格、命名、注释类变量名不达意、缺注释这个表我直接放进 prompt让模型对照着判。实测下来定级一致性比不给标准时高很多。当然还是会有偏差所以我在解析层加了一个后处理如果模型把明显的空指针判成 medium我会根据关键词比如 message 里出现空、null、越界自动升到 high。这是兜底不是主力。4. 实操过程与核心环节实现4.1 环境准备Git 和 CLI 基础先把地基打好。Git 的安装和配置是绕不开的热词里问的人特别多我快速过一遍关键点。Windows 上装 Git去官网下安装包一路下一步就行但有两个选项要注意默认编辑器建议选你顺手的我选 VS CodePATH 环境变量选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 gitgitee.com。open-code-review本身是个 CLI 工具我用 Python 写的因为生态成熟、上手快。依赖就几个click做命令行解析rich做终端美化openai或对应厂商的 SDK 做模型调用pygit2或者直接 subprocess 调 git 命令。我选的是 subprocess因为不想引入 libgit2 的编译依赖直接调系统 git 更省事。pip install click rich openai4.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(modeworktree, baseNone): args [git, -c, diff.mnemonicprefixfalse, -c, core.quotepathfalse, --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(--unified3) result subprocess.run(args, capture_outputTrue, textTrue) 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( filefile_path, diffrender_hunk(hunk), schemaJSON_SCHEMA )调用的时候设置temperature0.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 内容的 hashvalue 是评审结果。简单有效不用引入 Redis。5.4 常见问题速查表现象可能原因排查方向报错找不到 gitPATH 没配好终端敲git --version验证diff 为空分支名写错或没改动手动跑git diff对比中文文件名乱码缺core.quotepathfalse检查 git 调用参数模型返回非 JSONprompt 约束不够加强格式要求加 few-shot行号偏移模型数错行改用锚点文本匹配费用异常高hunk 太多没限制加--max-hunks参数并发时索引锁冲突多进程同时跑 git加--no-optional-locks5.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先在本地跑一两周你自己手动核对模型的每一条意见看看它的准确率和偏好。摸清脾气之后再决定哪些级别可以自动阻断、哪些只做提示。这个磨合期省不得省了后面一定出乱子。
返回列表