ByteNoteByteNote
三个装饰器,任意组合出韧性
字

字节笔记本

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

三个装饰器,任意组合出韧性

API中转
¥120

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

本文是《从 0 写 Agent》系列第 24 章。这个系列基于开源项目 agents-go(OpenAI Agents SDK 的 Go 移植版),用 Go 从零实现一个生产级 Agent 运行时;本章讲「三装饰器可任意组合」。

Retry、Fallback、Router,三个模型装饰器,各自解决一个问题。但它们的真正威力在于,可以任意嵌套组合。

这一章用几个实战组合,讲透装饰器模式为什么是 Agent 模型工程的基石。


引子:装饰器模式的本质

先回顾三个装饰器:

装饰器解决什么包装什么
RetryModel临时故障(重试就好)包装 Model
FallbackModel彻底故障(换备用)包装 Model
RouterProvider多 provider 路由包装 ModelProvider

它们的共同点,都实现被包装对象的接口。 RetryModel 实现 Model,FallbackModel 实现 Model,RouterProvider 实现 ModelProvider。

这意味着,装饰器可以包装装饰器。 一个 RetryModel 可以包一个 FallbackModel,后者又包了 Router 返回的 Model。任意嵌套,像俄罗斯套娃。


成本管理:token 用量与预算

为什么能任意组合

装饰器模式的核心契约,装饰器实现和被装饰对象相同的接口。

go
// RetryModel 实现了 Model 接口
type retryModel struct {
    inner Model  // 被装饰的,也是 Model
}
func (r *retryModel) GetResponse(...) { /* 失败时调 inner 重试 */ }

// FallbackModel 也实现了 Model
type fallbackModel struct {
    models []Model  // 一串 Model
}
func (f *fallbackModel) GetResponse(...) { /* 依次试 models */ }

因为都是 Model 接口,所以:

go
// Fallback 里包 Retry
m := NewFallbackModel(
    NewRetryModel(openai, policy),    // 这个是 Model
    NewRetryModel(anthropic, policy), // 这个也是 Model
)
// m 是 Model,可以被再包
m2 := NewRetryModel(m, anotherPolicy)  // 给整个降级链再套一层 Retry

无限嵌套,类型永远对。 这是接口 + 装饰器的威力,你不用为"组合方式"写特殊代码,接口天然支持。


实战组合一:Retry + Fallback(最常用)

第 23 章讲过的最佳实践,每个模型先 Retry,再 Fallback:

go
model := agents.NewFallbackModel(
    agents.NewRetryModel(openaiModel, retryPolicy),
    agents.NewRetryModel(anthropicModel, retryPolicy),
)

行为:

OpenAI 重试 3 次 → 全失败 → Anthropic 重试 3 次 → 全失败 → 返回合并错误

最坏延迟 = OpenAI 3 次(含退避)+ Anthropic 3 次。可接受。


实战组合二:Provider 级 Fallback + Router

go
// 1. Provider 级 Fallback(OpenAI 挂 → Anthropic)
robust := agents.NewFallbackProvider(openaiP, anthropicP)

// 2. Router 路由(按前缀分发)
router := agents.NewRouterProvider(map[string]agents.ModelProvider{
    "fast": groqP,     // groq/xxx 走 Groq
    "smart": robust,   // smart/xxx 走降级链
}).WithFallback(robust) // 无前缀走降级链

行为:

  • groq/llama-3.3 → 走 Groq(快、便宜)
  • smart/gpt-5 → 走 OpenAI(重试+降级到 Anthropic)
  • claude-sonnet(无前缀)→ 走 Fallback 链

一个 provider 入口,按模型名分发到不同后端,每个后端有自己的容错策略。


实战组合三:全防护(生产级)

go
retryPolicy := agents.RetryPolicy{MaxAttempts: 3, BaseDelay: 500 * time.Millisecond}

// 每个 provider 自带 Retry
robustOpenAI := agents.NewRetryProvider(openaiP, retryPolicy)
robustAnthropic := agents.NewRetryProvider(anthropicP, retryPolicy)
robustLocal := agents.NewRetryProvider(ollamaP, retryPolicy)

// Provider 级 Fallback(云挂 → 本地兜底)
cloudWithLocalBackup := agents.NewFallbackProvider(
    robustOpenAI,
    robustAnthropic,
    robustLocal,  // 本地做最后保险
)

// Router 路由
router := agents.NewRouterProvider(map[string]agents.ModelProvider{
    "openai":    cloudWithLocalBackup,
    "anthropic": cloudWithLocalBackup,
}).WithFallback(cloudWithLocalBackup)

三层防护:

  1. Router,按模型名路由
  2. Fallback,云挂了切本地
  3. Retry,每个 provider 内部重试

这是"即使 OpenAI 和 Anthropic 同时挂,本地模型也能顶上"的极限容错。 适合不能停服的关键业务。


设计权衡:嵌套层次与延迟

装饰器可以无限嵌套,但不应该。每多一层,最坏延迟增加。

text
3 层 Fallback × 每层 3 次 Retry × 每次 500ms~30s 退避
最坏延迟 = 3 × (3 × 15s 平均) = 135 秒

用户等不了 135 秒。 所以实战中要平衡:

  • Retry 的 MaxAttempts 别太大(2-3 次够)
  • Fallback 的层级别太深(2-3 个备用够)
  • 配合 context timeout 兜底(整个调用最多等 N 秒)
go
ctx, cancel := context.WithTimeout(parentCtx, 30 * time.Second)
defer cancel()
agents.Run(ctx, ...)  // 30 秒还没结果就放弃

容错不是无限重试,是"在可接受时间内,尽可能成功"。


设计权衡:装饰器 vs 配置

有人会想,为什么不做成"一个配置对象"(比如 YAML 描述重试/降级策略),而要用代码嵌套?

配置适合"参数调整"(改 MaxAttempts、改 BaseDelay),但不适合"结构组合"(Retry 套 Fallback 还是 Fallback 套 Retry,是不同的拓扑)。

代码嵌套的好处:

  1. 类型安全,编译器检查接口匹配
  2. 显式拓扑,嵌套结构一目了然,不像配置要脑补解析
  3. 灵活,想怎么组合怎么组合,不受配置 schema 限制

agents-server(Part VI)的做法,数据库存参数(MaxAttempts、BaseDelay、模型名),代码里组装装饰器拓扑。参数可配置,拓扑在代码里。这是平衡点。


装饰器模式的普适性

装饰器不只用于模型层。它是软件工程的通用模式:

场景装饰器
HTTP middleware日志 → 认证 → 限流 → handler
io.Readerbufio.Reader(GzipReader(FileReader))
Agent ModelRetry(Fallback(Router(openai)))

核心都是,实现相同接口,包装增强能力,任意组合。 理解了 agents-go 的模型装饰器,你就理解了装饰器模式本身,它无处不在。


Part IV 模型工程小结

第 21-24 章构成了 Agent 的"模型工程":

章节能力
21 Router多 provider 路由
22 Retry容忍临时故障
23 Fallback容忍彻底故障
24 组合任意嵌套,生产级容错

这四章让 Agent 的模型调用"健壮",不怕抖动、不怕故障、多后端混用。 但"健壮"还不够,你还要"知道发生了什么"。这就是 Tracing(第 25-27 章)。


下一篇

第 25 章,Tracing:从 0 造一套可观测系统。Agent 出错时,你需要知道是哪一步炸的。agents-go 自实现了一套 trace/span/processor/exporter 四层架构(不用 OpenTelemetry),对标 Python SDK 保证一致性。这是 Agent 可观测性的核心。


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

相关文章

分享: