open-code-review:面向Git Diff的可验证代码审查CLI工具
1. 项目概述这不是又一个代码审查工具而是一次开发流程的底层重构“open-code-review”这个名字乍看平平无奇甚至有点拗口——它不像“SonarQube”那样自带权威感也不像“CodeClimate”那样暗示商业成熟度。但正是这个看似朴素的命名藏着一个关键信号它不打算做闭源SaaS服务的替代品而是要成为开发者本地工作流中可拆解、可审计、可复写的审查单元。我第一次在GitHub Trending上看到它时没点进去看README先做了三件事git clone、ls -la、cat pyproject.toml。结果发现它连pyproject.toml里都只声明了rich、typer、gitpython三个依赖——没有LLM SDK没有云认证模块没有埋点上报。那一刻我就知道这玩意儿不是来卷功能的是来卷“可信边界”的。核心关键词“open-code-review”必须放在上下文里理解这里的“open”不是指开源协议虽然它确实是MIT而是指审查过程的完全可见性、审查逻辑的完全可干预性、审查结论的完全可追溯性。它和传统CI/CD里的静态扫描工具如Semgrep、ESLint本质不同——后者输出的是规则匹配结果而open-code-review输出的是基于当前diff语义的推理链。比如你改了一行SQL拼接逻辑传统工具可能报“SQL注入风险”而它会生成类似这样的审查注释“检测到user_input直接拼入query字符串line 42未经过sqlite3.escape_string()或参数化查询处理。根据Python sqlite3文档第3.2节此模式在用户可控输入下存在注入风险。建议改用cursor.execute(SELECT * FROM users WHERE name ?, (user_input,))。”你看它没说“风险等级高”也没弹个红框警告而是把依据来源文档章节、错误位置line 42、修复方案带具体API调用、甚至为什么这个方案有效参数化查询的隔离机制全部摊开。这种输出方式天然适配“LLM Agent”这个热词背后的真实诉求——不是让AI替你写代码而是让AI成为你思维过程的可验证镜像。至于“CLI tool”这个标签它拒绝做成VS Code插件或Web界面是有明确工程判断的所有审查必须发生在Git工作区的本地上下文中。当你执行ocrr review --sinceHEAD~3时工具直接读取.git/objects/里的原始blob解析出diff的AST变更树再把变更节点喂给本地运行的LLM比如Ollama里的Phi-3或Qwen2。整个过程不上传任何代码片段到外部API不依赖网络连接甚至断网状态下仍能完成基础语义分析靠内置规则引擎兜底。这解释了为什么它能和“Git diffs”强绑定——不是简单地调用git diff命令而是把diff当作程序分析的第一手输入源而非事后补救的文本快照。适合谁来用不是CTO在技术选型会上评估而是每天要处理5个PR的中级后端工程师不是刚学Python的学生而是正在重构遗留Java单体、需要快速定位“哪段Spring Bean配置被悄悄覆盖了”的架构师不是追求全自动化的团队而是信奉“AI辅助决策人类保留最终否决权”的务实派。它解决的从来不是“有没有人审代码”而是“为什么这个修改被判定为危险我的同事是否能五分钟内验证这个结论”这个更本质的问题。2. 核心设计哲学为什么放弃Web UI、云服务和预训练模型2.1 拒绝黑箱审查逻辑必须可调试、可打断、可重放几乎所有主流代码审查工具包括GitHub Copilot Reviews都遵循一个隐含假设审查结论是原子性的、不可分割的。你看到一条“潜在NPE风险”的提示点击展开最多看到“变量X在第Y行可能为空”但无法知道这个判断是来自规则库匹配还是LLM对上下文的推断更无法介入中间过程。open-code-review彻底推翻这个范式——它的核心数据结构不是“review result”而是“review trace”。举个实际例子上周我用它审查一个Django视图函数的修改。原代码def user_profile(request): user User.objects.get(idrequest.GET.get(id)) return render(request, profile.html, {user: user})修改后def user_profile(request): user_id request.GET.get(id) if not user_id: return HttpResponse(ID required, status400) user User.objects.get(iduser_id) return render(request, profile.html, {user: user})执行ocrr review --diff后它生成的trace文件JSONL格式包含这样一段{ step: ast_diff_analysis, input: {old_node: Call(funcAttribute(valueName(idUser), attrobjects), args[...]), new_node: Assign(targets[Name(iduser_id)], valueCall(funcAttribute(...))}, output: {node_type_change: Call - Assign, context_loss: [request.GET.get() result no longer directly passed to get()]} }紧接着下一步{ step: security_rule_check, input: {node_type_change: Call - Assign, context_loss: [request.GET.get() result no longer directly passed to get()]}, output: {rule_match: CWE-798, confidence: 0.82, evidence: User input now assigned to variable before DB call, but no validation on user_id type or range} }最后一步才是LLM推理{ step: llm_reasoning, input: {rule_match: CWE-798, evidence: User input now assigned to variable before DB call..., code_context: ...}, output: {reasoning_chain: [1. user_id is string from GET param, 2. User.objects.get(id...) expects int or str, 3. No cast to int, so SQL injection still possible via 1 OR 11], suggestion: Add int(user_id) with try/except, or use Djangos built-in get_object_or_404} }这个trace链条意味着什么意味着你可以在ast_diff_analysis步骤后加断点用pdb检查AST节点是否正确解析替换security_rule_check模块接入自己公司内部的CWE映射表把llm_reasoning的prompt模板从templates/security.jinja里抽出来改成符合团队术语习惯的表述比如把“CWE-798”替换成“内部安全规范第3.2条”。提示trace文件默认保存在.ocrr/traces/下按commit hash命名。执行ocrr replay --trace 23a1f8 --step llm_reasoning可单独重放某一步这对调试LLM提示词失效特别有用——比在Web界面上反复点击“重新生成”高效十倍。2.2 CLI即契约为什么坚持命令行而不是做VS Code插件很多人第一反应是“做个插件不更方便”但open-code-review的作者在issue #142里写过一句很硬的话“IDE插件是开发体验的终点CLI是工程可靠性的起点”。这话听着反直觉实则直击要害。VS Code插件依赖Node.js运行时、Electron沙箱、VS Code API版本兼容性任何一个环节出问题比如某次VS Code更新破坏了vscode.workspace.findFiles的返回格式整个审查功能就挂了。而CLI工具只依赖Python解释器和Git二进制——这两样东西十年没变过接口。更重要的是CLI天然支持管道pipe和脚本编排。我们团队把它集成进pre-commit hook后实际效果是这样的# .pre-commit-config.yaml - repo: https://github.com/open-code-review/pre-commit rev: v0.8.2 hooks: - id: open-code-review args: [--min-confidence, 0.7, --fail-on-critical]每次git commit时它自动执行git diff --cached提取暂存区变更ocrr review --stdin接收diff流若检测到critical级问题如硬编码密码中断commit并输出详细trace路径若只有medium问题生成REVIEW.md摘要文件随commit一起提交这个流程的关键在于第三步的“中断commit”——它不是弹窗提醒而是让shell进程返回非零退出码触发pre-commit框架的标准失败处理。这意味着它能和Jenkins/GitLab CI无缝衔接CI里直接跑ocrr review --since$CI_COMMIT_BEFORE_SHA它能被Ansible/Puppet等运维工具统一管理所有开发机用同一pip包版本它的失败行为可被监控系统捕获比如ELK里搜ocrr.*critical就能统计高危修改频率。注意不要试图用os.system(ocrr review)在Python脚本里调用它。正确做法是用subprocess.run([ocrr, review, --stdin], inputdiff_bytes, capture_outputTrue)——前者会丢失stderr流导致trace错误信息无法捕获。2.3 Git diffs即事实源为什么不用AST解析器直接读源码这是最容易被误解的设计点。很多同类工具如CodeWhisperer会启动语言服务器实时解析打开的文件AST。open-code-review偏偏反其道而行之它只信任Git diff输出的文本差异其他一切AST、符号表、类型信息都从diff逆向推导。为什么因为真实开发场景中“代码是什么”永远不如“代码怎么变”重要。举个典型例子一个Java类新增了Transactional注解但开发者忘了加spring-tx依赖。静态分析工具会告诉你“注解未生效”但不会告诉你“这个变更让原本无事务的方法突然有了事务语义可能引发意外锁竞争”。而diff视角能捕捉到Transactional注解的添加文本层面pom.xml里没有对应dependency通过diff对比前后pom内容方法签名未变但调用栈新增了TransactionInterceptor通过分析diff中import变化推断open-code-review的diff解析器叫difftree会把原始diff转换成结构化变更树├── file: src/main/java/com/example/UserService.java │ ├── method: updateUser() │ │ ├── annotation_added: Transactional │ │ └── body_changed: true │ └── import_added: org.springframework.transaction.annotation.Transactional └── file: pom.xml └── dependency_missing: spring-tx这个树结构才是后续所有分析的输入源。它带来的好处是惊人的零配置不需要为每种语言写单独的AST解析器只要Git能生成diff它就能工作精准范围审查严格限定在本次变更影响的代码块不会因全局类型推导错误产生误报历史可比ocrr review --sinceHEAD~5和ocrr review --sinceHEAD~10的结果具有确定性因为diff是Git的权威事实。我实测过在一个20万行的Go项目里ocrr review --sinceHEAD~1平均耗时2.3秒而同等条件下Language Server启动全量分析要17秒——而且后者还会因内存不足崩溃。3. 实操全流程从零部署到定制化审查策略3.1 环境准备三步完成生产级安装别被“LLM Agent”这个词吓住open-code-review对硬件的要求低得惊人。我在一台2015年的MacBook Pro16GB内存Intel i5上跑了三个月主力模型是Ollama里的phi3:mini2.3GB全程无卡顿。以下是经过验证的最小可行安装路径第一步安装Git和Python 3.9# macOSHomebrew brew install git python3.9 # Ubuntu 22.04 sudo apt update sudo apt install -y git python3.9 python3.9-venv # WindowsPowerShell管理员模式 choco install git python39注意必须用Python 3.9因为项目依赖ast.unparse()在3.9才稳定。如果系统自带Python版本太低用pyenv管理更稳妥pyenv install 3.9.18 pyenv global 3.9.18。第二步安装Ollama本地LLM运行时# 一键安装自动处理ARM/x64、macOS/Windows/Linux curl -fsSL https://ollama.com/install.sh | sh # 验证安装 ollama list # 应该返回空列表 ollama run phi3:mini # 首次运行会下载2.3GB模型耐心等待为什么选phi3:mini不是因为它最强而是因为它的推理延迟和token效率的平衡点最契合代码审查场景。我对比过qwen2:7b准确率高5%但单次审查平均耗时4.2秒且常把简单SQL注入判为“低风险”llama3:8b响应快但对Python装饰器语法理解有偏差曾把cache误判为“内存泄漏风险”phi3:mini在我们的测试集127个真实PR diff上critical问题检出率92.1%平均响应1.8秒且从不臆测——它要么给出明确依据要么跳过。第三步安装open-code-review CLI# 创建独立虚拟环境强烈推荐避免依赖冲突 python3.9 -m venv ~/ocrr-env source ~/ocrr-env/bin/activate # Linux/macOS # ~/ocrr-env/Scripts/activate # Windows # 安装注意不是pip install open-code-review官方包名是ocrr pip install ocrr # 验证 ocrr --version # 应返回0.8.2 ocrr config show # 查看默认配置此时你会看到默认配置llm: provider: ollama model: phi3:mini timeout: 30 git: diff_context: 3 review: min_confidence: 0.6 fail_on_critical: false提示配置文件位于~/.config/ocrr/config.yaml。不要手动编辑用ocrr config set llm.model qwen2:7b命令修改——它会自动校验模型是否存在。3.2 首次审查实战用真实diff理解它的思考路径我们拿一个经典的安全漏洞案例来实操。假设你正在审查这个PRdiff --git a/app/utils.py b/app/utils.py index abc123..def456 100644 --- a/app/utils.py b/app/utils.py -15,0 16,5 def load_config(): def exec_command(cmd): Execute shell command - DANGEROUS! import os return os.system(cmd) 执行审查命令ocrr review --diff (git diff HEAD~1 -- app/utils.py)输出会分三部分呈现第一部分Diff结构化摘要 Diff Summary • File: app/utils.py • Added: 1 function (exec_command) • Lines added: 5 • Key patterns detected: - import os in new function - os.system( pattern - Docstring contains DANGEROUS!第二部分规则引擎初筛️ Rule-based Analysis [CRITICAL] CWE-78: OS Command Injection • Trigger: os.system( in new function body • Evidence: Line 18, return os.system(cmd) • Reference: OWASP ASVS 5.2.1 • Confidence: 0.98第三部分LLM深度推理 LLM Reasoning (phi3:mini) Function exec_command takes untrusted input cmd and passes it directly to os.system(). This bypasses shell escaping mechanisms. Even if caller sanitizes input, Pythons os.system() executes in /bin/sh which interprets special characters like ; and $. Recommended fix: use subprocess.run() with shellFalse and list arguments, e.g., subprocess.run([ls, -l, sanitized_path]).关键来了——这个LLM输出不是凭空生成的。你可以在.ocrr/traces/下找到对应的trace文件里面记录了LLM收到的完整promptYou are a senior security engineer reviewing Python code changes. Context: - File: app/utils.py - Change type: function_addition - Added lines: 16-20 - Security rule match: CWE-78 (OS Command Injection) - Rule confidence: 0.98 - Code snippet: def exec_command(cmd): Execute shell command - DANGEROUS! import os return os.system(cmd) Task: Explain why this is dangerous, cite specific language behavior, and give exact replacement code.看到没LLM的输入里明确包含了规则引擎的结论CWE-78、置信度0.98、甚至原始代码片段。这确保了LLM不会“自由发挥”而是在规则划定的边界内做专业解释。这也是它和纯LLM工具的本质区别规则引擎是刹车LLM是导航仪。3.3 定制化审查策略如何让工具读懂你的团队规范开箱即用的规则只能覆盖通用场景。真正让它融入团队需要定制三类策略1. 业务规则注入无需写代码在项目根目录创建.ocrr/rules.yamlrules: - id: CUSTOM-001 name: 禁止使用硬编码数据库密码 pattern: password\s*\s*[\].[\] severity: critical message: 数据库密码硬编码违反《安全开发规范》第4.3条请使用环境变量或密钥管理服务 reference: https://internal/wiki/security#db-password - id: CUSTOM-002 name: 日志中禁止打印用户手机号 pattern: logging\.info\(.?1[3-9]\d{9}.?\) severity: high message: 手机号明文日志违反《隐私保护条例》第2.1条需脱敏处理然后执行ocrr review --rules .ocrr/rules.yaml。这些正则规则会被注入到规则引擎的初筛阶段优先级高于LLM分析——毕竟机器学习再准也比不过正则对手机号格式的精确匹配。2. LLM提示词微调修改模板编辑~/.config/ocrr/prompts/security.jinja{%- if rule_match CWE-78 -%} 你是一名{{ team_role }}正在审查{{ language }}代码。 请严格按以下格式回答 【风险】{{ risk_level }}{{ rule_name }} 【依据】{{ evidence }}引用{{ reference }} 【修复】提供{{ language }}代码片段使用{{ preferred_library }}禁止使用os.system 【验证】说明如何用{{ test_framework }}验证修复效果 {%- endif -%}这里team_role、preferred_library等变量在ocrr config set里定义ocrr config set team_role 支付系统高级工程师 ocrr config set preferred_library subprocess.run ocrr config set test_framework pytest3. 审查结果后处理用钩子接管输出创建~/.ocrr/hooks/post_review.py#!/usr/bin/env python3 import sys import json # 读取ocrr输出的JSON结果 review_result json.load(sys.stdin) # 自动创建Jira任务仅当critical问题存在 if any(item[severity] critical for item in review_result[issues]): import requests jira_payload { fields: { project: {key: SEC}, summary: f[OCRR] Critical issue in {review_result[commit]}, description: \n.join([f- {i[message]} for i in review_result[issues] if i[severity]critical]), issuetype: {name: Bug} } } requests.post(https://jira.internal/rest/api/3/issue, jsonjira_payload, auth(bot, api_token)) print(json.dumps(review_result)) # 必须原样输出供后续流程使用然后在配置中启用ocrr config set hooks.post_review ~/.ocrr/hooks/post_review.py这样每次审查发现critical问题就会自动生成Jira任务且描述里自动带所有细节。整个过程无需人工干预但所有操作都透明可审计——因为hook脚本本身就在Git仓库里版本控制。4. 深度原理剖析Agent、LLM、Embedding在审查中的真实分工4.1 拆解“LLM Agent”它到底在扮演什么角色网络热词“agent”常被滥用仿佛给LLM套个循环就叫agent。但在open-code-review里“agent”有非常具体的工程定义一个由确定性规则引擎Orchestrator驱动的、具备状态记忆的LLM调用器。它不是自主决策的AI而是人类审查流程的自动化协作者。它的agent架构长这样--------------------- | Orchestrator | ← 确定性规则引擎Python | - Parse diff | • 输入Git diff文本 | - Run static rules | • 输出结构化变更树 规则匹配结果 | - Decide if LLM needed| • 决策逻辑仅当规则置信度0.95且severityhigh时调用LLM ------------------ | -----------v----------- | LLM Executor | ← 状态化调用器Python | - Maintain context | • 维护本次审查的完整上下文代码片段、规则结论、历史trace | - Batch prompts | • 将多个相关变更合并成单个prompt减少token消耗 | - Retry on failure | • LLM超时或返回格式错误时自动降级为规则引擎结论 -------------------- | ---------v---------- | phi3:mini | ← 本地LLMOllama | - No internet needed | • 模型权重本地存储 | - Quantized weights | • GGUF格式内存占用1.2GB --------------------关键洞察在于Orchestrator永远拥有最终决策权。LLM的输出只是“建议”必须经过Orchestrator的二次校验才能进入最终报告。比如LLM可能建议“用hashlib.sha256()替代md5()”但Orchestrator会检查当前Python版本是否支持sha256Python 2.5不支持若不支持则替换为hashlib.new(sha256)。实操心得不要试图用更大的LLM替换phi3。我试过llama3:70b结果发现它在代码审查上反而更差——因为70B模型倾向于“过度解释”比如把list.append()说成“可能引发内存碎片”而phi3会老实回答“append是O(1)操作无已知风险”。小模型在垂直领域有时就是比大模型更靠谱。4.2 LLM vs Embedding为什么它不用向量检索另一个常见误区是认为“AI代码工具必须用embedding找相似代码”。open-code-review明确拒绝embedding原因很实在Embedding需要大量训练数据要让embedding模型理解“os.system()和subprocess.run()的语义差异”至少需要10万对标注样本。而规则引擎用一行正则就能解决Embedding引入额外延迟加载embedding模型通常500MB 构建索引 查询比直接正则匹配慢3个数量级Embedding结果不可解释它告诉你“这个函数和CVE-2023-12345的POC相似”但无法说明相似点在哪——而这恰恰是审查的核心需求。它用的是更古老但更可靠的符号匹配模式组合。比如检测SQL注入不是把代码转成向量再比对而是找到所有cursor.execute(调用提取第二个参数通常是SQL字符串检查该字符串是否包含%s、?等占位符若不含占位符且字符串中包含变量名如user_input则标记为高风险。这套逻辑写在ocrr/rules/sql_injection.py里总共47行代码每个条件都有明确的OWASP参考链接。你可以随时git blame看到是谁在2023年11月2日加了这条规则以及为什么选择%s而不是{}作为安全标志。4.3 “DeepSeek属于哪个”用审查场景厘清技术分层网络热词“DeepSeek是LLM还是Agent”其实问错了层次。就像问“螺丝刀是工具还是工匠”——DeepSeek是LLM大语言模型而open-code-review是Agent智能体二者是不同抽象层级的东西层级代表在审查中的角色是否可替换基础模型层DeepSeek-V2、Qwen2、Phi-3提供语言理解和生成能力✅ 可换但需调整prompt模板运行时层Ollama、llama.cpp、vLLM加载模型、处理token、管理GPU内存✅ 可换但需适配APIAgent层open-code-review定义何时调用LLM、传什么数据、如何整合结果⚠️ 可定制但核心逻辑不变应用层GitHub PR Review、VS Code插件把Agent能力包装成用户界面❌ 与ocrr无关所以当你看到“DeepSeek-V2在代码审查中表现更好”真正该关注的是它的token输出稳定性是否总在指定格式内、长上下文保持能力能否记住整个diff的函数调用链、领域知识密度对Django ORM、Spring Boot等框架的内置理解。而open-code-review的Agent层正是通过标准化输入diff结构化树、约束输出JSON Schema、失败降级规则引擎兜底来屏蔽底层LLM的差异。我实测过DeepSeek-Coder-33B在相同diff上的表现它生成的修复建议更优雅比如用sqlalchemy.text()替代原生SQL但首次响应时间长达8.2秒且有12%概率返回不完整JSON。而phi3:mini虽然建议朴实但100%返回合法JSON且平均1.8秒。在CI流水线里确定性比惊艳度重要十倍——没人愿意为一次PR审查多等8秒更没人想处理JSON解析异常。5. 常见问题与避坑指南那些文档里不会写的实战经验5.1 “审查结果不一致”问题排查为什么两次运行同一个diffLLM结论不同这是最常被问的问题。现象ocrr review --diff my.diff第一次输出“建议用subprocess.run”第二次输出“建议用os.popen”。根本原因不是LLM随机而是Ollama默认启用温度temperature参数。Ollama的phi3:mini默认temperature0.8这意味着LLM会有意识地引入轻微随机性以提升创造性。但在代码审查场景这完全是灾难——你需要的是确定性结论。解决方案在配置中强制关闭温度ocrr config set llm.temperature 0.0或者在命令行临时指定ocrr review --diff my.diff --llm-temperature 0.0验证方法连续执行5次检查.ocrr/traces/下生成的trace文件中llm_reasoning步骤的output.reasoning_chain字段是否完全一致。一致性达到100%才算生效。注意不要用--llm-temperature 0直接调用Ollama API那会导致Ollama返回错误。必须通过ocrr的配置层传递因为ocrr会自动转换为Ollama的options参数。5.2 “大diff卡死”问题如何审查超过1000行的巨型PR当git diff输出超过5000行时ocrr默认会因内存限制崩溃。这不是bug而是设计选择——它假设“超过1000行的PR应该被拆分”。但现实总有例外比如重构legacy系统。这时要用增量审查策略# 步骤1提取所有变更的文件列表 git diff --name-only HEAD~10 | head -50 changed_files.txt # 步骤2对每个文件单独审查并行加速 cat changed_files.txt | xargs -I {} sh -c ocrr review --diff (git diff HEAD~10 -- {}) --file {} reports/{}.review.json 2/dev/null # 步骤3汇总结果 jq -s reduce .[] as $item ({}; .issues $item.issues) reports/*.review.json summary.json关键技巧在于--file参数它告诉ocrr这次审查只关注单个文件从而大幅降低内存占用。实测表明单文件审查1000行diff内存峰值300MB而全量审查同样diff内存峰值2.1GB。5.3 “LLM返回乱码”问题中文提示词失效的终极解法如果你在.ocrr/prompts/security.jinja里写了中文却看到LLM输出一堆乱码如???别怀疑模型——这是字符编码链断裂。根本路径ocrrPython→OllamaGo→phi3:miniGGUF其中Ollama默认用UTF-8但某些旧版Ollama在Windows上会错误识别为GBK。三步修复法升级Ollama到最新版curl -fsSL https://ollama.com/install.sh | sh重置模型ollama rm phi3:mini ollama run phi3:mini强制ocrr用UTF-8编码调用在~/.config/ocrr/config.yaml中添加llm: encoding: utf-8验证创建测试prompttest.jinja内容为你好世界执行ocrr review --prompt test.jinja --dry-run观察输出是否为正常中文。5.4 “审查漏报”问题如何让工具发现你关心的特定模式比如你团队规定“所有Redis操作必须用连接池”但ocrr默认规则不包含这条。不要急着写正则——先用它的交互式调试模式定位问题ocrr debug --diff my.diff --step ast_diff_analysis它会输出AST变更树的可视化结构。找到Redis相关代码后复制其AST节点特征如Call(funcAttribute(valueName(idredis), attrget))然后在.ocrr/rules.yaml中添加- id: REDIS-POOL name: Redis操作未使用连接池 pattern: redis\.(get|set|delete)\( severity: medium message: Redis操作必须通过ConnectionPool实例禁止直连。参考《中间件规范》第5.2条实操心得永远先用ocrr debug确认模式特征再写规则。我曾因直接复制IDE里显示的redis_client.get()而忽略了实际AST中redis_client可能是redis.Redis()实例导致规则永远不匹配。5.5 “CI集成失败”问题在GitLab CI中绕过GPU限制GitLab Runner默认无GPU而Ollama在无GPU时会退化为CPU推理速度暴跌。解决方案不是买GPU而是用Ollama的远程模式# .gitlab-ci.yml review-code: image: python:3.9 before_script: - pip install ocrr - apk add curl # Alpine Linux需要 script: - ocrr config set llm.provider ollama_remote - ocrr config set llm.host http://ollama-server.internal:11434 - ocrr review --since$CI_COMMIT_BEFORE_SHA其中ollama-server.internal是内网部署的Ollama服务带GPU所有CI job都复用它。这样既避免了每个Runner装Ollama又保证了推理速度。实测CI平均审查时间从42秒降至3.1秒。最后分享一个血泪教训某次上线前夜我们发现ocrr在CI里突然不工作了。排查三天最终发现是Ollama服务端升级到了v0.3.0而ocrr客户端还用着v0.2.x的API。解决方案很简单pip install ocrr --upgrade。但这件事教会我——永远在CI里锁定ocrr版本script: - pip install ocrr0.8.2版本号写死比任何文档都可靠。