我们用免费本地模型对 OpenClaw 仓库进行实时分类
Hugging Face 演示了用本地模型自动 triage GitHub issue 的完整方案,包括只读 shell 防注入、agent harness 等工程技巧。对想用本地模型替代 API 做分类任务的团队,这是一套可直接借鉴的 recipe。
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 订阅,可实现近乎实时的通知且仅消耗电费。
我们用本地模型对 OpenClaw 仓库进行了免费分类!*
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 和 PR 进行分类
我们设计了一组有限的标签,代表需要分类的 Issue 类别,然后使用本地模型将每个 Issue 归入其中一类,例如 local_models、self_hosted_inference、acp、agent_runtime、codex、ui_tui 等等。[^3]
那么如何处理 Pull Request 的分类呢?是否只需向 Chat Completions 端点发送一个带有工具 JSON Schema(其中主题作为枚举值)的简单请求?
差不多。但现在是 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 进行分类,该议题标题为“修复 Kimi 工具调用重写停止原因处理”。思考模块显示,模型最初考虑的是 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,以 main project 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 通知之间协调一切呢?
围绕这个的编排非常简单;只有分类步骤涉及到大语言模型:
- 我们使用 openclaw/gitcrawl 作为仓库的本地镜像。每当有新的 PR 或 issue 时,每个项都会被规范化为相同的格式,并写入 localpager 自己的 SQLite 数据库中。如果该项是新的,localpager 会为其创建一个分类任务。
- 然后一个工作进程从该队列中领取任务。它构建一个 GitHub 上下文对象,包含 issue 或 PR 的标题、正文、标签、作者、状态,以及可选地包含评论、更改的文件和选定的差异摘要。这意味着本地模型大多数时候不需要浏览 GitHub 或自行打开 URL。所有相关上下文都会被传递给它。
- 上下文对象会被渲染成提示词,并按照上一节所述的方式传递给 localpager-agent。智能体可以进行思考并使用 reposhell,但最终必须按照定义的架构输出分类结果。
- 输出结果会存入 localpager 的 SQLite 数据库,并根据用户配置的通知策略(例如:这些主题需要通知我,但那些主题不需要)中继发送到 Discord。
下图展示了 localpager 的整体架构:
该架构属于半智能体式。标签标注由智能体完成,而发送通知则通过确定性规则处理。这样做是为了将任务中最直接的部分省去推理环节,从而加快通知管道的速度。本地推理虽然免费,但每个任务都存在资源争用成本:GPU 带宽应留给那些确实需要推理的任务。同时,这也降低了通知环节出错的可能性。
本地模型能对 PR 进行分类吗?
坦率地说:该系统的早期本地版本噪声很大。第一个测试的模型——gemma-4-e4b-it——对于打通端到端本地管道很有用,但它也有一个问题,就是倾向于给 PR 或 Issue 打上太多不相关的标签。误报的标签会让 Discord 信息流变得嘈杂,也无法让我将注意力集中在正确的问题上。这促使我们转向测试更大的本地模型,包括 gemma-4-26b-a4b 和 qwen3.6-35b-a3b,在下面包含 330 行的评测集上进行。
在早期的提示词工作中,我们还通过 antirez DS4 实现使用了 DeepSeek-V4-Flash [^4] 来创建早期的数据集标签。该方案在 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 |
| 每个 worker 的输出 token/秒 | 25 | 50 | 13 |
| 聚合输出 token/秒 | 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小时)在OpenClaw中运行一次使用SOTA云端模型(如GPT-5.5)的批量作业,以达到相同的目的。[^5]
在这种情况下,我们需要一个ChatGPT Pro订阅计划。由于该模型是SOTA的,尽管将2小时的issue/PR合并批量处理,我们仍可预期其表现合理。
因为我们想查看本地分类器与GPT-5.5相比的表现,所以让两者同时运行,并每2小时让GPT-5.5评判假阳性和假阴性。
为安全起见,我们在沙箱中运行OpenClaw作业,仅允许其访问我们汇报结果的公共仓库。在我们的场景中,我们让OpenClaw作业更新一个机器可读的文件,然后由简单的脚本读取Codex分配的标签,并计算假阳性/假阴性状态。示例输出如下:
假阴性
- Issue #88499 openai-responses provider: 404 on previous_response_id when store=false (default)
- inventory区域:OpenAI兼容/代理;notifier主题:agent_runtime, api_surface, sessions;通知:无
假阳性
- PR #88275 fix(models-config): allow self-hosted providers without apiKey in models.json (#88267)
- notifier兴趣:i0;主题:self_hosted_inference, local_model_providers, config;通知:已发送
- PR #88266 refactor: extract model catalog core package
- notifier兴趣:i1;主题:config, api_surface, local_model_providers;通知:已发送
- PR #88247 feat: add hosted model providers
- notifier interest: i0; topics: local_model_providers, model_serving, docs, api_surface; notification: sent
关于如何分类、编辑机器可读文件、使用脚本获取假阳性和假阴性的说明,都包含在一条OpenClaw定时任务所引用的智能体技能中,该任务每2小时执行一次。随后,OpenClaw智能体会摄取任何新的issue或PR,将它们以适当标签添加到JSON文件中,运行脚本,并在同一个Discord频道中报告结果。通过这种方式,我们可以每隔几小时观察本地模型的性能,并在出现遗漏时收到通知。
结论
我们认为,issue/PR分类任务是一类更广泛任务(我们称之为“高通量分类”)的一个特例。本文探讨了仅在一个领域(即开源贡献)中使用本地模型实时过滤信息的思路。像gemma-4-26b-a4b和qwen3.6-35b-a3b这样的中等规模本地模型,无需任何微调就能以较高的准确率进行一次性分类,这使它们成为快速原型验证的良好首选——之后再转向更具成本效益的传统分类器模型。
然而,同样的方法也可以应用于其他领域:
- 新闻行业的文章分类
- 过滤社交媒体和论坛(如X或Reddit)中的感兴趣帖子
- 客服工单分类
- 内容审核申诉分类
- 销售过程中过滤潜在客户线索
- 研究过程中过滤arXiv上的特定主题
这个列表可以继续扩展,但我们认为思路已经明确。
除了分类之外,我们还探索了如何通过智能体框架以安全方式运行快速本地模型来执行分类。这种方法的一个好名称是“智能体分类”:模型并非一开始就被喂入全部信息,而是可以在返回结构化数据之前搜索更多上下文。虽然我们不能完全称之为新颖方法,但我们希望这篇博文能为具体的Pi + restricted shell + final_json组合提供一份不错的参考。
[^1]: 对于本文所述的用例,我们发现以能够正确理解并标注产品表面的方式拆分 PR/Issue 是一个难题。[^2]: 虽然在我们的测试中没有出现这种情况——但模型得出“下一步需要收集信息”的结论并使用外部分类器是完全合理的。智能体方法与传统方法并非互斥。[^3]: 此处可查看完整的话题列表及其他配置。[^4]: 我们使用了来自 antirez/deepseek-v4-gguf 的 DeepSeek-V4-Flash-IQ2XXS-w2Q2K-AProjQ8-SExpQ8-OutQ8-chat-v2.gguf 模型。[^5]: 尽管我们清楚使用大语言模型作为评判者会抵消“免费”这一优势,但我们的具体实现出于研究目的才这样做。在实践中,可以在试用期同时使用一个更大、更昂贵的模型进行校准,之后系统将完全过渡到较小的模型。在最近的运行中,此审计循环每两小时检查一次大约消耗 40,000 个 GPT-5.5 模型 token,其中大部分是缓存的上下文,按 API 定价每次运行成本约 2-3 美分,如果每天运行 12 次,则每月约 9 美元。这是对所有新项目进行的单批次审计,并非每个项目单独调用评判者;若按项目单独进行,成本可能会高出数倍。
本文提及的模型 1
本文提及的数据集 1
社区
· 或发表评论
