Skip to content

路由与默认

Ongrid 并行跑 N 个 provider,每次 LLM 调用派给其中一个。这一页讲三层的派 发是怎么工作的:

  1. MultiClient —— 给遗留 llm.Chat 路径(translate、知识搜索)用 的 wire 级路由器。
  2. RoutingChatModel —— 给 graph-kernel ReAct agent 用的 eino model.ChatModel 包装。
  3. DefaultResolver —— 让 default_provider 设置中途生效的动态默认 hook。

MultiClient

路由器栈的底层住在 internal/pkg/llm/router.go

go
// router.go:67
type MultiClient struct {
    // map: provider id -> sub-Client built from a ProviderConfig
    // ...
}

func (m *MultiClient) Chat(ctx context.Context, req ChatReq) (*ChatResp, error)

每个 provider 配置在构造时产生一个子 client:

go
// cmd/ongrid/main.go:492
providerCfgs := []llm.ProviderConfig{}
if cfg.OpenAI.APIKey != "" {
    providerCfgs = append(providerCfgs, llm.ProviderConfig{
        ID: "openai", Label: "OpenAI",
        APIKey: cfg.OpenAI.APIKey,
        Model: firstNonEmpty(cfg.OpenAI.Model, "gpt-5.4"),
        BaseURL: cfg.OpenAI.BaseURL,
        Models: dedupeModels(...),
    })
}
// ...same for each provider...
llmRouter := llm.NewMultiClient(providerCfgs, cfg.LLM.Default, openaiClient)

Chatreq.Provider 派发:

  • 非空 → 查子 client;404 → ErrUnknownProvider
  • 空 → 回退到 defaultProvider

MultiClient.SetProvidersResolver(r) 接线把 DB 后端的 resolver 叠在上面 —— 每次调用激活的子 client 集都被重新解析(SetResolveTTL 缓存 60s)。

RoutingChatModel

graph kernel 用 eino 的 model.ChatModel 接口,不是 llm.ChatRoutingChatModeleino_routing.go:89) 包 N 个内部 ChatModel,通过 impl-specific 选项派发:

go
type RoutingChatModel struct {
    inner           map[string]model.ChatModel
    defaultProvider string
    defaultResolver func(context.Context) (provider, mdl string)
}

func WithProvider(provider string) model.Option {
    return model.WrapImplSpecificOptFn(func(o *providerOpts) {
        o.provider = provider
    })
}

调用方用法:

go
resp, err := chatModel.Generate(ctx, msgs,
    model.WithModel("glm-4.7-flash"),
    llm.WithProvider("zhipu"),
)

pick() 解内部:

go
// eino_routing.go:173
func (r *RoutingChatModel) pick(opts ...model.Option) (model.ChatModel, string, error) {
    po := model.GetImplSpecificOptions(&providerOpts{}, opts...)
    prov := po.provider
    if prov == "" {
        prov = r.defaultProvider
    }
    inner, ok := r.inner[prov]
    if !ok {
        return nil, prov, fmt.Errorf("%w: %s", ErrUnknownProvider, prov)
    }
    return inner, prov, nil
}

DefaultResolver —— 动态默认

它修的 bug:admin 在 /settings/llm 里把 default_provider 从 Anthropic 翻到 GLM。chat picker UI 立即尊重新默认(每次加载重新拉 /v1/aiops/models)。但 RCA investigator worker —— 进程内、启动时绑定 provider —— 在重启前都继续路由到 Anthropic。

修法: RoutingChatModelConfig.DefaultResolver

go
var defaultResolver func(context.Context) (string, string)
if resolver != nil {
    defaultResolver = func(rctx context.Context) (string, string) {
        provCfgs, resolvedDefault, rerr := resolver.ResolveProviders(rctx)
        if rerr != nil || resolvedDefault == "" {
            return "", ""
        }
        for _, pc := range provCfgs {
            if pc.ID == resolvedDefault {
                return resolvedDefault, pc.Model
            }
        }
        return resolvedDefault, ""
    }
}
chatModel, err := llm.NewRoutingChatModel(llm.RoutingChatModelConfig{
    Inner:           innerModels,
    DefaultProvider: defProv,
    DefaultResolver: defaultResolver,
})

withDynamicDefault 把 resolver 的输出作为 WithProvider + WithModel 注入到既没钉 provider 也没钉 model 的调用 —— chat picker 按消息钉 provider 完全绕过 resolver;默认路由调用(investigator、translate、知识扇 出)现在跟随实时配置。

Chat 里按调指定 provider

MultiClient.Chat 路径(非 graph-kernel 调用方),显式设 ChatReq.Provider

go
resp, err := llmClient.Chat(ctx, llm.ChatReq{
    Provider: "openai",
    Model:    "gpt-5.5",
    Messages: msgs,
})

Provider → MultiClient 调用时解默认。空 Model → 解出的子 client 用 它配置的默认。

graph kernel 里按调指定 provider

chat 发送信封里 provider 非空时 agent 运行时注入 llm.WithProvider。 persona registry 也能给整个 persona 钉 provider(比如便宜的抽取 persona 钉 Anthropic Haiku)。见 agent persona 格式参考。

  • 忘了 default_provider —— resolver 选字典序第一个 provider id; 你会把 model id 发到错的端点。永远设 ONGRID_LLM_DEFAULT_PROVIDER (或者写 DB 行)。
  • 钉一个没有内部 ChatModel 的 provider —— 当 resolver 返回一个启动时 没注册没预注册的 provider id 时发生。custom 槽就是为这个预注册的; 其他都在 cfg.LLM.*.APIKey != "" 上把门。
  • 热切时机 —— resolver 缓存 TTL 60s,MultiClient 缓存 60s。最坏 120s admin 编辑才生效。Invalidate 路径已暴露但还没接到 SPA 的保存动作。

另见

  • 模型概览 —— []llm.ProviderConfig 装配。
  • 预算 —— 跟路由正交;同一上限适用于每个 provider。
  • RCA —— investigator worker 路由。