ByteNoteByteNote
主模型挂了怎么办:模型降级链
字

字节笔记本

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

主模型挂了怎么办:模型降级链

API中转
¥120

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

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

Retry 解决"临时故障",等一下再试就好。但如果模型彻底挂了(API key 过期、服务下线、区域故障),retry 多少次都没用。

Fallback 解决"彻底不可用",主模型失败,自动切到备用模型。agents-go 的 NewFallbackModel 依次尝试一个模型链,第一个成功就返回。这一章拆它,以及和 Retry 的最佳组合。


引子:主模型彻底挂了

场景:

  • 你的主模型是 OpenAI GPT-5
  • OpenAI 突然区域故障(真发生过)
  • Retry 重试 5 次都失败
  • Agent 完全不可用

但如果你有备用模型(Anthropic Claude、本地模型),应该自动切过去。 这就是 Fallback,主模型挂了,用备用的顶上,保证服务不中断。


成本管理:token 用量与预算

源码对照:FallbackModel

NewFallbackModel(model_fallback.go:27)创建一个降级链:

go
// agents/model_fallback.go:27
func NewFallbackModel(primary Model, fallbacks ...Model) Model {
    models := append([]Model{primary}, fallbacks...)
    return &fallbackModel{models: models, shouldFallback: DefaultRetryIf}
}

主模型 + 任意多个备用,组成一个链。 返回的还是 Model 接口,装饰器模式。

阻塞版的逻辑

go
// agents/model_fallback.go:34(骨架)
func (m *fallbackModel) GetResponse(ctx, req) (*ModelResponse, error) {
    var errs []error
    for _, inner := range m.models {
        resp, err := inner.GetResponse(ctx, req)
        if err == nil {
            return resp, nil  // 成功 → 返回
        }
        errs = append(errs, err)
        if !m.shouldFallback(err) {
            break  // context 取消等错误,不降级
        }
        // 否则继续试下一个
    }
    return nil, errors.Join(errs...)  // 全失败 → 合并所有错误
}

逻辑极其简单,依次试,第一个成功就返回,全失败返回合并错误。 三个细节:

1. errors.Join 合并所有错误

go
return nil, errors.Join(errs...)

不是返回最后一个错误,而是合并所有模型的错误。 为什么?因为调试时,你要知道"主模型为什么失败、备用为什么也失败",合并的错误信息最全。Go 1.20 的 errors.Join 正是为这种场景设计的。

2. context 取消不降级

go
if !m.shouldFallback(err) {
    break  // DefaultRetryIf:context 取消不重试/降级
}

如果错误是 context 取消(调用方放弃了),立即停止,不试备用。 因为调用方都不要结果了,试备用是浪费。

3. 流式版的"首 event 前"约束

和 Retry 一样,流式 Fallback 也受"首 event 前"约束(model_fallback.go:69):

go
if producedAny || i == len(m.models)-1 || !m.shouldFallback(streamErr) {
    yield(nil, errors.Join(errs...))
    return  // 吐过 token 了,不降级
}

一旦某个模型吐过 token,就"锁定"它,不切备用。 原因相同,已经吐给用户的内容改不回来,切备用会让内容跳变。


设计权衡:Fallback 和 Retry 的最佳组合

注释给了明确建议(model_fallback.go:21):

Wrap each backend in NewRetryModel first so every provider exhausts its own retries before the chain moves on.

翻译,先给每个模型包 Retry,再组 Fallback。 让每个 provider 先耗尽自己的重试,再降级:

go
// 推荐组合
model := agents.NewFallbackModel(
    agents.NewRetryModel(openaiModel, retryPolicy),      // OpenAI 先重试 5 次
    agents.NewRetryModel(anthropicModel, retryPolicy),   // 还失败 → Claude 重试 5 次
    agents.NewRetryModel(localModel, retryPolicy),       // 还失败 → 本地重试 5 次
)

为什么不直接 Fallback 不 Retry?

