Agent 降级策略:模型挂了,业务不能跟着停

一、一个凌晨三点的告警:模型超时引发订单积压

凌晨三点,监控面板上订单队列的红色数字在跳动。12 分钟内积压了 3000 多单,原因很直接——大模型 API 返回了 503。Agent 在等待模型响应时同步阻塞,整个任务链卡死。

这不是模型质量问题,这是架构设计缺陷。把 Agent 的生命周期和大模型可用性强绑定,等于把业务的命脉交给了一个你无法控制的外部服务。任何依赖外部 API 的系统,默认假设都应该是"它随时可能挂"。

生产环境中模型不可用的原因比想象中多:服务商限流、网关超时、内容安全拦截、模型版本回滚。每一条都足以让你的 Agent 停摆。所以降级策略不是"锦上添花",而是上线前的必选项。

二、降级决策的核心模型:条件判断 + 路由分发

Agent 降级的本质是"在模型不可用时,系统如何优雅地继续运作"。设计上分三层:

检测层:捕获错误类型(超时、限流、内容过滤、服务不可用),区分可重试和不可重试错误。

决策层:根据错误类型和上下文,选择降级路径——重试、切换备选模型、使用缓存结果、回退到规则引擎,或者直接返回兜底响应。

执行层:执行降级动作,并记录决策链路用于后续分析。

flowchart TD
    A[用户请求] --> B[Agent 接收任务]
    B --> C{调用大模型}
    C -->|成功| D[返回结果]
    C -->|失败| E{错误类型判断}
    E -->|超时 5xx| F[指数退避重试]
    F -->|重试耗尽| G{降级决策}
    E -->|限流 429| H{降级决策}
    E -->|内容过滤| G
    H --> G
    G -->|缓存命中| I[返回缓存结果]
    G -->|备选模型| J[切换到 GPT-4o-mini]
    G -->|规则引擎| K[规则匹配返回]
    G -->|兜底响应| L[返回预设回复]
    I --> M[记录降级日志]
    J --> M
    K --> M
    L --> M
    M --> D

这个流程的关键在于:决策层不是固定的 if-else,而是可配置的。不同业务场景对"可接受的降级"定义不同。客服场景可以返回预设话术,但金融交易场景必须保证数据一致性,宁可排队也不返回不确定的结果。

三、Go 实现:带重试和降级的 Agent 调用器

package agent

import (
    "context"
    "errors"
    "fmt"
    "math"
    "sync"
    "time"

    "github.com/sony/gobreaker"
)

// FallbackStrategy 降级策略枚举
type FallbackStrategy int

const (
    FallbackCache    FallbackStrategy = iota // 使用缓存
    FallbackAltModel                         // 切换备选模型
    FallbackRule                             // 规则引擎
    FallbackDefault                          // 兜底响应
)

// DegradeDecision 降级决策结果
type DegradeDecision struct {
    Strategy FallbackStrategy
    Reason   string
    Latency  time.Duration
}

// ModelCaller 模型调用器,内置重试+降级
type ModelCaller struct {
    primary     LLMClient        // 主模型客户端
    fallback    LLMClient        // 备选模型客户端(如 GPT-4o-mini)
    cache       *ResponseCache   // 响应缓存
    rules       *RuleEngine      // 规则引擎
    breaker     *gobreaker.CircuitBreaker
    maxRetries  int
    baseBackoff time.Duration
    mu          sync.RWMutex
}

// CallWithFallback 带降级策略的模型调用
func (m *ModelCaller) CallWithFallback(
    ctx context.Context, 
    prompt string, 
    msgHistory []Message,
) (string, *DegradeDecision, error) {
    // 先查缓存——零成本降级的第一选择
    if cached, ok := m.cache.Get(prompt); ok {
        return cached, &DegradeDecision{
            Strategy: FallbackCache,
            Reason:   "cache_hit",
        }, nil
    }

    // 熔断器检查:如果主模型已经被熔断,直接降级
    if m.breaker.State() == gobreaker.StateOpen {
        return m.degrade(ctx, prompt, msgHistory, "circuit_open")
    }

    // 尝试主模型调用,带指数退避重试
    result, err := m.callWithRetry(ctx, prompt, msgHistory)
    if err == nil {
        // 成功:异步写入缓存
        go m.cache.Set(prompt, result, 5*time.Minute)
        return result, nil, nil
    }

    // 主模型失败,触发降级决策
    return m.degrade(ctx, prompt, msgHistory, err.Error())
}

