// 模型预设路由(config 的 fallback_model / multimodal_model): // - 含图回合临时切到多模态模型,回合结束恢复原模型——防"图片打到不支持 // 视觉的模型直接报错"(用户实报的痛点),事后恢复让普通回合仍走首选; // - 调用失败回落备选模型,粘性切换(切过去就留着)——每回合都先撞一次 // 首选的失败再回落太浪费,粘到 /model 或配置显式切回为止。 // // 两条都要求 provider 支持 ModelSwitcher + CurrentModeler:不知道现状就 // 不能安全地切换/恢复,宁可路由不生效也不盲切。provider 是共享单例, // SetModel 与 /model 命令是同一机制、同一并发边界(见 llm 注释)。 package agent import ( "context" "errors" "fmt" "git.gopher.ink/mirrors2/AutoZero/llm" ) // routeMultimodal 含图回合切到多模态模型;返回恢复函数(回合结束调用), // 无需路由时返回 nil。 func (a *Agent) routeMultimodal(hasImages bool) func() { if !hasImages || a.MultimodalModel == "" { return nil } sw, ok := a.Provider.(llm.ModelSwitcher) if !ok { return nil } g, ok := a.Provider.(llm.CurrentModeler) if !ok { return nil } prev := g.CurrentModel() if prev == a.MultimodalModel { return nil // 已经在多模态模型上(可能是上一次回落),不必切 } sw.SetModel(a.MultimodalModel) if a.Opt.Stderr != nil { fmt.Fprintf(a.Opt.Stderr, "[model] 本回合含图 → 多模态模型 %s(回合后恢复 %s)\n", a.MultimodalModel, prev) } return func() { sw.SetModel(prev) } } // fallbackTo 本步调用失败时决定是否回落备选模型;回落则直接切换(粘性) // 并返回 true,调用方应重试本步。 // // 收窄条件(任一命中即不回落):未配置备选;ctx 已取消/超时(用户主动停 // 不重试);错误发生前已有增量文本流出(重试会在回复里叠加重复内容); // 当前已经在备选模型上(防回环)。任意错误都回落是有意的:备选语义就是 // "首选挂了换人上",凭据错误时备选同样会失败,不会造成重试风暴。 func (a *Agent) fallbackTo(ctx context.Context, err error, streamed bool) bool { if err == nil || a.FallbackModel == "" || streamed { return false } if ctx.Err() != nil || errors.Is(err, context.Canceled) { return false } sw, ok := a.Provider.(llm.ModelSwitcher) if !ok { return false } if g, ok := a.Provider.(llm.CurrentModeler); !ok || g.CurrentModel() == a.FallbackModel { return false } sw.SetModel(a.FallbackModel) if a.Opt.Stderr != nil { fmt.Fprintf(a.Opt.Stderr, "[model] 调用失败(%v)→ 回落备选模型 %s 重试\n", err, a.FallbackModel) } return true }