Skip to content

Latest commit

 

History

History
350 lines (265 loc) · 9.9 KB

File metadata and controls

350 lines (265 loc) · 9.9 KB

最后更新:2026-04-16 | 模块:通用适配器架构

决策 rationale 见 ../adr/002-native-first.md,数据结构定义见 ../CODE_WIKI.md

返回文档索引

TokenRouter 通用适配器架构设计

适配层只做格式转换,不碰语义。所有请求统一收敛为内部 EnvelopeBlock,出站再还原为厂商原生格式。


1. 设计背景与问题定义

核心挑战:不同厂商 API 在请求格式、响应结构、流式协议、认证方式上差异显著,需要一套适配器架构来屏蔽这些差异,同时避免传统"归一化"方案导致的能力损失。

设计目标

  • 零能力损失 — 任何原生能力完整保留和透传
  • 双向任意接口 — 适配任意上游 Provider,暴露任意格式 API
  • 薄适配 — 适配层只负责 Parse / Serialize,不做业务逻辑(分块、排列、缓存策略在更上层处理)
  • 配置优先 — 80% 的入站协议可通过 YAML 配置声明字段映射
  • 原生透传 — 出站时尽量还原为厂商最原生的格式

2. 核心数据接口

2.1 内部统一格式:Envelope

Envelope 是内部最小公共格式,只包含后续路由和处理必需的字段,其余全部放入 Raw

完整类型定义详见 ../CODE_WIKI.md

关键设计

  • Contentjson.RawMessage,因为 LLM content 可能是 string 或多模态数组
  • Raw 字段保存所有未标准化字段,出站时按需透传
  • CacheControl 在入站时通常为空,由后续 CacheInjector 填充

2.2 出站通用格式:Block

Block 是 Chunker 的输出,Arranger 和后续模块的操作单位。

完整类型定义详见 ../CODE_WIKI.md


3. 入站适配层(Inbound Adapter)

3.1 接口定义

package inbound

type InboundAdapter interface {
    Match(method, path string) bool
    Parse(req *http.Request) (*envelope.Envelope, error)
    Name() string
}

3.2 注册表

package inbound

type Registry struct {
    adapters []InboundAdapter
}

func NewRegistry(adapters ...InboundAdapter) *Registry {
    return &Registry{adapters: adapters}
}

func (r *Registry) Parse(req *http.Request) (*envelope.Envelope, InboundAdapter, error) {
    for _, adapter := range r.adapters {
        if adapter.Match(req.Method, req.URL.Path) {
            env, err := adapter.Parse(req)
            return env, adapter, err
        }
    }
    return nil, nil, fmt.Errorf("unsupported inbound protocol: %s %s", req.Method, req.URL.Path)
}

3.3 配置驱动适配器

用于 80% 与 OpenAI 格式接近的厂商 API。

package inbound

type ConfigDrivenAdapter struct {
    config AdapterConfig
}

type AdapterConfig struct {
    Name        string            `yaml:"name"`
    MatchMethod string            `yaml:"match_method"`
    MatchPath   string            `yaml:"match_path"`
    FieldMap    map[string]string `yaml:"field_map"`
    ContentType string            `yaml:"content_type"`
}

3.4 编程式适配器:OpenAI

MVP v0.1 唯一必须手写的入站适配器。

package inbound

type OpenAIAdapter struct{}

func (a *OpenAIAdapter) Name() string { return "openai" }

func (a *OpenAIAdapter) Match(method, path string) bool {
    return method == http.MethodPost && path == "/v1/chat/completions"
}

func (a *OpenAIAdapter) Parse(req *http.Request) (*envelope.Envelope, error) {
    body, err := io.ReadAll(req.Body)
    if err != nil {
        return nil, err
    }
    defer req.Body.Close()

    var payload struct {
        envelope.Envelope
        Stop             interface{} `json:"stop,omitempty"`
        PresencePenalty  *float64    `json:"presence_penalty,omitempty"`
        FrequencyPenalty *float64    `json:"frequency_penalty,omitempty"`
    }

    if err := json.Unmarshal(body, &payload); err != nil {
        return nil, err
    }

    env := &payload.Envelope
    env.Raw = make(map[string]json.RawMessage)

    var rawMap map[string]json.RawMessage
    if err := json.Unmarshal(body, &rawMap); err == nil {
        for k, v := range rawMap {
            if !isStandardField(k) {
                env.Raw[k] = v
            }
        }
    }

    return env, nil
}

4. 出站适配层(Outbound Adapter)

4.1 接口定义

package outbound

type OutboundAdapter interface {
    Name() string
    BuildRequest(blocks []block.Block, env *envelope.Envelope) ([]byte, error)
    ParseStreamChunk(chunk []byte) (*StreamEvent, error)
    ParseResponse(body []byte) (*Response, error)
}

type StreamEvent struct {
    Type    string
    Delta   json.RawMessage
    Done    bool
    Usage   *Usage
}

type Response struct {
    Choices []Choice
    Usage   *Usage
    Raw     map[string]json.RawMessage
}

type Choice struct {
    Index        int
    Message      *envelope.Message
    FinishReason string
}

type Usage struct {
    PromptTokens     int
    CompletionTokens int
    CacheReadTokens  int
    CacheWriteTokens int
}

