我们用免费本地模型对 OpenClaw 仓库进行实时分类
We got local models to triage the OpenClaw repo for FREE!*
Hugging Face 在 OpenClaw 仓库上测试用 Gemma 和 Qwen 等本地模型实时分类 issue 和 PR。他们使用 Pi agent harness 驱动模型,配合 reposhell 只允许读操作防止提示词注入。测试的模型包括 gemma-4-26b-a4b 和 qwen3.6-35b-a3b,经性能优化后均可在本地生成数百 token/s。该方案运行在 NVIDIA GB10(128 GB 统一内存)上,相比每月 200 美元的 ChatGPT Pro 订阅,可实现近乎实时的通知且仅消耗电费。
Hugging Face 演示了用本地模型自动 triage GitHub issue 的完整方案,包括只读 shell 防注入、agent harness 等工程技巧。对想用本地模型替代 API 做分类任务的团队,这是一套可直接借鉴的 recipe。
2026 年 6 月将作为一个被人们意识到闭源模型可以被夺走的时刻而被铭记。随着 Anthropic 最新的旗舰模型 Claude Fable 5 被下架一事仍历历在目,人们不难理解为什么拥有自己的 AI 技术栈、能够在本地运行模型比以往任何时候都更加重要,尤其是当你把自己的业务建立在 AI 之上时。
有鉴于此,我们想分享我们如何在智能体框架中使用 Gemma 和 Qwen 这样的本地模型来执行分类任务[^1]。这种方法不同于使用 BERT 之类的模型进行分类。在 Pi 这样的智能体框架中,本地模型可以与结构化输出配合使用,来分配标签。我们选择这种方法,是因为我们手头已经有本地模型和该框架,并且我们坚信,随着本地模型能力的提升,类似的配置会越来越流行。[^2]
我们的起点是 OpenClaw 仓库中的开源贡献。OpenClaw 每天会收到数百个 issue 和 PR,这些都需要被分诊、排定优先级并路由给维护者。我,Onur,正在努力让本地模型与 OpenClaw 良好协作。作为这个特定垂直领域的维护者,我需要快速响应任何 P0 级别的 issue。
对于 GPT-5、Opus 或 Sonnet 这样的 SOTA 闭源模型来说,这是一项相当直接的任务。但我恰好拥有 128 GB 的统一内存,也就是一块 NVIDIA GB10。于是我接下了这个任务:
我能否用本地的开放权重模型,构建一个实时通知系统,只针对由我负责的那些 issue 进行过滤并通知我?
如果我把我运行在每月 200 美元 ChatGPT Pro 方案上的 OpenClaw 主智能体设置为在每个新 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,并把主题作为 enum?
差不多是这样。但现在是 2026 年,不是 2023 年,而且我们有 AGENTS。我们可以做得更好!
在本地模型选择方面,我们测试了 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
那么,是什么在传入的 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 信息流变得嘈杂,也无法把我的注意力集中到真正该关注的问题上。这促使我们去测试更大的本地模型,包括 gemma-4-26b-a4b 和 qwen3.6-35b-a3b,在下面这个 330 行的评测集上进行。
在早期的提示词工作中,我们还通过 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 cache、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 任务,仅允许访问我们向其报告结果的 public repo。在我们的场景中,我们让 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 中,该技能被一个每 2 小时运行一次的 OpenClaw cron job 所引用。随后,OpenClaw 智能体会摄取任何新的 issue 或 PR,将它们连同适当的标签添加到 JSON 文件中,运行脚本,并在同一个 Discord 频道中回报结果。这样,我们就能每隔几小时观察一次本地模型的表现,并在出现漏判时收到通知。
结论
我们认为,issue/PR 分诊任务是一类更广泛任务的特定案例,我们将其称为“高吞吐量分诊”。本文探讨了使用本地模型在单一领域(即开源贡献)中实时过滤信息的思路。像 gemma-4-26b-a4b 和 qwen3.6-35b-a3b 这样的中等规模本地模型,能够在无需任何微调的情况下以良好准确率进行单次分类,这使它们成为快速原型开发的首选,之后人们再转向更具成本效益的传统分类器模型。
然而,同样的方法也可以应用于其他领域:
- 新闻业中的新闻分类
- 在 X 或 Reddit 等社交媒体和论坛中筛选感兴趣的帖子
- 客户支持工单分诊
- 内容审核申诉分诊
- 在销售过程中过滤潜在的外联对象
- 在做研究时对 arXiv 上的特定主题进行过滤
这个列表还可以继续扩展,但我们认为其中的思路应该已经很清楚了。
除了分诊之外,我们还探索了如何以安全的方式,通过运行快速本地模型的智能体框架来执行分类。为这种方法起一个好名字,可以叫智能体分类:模型不会在一开始就被喂入全部信息正文,而是可以在返回结构化数据之前先搜索更多上下文。虽然我们不能完全称这是一种新颖的方法,但我们希望这篇博文能成为针对特定Pi+受限 shell+final_json配方的一份良好参考。
[^1]: 对于本文中的用例,我们发现,以能够正确理解并标注产品表面的方式来拆解一个 PR/Issue 是一个难题。[^2]: 尽管在我们的测试中并未如此——但模型得出下一步是收集信息、使用外部分类器这样的结论是相当合理的。智能体方法和传统方法并不互斥。[^3]: 查看主题完整列表及其他配置此处[^4]: 我们使用了DeepSeek-V4-Flash-IQ2XXS-w2Q2K-AProjQ8-SExpQ8-OutQ8-chat-v2.gguf来自antirez/deepseek-v4-gguf。[^5]: 虽然我们意识到使用 LLM 作为评判者会抵消“免费”这一优势,但我们的具体实现出于研究目的而这样做。在实践中,可以在试用期内搭配使用一个更大、更昂贵的模型进行校准,之后系统将完全过渡到较小的模型。在最近的运行中,这个审计循环每 2 小时检查一次,总共使用约 40k 个 GPT-5.5 token,其中大部分是缓存上下文,按 API 定价每次运行花费约 2-3 美分,若每天运行 12 次则约为每月 $9。这是对所有新增条目进行的一次批量审计,而不是对每个条目单独调用一次评判者;若逐条目进行,成本可能会高出数倍。
来源:Hugging Face:Blog(RSS) · huggingface.co