Skip to content

라우팅 & 기본

Ongrid 는 N 개의 provider 를 병렬로 실행하고 각 LLM 호출을 정확히 하나로 디스패치합니다. 이 페이지는 세 레이어에서 그 디스패치가 어떻게 동작하는지 다룹니다:

  1. MultiClient — 레거시 llm.Chat 경로 (번역, 지식 검색) 가 사용하는 와이어 레벨 라우터.
  2. RoutingChatModel — 그래프 커널 기반 ReAct 에이전트가 사용하는 eino model.ChatModel 래퍼.
  3. DefaultResolverdefault_provider 설정이 프로세스 중간에 효과를 내게 하는 동적 기본 훅.

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 구성이 생성 시 하나의 서브 클라이언트를 생산:

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 에 디스패치:

  • 비어 있지 않음 → 서브 클라이언트 조회; 404 → ErrUnknownProvider.
  • 비어 있음 → defaultProvider 로 폴백.

MultiClient.SetProvidersResolver(r) 배선은 그 위에 DB 기반 resolver 를 레이어 — 매 호출마다 활성 서브 클라이언트 셋이 재해석 됨 (SetResolveTTL 로 60 초 캐시).

RoutingChatModel

그래프 커널은 llm.Chat 가 아닌 eino 의 model.ChatModel 인터페이스 사용. RoutingChatModel (eino_routing.go:89) 은 N 개의 내부 ChatModel 을 래핑하고 impl 특화 옵션으로 디스패치:

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 — 동적 기본

이것이 고치는 버그: 관리자가 /settings/llm 에서 default_provider 를 Anthropic 에서 GLM 으로 뒤집음. 채팅 picker UI 는 즉시 새 기본을 존중 (각 로드마다 /v1/aiops/models 재 fetch). 그러나 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 로 주입 — 채팅 picker 는 메시지별로 provider 고정하고 resolver 를 완전히 우회; 기본 라우팅 호출 (investigator, 번역, 지식 fan-out) 은 이제 라이브 구성을 따릅니다.

Chat 의 호출별 provider

MultiClient.Chat 경로 (비 그래프 커널 호출자) 의 경우 ChatReq.Provider 를 명시적으로 설정:

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

Provider → MultiClient 가 호출 시 기본 해석. 빈 Model → 해석된 서브 클라이언트가 구성된 기본 사용.

그래프 커널의 호출별 provider

에이전트 런타임이 채팅 send envelope 에 비어 있지 않은 provider 가 포함될 때마다 llm.WithProvider 를 주입. Persona 레지스트리는 전체 persona 에 대해 provider 를 고정할 수도 있음 (예: 저렴한 추출기 persona 가 Anthropic Haiku 에 고정). 에이전트 persona 포맷 참조 참고.

함정

  • default_provider 잊기 — resolver 가 첫 정렬 provider id 를 고름; 모델 id 를 잘못된 엔드포인트로 보내게 됩니다. 항상 ONGRID_LLM_DEFAULT_PROVIDER 설정 (또는 DB 행 작성).
  • 내부 ChatModel 이 없는 provider 고정 — 부팅 시 등록되지 않았고 사전 등록되지 않은 provider id 를 resolver 가 반환할 때 발생. 정확히 이 이유로 custom 슬롯이 사전 등록; 나머지는 모두 cfg.LLM.*.APIKey != "" 에 게이트.
  • 핫 스왑 타이밍 — 캐시 TTL 은 resolver 에 60 초, MultiClient 에 60 초. 관리자 편집이 효과 내기 전 최악 120 초. Invalidate 경로는 노출됐지만 아직 SPA 의 save 액션에 배선 안 됨.

같이 보기

  • 모델 개요[]llm.ProviderConfig 조립.
  • 예산 — 라우팅과 직교; 같은 상한이 모든 provider 에 적용.
  • RCA — investigator worker 라우팅.