ByteNoteByteNote
模型工厂与路由:ModelProvider
字

字节笔记本

2026年10月24日 · 约 13 分钟读完

模型工厂与路由:ModelProvider

API中转
¥120

模型配置:模型选择与上下文窗口

本文是《从 0 写 Agent》系列第 21 章。这个系列基于开源项目 agents-go(OpenAI Agents SDK 的 Go 移植版),用 Go 从零实现一个生产级 Agent 运行时;本章讲「ModelProvider:模型的工厂与路由」。

一个 Agent 运行时,可能同时配置了多个模型后端,这个 Agent 用 OpenAI,那个用 Anthropic,还有一个用本地 Ollama。怎么管理?

agents-go 用 ModelProvider 接口,"按名字查 Model"。这一章讲它的基本实现和最实用的 RouterProvider,用模型名前缀路由到不同 provider,让一个 Run 里不同 Agent 走不同后端。


引子:多模型的现实需求

你的 Agent 平台可能这样配置:

  • 主 Agent 用 GPT-5(强,贵)
  • 子 Agent 用 GPT-5-mini(便宜,够用)
  • 隐私敏感任务用本地 Ollama(数据不出本机)
  • 某些任务用 Claude(某些能力更强)

每个 Agent 用不同模型,怎么管理这些 provider?

如果每个 Agent 自己配 provider,代码会很乱。更好的方式,一个统一的 provider 入口,根据模型名自动路由。这就是 RouterProvider。


成本管理:token 用量与预算

ModelProvider 接口回顾

第 2 章讲过,ModelProvider(model.go:66)只有一个方法:

go
type ModelProvider interface {
    GetModel(modelName string) (Model, error)
}

给模型名,返回 Model 实例。 最简单的实现是 OpenAI provider,不管什么名字,都返回同一个 OpenAI client:

go
// OpenAI provider 的概念实现
type OpenAIProvider struct{ client *openai.Client }

func (p *OpenAIProvider) GetModel(name string) (Model, error) {
    return &openaiModel{client: p.client, name: name}, nil
}

但当你有多个 provider 时,需要一个"路由器",根据名字前缀分发。


源码对照:RouterProvider

RouterProvider(model_router.go:17)是按前缀路由的 provider:

go
// agents/model_router.go:48
func (r *RouterProvider) GetModel(modelName string) (Model, error) {
    // 按分隔符(默认 "/")切分
    if prefix, rest, found := strings.Cut(modelName, r.separator); found {
        if p, ok := r.routes[prefix]; ok {
            return p.GetModel(rest)  // 前缀匹配 → 路由到对应 provider
        }
    }
    // 无匹配前缀 → fallback
    if r.fallback != nil {
        return r.fallback.GetModel(modelName)
    }
    return nil, fmt.Errorf("no provider for model %q", modelName)
}

核心逻辑,用 strings.Cut 在第一个 / 切分:

  • groq/llama-3.3-70b → 前缀 groq,模型 llama-3.3-70b
  • anthropic/claude-sonnet → 前缀 anthropic,模型 claude-sonnet
  • gpt-5(无前缀)→ 走 fallback

配置示例

go
router := agents.NewRouterProvider(map[string]agents.ModelProvider{
    "openai":    openai.NewProvider(),
    "anthropic": anthropic.NewProvider(),
    "groq":      groq.NewProvider(),
    "local":     ollama.NewProvider(),
}).WithFallback(openai.NewProvider())  // 无前缀的走 OpenAI

opts := agents.RunOptions{ModelProvider: router}

// 不同 Agent 用不同模型
mainAgent   := &agents.Agent{Name: "main",   Model: "openai/gpt-5"}
cheapAgent  := &agents.Agent{Name: "cheap",  Model: "groq/llama-3.3-70b"}
localAgent  := &agents.Agent{Name: "local",  Model: "local/llama3"}
plainAgent  := &agents.Agent{Name: "plain",  Model: "gpt-5"}  // 走 fallback

模型名本身携带路由信息,groq/llama-3.3-70b 既说明用 Groq provider,又说明用 llama 模型。 一行配置,多 provider 混用。


设计权衡:为什么用前缀路由