// callWithRetry 指数退避重试
func (m *ModelCaller) callWithRetry(
    ctx context.Context, prompt string, history []Message,
) (string, error) {
    var lastErr error
    for attempt := 0; attempt < m.maxRetries; attempt++ {
        // 每次重试前检查 context 是否已取消
        select {
        case <-ctx.Done():
            return "", fmt.Errorf("context cancelled: %w", ctx.Err())
        default:
        }

        result, err := m.primary.Call(ctx, prompt, history)
        if err == nil {
            return result, nil
        }

        // 不可重试的错误直接返回
        if isNonRetryable(err) {
            return "", fmt.Errorf("non-retryable error: %w", err)
        }

        lastErr = err
        // 指数退避:100ms, 200ms, 400ms...
        backoff := m.baseBackoff * time.Duration(math.Pow(2, float64(attempt)))
        // 加上随机抖动,避免惊群效应
        backoff += time.Duration(randInt63n(int64(backoff / 2)))
        
        select {
        case <-ctx.Done():
            return "", ctx.Err()
        case <-time.After(backoff):
        }
    }
    return "", fmt.Errorf("max retries exceeded: %w", lastErr)
}

// degrade 执行降级决策
func (m *ModelCaller) degrade(
    ctx context.Context, prompt string, history []Message, reason string,
) (string, *DegradeDecision, error) {
    // 策略 1:尝试备选模型
    if m.fallback != nil {
        if result, err := m.fallback.Call(ctx, prompt, history); err == nil {
            return result, &DegradeDecision{
                Strategy: FallbackAltModel,
                Reason:   fmt.Sprintf("primary_failed: %s", reason),
            }, nil
        }
    }

    // 策略 2:规则引擎匹配
    if m.rules != nil {
        if result, ok := m.rules.Match(prompt); ok {
            return result, &DegradeDecision{
                Strategy: FallbackRule,
                Reason:   fmt.Sprintf("rule_match: %s", reason),
            }, nil
        }
    }

    // 策略 3:兜底响应(根据业务场景定制)
    defaultResp := "系统繁忙,请稍后重试。您的问题已记录,客服将在工作时间回复。"
    return defaultResp, &DegradeDecision{
        Strategy: FallbackDefault,
        Reason:   fmt.Sprintf("all_fallback_exhausted: %s", reason),
    }, nil
}

// isNonRetryable 判断是否不可重试
func isNonRetryable(err error) bool {
    // 内容安全过滤、参数错误等不应重试
    return errors.Is(err, ErrContentFilter) || 
           errors.Is(err, ErrInvalidParam)
}

四、降级策略的边界与权衡

不是所有场景都适合降级。比如支付确认、合同签署这类强一致性场景,返回一个"可能对"的结果比返回错误更危险。对于这类场景,降级策略应该是"快速失败 + 异步补偿":立即返回失败,同时把请求写入死信队列,等服务恢复后重放。

缓存是把双刃剑。缓存能大幅降低延迟和成本,但过期数据可能导致过时回答。必须设置合理的 TTL,并且在知识库更新时主动失效相关缓存。

备选模型有隐形成本。GPT-4o-mini 比 GPT-4o 便宜,但如果降级频率过高,总额外成本可能超预期。建议对降级调用量做独立计费和告警。

熔断器阈值需要调优。太敏感会导致误降级,太迟钝则失去保护作用。建议基于历史错误率设定阈值,并在运行时动态调整。半开状态的探测间隔也需要根据恢复速度灵活配置。

五、总结

Agent 的降级策略本质上是"外部依赖不可靠"这一前提下的系统工程。三层架构(检测→决策→执行)提供了清晰的关注点分离。核心原则:缓存优先、备选兜底、规则保底、快速失败留给强一致性场景。生产部署时务必监控降级率、降级延迟和降级类型分布,这些指标直接反映系统的韧性边界。模型会挂,业务不能停——这是 AI 工程化的第一课。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