feat(llm): T2.1 模型路由 + Fallback —— 单 provider 抖动不再整体宕
现状:单 provider,一家 API 抖动/挂掉全平台不可用。加主备 failover:主模型调用失败 自动按序切备用,compose/ReAct/Chat 全路径透明白嫖。 - llm/failover.go: failoverModel 把多个 ToolCallingChatModel 串成主备链,按序调用、 遇错切下一个;它本身是 model.ToolCallingChatModel 故全路径透明。调用方主动取消 (ctx.Err()!=nil) 不切;模型自身请求超时走内部 ctx、不污染父 ctx 故仍 failover。 局限(v1):Stream 仅建流同步报错时切(已回流 token 的中途失败不切)。 - llm/pool.go: SetConfig 用激活配置(含 Fallbacks)重建——主+可用备用串成 failover 链, 无备用则直接用主;备用单个构建失败跳过不影响主链。 - contract.ModelConfig: 加 Fallbacks 字段(骑在主配置里下发,不改任何 bus/ServeConfig 签名)。 - gateway store.ActiveConfig: chat 把"其它已登记 chat 模型"按序填进 Fallbacks; provide(main) + broadcast(admin) 共用 → 注册多个 chat 模型即自动成主备。 - bus.decryptConfig: 备用模型的 api_key(密文)一并解密。 测试:failover 单测(主可用不调备/主挂切备/全挂报错/取消不切/Stream 切备/WithTools 链)。 live 验证:active=死 ollama 主 + deepseek 备 → 任务连主拒连→自动切 deepseek→4s 完成。 DEPTH_ROADMAP T2.1(admin 注册多模型即主备,无需新 UI)。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -45,16 +45,11 @@ func NewPool() *Pool { return &Pool{} }
|
||||
// 绕开真实 LLM 推理与计费,只压全链路 plumbing(网关→NATS→调度→工具RTT→回流)。
|
||||
func forceStub() bool { return os.Getenv("LLM_FORCE_STUB") == "1" }
|
||||
|
||||
// SetConfig 热更新后端配置:重建 ChatModel 实例(控制面变更时调用)。
|
||||
// SetConfig 热更新后端配置:用激活配置(含备用模型)重建 ChatModel(控制面变更时调用)。
|
||||
func (p *Pool) SetConfig(cfg *contract.ModelConfig) {
|
||||
var cm model.BaseChatModel
|
||||
if cfg != nil && cfg.Ready() && !forceStub() {
|
||||
built, err := buildChatModel(cfg)
|
||||
if err != nil {
|
||||
fmt.Printf("[llm] 构建 ChatModel 失败(降级桩运行): %v\n", err)
|
||||
} else {
|
||||
cm = built
|
||||
}
|
||||
cm = buildWithFallbacks(cfg)
|
||||
}
|
||||
p.mu.Lock()
|
||||
p.cfg = cfg
|
||||
@@ -62,10 +57,47 @@ func (p *Pool) SetConfig(cfg *contract.ModelConfig) {
|
||||
p.mu.Unlock()
|
||||
if cfg != nil {
|
||||
// 不打印 api_key。
|
||||
fmt.Printf("[llm] model config set: provider=%s base=%s model=%s\n", cfg.Provider, normalizeBaseURL(cfg), cfg.Model)
|
||||
fmt.Printf("[llm] model config set: provider=%s base=%s model=%s fallbacks=%d\n",
|
||||
cfg.Provider, normalizeBaseURL(cfg), cfg.Model, len(cfg.Fallbacks))
|
||||
}
|
||||
}
|
||||
|
||||
// buildWithFallbacks 构建主模型,并把可用的备用模型串成 failover 链(无备用则直接返回主模型)。
|
||||
// 主模型构建失败 → 返回 nil(降级桩);备用单个失败 → 跳过该备用,不影响主链。
|
||||
func buildWithFallbacks(cfg *contract.ModelConfig) model.BaseChatModel {
|
||||
primary, err := buildChatModel(cfg)
|
||||
if err != nil {
|
||||
fmt.Printf("[llm] 构建主 ChatModel 失败(降级桩运行): %v\n", err)
|
||||
return nil
|
||||
}
|
||||
ptcm, ok := primary.(model.ToolCallingChatModel)
|
||||
if !ok || len(cfg.Fallbacks) == 0 {
|
||||
return primary // 不支持 WithTools 或无备用 → 直接用主模型
|
||||
}
|
||||
models := []model.ToolCallingChatModel{ptcm}
|
||||
for i := range cfg.Fallbacks {
|
||||
fb := cfg.Fallbacks[i]
|
||||
if !fb.Ready() {
|
||||
continue
|
||||
}
|
||||
fbm, ferr := buildChatModel(&fb)
|
||||
if ferr != nil {
|
||||
fmt.Printf("[llm] 构建备用模型 %s 失败,跳过: %v\n", fb.Model, ferr)
|
||||
continue
|
||||
}
|
||||
if t, ok := fbm.(model.ToolCallingChatModel); ok {
|
||||
models = append(models, t)
|
||||
}
|
||||
}
|
||||
if len(models) == 1 {
|
||||
return primary // 无有效备用
|
||||
}
|
||||
fmt.Printf("[llm] 启用模型 failover:主 %s + %d 个备用\n", cfg.Model, len(models)-1)
|
||||
return newFailoverModel(models, func(idx int, ferr error) {
|
||||
fmt.Printf("[llm] 模型 failover:第 %d 个模型失败(%v),切下一个\n", idx, ferr)
|
||||
})
|
||||
}
|
||||
|
||||
// buildChatModel 据 provider 归一化连接参数后构建 OpenAI 兼容 ChatModel。
|
||||
// vLLM 与 Ollama 都暴露 OpenAI 兼容 API(底层 go-openai 请求 {base}/chat/completions),
|
||||
// 故统一走 openai 客户端,仅差在 BaseURL 是否带 /v1 与是否需要占位 key:
|
||||
|
||||
Reference in New Issue
Block a user