
字节笔记本
2026年10月24日 · 约 13 分钟读完
模型工厂与路由:ModelProvider

本文是《从 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。

ModelProvider 接口回顾
第 2 章讲过,ModelProvider(model.go:66)只有一个方法:
type ModelProvider interface {
GetModel(modelName string) (Model, error)
}给模型名,返回 Model 实例。 最简单的实现是 OpenAI provider,不管什么名字,都返回同一个 OpenAI client:
// 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:
// 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-70banthropic/claude-sonnet→ 前缀anthropic,模型claude-sonnetgpt-5(无前缀)→ 走 fallback
配置示例
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),而要用模型名前缀?
前缀路由有三个优势:
-
模型名是配置友好。 Agent 定义存在数据库时,
Model: "groq/llama-3.3-70b"是个字符串,好存好改。如果存 provider 引用,要序列化/反序列化 provider 对象,复杂。 -
运行时可改。 想把某 Agent 从 OpenAI 换到本地?改模型名字符串就行,不用改代码。这在 agents-server(Part VI)里是"改配置立即生效"的基础。
-
统一入口。 RunOptions 只接一个
ModelProvider(router),不用为每个 Agent 单独传 provider。Agent 之间 handoff 时,模型自动路由,不用手动切 provider。
这呼应了前面 Pi 那篇文章讲的"模型路由"趋势,按任务特征分发到不同模型,路由用模型名前缀实现,简洁优雅。
RouterProvider 的链式配置
RouterProvider 用 builder 风格(model_router.go:33-44):
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):
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 最实用的场景,一个平台支持多家模型,用户按需选:
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 那篇文章讲的"模型自由"的实现基础。
小结
读完这一章,你应该理解:
- ModelProvider 是什么,按模型名查 Model 的工厂接口
- RouterProvider 怎么路由,模型名前缀(
groq/llama),前缀选 provider,余下传给 provider - 为什么用前缀路由,模型名好配置、运行时可改、统一入口
- fallback 的作用,无前缀的模型名走默认 provider,向后兼容
- 防御性复制,构造时复制 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 移植版)。全系列目录见总览。



