跳转至

架构总览

关于本节

「源码解析」一节面向贡献者和好奇的读者,讲解 llm 包内部如何工作。公开 API 的用法见 LLM 一节;本节关注的是实现。

or/llm 是一个无状态的翻译层。它只决定一次请求该发送什么、以及如何解读流式响应,而把历史存储、上下文压缩和工具循环编排留给调用方。同一段对话可以发往任意一种协议下的任意模型,目标模型还能在轮次之间切换;本库会按请求重新适配历史。

包结构

这里没有分开的「门面」与「核心」:公开类型和实现都在同一个包 llm 里。协议适配器各自独立成子包,import 时自行注册,因此应用只会链接它真正用到的厂商 SDK。

路径 职责
llm/ 整个中立核心与公开 API:模型、消息、选项、流式、迁移、适配器与 provider 两张注册表,以及默认 client
llm/openai/ openai-completionsopenai-responses 适配器;在 init 中同时注册
llm/openai/internal/chatcompletions/ Chat Completions 请求转换、兼容方言与流状态
llm/openai/internal/responses/ Responses input item 转换与事件状态机
llm/openai/internal/transport/ HTTP client、请求 Hook 与共享 SSE 过滤
llm/anthropic/ anthropic-messages 适配器;在 init 中自行注册
llm/all/ 空导入两个 provider 包,供想要全部内置协议的调用方使用
llm/internal/ jsonx(宽容的 JSON 辅助)与 genmodels(模型清单生成器)

适配器通过副作用被引入:

import (
    "github.com/ktsoator/or/llm"
    _ "github.com/ktsoator/or/llm/anthropic" // 注册 anthropic-messages
)

两张注册表、适配器与 client

调度由几个小部件组成,全部在核心包内:

  • ProtocolAdapter —— 一个接口,含 Protocol()(它在注册表中的键)和 Stream()(单个协议的请求生命周期)。见协议适配器
  • AdapterRegistry —— 一个并发安全的 map[Protocol]ProtocolAdapter。厂商的 init 函数调用 llm.Register 把自己加入包默认注册表;偏好显式接线的调用方用 NewAdapterRegistryAdapterRegistry.Register 自建。
  • ProviderRegistry —— 一张并发安全的 per-vendor 配置表:凭证来源、静态 header,以及可选的 ProviderOverride。它的 ResolveRequest 在派发前为每次请求填充 API key 并套用 override;默认是从内置模型清单填充的 NewBuiltInProviderRegistry。见 provider
  • Client —— 同时持有两张注册表,为每次请求路由:先经 ProviderRegistry 解析 provider 配置,再派发到模型协议对应的适配器。llm.Streamllm.Complete 只是绑定到默认注册表的默认 client 的薄封装。
flowchart LR
    subgraph core["package llm"]
        R["AdapterRegistry"]
        P["ProviderRegistry"]
        C["Client"]
    end
    OA["llm/openai · init()"] -->|Register| R
    AN["llm/anthropic · init()"] -->|Register| R
    C -->|"ResolveRequest(model, options)"| P
    C -->|"Get(model.Protocol)"| R

请求的数据流

flowchart TD
    A["llm.Complete / Stream"] --> B["Client.Stream"]
    B --> V["options.Validate(protocol, tools)"]
    V --> RR["providers.ResolveRequest<br/>key · override · headers"]
    RR --> C{"adapters.Get(model.Protocol)"}
    C -->|anthropic-messages| D["Anthropic 适配器"]
    C -->|openai-completions| E["OpenAI 适配器"]
    C -->|openai-responses| F["OpenAI Responses 适配器"]
    D --> T["TransformMessages → convert → SDK 请求"]
    E --> T
    F --> T
    T --> G["StreamWriter: Emit / Done / Fail"]
    G --> H["chan Event → 调用方"]

模型上的 Protocol 字段是判别器:Client.Stream 用它从注册表中选出适配器。适配器左侧的一切都与厂商无关;适配器内部则可以讲一种具体的线路协议。

逐步解读一次请求

func (c *Client) Stream(ctx context.Context, model Model, input Context, options StreamOptions) (<-chan Event, error) {
    if c.adapters == nil {
        return nil, errors.New("adapter registry is nil")
    }
    if err := options.Validate(model.Protocol, input.Tools); err != nil { // (1)!
        return nil, err
    }

    // A nil provider registry still resolves the legacy environment API key.
    model, options = c.providers.ResolveRequest(model, options) // (2)!

    adapter, ok := c.adapters.Get(model.Protocol) // (3)!
    if !ok {
        return nil, fmt.Errorf(
            "no adapter registered for protocol %q",
            model.Protocol,
        )
    }

    return adapter.Stream(ctx, model, input, options)
}
  1. 协议专属选项会在构建任何 HTTP 请求之前先对目标协议做校验,因此不匹配会尽早失败。
  2. ProviderRegistry.ResolveRequest 填充 API key——依次取自 StreamOptions、provider override、厂商环境变量——并套用 per-provider 的 base-URL 与 header override。若模型的 provider 未注册,则回退到旧版环境变量查找。
  3. Protocol 选定适配器。同一段对话可以发往任意一种协议;本库会按请求重新适配历史。

源码:llm/client.gollm/adapters.gollm/default.go

延伸阅读