跳转到内容

声明式 Provider 注册表模式

声明式 Provider 注册表模式

学习目标

读完本章后,你将能够:

  • 理解全局 Provider 注册表的设计动机
  • 实现基于 JSON 配置文件的声明式 Provider 注册
  • 将任意兼容 OpenAI/Anthropic 接口的端点注册为可切换的模型
  • 区分 Builtin、Declarative、Custom、ACP 四种 Provider 类型

前置知识


核心概念

1. 为什么需要声明式注册表

在多模型 Agent 系统中,硬编码每个 Provider 会导致两个问题:

  1. 新模型上线需要改代码:每次支持新的 API 兼容端点,都需要修改 Provider 初始化逻辑并重新编译
  2. 用户无法自定义:企业内部模型、私有部署端点无法通过配置接入

声明式注册表将「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_urlAPI 端点,支持 ${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
>;

工作原理

  1. ACP Provider 通过 stdio 启动外部 CLI(如 claudecodex
  2. 通过 ACP 协议(JSON-RPC over stdio)与该 CLI 通信
  3. 将外部 CLI 的能力封装为 Provider trait 接口
  4. 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 模式实现了一种轻量预调用动态路由策略:

工作机制

  1. 每轮实际请求前,先向 deepseek-v4-flash 发送一次廉价路由调用(thinking off)
  2. 路由调用分析最新请求 + 近期上下文,选择具体模型 + 推理级别
  3. 映射规则:短/简单任务 → Flash + thinking off;编码/调试/架构 → Pro + high/max thinking
  4. 失败回退:路由调用失败或返回无效答案时,回退到本地启发式策略

关键设计决策

  • Auto 是本地概念,上游 API 从不收到 model: "auto"——只收到具体的模型和推理级别
  • 子 Agent 继承父的 Auto 模式,除非显式指定模型
  • 成本追踪对实际运行的模型计费(不是对”auto”计费)
  • TUI 显示选中的路由路径,用户可观察到路由决策
  • Shift+Tab 可在 TUI 中手动切换推理级别(off → high → max)

与传统模型路由的区别

  • 不是基于静态规则(如”代码任务用模型 A,写作任务用模型 B”)
  • 是基于请求理解的动态决策——模型自己判断需要什么级别的计算能力
  • 成本极低:一次 Flash 调用的成本约 $0.14/M input,通常只消耗几百 tokens