如何用本地模型免费分诊 OpenClaw 仓库的 PR 和 issue
We got local models to triage the OpenClaw repo for FREE!*
Hugging Face 博客分享了用本地模型分诊 OpenClaw 仓库 issue 和 PR 的实践:在 Pi agent harness 中运行 gemma-4-26b-a4b 和 qwen3.6-35b-a3b,配合只读的 reposhell 和 final_json 工具输出结构化分类标签。
*免费如啤酒,不含电费,且假设你已拥有硬件
2026 年 6 月将作为人们意识到闭源模型可能被收回的时刻而被铭记。随着 Anthropic 最新旗舰模型 Claude Fable 5 被下架的记忆犹新,不难理解为何拥有自己的 AI 技术栈并能在本地运行模型比以往任何时候都更加重要,尤其是当你正在 AI 之上构建自己的业务时。
有鉴于此,我们想分享我们如何在 agent harness 中使用 Gemma 和 Qwen 等本地模型来运行分类任务[1]。这种方法不同于使用 BERT 之类的模型进行分类。像 Pi 这样的 agent harness 中的本地模型可以与结构化输出配合使用,来分配标签。我们选择这种方法,是因为我们手头已经有本地模型和 harness,并且坚信随着本地模型能力的提升,类似的配置会越来越流行。[2]
我们的起点是 OpenClaw 仓库中的开源贡献。OpenClaw 每天收到数百个 issue 和 PR,需要对其进行分类、优先级排序并路由给维护者。我,Onur,正在努力让本地模型与 OpenClaw 良好配合。作为这个特定垂直领域的维护者,我需要快速响应任何 P0 级别的 issue。
对于像 GPT-5、Opus 或 Sonnet 这样的 SOTA 闭源模型来说,这是一项相当简单的任务。但我恰好拥有 128 GB 统一内存,即一块 NVIDIA GB10。于是我接下了这个任务:
我能否构建一个实时通知系统,只针对我负责的 issue 进行过滤并通知我……用本地的开放权重模型?
如果我把我运行在每月 200 美元 ChatGPT Pro 套餐上的 OpenClaw 主 agent 设置为在每个新 issue 或 PR 上触发一个任务,那会耗尽我的配额。我可能会改为每 2 小时或 6 小时运行一次。这会将较长时间段内的 issue 批量处理,因此我们是在用实时通知换取延迟处理。
如果我在已经启动并运行的硬件上用本地模型来运行这个,我不仅能获得近乎即时的通知,还能免费完成(或者更确切地说,只需支付电费)。
对 issue 和 PR 进行分类
我们想出了一组有限的标签,代表我们需要分类的 issue 类别,然后使用本地模型将每个 issue 归入其中一类,例如 local_models、self_hosted_inference、acp、agent_runtime、codex、ui_tui 等等。[3]
但我们如何对 pull request 进行分类呢?向 Chat Completions 端点发送一个简单的单一请求,附带一个工具 JSON schema,将主题作为枚举?
差不多是这样。但现在是 2026 年,不是 2023 年,我们有 AGENTS。我们可以做得更好!
在本地模型的选择上,我们测试了 gemma-4-26b-a4b 和 qwen3.6-35b-a3b。经过性能优化,两者都能在本地每秒生成数百个 token。
我们使用一个 agent harness 来驱动分类运行。为此,我们打包了 pi 作为可以调用本地模型端点的 harness。
该 agent 默认在第一个提示中接收 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
那么,是什么在传入的 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 的整体架构:
该架构是半代理式的。标签由代理方式完成,而发送通知则由确定性规则处理。这是为了通过消除任务中最直接部分对推理的需求来加快通知流水线。本地推理是免费的,但每个任务都有资源争用成本:GPU 带宽应保留给绝对需要推理的任务。这也减少了通知出错的可能性。
本地模型能对 PR 进行分类吗?
坦白说:这个系统最初的本地版本很嘈杂。测试的第一个模型——gemma-4-e4b-it 对于让端到端本地流水线跑起来很有用,但它也倾向于在一个 PR 或 issue 上打太多不相关的标签。误报标签会让 Discord 信息流变得嘈杂,也无法让我的注意力集中在正确的问题上。这促使我们在下面这个 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 |
qwen3.6-35b-a3b |
DeepSeek-V4-Flash |
|---|---|---|---|
| 精确率 | 0.716 ± 0.010 | 0.831 ± 0.007 | 0.938 |
| 召回率 | 0.905 ± 0.004 | 0.818 ± 0.006 | 0.714 |
| F1 | 0.800 ± 0.008 | 0.824 ± 0.002 | 0.811 |
| 完全匹配 | 0.410 ± 0.014 | 0.540 ± 0.014 | 0.509 |
| 误报 | 227.0 ± 10.5 | 105.7 ± 6.4 | 30 |
| 漏报 | 60.0 ± 2.6 | 115.3 ± 4.0 | 181 |
| 墙钟秒数 / 行 | 1.41 ± 0.04 | 13.51 ± 0.79 | 144.14 |
| 输出 tok/s / worker | 25 | 50 | 13 |
| 输出 tok/s 总计 | 402.6 | 145.3 | 13 |
| 并发 | 16 | 4 | 1 |
| 总参数 | 26B | 35B | 284B |
| 活跃参数 | 4B | 3B | 13B |
这里的吞吐量和墙钟时间数字并不是这些模型在此硬件上的确定性最大性能数字。它们是我们当时在可用优化下使用的设置。例如,在一次单独的探测中,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 分钟完成。
使用 OpenClaw 跟踪和验证实时性能
我们之前提到过,与其为每个新 issue 或 PR 用本地模型运行一个作业,我们可以每隔 n 小时(例如每 2 小时)用 SOTA 云模型(例如在 OpenClaw 中运行的 GPT-5.5)运行一个批处理作业,以达到相同的目的。[5]
在这种情况下,我们需要一个 ChatGPT Pro 计划。由于该模型是 SOTA,尽管将 2 小时的 issue/PR 批量合并在一起,我们仍然可以期望它表现相当不错。
因为我们想看看本地分类器相对于 GPT-5.5 的表现如何,我们同时运行两者,并让 GPT-5.5 每 2 小时评判一次假阳性和假阴性。
为了安全起见,我们在沙箱中运行 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
假阳性
- 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
- PR #88266 refactor: extract model catalog core package
- 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
关于如何分类、编辑机器可读文件、使用脚本获取假阳性和假阴性的说明存在于一个 agent skill 中,该 skill 被一个每 2 小时运行一次的 OpenClaw cron job 引用。然后 OpenClaw agent 会摄取任何新的 issue 或 PR,将它们连同适当的标签添加到 JSON 文件中,运行脚本并在同一个 Discord 频道中报告。这样,我们可以每隔几小时观察本地模型的性能,并在出现遗漏时收到通知。
结论
我们认为 issue/PR 分类任务是更广泛任务集的一个具体案例,我们称之为“高吞吐量分类”。本文探讨了使用本地模型仅在一个领域(即开源贡献)中实时过滤信息的想法。像 gemma-4-26b-a4b 和 qwen3.6-35b-a3b 这样的中型本地模型能够在无需任何微调的情况下以良好的准确率进行零样本分类,这使它们成为快速原型设计的不错首选,然后再转向更具成本效益的传统分类器模型。
然而,同样的方法也可以应用于其他领域:
- 新闻业中的新闻分类
- 在 X 或 Reddit 等社交媒体和论坛中过滤感兴趣的帖子
- 对客户支持工单进行分类
- 对内容审核申诉进行分类
- 在销售过程中过滤潜在的外联对象
- 在研究过程中对 arXiv 上的某些主题进行筛选
这个列表还可以继续扩展,但我们认为思路应该已经清楚了。
除了分诊之外,我们还探索了如何通过运行快速本地模型的 agent harness 以安全的方式执行分类。这种方法的合适名称是智能体分类:模型不会预先获得全部信息,而是可以在返回结构化数据之前搜索更多上下文。虽然我们不能确切地称其为一种新颖的方法,但我们希望这篇博文能成为特定 Pi+受限 shell+final_json 方案的良好参考。
脚注
- 对于本文中的用例,我们发现以能够正确理解并标注产品表面的方式拆解 PR/Issue 是一个难题。返回
- 尽管在我们的测试中并没有这样做——但模型得出下一步是收集信息、使用外部分类器的结论是相当合理的。智能体方法和传统方法并不互斥。返回
- 查看主题完整列表及其他配置此处。返回
- 我们使用了来自 antirez/deepseek-v4-gguf 的
DeepSeek-V4-Flash-IQ2XXS-w2Q2K-AProjQ8-SExpQ8-OutQ8-chat-v2.gguf。返回 - 虽然我们意识到使用 LLM 作为评判者会抵消“免费”这一优势,但我们的具体实现是出于研究目的这样做的。在实践中,可以在试用期内同时使用一个更大、更昂贵的模型进行校准,之后系统将完全过渡到较小的模型。在最近的运行中,这个审计循环每 2 小时检查一次,总共使用约 40k 个 GPT-5.5 token,其中大部分是缓存上下文,按 API 定价每次运行成本约为 2-3 美分,按每天 12 次运行计算约为每月 9 美元。这是对所有新项目进行的单批次审计,而不是对每个项目单独调用一次评判;如果逐项进行,成本可能会高出数倍。返回
来源:Hugging Face Blog · huggingface.co