有人会想,为什么不在 Agent 上单独配 provider 字段(Agent.Provider),而要用模型名前缀?

前缀路由有三个优势:

  1. 模型名是配置友好。 Agent 定义存在数据库时,Model: "groq/llama-3.3-70b" 是个字符串,好存好改。如果存 provider 引用,要序列化/反序列化 provider 对象,复杂。

  2. 运行时可改。 想把某 Agent 从 OpenAI 换到本地?改模型名字符串就行,不用改代码。这在 agents-server(Part VI)里是"改配置立即生效"的基础。

  3. 统一入口。 RunOptions 只接一个 ModelProvider(router),不用为每个 Agent 单独传 provider。Agent 之间 handoff 时,模型自动路由,不用手动切 provider。

这呼应了前面 Pi 那篇文章讲的"模型路由"趋势,按任务特征分发到不同模型,路由用模型名前缀实现,简洁优雅。


RouterProvider 的链式配置

RouterProvider 用 builder 风格(model_router.go:33-44):

go
router := agents.NewRouterProvider(routes).
    WithFallback(openaiProvider).
    WithSeparator("/")  // 默认就是 /,可改

WithFallback 和 WithSeparator 都返回 *RouterProvider,可以链式调用。这是 Go 里配置对象的常见风格。

为什么 fallback 重要

没有 fallback 时,模型名没匹配前缀就报错(model_router.go:57)。但很多场景你希望"没指定的就走默认",比如老配置只写了 gpt-5(没前缀),应该走 OpenAI 而不是报错。

WithFallback 提供这个"默认 provider",让 RouterProvider 兼容无前缀的模型名。向后兼容的工程考虑。


maps.Copy,防御性复制

注意构造函数里的细节(model_router.go:26):

go
func NewRouterProvider(routes map[string]ModelProvider) *RouterProvider {
    cp := make(map[string]ModelProvider, len(routes))
    maps.Copy(cp, routes)  // 复制,不用原 map
    return &RouterProvider{routes: cp, separator: "/"}
}

不直接用传入的 map,而是复制一份。 为什么?因为调用方可能之后修改原 map(增删路由),如果 RouterProvider 直接引用原 map,行为会不确定。

防御性复制是并发/生命周期安全的惯用法,构造时拍快照,之后互不影响。 Go 1.21 的 maps.Copy 让这个操作很简洁。


多 provider 的实战场景

RouterProvider 最实用的场景,一个平台支持多家模型,用户按需选:

text
agents-server 的 ProviderRoute 表(Part VI 第 35 章):
  前缀 "openai"    → api_key=sk-xxx,   base_url=api.openai.com
  前缀 "deepseek"  → api_key=sk-yyy,   base_url=api.deepseek.com
  前缀 "glm"       → api_key=sk-zzz,   base_url=open.bigmodel.cn
  fallback         → openai

用户在 UI 里配 Agent 时,选模型 "deepseek/deepseek-chat",RouterProvider 自动路由到 DeepSeek。这就是前面 DeepSeek V4 那篇文章讲的"模型自由"的实现基础。


小结

读完这一章,你应该理解:

  1. ModelProvider 是什么,按模型名查 Model 的工厂接口
  2. RouterProvider 怎么路由,模型名前缀(groq/llama),前缀选 provider,余下传给 provider
  3. 为什么用前缀路由,模型名好配置、运行时可改、统一入口
  4. fallback 的作用,无前缀的模型名走默认 provider,向后兼容
  5. 防御性复制,构造时复制 map,避免外部修改影响内部状态

RouterProvider 让多模型混用变成"模型名字符串"的配置问题。 但模型会挂、会限流,下一章讲 Retry,让模型调用更健壮。


下一篇

第 22 章,Retry:指数退避与 equal jitter。模型 API 会超时、会 429 限流、会 500。agents-go 的 NewRetryModel 装饰器用几何退避 + equal jitter 算法重试,还有一个流式重试的关键约束,"首 event 前"才能重试。


本文是《从 0 写 Agent》系列第 21 章,案例代码基于开源项目 agents-go(OpenAI Agents SDK 的 Go 移植版)。全系列目录见总览。

相关文章

分享: