声明式 Provider 注册表模式
声明式 Provider 注册表模式
学习目标
读完本章后,你将能够:
- 理解全局 Provider 注册表的设计动机
- 实现基于 JSON 配置文件的声明式 Provider 注册
- 将任意兼容 OpenAI/Anthropic 接口的端点注册为可切换的模型
- 区分 Builtin、Declarative、Custom、ACP 四种 Provider 类型
前置知识
- 多模型适配架构
- Factory 模式与泛型注册
核心概念
1. 为什么需要声明式注册表
在多模型 Agent 系统中,硬编码每个 Provider 会导致两个问题:
- 新模型上线需要改代码:每次支持新的 API 兼容端点,都需要修改 Provider 初始化逻辑并重新编译
- 用户无法自定义:企业内部模型、私有部署端点无法通过配置接入
声明式注册表将「Provider 是什么」从代码中分离,通过 JSON 配置文件描述端点信息,运行时动态注册。
2. 注册表架构
注册表是一个 OnceCell<RwLock<ProviderRegistry>> 全局单例,懒初始化。三种 Provider 来源:
| 类型 | 来源 | 生命周期 | 示例 |
|---|---|---|---|
| Builtin | 代码中 registry.register::<T>() | 编译时固定 | Anthropic、OpenAI、Google |
| Declarative | 编译内嵌 JSON + 磁盘 JSON | 运行时加载 | NVIDIA、Groq、Alibaba |
| Custom | ~/.config/goose/custom_providers/*.json | 用户动态创建 | 企业私有模型端点 |
| ACP | 外部 AI CLI 封装 | 运行时封装 | Claude Code、Codex |
3. JSON 配置 Schema
{ "name": "nvidia", "engine": "openai", "display_name": "NVIDIA", "base_url": "https://integrate.api.nvidia.com/v1", "api_key_env": "NVIDIA_API_KEY", "models": [ { "name": "meta/llama-3.1-70b-instruct", "display_name": "Llama 3.1 70B", "context_limit": 131072 } ], "requires_auth": true, "preserves_thinking": false}关键字段:
| 字段 | 说明 |
|---|---|
name | 唯一标识,如 "nvidia"、"custom_myapi" |
engine | 复用哪个内置 Provider 的适配器:openai / anthropic / ollama |
base_url | API 端点,支持 ${VAR_NAME} 占位符 |
api_key_env | 读取 API Key 的环境变量名 |
models | 该端点提供的模型列表 |
requires_auth | 是否需要 API Key 认证 |
preserves_thinking | 该端点是否保留 <think> 推理块 |
dynamic_models | 是否支持动态拉取模型列表 |
4. 注册流程
关键设计:注册阶段只存储配置和构造函数闭包,实际实例化延迟到首次使用时。这使得环境变量和 UI 配置可以在注册后被设置。
5. 配置解析与占位符展开
JSON 中的 ${VAR_NAME} 占位符在 Provider 实例化时展开:
base_url: "https://${CUSTOM_HOST}/v1"→ 读取 Config::get_param("CUSTOM_HOST")→ 如果未配置,使用 JSON 中的默认值为什么延迟展开? 如果注册时就展开,用户通过 UI 修改配置后不会生效。延迟展开确保每次 Provider 实例化都读取最新配置。
设计模式详解
泛型注册与构造函数闭包
注册表使用泛型方法 register::<T>() 注册具体 Provider 类型:
// 伪代码:注册过程registry.register::<OpenAiProvider>(true);
// 内部实现:// 1. 调用 ProviderDef::metadata() 获取该 Provider 的元信息// 2. 创建构造函数闭包:|| -> Result<Arc<dyn Provider>> { Ok(Arc::new(OpenAiProvider::new(...))) }// 3. 将 (metadata, constructor) 存入注册表对于 Declarative Provider,使用 register_with_name() 指定自定义名称:
// 伪代码:Declarative 注册let config = load_json("nvidia.json");registry.register_with_name::<OpenAiProvider, _, _>("nvidia", || { let resolved = resolve_config(&config)?; // 展开 ${VAR} OpenAiProvider::from_custom_config(model, resolved)});Engine 路由
JSON 中的 engine 字段决定路由到哪个内置 Provider 类型:
engine: "openai" → OpenAiProvider::from_custom_config()engine: "anthropic" → AnthropicProvider::from_custom_config()engine: "ollama" → OllamaProvider::from_custom_config()为什么不是每个新端点写一个 Provider? 大多数 API 兼容端点遵循 OpenAI 或 Anthropic 的协议规范,复用已有的 Provider 适配器比从零编写成本低得多。Engine 路由让一个 JSON 文件就能接入新端点。
ACP 包装器模式
ACP Provider 将外部 AI CLI 工具(Claude Code、Codex、Gemini CLI)封装为注册表中的 Provider:
// 伪代码:ACP Provider 工厂type AcpProviderFactory = Arc< dyn Fn( String, // provider name ModelConfig, // model configuration Vec<ExtensionConfig>, // extensions Option<PathBuf>, // working directory ) -> BoxFuture<'static, Result<Arc<dyn Provider>>> + Send + Sync>;工作原理:
- ACP Provider 通过 stdio 启动外部 CLI(如
claude、codex) - 通过 ACP 协议(JSON-RPC over stdio)与该 CLI 通信
- 将外部 CLI 的能力封装为
Providertrait 接口 - Agent 可以像调用内置 Provider 一样调用 ACP Provider
设计价值:Agent 可以无缝切换到其他 AI 编程工具,用户既可以用 Goose 自身的 Agent 能力,也可以通过 ACP Provider 利用 Claude Code 或 Codex 的独特功能。
问题与规避
配置热更新
问题:用户通过 UI 修改了某个 Declarative Provider 的 API Key,已创建的 Provider 实例仍使用旧配置。
对策:
- 配置解析延迟到实例化时,而非注册时
- 每次创建新的 Agent Session 时重新实例化 Provider
- 旧 Session 中的 Provider 不受影响(Turn 上下文隔离)
JSON 配置校验
问题:用户编写的 JSON 配置格式错误或必填字段缺失。
对策:
- 使用 Serde 反序列化,字段缺失时自动报错
- 启动时校验所有已存在的 Custom Provider JSON
- 提供
goose doctor命令检查配置健康状态
同名冲突
问题:Declarative JSON 中的 name 与 Builtin Provider 同名。
对策:
- 注册时检查名称唯一性
- Declarative/Custom 类型不允许覆盖 Builtin 名称
- 用户自定义 Provider 建议使用
custom_前缀
设计取舍
声明式配置 vs 代码 Provider
| 维度 | 声明式 JSON | 代码 Provider |
|---|---|---|
| 新增成本 | 写一个 JSON 文件 | 实现 Provider trait + 注册 |
| 灵活度 | 受限于 engine 抽象 | 完全自定义 |
| 维护成本 | 低(无需编译) | 高(需改代码+编译) |
| 适用场景 | API 兼容端点 | 协议差异大的模型 |
推荐策略:API 兼容的模型(OpenAI、Anthropic 协议)用声明式 JSON;协议差异大的(如需要特殊鉴权、自定义请求格式)用代码 Provider。
延迟实例化 vs 预加载
| 维度 | 延迟实例化 | 预加载 |
|---|---|---|
| 启动速度 | 快(不创建实例) | 慢 |
| 首次调用延迟 | 稍高(需要创建) | 无 |
| 配置热更新 | 支持 | 不支持 |
| 错误发现时机 | 首次使用时 | 启动时 |
大多数 Agent 系统选择延迟实例化,因为 Provider 数量多(25+),用户可能只用其中少数几个。
参考来源
案例补充:CodeWhale Auto 模型路由
本补充基于 CodeWhale 源码分析,首次覆盖于 2026-06-01。
CodeWhale 的 --model auto 模式实现了一种轻量预调用动态路由策略:
工作机制:
- 每轮实际请求前,先向
deepseek-v4-flash发送一次廉价路由调用(thinking off) - 路由调用分析最新请求 + 近期上下文,选择具体模型 + 推理级别
- 映射规则:短/简单任务 → Flash + thinking off;编码/调试/架构 → Pro + high/max thinking
- 失败回退:路由调用失败或返回无效答案时,回退到本地启发式策略
关键设计决策:
- Auto 是本地概念,上游 API 从不收到
model: "auto"——只收到具体的模型和推理级别 - 子 Agent 继承父的 Auto 模式,除非显式指定模型
- 成本追踪对实际运行的模型计费(不是对”auto”计费)
- TUI 显示选中的路由路径,用户可观察到路由决策
Shift+Tab可在 TUI 中手动切换推理级别(off → high → max)
与传统模型路由的区别:
- 不是基于静态规则(如”代码任务用模型 A,写作任务用模型 B”)
- 是基于请求理解的动态决策——模型自己判断需要什么级别的计算能力
- 成本极低:一次 Flash 调用的成本约 $0.14/M input,通常只消耗几百 tokens