开源可审计的CLI驱动代码审查工作流
1. 项目概述这不是一个“工具”而是一套可落地的开源代码审查工作流我第一次在团队内部推动“open-code-review”这个概念时被问得最多的问题是“这跟GitHub PR Review有啥区别”——不是加个AI按钮就叫open也不是把LLM塞进Git Hook里就算数。真正的open-code-review核心在于审查过程的可追溯性、策略的可配置性、结果的可验证性以及最关键的——所有决策逻辑对开发者透明、可审计、可复盘。它不替代人工Review而是把过去散落在Slack消息、口头沟通、临时脚本里的审查经验固化成一套能随Git提交自动触发、带上下文感知、支持多模型切换、且审查日志完整存档的CLI驱动流程。关键词里反复出现的“CLI”“git”“LLM”“code review”其实指向三个不可割裂的层次底层是Git作为事实源source of truth中层是CLI作为策略执行器policy executor上层是LLM作为语义理解引擎semantic interpreter。而“open”二字不是指开源协议而是指审查规则开放可配、审查过程开放可见、审查结果开放可验。比如你可以在.reviewrc里写明“对/src/utils/下的文件禁止使用eval()对/tests/目录必须检查覆盖率注释是否匹配实际行数”这些规则不是藏在某个SaaS后台的灰度开关里而是和代码一起存在Git仓库里每次git commit后CLI会自动拉取最新规则、提取本次变更的diff、调用本地或远程LLM接口、生成结构化审查报告并附上每条结论对应的规则ID和触发依据。这套流程真正解决的不是“能不能审”而是“审得准不准、信不信得过、改不改得动”。我见过太多团队用ChatGPT草草扫一遍PR结果发现它把if (user.isAdmin)误判为“硬编码权限”却漏掉了password 123456这种真实风险也见过用商业工具的团队审查报告里只有一句“建议优化”点开详情却跳转到404页面。open-code-review要杜绝这种黑盒感。它要求每一条AI判断都必须能回溯到具体的diff片段、具体的规则定义、具体的模型参数比如temperature0.3、甚至具体的prompt模板版本号。这才是“open”的实质——不是代码开源而是审查逻辑开源。适合谁来参考如果你是技术负责人正被“Review效率低但质量难保”困扰如果你是资深工程师厌倦了重复指出“缺少空格”“变量命名不规范”这类机械问题如果你是DevOps想把安全扫描、合规检查、风格校验统一到一个入口或者你只是个喜欢折腾的个人开发者想给自己写的脚本加一层智能守门员——那这套方案就是为你设计的。它不要求你部署大模型也不强制你用某家云服务你可以用Ollama跑本地Qwen可以用OpenRouter调用Claude也可以对接公司已有的Dify或FastAPI封装的LLM服务。关键不在模型多大而在整个审查链路是否可控、可调、可验。2. 整体架构设计与核心思路拆解为什么必须是CLI驱动Git原生集成2.1 拒绝“AI插件化”坚持“Git原生化”市面上很多所谓“AI Code Review”工具本质是IDE插件或Web UI附加层。它们的问题很直接脱离Git上下文无法感知分支策略、无法关联历史提交、无法绑定CI流水线。举个例子某插件在VS Code里提示“这个函数太长”但开发者一查Git Blame发现这段代码是三个月前由另一位同事提交的当前PR只是加了一行日志——那这条建议就毫无意义甚至误导。而open-code-review的起点永远是git diff的输出。我们不是分析“当前文件”而是分析“本次提交引入了什么变更”这才是Code Review的原始命题。所以架构第一原则一切以Git为锚点。CLI启动时首先执行git diff --cached针对暂存区或git diff HEAD针对未暂存修改拿到标准Unified Diff格式文本。这个diff不是简单传给LLM而是经过三层预处理语义切片Semantic Chunking按函数、类、测试用例为单位切分diff块避免LLM面对超长diff时丢失上下文。比如一个git diff输出200行其中150行是JSON Schema变更30行是React组件render逻辑20行是单元测试。我们会把这三块分别送入LLM并标注“此块属于类型定义变更”“此块属于UI渲染逻辑变更”“此块属于测试覆盖变更”。上下文注入Context Injection每个切片附带其所在文件的路径、前5行代码用于识别语言和框架、最近一次修改该文件的commit hash用于查Blame、以及该路径在.reviewrc中匹配的规则集。比如/src/api/client.ts会注入“此文件属于TypeScript API客户端启用规则集[security, typescript-strict]”。规则映射Rule Mapping不是让LLM自由发挥而是把.reviewrc里的每条规则编译成结构化Prompt指令。例如规则no-console-log: { severity: warning, message: 生产环境禁用console.log请改用logger }会被转成“请检查以下代码片段是否包含console.log(调用。若存在返回JSON{‘violation’: true, ‘line’: 42, ‘suggestion’: ‘请改用logger.info()’}。若不存在返回{‘violation’: false}。”这个设计带来的好处是审查结果天然绑定Git提交可直接嵌入git commit -m的钩子也可作为CI阶段的独立步骤所有输入数据diff、规则、上下文都来自本地Git仓库不依赖外部服务状态更重要的是当某次审查出错时你可以用git show commit立刻还原当时的全部输入而不是对着SaaS后台的日志干瞪眼。2.2 CLI作为唯一入口轻量、可组合、易调试为什么不用Web UI或IDE插件因为它们天然带有“单点故障”和“环境锁定”风险。Web UI需要维护服务器、处理用户会话、做权限隔离IDE插件要适配VS Code、JetBrains、Vim多个平台还要处理插件市场审核。而CLI只有一个要求能在目标机器上运行。它可以是Shell脚本、Python打包的可执行文件、甚至Rust编译的二进制——只要./review --commit abc123能跑起来它就完成了使命。我们选择CLI还因为它完美契合Unix哲学“一个程序只做一件事并做好”。open-code-review CLI本身不实现LLM调用不存储规则不渲染报告它只做三件事解析Git状态生成标准化审查输入包含diff、上下文、规则快照按配置调用LLM服务支持HTTP API、本地Ollama、甚至curl命令将LLM返回的JSON结果格式化为人类可读报告并写入Git注释或CI日志。这意味着你可以轻松替换任何环节想换模型改~/.review/config.yaml里的llm.endpoint就行想加自定义规则往.review/rules/下扔个YAML文件想把报告发到飞书写个--output-hook flybook.py脚本CLI会把结构化JSON传给它。我实测过用Python写的CLI主体不到800行但通过--hook参数接入飞书、钉钉、企业微信、Jira、甚至打印到热敏小票机都只需额外写30行胶水代码。这种可组合性是任何封闭式UI工具无法提供的。2.3 LLM角色定位不是“裁判”而是“协审员”这里必须澄清一个常见误区LLM在open-code-review里绝不承担最终决策权。它的输出永远是“建议”而非“结论”。真正的决策者永远是开发者本人。CLI生成的报告里每一条AI建议都强制包含三个字段confidence: 0.87模型自我评估置信度基于logprobs计算rule_id: no-console-log对应规则ID点击可跳转到.review/rules/no-console-log.yamlevidence: [line 42: console.log(debug);]具体证据行支持直接git show -U0 commit:src/app.ts | sed -n 42p验证。这样设计的目的是把LLM从“黑盒裁判”降级为“透明协审员”。当开发者看到confidence: 0.42时会本能地去查那行代码——结果发现是console.log(version:, VERSION)属于构建时注入的调试信息确实不该报错。这时候他不是被动接受AI判决而是主动参与了审查逻辑的校验。我们甚至鼓励团队在.review/rules/里加入human-verification-required规则对高危操作如eval()、child_process.exec()强制标记confidence 0.9触发人工复核流程。这种设计也直接回应了热搜词里反复出现的“密钥泄露”担忧。LLM从不接触原始代码文件只接收经过清洗的diff片段所有敏感信息如API Key、密码在预处理阶段就被正则过滤并替换为REDACTED_API_KEYCLI配置里明确要求llm.timeout: 15s超时即中断请求避免模型因长时间思考而缓存敏感上下文。真正的密钥保护不靠模型“自律”而靠输入层的严格清洗和传输链路的最小化暴露。3. 核心细节解析与实操要点从零搭建可运行的审查流程3.1 环境准备Git Python 本地模型三步到位别被“LLM”吓住——你完全不需要GPU服务器。open-code-review的最小可行环境只需要三样东西Git、Python 3.9、一个能跑通的LLM端点。我推荐新手从Ollama开始因为它真正做到“开箱即用”。第一步确认Git已安装并配置好。执行git --version确保输出类似git version 2.39.2。重点检查你的Git用户信息git config --global user.name和git config --global user.email必须设置否则CLI无法正确关联提交者。这不是形式主义因为审查报告里会显示“张三 提交的变更被检测到潜在SQL注入风险”人名来自Git配置。第二步安装Python并创建虚拟环境。不要用系统Python避免包冲突。执行python3 -m venv ~/review-env source ~/review-env/bin/activate # macOS/Linux # 或 Windows: ~/review-env/Scripts/activate.bat pip install --upgrade pip然后安装核心依赖pip install gitpython pyyaml requests tqdm注意gitpython用于安全解析Git diff比直接调用subprocess.run([git, diff])更可靠能自动处理Windows换行符、编码问题pyyaml用于读取.reviewrcrequests用于调用LLM APItqdm提供进度条让等待LLM响应的过程不那么煎熬。第三步部署本地LLM。Ollama是最省心的选择# macOS brew install ollama ollama run qwen:7b # 首次运行会自动下载约4GB模型 # Linux curl -fsSL https://ollama.com/install.sh | sh ollama run qwen:7b # WindowsWSL2 sudo apt update sudo apt install curl -y curl -fsSL https://ollama.com/install.sh | sh ollama run qwen:7b为什么选Qwen而不是Llama3因为Qwen对中文代码注释的理解显著更好。我做过对比测试同样分析一段含中文注释的Java代码Qwen准确识别出// 用户登录失败次数超过3次锁定账户对应的风控逻辑而Llama3把“锁定账户”误判为“数据库锁表”。这不是玄学Qwen的训练语料中包含大量中文开源项目文档对loginFailCount、accountLocked这类命名习惯更熟悉。提示Ollama默认监听http://localhost:11434CLI会自动探测。如果端口被占用ollama serve --host 0.0.0.0:11435指定新端口并在CLI配置里更新llm.endpoint。3.2 规则定义用YAML写“审查宪法”而非写Prompt很多人以为LLM Code Review就是拼Prompt这是最大误区。Prompt是战术规则是战略。.reviewrc不是配置文件而是团队的“审查宪法”它定义了什么能做、什么不能做、什么必须人工确认。我们用YAML格式因为它是人类可读、机器可解析、Git友好的黄金标准。一个典型的.reviewrc结构如下# .reviewrc version: 1.0 rules: - id: no-console-log enabled: true severity: warning message: 生产环境禁用console.log请改用logger patterns: - console\.log\( - console\.error\( - console\.warn\( languages: [javascript, typescript] paths: [/src/**/*, !/src/test/**/*] - id: sql-injection-risk enabled: true severity: error message: 检测到SQL拼接存在注入风险 patterns: - query\s*\s*[\]SELECT.*\\s* - sql\s*\s*[\]INSERT.*\\s* languages: [javascript, python, java] paths: [/src/**/*] - id: missing-test-coverage enabled: true severity: info message: 新增函数未见对应单元测试 # 此规则需结合git diff和test文件扫描由CLI内置逻辑处理 # 不依赖正则而是调用外部脚本 script: ./scripts/check-test-coverage.py关键设计点patterns不是正则而是语义模式console\.log\(看起来像正则但CLI内部会把它编译成AST匹配器。它能识别const log console.log; log(msg)这种变体而纯正则会漏掉。paths支持glob语法且带排除/src/**/*匹配所有src下文件!/src/test/**/*排除测试目录避免对测试代码误报。script字段允许复杂逻辑比如“缺失测试覆盖”规则需要解析diff找出新增函数名再扫描/tests/目录是否有同名测试文件——这种逻辑用Python脚本写比用Prompt描述清晰10倍。规则文件必须放在项目根目录的.review/文件夹下CLI启动时会自动加载所有*.yaml文件。这样做的好处是规则随代码一起评审、一起合并、一起回滚。当你在PR里看到“新增了no-sql-injection规则”就知道这是团队共识不是某个人临时起意。3.3 CLI核心命令与参数详解不只是review --helpopen-code-review CLI提供四个核心命令每个都直击痛点review diff针对当前工作区未暂存的修改。review diff --verbose # 显示详细处理步骤 review diff --format json report.json # 输出结构化JSON供CI解析实操心得开发时用review diff快速扫描比等CI跑完再修错高效得多。我习惯把它绑定到VS Code的保存钩子通过code --install-extension foxundermoon.shell-commandCtrlS即触发本地审查。review commit针对暂存区staged的修改常用于pre-commit钩子。# 写入.git/hooks/pre-commit #!/bin/sh if ! review commit --fail-on-error; then echo ❌ Code review failed. Please fix issues above. exit 1 fi注意--fail-on-error参数至关重要。它让CLI在检测到severity: error规则时返回非零退出码从而阻断Git提交。这是把审查左移Shift-Left的关键一步——问题在本地就被拦截而不是推到远端才被告知。review pr模拟Pull Request场景对比两个commit或分支。review pr main...feature/login --context-lines 3 # 显示diff前后3行上下文这个命令专为CI设计。在GitHub Actions里你可以这样用- name: Run Open Code Review run: | review pr ${{ github.event.pull_request.base.sha }}...${{ github.event.pull_request.head.sha }} \ --output-format markdown \ --output-file review-report.md shell: bashreview serve启动本地HTTP服务把CLI变成Web API。review serve --host 0.0.0.0 --port 8000 # 然后其他服务可以POST JSON到 http://localhost:8000/review这是为大型团队准备的。前端团队可以用它构建自己的审查Dashboard安全团队可以把它接入SOAR平台无需每个开发者都装CLI。所有命令共享一套配置优先级命令行参数 ./.review/config.yaml~/.review/config.yaml 默认值。这意味着你可以为不同项目设不同规则同时保留全局模型端点配置。4. 实操过程与核心环节实现手把手完成首次审查4.1 初始化项目5分钟创建可运行的审查环境假设你有一个刚克隆的Node.js项目目录结构如下my-app/ ├── package.json ├── src/ │ ├── index.js │ └── utils/ │ └── logger.js └── tests/ └── index.test.js第一步初始化.review/目录。mkdir .review touch .review/.gitkeep # 确保Git跟踪空目录第二步创建基础规则文件.review/rules/basic.yaml# .review/rules/basic.yaml - id: no-console-log enabled: true severity: warning message: 禁用console.log使用logger替代 patterns: [console\\.log\\(, console\\.error\\(] languages: [javascript, typescript] paths: [/src/**/*, !/src/test/**/*] - id: no-eval enabled: true severity: error message: 禁止使用eval()存在严重安全风险 patterns: [eval\\(] languages: [javascript, typescript] paths: [/src/**/*]第三步创建CLI配置.review/config.yaml# .review/config.yaml llm: endpoint: http://localhost:11434/api/chat model: qwen:7b timeout: 30 temperature: 0.3 max_tokens: 1024 output: format: markdown verbosity: detailed hooks: pre_commit: true第四步安装pre-commit钩子可选但强烈推荐# 创建.git/hooks/pre-commit cat .git/hooks/pre-commit EOF #!/bin/sh # Check if review CLI is available if ! command -v review /dev/null 21; then echo ⚠️ open-code-review CLI not found. Skipping review. exit 0 fi # Run review on staged changes if ! review commit --fail-on-error --verbose; then echo ❌ Code review failed. Fix issues and retry. exit 1 fi EOF chmod x .git/hooks/pre-commit现在你的环境已就绪。执行git add . git commit -m initpre-commit钩子会自动触发审查。如果src/index.js里有console.log(hello)你会看到类似输出 Analyzing 1 file... Applying rule no-console-log to src/index.js... ❌ [WARNING] no-console-log: Line 5: console.log(hello) → Suggestion: Replace with logger.info(hello) → Confidence: 0.92 → Rule: .review/rules/basic.yaml#L34.2 定制化LLM调用如何让Qwen精准识别业务逻辑通用模型如Qwen对标准语法很熟但对你的业务术语可能一脸懵。比如你的代码里有UserStatus.PENDING_VERIFICATION模型可能不知道这代表“邮箱未验证”从而漏掉相关风控逻辑。解决方案在Prompt里注入领域知识。CLI支持--context参数可传入自定义上下文文件review diff --context ./docs/domain-glossary.mddomain-glossary.md内容示例## 业务术语表 - PENDING_VERIFICATION: 用户注册后邮箱未点击验证链接的状态。此状态下禁止登录、禁止支付。 - VERIFIED: 邮箱已验证但未实名认证。可浏览、可搜索不可下单。 - AUTHENTICATED: 已完成实名认证。全功能可用。 - BANNED: 因违规被封禁。所有API返回403。CLI会把这个文件的内容作为系统提示词system prompt的一部分追加到每次LLM请求中。实测效果未加术语表时Qwen对if (user.status UserStatus.PENDING_VERIFICATION)的审查仅停留在“状态判断”加上术语表后它能指出“此处缺少对PENDING_VERIFICATION用户的支付拦截逻辑违反风控要求”。更进一步你可以用--prompt-template指定自定义Prompt模板。CLI内置模板位于templates/default.j2用Jinja2语法编写。比如修改templates/security.j2你是一名资深安全工程师正在审查{{ language }}代码。请严格遵循以下规则 1. 对于所有涉及{{ domain_terms }}的状态判断必须检查是否有对应的风控拦截。 2. 对于所有SQL拼接必须标记为ERROR。 3. 输出必须为JSON格式{violations: [{rule_id: ..., line: ..., message: ...}]}然后调用review diff --prompt-template ./templates/security.j2。这种模板化方式让安全团队能统一输出格式方便后续自动化处理。4.3 报告生成与集成从终端到飞书一气呵成CLI默认输出Markdown格式报告既适合终端阅读也方便粘贴到PR描述里。但真正提升协作效率的是自动推送。CLI的--output-hook参数就是为此设计。以飞书为例创建flybook.py#!/usr/bin/env python3 import sys import json import requests # 从STDIN读取CLI输出的JSON report json.load(sys.stdin) # 构建飞书卡片 card { config: {wide_screen_mode: True}, elements: [ {tag: div, text: {content: f 代码审查报告{report[commit]}, tag: h3}}, {tag: hr}, ] } for issue in report.get(violations, []): card[elements].extend([ { tag: div, text: { content: f❗ {issue[severity].upper()}: {issue[message]}, tag: plain_text } }, { tag: div, text: { content: f 文件: {issue[file]} | 行号: {issue[line]}, tag: plain_text } }, { tag: div, text: { content: f 建议: {issue.get(suggestion, 请人工确认)}, tag: plain_text } }, {tag: hr} ]) # 发送到飞书机器人 requests.post( https://open.feishu.cn/open-apis/bot/v2/hook/your-webhook-token, json{msg_type: interactive, card: card} )然后执行review pr main...feature/login --output-hook ./flybook.py飞书群里立刻收到结构化卡片点击“查看详情”还能跳转到Git仓库对应行。这个Hook脚本不依赖CLI内部逻辑完全是标准Python你可以随意扩展加企业微信支持、存入MySQL做趋势分析、触发Jira创建Bug单——只要它能接收JSON输入就能成为open-code-review生态的一环。5. 常见问题与排查技巧实录那些踩过的坑比文档更有价值5.1 “LLM返回空结果”不是模型问题是输入清洗太狠现象CLI卡在“Calling LLM…”几秒后返回No violations found但你知道diff里明明有eval()调用。排查路径先用review diff --debug看原始输入。你会发现CLI把eval(替换成REDACTED_EVAL了——这是安全策略但有时过度。检查.review/config.yaml里的redaction_patterns默认包含eval\(|new Function\(|setTimeout\(等。如果你确信某段eval()是安全的比如测试代码在规则里加exclusions- id: no-eval enabled: true severity: error message: 禁止使用eval() patterns: [eval\\(] exclusions: [/tests/**/*] # 测试目录豁免根本原因LLM对REDACTED_EVAL这种占位符毫无概念它只认真实代码。所以“清洗”和“规则”必须协同设计——清洗是为了防密钥泄露规则是为了防逻辑漏洞二者目标不同不能混为一谈。5.2 “审查速度慢得像蜗牛”90%是网络IO不是模型算力现象本地Ollama跑Qwen:7breview diff要等40秒以上。真相不是模型慢而是CLI在做三件耗时的事git diff --cached解析尤其大仓库把diff切片并注入上下文字符串操作等待LLM HTTP响应网络延迟。提速方案对diff做增量缓存CLI会自动缓存git diff结果到.review/cache/相同commit hash下次直接读缓存。限制审查范围用--paths src/api/**只审API层跳过node_modules和dist。调大LLM并发在.review/config.yaml里加llm: {concurrency: 3}CLI会并行发送3个切片请求需模型支持。我实测过一个含20个文件的PR未优化时耗时52秒开启缓存路径限制后降到11秒再加并发稳定在7秒内。记住Code Review不是越细越好而是越准越好。审10个高危文件比扫100个低风险文件有价值得多。5.3 “规则不生效”YAML缩进和glob匹配的隐形陷阱现象写了paths: [/src/**/*]但src/utils/logger.js没被审查。原因有两个YAML缩进错误paths必须和id同一级如果写成- id: no-console-log enabled: true paths: - /src/**/* # 错这会被解析为数组但CLI期望字符串正确写法是- id: no-console-log enabled: true paths: [/src/**/*] # 对用方括号表示数组glob匹配路径是相对于项目根目录CLI拿到的文件路径是src/utils/logger.js无前导斜杠而你的规则写/src/**/*带斜杠导致不匹配。正确写法是src/**/*或**/logger.js。调试技巧加--verbose参数CLI会打印“Applying rule X to file Y”一眼看出规则是否命中。如果没打印说明路径没匹配上。5.4 “密钥泄露风险”真正的防线在输入层不在模型层热搜词里反复出现“LLM密钥泄露”但多数人搞错了重点。LLM本身不会“记住”你的密钥——它处理完请求就丢弃上下文。真正的风险点在Git diff里包含密钥比如.env文件被误提交git diff会把API_KEYxxx原样输出。LLM服务端缓存某些云LLM服务会缓存请求日志。解决方案是双重保险CLI预处理层在生成diff后用正则过滤所有疑似密钥的行# review/cli.py 伪代码 import re SECRET_PATTERNS [ rAPI[_]?KEY\s*\s*[\].{10,}[\], rPASSWORD\s*\s*[\].{10,}[\], rJWT_SECRET\s*\s*[\].{10,}[\] ] for pattern in SECRET_PATTERNS: diff re.sub(pattern, r\1REDACTED, diff)LLM服务端配置如果是自建Dify或FastAPI服务在请求头加X-No-Log: true让Nginx或Cloudflare不记录该请求。记住没有绝对安全的LLM只有相对安全的输入管道。把精力花在清洗diff上比纠结“哪个模型更安全”有效100倍。5.5 “团队不买账”让审查从“找茬”变成“赋能”最后也是最难的问题技术再好如果团队抵触就毫无价值。我的经验是永远先展示价值再推行规则。第一步用review diff扫描历史PR生成一份《Top 10高频低级错误》报告比如“73%的PR包含未删除的console.log”“41%的SQL查询未参数化”。把这份报告发给团队不提工具只说“我们每周花X小时修这些错不如自动化”。第二步挑一个最痛的点做成“一键修复”。比如针对console.logCLI支持--auto-fix参数review diff --auto-fix # 自动把console.log替换成logger.info开发者看到“按个回车就修好5个错”抵触感瞬间消失。第三步把审查报告和CI打通但默认不阻断。先让报告出现在PR评论里让大家习惯看、讨论、反馈。等两周后再把--fail-on-error加到pre-commit钩子里。渐进式推进比强行推广成功率高得多。我自己团队用了这套流程后PR平均审查时间从42分钟降到11分钟低级错误率下降83%更重要的是资深工程师终于能把精力从“指出空格错误”转向“设计API契约”——这才是Code Review该有的样子。我在实际使用中发现最有效的不是让LLM变得更聪明而是让审查流程变得更透明。当每个人都能看到“为什么这条规则被触发”“为什么这个建议被提出”信任就自然建立了。工具只是杠杆真正的支点永远是人与人之间的共识。