前言
上一篇写了如何调整 dev-orchestra 的审查者和角色。 这一次,我要用自己写的适配器接入 Claude Code、Codex、Antigravity 之外的 AI。作为例子,我把在本机运行的本地 LLM 加为审查者。
关于接入本地 LLM,我之前已经写过两篇。
- 把本地 LLM 添加为 dev-orchestra 的代码审查者(英文):接入过程中的反复试错,以及 0.9.0 如何加入官方机制
- 不把机密代码发到云端,用 AI 做审查(英文):只用本地 LLM 进行审查的做法
本文省略来龙去脉,专注于 在 dev-orchestra 1.0.0 中从零编写适配器的步骤。
dev-orchestra 已在 GitHub 开源(MIT 许可证)。如果觉得有用,给个 Star 我会很高兴。
工作原理:适配器只负责“拼出命令”
dev-orchestra 不会直接调用 AI。它只是启动一个 CLI,通过标准输入传入提示词,再从标准输出读取回答。 为这个 CLI 拼出命令的,就是适配器(provider)。
| 组成部分 | 职责 |
|---|---|
| dev-orchestra | 生成提示词,并启动适配器返回的命令 |
| 适配器 | 返回“用哪些参数启动哪个命令” |
| CLI | 把标准输入中的提示词交给 AI,并把回答写入标准输出 |
也就是说,只要有一个从标准输入读取、向标准输出写入的 CLI,任何 AI 都能接入。
步骤
1. 准备一个接收提示词并返回回答的 CLI
本地 LLM(例如 llama.cpp 的 llama-server)提供与 OpenAI 兼容的 API。我写了一个 CLI,只负责把提示词发送过去并输出回答,只用标准库就够了。
"""Send a prompt from stdin to a local OpenAI-compatible server and print the answer."""
import json
import os
import sys
import urllib.request
URL = os.environ.get("LOCAL_LLM_URL", "http://127.0.0.1:8080") + "/v1/chat/completions"
def main():
# dev-orchestra writes the prompt as UTF-8, whatever the OS locale is
sys.stdin.reconfigure(encoding="utf-8")
prompt = sys.stdin.read()
body = json.dumps({"messages": [{"role": "user", "content": prompt}]}).encode("utf-8")
request = urllib.request.Request(URL, data=body, headers={"Content-Type": "application/json"})
# Ignore proxy settings so the prompt never leaves this machine
opener = urllib.request.build_opener(urllib.request.ProxyHandler({}))
with opener.open(request, timeout=1800) as response:
answer = json.load(response)["choices"][0]["message"]["content"]
sys.stdout.reconfigure(encoding="utf-8")
sys.stdout.write(answer)
if __name__ == "__main__":
main()
把它保存为 local_llm_review.py,位置随意。
这个 CLI 我注意了两点。
| 写了什么 | 原因 |
|---|---|
sys.stdin.reconfigure(encoding="utf-8") | dev-orchestra 以 UTF-8 写入提示词。日文版 Windows 默认按 cp932 读取,所以含有日文注释的 diff 会乱码,或在读取时崩溃 |
使用以 ProxyHandler({}) 创建的 opener | urlopen 会遵循 HTTP_PROXY 等环境变量和 Windows 的代理设置。即使目标是 127.0.0.1,代码也可能经由代理流出本机 |
请让本地 LLM 服务器只监听
127.0.0.1(localhost)。如果监听0.0.0.0,同一网络中的任何人都能使用它。
2. 编写适配器
接下来是适配器。我以 dev-orchestra 文档中的最小示例为基础,为本地 LLM 编写了它。
"""Adapter for a local LLM behind an OpenAI-compatible server. Review only."""
from __future__ import annotations
import sys
from typing import Any, Dict, List, Optional, Sequence
from orchestrator.providers.base import (
READ_ONLY_MODES,
ModelCandidate,
ModelResolutionError,
Provider,
ResolvedModel,
)
# The small CLI that sends the prompt to the local server
CLI = r"C:\path\to\local_llm_review.py"
class LocalLLMProvider(Provider):
name = "localllm"
display_name = "Local LLM"
executable = sys.executable
fallback_models = (ModelCandidate("", "default", "server default", "builtin-fallback"),)
fallback_updated = "2026-10-12"
option_keys = ("args",)
def resolve_latest(self, family: str) -> ResolvedModel:
if family in ("", "default"):
return ResolvedModel(self.name, "default", "latest", None, "server default", "cli-default")
raise ModelResolutionError("localllm: only the default model is supported, not %r" % family)
def build_command(
self,
mode: str,
resolved: ResolvedModel,
cwd: str,
extra_args: Sequence[str] = (),
options: Optional[Dict[str, Any]] = None,
) -> List[str]:
# The CLI cannot read or write files, so it can only review a diff it is given.
if mode not in READ_ONLY_MODES:
raise ModelResolutionError("localllm cannot implement: it has no file access")
return [self.executable, CLI] + self.option_args(options)
def build_provider(executable: Optional[str] = None) -> LocalLLMProvider:
return LocalLLMProvider(executable)
要点有四个。
| 要点 | 原因 |
|---|---|
from orchestrator.providers.base import ... | 这个文件是从插件的包之外加载的,所以不能使用相对导入(from .base) |
name = "localllm" | 在配置的 provider: 中填写的名称。不能使用 claude、codex、agy、mock |
resolve_latest 只接受 default | 对于无法确认的模型名,不去猜测而是直接停下,与 dev-orchestra 整体的规则保持一致 |
| 拒绝非只读的运行(实现) | 这个 CLI 无法读写文件。即使被放在实现者的位置,也绝不能在什么都没改的情况下假装成功 |
请把 CLI 改成你在步骤 1 中保存的文件的真实位置。示例中的 r"..." 是为了原样写出 Windows 路径中的 \。在 macOS / Linux 上,写成 CLI = "/home/you/local_llm_review.py" 这样即可。
由于
executable = sys.executable,dev-orchestra 检查安装情况时运行的是python --version。CLI 本身不需要提供--version。
3. 放到配置目录的 providers/ 中
适配器要放在配置目录的 providers/ 中。
| 操作系统 | 位置 |
|---|---|
| Windows | %APPDATA%\dev-orchestra\providers\ |
| macOS / Linux | ~/.config/dev-orchestra/providers/ |
文件名要能用作标识符,例如 localllm.py。
不要放在插件内部(
~/.claude/plugins/cache/...)。那里的内容会在插件更新时被删除。配置目录是配置文件(config.yaml)所在的位置,所以更新后也不会丢失。
在 Windows 上,如果使用的是 Microsoft Store 版 Python,放在
%APPDATA%中的文件有时 Python 看不到(我在之前的文章(英文)里为此耽误了半天)。doctor发现这种情况时,会显示真实的位置和处理方法。
4. 用 doctor 确认已加载
放好适配器后,先运行 dev-orchestra doctor(让 agent 来运行也一样)。我得到的输出如下(节选,省略了路径)。
Local LLM (localllm)
Installed: yes
Source: user module <config dir>\providers\localllm.py
Model selection: supported
Models (builtin-fallback): server default
Read-only runs: not reported by this adapter
Resume: NOT SUPPORTED -- localllm does not resume sessions
User providers
Directory: <config dir>\providers
Code in this directory is imported at startup, from outside the plugin.
Imported: localllm <- <config dir>\providers\localllm.py
它甚至会显示是从哪个文件加载的。如果最后出现 No problems found.,就成功了。
即使写错了,dev-orchestra 整体也不会停止,doctor 会告诉你是哪个文件、什么错误。
5. 注册为审查者
把它加为审查者。别忘了加上 --model default。
dev-orchestra reviewer add --provider localllm --model default --role general
我一开始忘了加
--model。结果它以系列名recommended-coding注册,审查时我自己的适配器就以only the default model is supported, not 'recommended-coding'拒绝了。这正是步骤 2 中所说的“无法确认的名称就停下”的行为。
6. 让它试着审查
我用上一篇文章中埋了 bug 的示例做了尝试。 不过,这次想确认的是整个流程能否走通。所以我没有使用本地 LLM,而是用了返回固定回答的模拟服务器。
ok localllm-general localllm server default 1 finding(s)
1 successful, 0 failed
模拟服务器的回答通过我自己的适配器返回了。由此确认的是适配器的接入,以及回答格式的解析。只要回答符合 dev-orchestra 规定的格式(## Finding …),就会像其他审查者的意见一样,经过去重合并和分诊。
真正的本地 LLM 的审查能力究竟如何,请看之前的文章(英文)(与两个商用模型相比,6 条意见中有 1 条被采纳)和机密代码那篇文章(英文)。
7. 确认让它做实现时会拒绝
为保险起见,我把这个适配器设为实现者并运行了一次。
localllm cannot implement: it has no file access
它在启动 CLI 之前就停下了。即使不小心把它放在实现者的位置,也不会出现“显示成功但什么都没变”这种难以察觉的失败。
能胜任和不能胜任的角色
| 角色 | 使用此适配器 | 原因 |
|---|---|---|
| 审查 | 可以 | diff 通过提示词传入,所以不能读文件也能成立 |
| 设计 | 部分可以 | 能运行,但只能看到传入的提示词,无法自己查找相关文件 |
| 实现、修复 | 不可以 | 无法写入文件,所以由适配器拒绝 |
Claude Code 和 Codex 能自己追踪并查看相关文件。这个 CLI 只能看到传给它的内容,所以适合用作补充另一种视角的审查者。
注意事项
providers/中的文件会在每次启动 dev-orchestra 时以你的权限执行。这里没有沙箱。只放自己写的,或者读过内容并信任的文件- 本地 LLM 服务器只监听 localhost(步骤 1 中的提醒)
- 在 CLI 一侧,不经过代理直接发送到
127.0.0.1(步骤 1 中的ProxyHandler({}))。修改LOCAL_LLM_URL时,也要使用本机地址 - 如果打算不让代码流出本机,就不要从 Claude Code 等云端 AI 调用 dev-orchestra,因为调用方的 AI 会读到代码。请直接在终端中运行,并用
--only限定为本地 LLM(机密代码那篇文章(英文)) - 自己写的适配器不会自动加入预设。只有在配置中指定名称时才会运行
doctor中的Read-only runs: not reported by this adapter表示这个适配器没有报告它如何保证只读。这个 CLI 本来就不会接触文件,所以没有实际危害;但在接入能够接触文件的 CLI 时,需要自己确认它是否真能保持只读- 适配器所依赖的部分(本文示例中用到的那些)包含在 1.0.0 的兼容性承诺之内。即便如此,更新 dev-orchestra 之后运行一次
doctor会更放心
总结
- dev-orchestra 只是启动一个 CLI。只要有从标准输入读取、向标准输出写入的 CLI,任何 AI 都能接入
- 适配器大约 50 行。从
orchestrator.providers.base导入,编写resolve_latest和build_command - 放在配置目录的
providers/中,插件更新后也不会丢失 - 放好之后,用
doctor确认是从哪个文件加载的 - 用
reviewer add --provider localllm --model default --role general注册 - 不能接触文件的 CLI 要设为仅限审查,并由适配器拒绝实现
providers/中的代码以你的权限执行。只放可信的内容
详细规则见 providers 文档。
本系列的文章可以在“dev-orchestra”文章列表中集中阅读。
GitHub: istb16/dev-orchestra(给个 Star 会是很大的鼓励)
相关文章
重新组合 dev-orchestra 的审查者与角色
介绍如何为 dev-orchestra 的每个角色(role)指定模型、如何选择审查者的侧重点,以及能提议领域专家角色的 config suggest-roles。我还把故意埋了 bug 的改动交给 9 位审查者,测了测增加人数会带来什么变化。
比较 dev-orchestra 的预设 quality / standard / fast
我在 3 种环境(只有 Claude Code、只有 Codex、两者都有)中实际展开了 dev-orchestra 初始设置里可选的预设 quality / standard / fast,并进行了比较。这里总结各角色的模型、审查者人数和成本削减程度有何不同,以及该选哪一个。
在 Antigravity 中试用 dev-orchestra
我也在 Antigravity 中装上了 dev-orchestra。作为对话的地方可以正常使用,但把设计和审查交给 Antigravity CLI(agy)是有限制的。这里整理了能做什么、不能做什么,以及我推荐的用法。