用本地模型零成本进行大规模代码库审查
展示如何用本地 AI 模型代替商业 API 进行代码审查,实现企业级代码检查无须额外费用。对成本敏感的团队有实用意义。
展示如何用本地 AI 模型代替商业 API 进行代码审查,实现企业级代码检查无须额外费用。对成本敏感的团队有实用意义。
2026 年 6 月将作为人们意识到闭源模型可能会被收回的时刻载入史册。Anthropic 最新旗舰模型 Claude Fable 5 被下架一事仍令人记忆犹新,因此不难理解,为什么掌控自己的 AI 技术栈并能够在本地运行模型比以往任何时候都更加重要——尤其是当你的业务建立在 AI 之上时。
有鉴于此,我们想分享一下如何在智能体运行框架中使用 Gemma 和 Qwen 等本地模型来执行分类任务[1]。这种方法不同于使用 BERT 之类的模型进行分类。在 Pi 这样的智能体运行框架中,本地模型可以结合结构化输出,为内容分配标签。我们之所以选择这种方法,是因为手头已经有本地模型和运行框架,而且我们确信,随着本地模型能力不断提升,类似的配置会越来越流行。[2]
我们的起点是 OpenClaw 仓库中的开源贡献。OpenClaw 每天都会收到数百个 issue 和 PR,需要对它们进行分类分流、确定优先级并转交给维护者。我是 Onur,目前正致力于让本地模型与 OpenClaw 良好协作。作为这一特定垂直领域的维护者,我需要快速响应所有 P0 级问题。
如果使用 GPT-5、Opus 或 Sonnet 等当前最先进的闭源模型,这是一项相当直接的任务。但我恰好拥有 128 GB 统一内存,具体来说是一台 NVIDIA GB10。于是,我接下了这个任务:
我能否构建一个实时通知系统,只筛选并通知我所负责的 issue……而且使用本地开放权重模型来实现?
如果我把 OpenClaw 主智能体设置为使用每月 200 美元的 ChatGPT Pro 套餐,并在每个新 issue 或 PR 出现时触发任务,就会耗尽我的配额。另一种做法是将它设置为每 2 小时或 6 小时运行一次。但这会让 issue 在更长的时间段内批量处理,相当于用延迟处理换取实时通知。
如果在已经拥有并持续运行的硬件上使用本地模型,我不仅能获得近乎即时的通知,还能免费完成这项工作(更准确地说,成本只有电费)。
我们设计了一组有限的标签,用来表示需要分类分流的 issue 类别,然后使用本地模型将每个 issue 归入其中一个类别,例如 local_models、self_hosted_inference、acp、agent_runtime、codex、ui_tui 等。[3]
但是,我们该如何对 pull request 进行分类?只需向 Chat Completions 端点发送一次简单请求,附上工具的 JSON schema,并将各个主题设为 enum?
差不多。但现在是 2026 年,不是 2023 年,而且我们已经有 AI 智能体了。我们可以做得更好!
在本地模型的选择上,我们测试了 gemma-4-26b-a4b 和 qwen3.6-35b-a3b。经过性能优化,两者在本地都能达到每秒生成数百个 token 的速度。
我们使用智能体运行框架来驱动分类过程。为此,我们将 pi 打包为一个能够调用本地模型端点的运行框架。
默认情况下,智能体会在第一条提示词中收到 PR 的标题、正文以及经过截断的 PR diff 摘要。随后,它可以选择使用 bash 工具对 OpenClaw 仓库执行只读操作(以便在需要时查看代码库),或者使用 final_json 工具提交最终分类结果。
你不会希望给在这种高吞吐量场景下运行的本地模型完整的 bash 权限,因为经过提示词注入的 issue 或 PR 可能会诱导模型执行与分类无关的操作。
因此,我们使用 reposhell 代替 bash:这是一个受限的类 bash shell,只允许对 OpenClaw 仓库执行只读操作(ls、find、cat、grep 等)。模型以为自己正在使用 bash,但任何不被允许的操作都会遭到拒绝:
reposhell bound cwd=/repo/openclaw repos=openclaw
type help for allowed commands; exit or quit to leave
reposhell /repo/openclaw> help
allowed: pwd, ls, find, rg, grep, sed -n, cat, head, tail, wc -l, git status --short, git show --name-only, git grep, git ls-files
search: rg -n -i "lm studio" or grep -R -n -i "lm studio" .
files: rg --files -g "*.ts" or git ls-files src
examples: rg -n reposhell README.md | sed is not allowed; use one simple command at a time
reposhell /repo/openclaw> head README.md
# 🦞 OpenClaw — Personal AI Assistant
<p align="center">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/openclaw/openclaw/main/docs/assets/openclaw-logo-text-dark.svg">
<img src="https://raw.githubusercontent.com/openclaw/openclaw/main/docs/assets/openclaw-logo-text.svg" alt="OpenClaw" width="500">
</picture>
</p>
<p align="center">
reposhell /repo/openclaw> curl localhost
reposhell policy denied command: unsupported command "curl"
exit_code=2
reposhell /repo/openclaw>
下面是一个体现这种机制重要性的具体示例。在某个保存下来的会话示例中,qwen3.6-35b-a3b 正在对 openclaw/openclaw#84621 进行分类,其标题为 Fix Kimi tool-call rewriting stop reason handling。思考区块显示,模型最初考虑将其归为 coding_agent_integrations,因为修改路径 extensions/kimi-coding 让这个标签看起来很合理。模型使用 reposhell,通过 ls extensions、ls extensions/kimi-coding 和 cat extensions/kimi-coding/package.json 等简单的只读命令检查了本地仓库。包元数据显示,该扩展实际上是 @openclaw/kimi-provider,即一个 OpenClaw Kimi 提供方插件。因此,模型将最终标签修正为 inference_api 和 tool_calling,并明确排除了 coding_agent_integrations。
前面提到过,我们打包了一套特定的 pi 配置,它只能执行只读操作并返回分类输出。我们将其称为 localpager-agent,这个名字取自此处的主项目 localpager。每个 PR 和 issue 都会生成一条提示词,然后与其他参数一起传给 CLI,如下所示:
localpager-agent \
--model "<model-id>" \
--base-url "<openai-compatible-base-url>" \
--session-dir "<session-output-dir>" \
--final-schema "<runtime-schema.json>" \
--tools bash,final_json \
--reposhell-socket "<reposhell.sock>" \
--reposhell-default-repo "<repo-id>" \
--reposhell-visible-repos "<repo-id>[,<repo-id>...]" \
-p "$(cat <rendered-prompt.md>)"
那么,从传入的 PR/issue 到 Discord 上的最终通知,这之间的一切是由什么来编排的?
围绕这套流程的编排非常简单;只有分类步骤涉及 LLM:
我们使用 openclaw/gitcrawl 作为仓库的本地镜像。每当有新的 PR 或 issue 出现时,每个条目都会被标准化为相同的数据结构,并写入 localpager 自己的 SQLite 数据库。如果该条目是新的,localpager 就会为它创建一个分类任务。
随后,worker 会从该队列中领取任务。它会构建一个 GitHub 上下文对象,其中包含 issue 或 PR 的标题、正文、标签、作者和状态,还可以选择性地包含评论、变更文件和选定的 diff 摘要。这意味着在大多数情况下,本地模型不需要浏览 GitHub,也不需要自行打开 URL。所有相关上下文都会直接提供给它。
上下文对象会被渲染成提示词,并按照上一节所述传递给 localpager-agent。智能体可以进行思考并使用 reposhell,但最终必须按照已定义的 schema 输出分类结果。
输出结果会被存回 localpager 的 SQLite 数据库,并根据用户配置的通知策略转发到 Discord(例如,只通知我这些主题,而不通知其他主题)。
下图展示了 localpager 的整体架构:
该架构是半智能体式的。标签分配由 AI 智能体完成,而发送通知则由确定性规则处理。这样可以省去在任务中最直接的部分进行推理的需要,从而让通知管线运行得更快。本地推理虽然免费,但每项任务都会产生资源争用成本:GPU 带宽应当留给那些绝对需要推理的任务。这也降低了通知出错的概率。
坦率地说,这套系统最初的本地版本噪声很大。最先测试的模型 gemma-4-e4b-it 对打通端到端本地管线很有帮助,但它也倾向于给 PR 或 issue 添加过多不相关的标签。假阳性标签会让 Discord 信息流充斥噪声,无法把我的注意力集中到真正需要关注的 issue 上。这促使我们在下文所述的 330 行评估集上测试更大的本地模型,包括 gemma-4-26b-a4b 和 qwen3.6-35b-a3b。
在早期的提示词开发中,我们还通过 antirez 的 DS4 实现[4] 使用 DeepSeek-V4-Flash 创建早期的数据集标签。该设置通过 CUDA 运行 DS4 服务器。我们最终放弃使用 DS4 作为标签生成器,因为它在不同运行之间的标签结果不一致。我们也没有考虑将其作为 localpager-agent 的主要模型,因为它太大,无法在我们的硬件上获得足够的吞吐量:DS4 服务器的速度约为每秒 14 个 token,最大并发数为 1。
为了测试模型性能,我们选择了 330 个 GitHub Issue 和 PR 并为其生成标签。每个条目都被标注五次(3 次使用 GPT-5.5,2 次使用 Opus 4.8),只有当这些模型达成一致时,标签才会被接受。这个过程涉及人工裁定、改进标签定义,以及向模型明确内部产品设计选择。由此,我们得到了一组稳定、可复现的标签,可用于与较小模型进行比较。
在这个评估集上,gemma-4-26b-a4b 和 qwen3.6-35b-a3b 无需进行提示词优化,就能取得实用的结果。使用相同的路由提示词时,Gemma 的召回率更高,单行实际耗时更短;Qwen 的精确率和完全匹配率更高,误报更少。我们还在同一个数据集上运行了 DeepSeek-V4-Flash 作为参考。它的误报最少,但其模型大小和吞吐量使其无法在 NVIDIA GB10 上实时执行这些任务。由于每一行都可能有多个标签,因此误报和漏报是所有行中相应标签数量的总和。下方的 Qwen 结果是在重新尝试结构化输出失败的条目后得到的;这些失败是因为模型在调用 final_json 之前耗尽了输出 token。对于 Gemma 和 Qwen,重复运行指标报告的是三次运行的均值 ± 样本标准差。DeepSeek-V4-Flash 仅运行一次,作为参考。
这里的吞吐量和实际耗时数据并不代表这些模型在该硬件上的绝对最高性能。它们只是我们当时在可用优化条件下采用的设置。例如,在另一项单独测试中,gemma-4-26b-a4b 同样支持 32 路并发,并达到了每秒超过 700 个聚合输出 token。
在 Gemma 基准测试中,我们使用 vLLM 部署 gemma-4-26b-a4b,并采用了该设置下我们能找到的优化方案。其中很重要的一项是 NVFP4 量化:在 GB10 级别的 Blackwell 硬件上,它不仅意味着更小的模型文件,还是一种对硬件更友好的格式,与 Q4_K_M 这类可移植的 GGUF 量化相比,可以更直接地利用 NVIDIA/vLLM 执行路径。实践中,这意味着更少的内存流量以及更大的批处理空间。我们还启用了前缀缓存、FP8 KV 缓存、CUTLASS MoE 后端和纯语言模型模式。整个 330 行数据的运行在并发数为 16 时大约用时 7.5 分钟。
我们之前提到过,与其针对每个新 Issue 或 PR 都使用本地模型运行一个任务,不如每隔 n 小时(例如每 2 小时)使用在 OpenClaw 中运行的 GPT-5.5 之类的 SOTA 云端模型执行一次批处理任务,也能达到相同目的。[5]
在这种情况下,我们需要订阅 ChatGPT Pro 计划。由于该模型是 SOTA 模型,即使将两小时内的 Issue/PR 一起批量处理,我们仍然可以期待它有相当不错的表现。
因为我们希望了解本地分类器相较于 GPT-5.5 的表现,所以会同时运行两者,并每隔两小时让 GPT-5.5 判定误报和漏报。
为安全起见,我们在沙箱中运行 OpenClaw 任务,并且只允许它访问用于报告结果的公开仓库。在我们的实现中,OpenClaw 任务会更新一个机器可读文件,随后由一个简单脚本读取 Codex 分配的标签,并计算误报/漏报状态。输出示例:
Issue #88499 openai-responses provider: 404 on previous_response_id when store=false (default) inventory area: OpenAI-compatible/proxy; notifier topics: agent_runtime, api_surface, sessions; notification: none
inventory area: OpenAI-compatible/proxy; notifier topics: agent_runtime, api_surface, sessions; notification: none
PR #88275 fix(models-config): allow self-hosted providers without apiKey in models.json (#88267) notifier interest: i0; topics: self_hosted_inference, local_model_providers, config; notification: sent
notifier interest: i0; topics: self_hosted_inference, local_model_providers, config; notification: sent
PR #88266 refactor: extract model catalog core package notifier interest: i1; topics: config, api_surface, local_model_providers; notification: sent
notifier interest: i1; topics: config, api_surface, local_model_providers; notification: sent
PR #88247 feat: add hosted model providers notifier interest: i0; topics: local_model_providers, model_serving, docs, api_surface; notification: sent
notifier interest: i0; topics: local_model_providers, model_serving, docs, api_surface; notification: sent
有关如何分类、编辑机器可读文件,以及如何使用脚本获取误报和漏报的说明,都包含在一个智能体技能中。这个技能由一个每两小时运行一次的 OpenClaw cron 任务引用。随后,OpenClaw 智能体会读取所有新的 Issue 或 PR,将它们连同适当的标签添加到 JSON 文件中,运行脚本,并在同一个 Discord 频道中报告结果。通过这种方式,我们可以每隔几小时观察一次本地模型的表现,并在出现遗漏时收到通知。
我们认为,Issue/PR 分类整理任务是我们所谓“高吞吐量分类整理”这一更广泛任务集合中的一个具体案例。本文探讨了在一个领域中使用本地模型实时过滤信息的思路,这个领域就是开源贡献。gemma-4-26b-a4b 和 qwen3.6-35b-a3b 等中等规模的本地模型无需任何微调,就能以较高准确率进行零样本分类,这使它们成为快速原型设计时的良好首选;之后再转向成本效益更高的传统分类器模型。
不过,同样的方法也可以应用于其他领域:
这个列表还可以继续扩展,但我们认为核心思路应该已经很清楚了。
除了分类整理之外,我们还探索了如何通过运行快速本地模型的智能体框架,以安全的方式执行分类。这个方法可以恰当地命名为“智能体式分类”:模型不会预先接收全部信息,而是可以在返回结构化数据之前搜索更多上下文。虽然我们不能确切地说这是一种新方法,但希望这篇博客文章能成为 Pi + 受限 shell + final_json 这一具体方案的良好参考。
对于本文的使用场景,我们发现,要以一种能够正确理解并标注产品功能面的方式拆解 PR/Issue,是一个困难的问题。返回
虽然在我们的测试中并没有这样做——但让模型得出“下一步应收集信息并使用外部分类器”的结论,是完全合理的。智能体式方法与传统方法并不相互排斥。返回
主题的完整列表及其他配置见此处。返回
我们使用了 antirez/deepseek-v4-gguf 中的 DeepSeek-V4-Flash-IQ2XXS-w2Q2K-AProjQ8-SExpQ8-OutQ8-chat-v2.gguf。返回
我们知道,使用 LLM 作为裁判会抵消“免费”这一点,但我们的具体实现是出于研究目的才这样做。在实践中,可以在试用期内同时使用一个更大、更昂贵的模型进行校准,之后系统再完全切换到较小的模型。在最近的运行中,这个审计循环每两小时检查一次,每次总共使用约 4 万个 GPT-5.5 token,其中大部分是缓存的上下文。按 API 定价计算,每次运行的成本约为 2~3 美分;若每天运行 12 次,每月约为 9 美元。这是一次覆盖所有新条目的批量审计,并非针对每个条目分别调用一次裁判模型;如果逐条调用,成本很可能会高出数倍。返回
在任意云上运行 AI 工作负载,将数据存储在 Hugging Face:通过 SkyPilot 实现零出口流量费用存储
使用 BentoML 部署 Hugging Face 模型:DeepFloyd IF 实战
· 注册或登录后发表评论