4.2 注册表

package outbound

type Registry struct {
    adapters map[string]OutboundAdapter
}

func NewRegistry(adapters ...OutboundAdapter) *Registry {
    r := &Registry{adapters: make(map[string]OutboundAdapter)}
    for _, a := range adapters {
        r.adapters[a.Name()] = a
    }
    return r
}

func (r *Registry) Get(name string) (OutboundAdapter, error) {
    a, ok := r.adapters[name]
    if !ok {
        return nil, fmt.Errorf("unsupported outbound adapter: %s", name)
    }
    return a, nil
}

4.3 Anthropic 出站适配器示例

package outbound

type AnthropicAdapter struct{}

func (a *AnthropicAdapter) Name() string { return "anthropic" }

func (a *AnthropicAdapter) BuildRequest(
    blocks []block.Block,
    env *envelope.Envelope,
) ([]byte, error) {
    req := make(map[string]interface{})
    var messages []envelope.Message
    var systemBlocks []envelope.Message
    var tools []envelope.Tool

    for _, b := range blocks {
        switch b.Type {
        case block.BlockSystem:
            systemBlocks = append(systemBlocks, b.Messages...)
        case block.BlockTool:
            tools = append(tools, b.Tools...)
        case block.BlockHistory, block.BlockQuery:
            messages = append(messages, b.Messages...)
        }
    }

    if len(systemBlocks) > 0 {
        if len(systemBlocks) == 1 {
            req["system"] = systemBlocks[0].Content
        } else {
            var sysContents []map[string]string
            for _, m := range systemBlocks {
                sysContents = append(sysContents, map[string]string{
                    "type": "text",
                    "text": string(m.Content),
                })
            }
            req["system"] = sysContents
        }
    }

    req["messages"] = messages
    if len(tools) > 0 {
        req["tools"] = tools
    }

    for k, v := range env.Raw {
        var val interface{}
        if err := json.Unmarshal(v, &val); err == nil {
            req[k] = val
        }
    }

    return json.Marshal(req)
}

5. 全厂商原生接入矩阵

5.1 厂商分类总览

分类 定义 代表厂商 接入策略
国际原生协议派 协议与 OpenAI 差异显著 OpenAI, Anthropic, Google Gemini, AWS Bedrock 手写专属 Inbound + Outbound Adapter。MVP v0.1 暂不接入,接口已预留
国内云服务原生派 自有原生协议 + 半生不熟的兼容层 阿里 DashScope, 腾讯 Hunyuan, 百度千帆, 火山 Ark, 智谱 GLM, 华为盘古, MiniMax 优先手写原生适配器
优质兼容派 原生协议就是 OpenAI 兼容 DeepSeek V3.2, Moonshot, Baichuan, Stepfun, Mistral, Cohere, Groq OpenAIAdapter 直接覆盖。MVP v0.1 只接入 DeepSeek V3.2
混合/Wrapper 派 提供自研 SDK,底层仍是标准 HTTP Azure OpenAI, Vertex AI ConfigDrivenAdapter + 认证中间件

5.2 核心结论:厂商 SDK 无状态调研

所有主流厂商的核心 Chat / Completions HTTP API 本质上都是无状态的。 单次对话请求都要求客户端在 messages 字段中发送完整的上下文历史。服务端不保存对话状态。

少数厂商的 Python SDK 在客户端封装了"有状态的对话对象"(如 Google Gemini 的 ChatSession),但 SDK 底层发向 TokenRouter 的仍然是一个包含完整历史的单次 HTTP 请求。

这意味着:TokenRouter 作为 HTTP 代理,只需单次请求级别的 Parse + Build,无需引入 session 存储或对话历史管理。

5.3 出站为何必须走 HTTP 层

平台的核心商业价值来自结构整合 + 缓存注入,要求在请求发送到厂商之前完成:

  1. Chunker 决定前缀边界
  2. Arranger 按固定顺序排列 Block
  3. Canonicalizer 生成字节级确定性的请求体
  4. Cache Injector 在精确位置插入 cache_control 等标记

如果走 SDK 层,上述步骤会被严重削弱:SDK 通常不接受「已序列化好的原始 JSON 字符串」,且无法保证内部序列化后的字节与 Canonicalizer 输出完全一致。

结论:TokenRouter 必须掌握最终发送到厂商的每一个字节,因此出站只能走 HTTP 层。


6. 与核心流水线的集成

[入站请求]
    │
    ▼
[Inbound Adapter] ──→ Envelope
    │
    ▼
[Chunker] ──→ []Block
    │
    ▼
[Arranger] ──→ 排序后的 []Block
    │
    ▼
[Canonicalizer] ──→ 确定性 JSON bytes
    │
    ▼
[CacheInjector] ──→ 注入 cache_control
    │
    ▼
[Outbound Adapter] ──→ 厂商原生请求体
    │
    ▼
[HTTP Proxy] ──→ 上游 Provider

适配层只负责首尾的格式转换:

  • Inbound:HTTP Request → Envelope
  • Outbound[]Block + Raw → HTTP Request Body

中间的 Chunker、Arranger、Canonicalizer、CacheInjector 处理的是统一的 Envelope / Block 结构,与具体厂商协议无关。