方式行为问题
只 FallbackOpenAI 失败 1 次 → 立刻切 ClaudeOpenAI 只是临时抖动,却切走了
只 RetryOpenAI 重试 5 次都失败 → 整个挂没有 Plan B
Retry + FallbackOpenAI 重试 5 次 → 还失败 → 切 Claude 重试 5 次 → ...最优

Retry 处理"临时故障"(多试几次就好),Fallback 处理"彻底故障"(换一个)。 两者解决不同问题,组合才完整。


Provider 级的 Fallback

除了 NewFallbackModel(模型级),还有 NewFallbackProvider(model_fallback.go:89),provider 级的降级:

go
// agents/model_fallback.go:89
func NewFallbackProvider(primary ModelProvider, fallbacks ...ModelProvider) ModelProvider

区别,Model 级是"一个模型名,多个 Model 实例";Provider 级是"一个 provider 入口,每次 GetModel 自动组成 FallbackModel"。

go
// Provider 级
provider := agents.NewFallbackProvider(
    openai.NewProvider(),     // 主
    anthropic.NewProvider(),  // 备
)
// 之后 provider.GetModel("gpt-5") 自动返回 FallbackModel

Provider 级更方便,配置一次,所有模型自动有降级。 适合"整个 provider 可能挂"的场景(OpenAI 全挂 → 全切 Anthropic)。


实战配置示例

一个生产级的模型配置,组合 Router + Retry + Fallback:

go
// 1. 各 provider
openaiP := openai.NewProvider()
anthropicP := anthropic.NewProvider()
localP := ollama.NewProvider()

// 2. 每个 provider 包 Retry(处理临时故障)
retryPolicy := agents.RetryPolicy{MaxAttempts: 3}

// 3. Provider 级 Fallback(OpenAI 挂 → Anthropic → 本地)
robustProvider := agents.NewFallbackProvider(
    wrapWithRetry(openaiP, retryPolicy),
    wrapWithRetry(anthropicP, retryPolicy),
    wrapWithRetry(localP, retryPolicy),  // 本地做最后兜底
)

// 4. 再套 Router(按模型名前缀路由)
router := agents.NewRouterProvider(map[string]agents.ModelProvider{
    "openai":    robustProvider,
    "anthropic": robustProvider,  // 都走同一个降级链
}).WithFallback(robustProvider)

opts := agents.RunOptions{ModelProvider: router}

三层防护,Router(路由)+ Retry(临时故障)+ Fallback(彻底故障)。 这是生产级 Agent 的标配。第 24 章会详述这个组合。


设计权衡:Fallback 的代价

Fallback 不是免费的:

  1. 延迟,主模型失败后,要等 Retry 耗尽,才试备用。最坏情况延迟 = 主 Retry 全程 + 备用 Retry 全程。
  2. 一致性,不同模型的输出风格不同。主用 GPT,备用 Claude,用户可能感觉"Agent 变了个人"。
  3. 成本,备用模型可能更贵(比如本地不可用切到贵的云 API)。

所以 Fallback 要配置得当,备用模型要"够用"(不一定和主一样强,但要能完成任务),Retry 的 MaxAttempts 不要太大(否则降级太慢)。


小结

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

  1. Fallback 解决什么,主模型彻底挂了,自动切备用
  2. 降级链逻辑,依次试,第一个成功返回,全失败合并错误
  3. errors.Join,合并所有错误,便于调试
  4. context 取消不降级,调用方放弃了,不浪费
  5. 流式"首 event 前"约束,吐过 token 不切换
  6. 最佳组合,每个模型先 Retry 再 Fallback,临时故障和彻底故障都覆盖
  7. Provider 级 vs Model 级,前者配置一次全覆盖,后者更精细

Retry + Fallback 让模型调用"既容忍抖动,又有 Plan B"。 加上第 21 章的 Router,三个装饰器可以任意组合,下一章讲怎么组合。


下一篇

第 24 章,三装饰器可任意组合。Retry、Fallback、Router 都实现 Model/ModelProvider 接口,是装饰器。它们可以任意嵌套,Retry 套 Fallback、Fallback 套 Router、Router 套 Retry……这一章用几个实战组合讲透装饰器模式的威力。


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

相关文章

分享: