Skip to content

Latest commit

 

History

History
393 lines (287 loc) · 16.9 KB

File metadata and controls

393 lines (287 loc) · 16.9 KB

TokenRouter 技术架构

最后更新:2026-04-16 | 定位:LLM 调用层的结构整合者

接口定义见 CODE_WIKI.md,部署细节见 modules/system-implementation.md


一、设计哲学

TokenRouter 不是网关,不是套利层,而是 LLM 调用层的结构整合者

  1. 无状态:平台内部不维护任何 session,每次请求独立处理。状态是敌人,它会破坏结构收敛。
  2. 只分块,不细处理:我们只按结构(role/type)把请求切成几块,然后按固定顺序拼好。不看内容、不改语义、不维护对话状态。语义属于用户,结构属于平台。
  3. 三方共赢的结构价值
    • 上游厂商吃到了我整理算力的红利——因为请求结构被标准化,厂商侧 KV Cache 命中率大幅提升,算力成本下降。
    • 下游客户吃到了我整理缓存的返利——平台通过跨用户共享缓存降低上游成本,并将部分收益以更低价格返还给客户。
    • 平台自身赚取整合层的合理差价,规模越大,结构收敛越强,议价能力越高。

流量在我这里不是「多客户」的集合,而是「单一超大客户」的资产池。平台拥有对请求结构的绝对控制权,但绝不触碰其语义本质。


二、核心架构

Client A ──┐
Client B ──┼──→ [Inbound Adapter] ──→ [Chunker] ──→ [Arranger] ──→ [Canonicalizer] ──→ [Cache Injector] ──→ [Hasher] ──→ [Upstream]
Client C ──┘                              ↑             ↑                  ↑
                                          └─────────────┴──────────────────┘
                                                         [Observer / Clusterer] (Phase 2)
层级 模块 职责 关键约束
入站适配 Inbound Adapter 把任意厂商 SDK 的调用格式转成统一 Envelope 配置驱动为主,编程式兜底
策略层 Chunker(分块器) role / type 把请求切成 Block 决定前缀边界,可静态可动态
结构层 Arranger(排列器) 按固定顺序排列 Block,做块级粗处理 最小单位是 Block,不改内容
物理层 Canonicalizer(规范器) 把 Block 序列化为字节级确定性的标准形 不碰语义,只做编码归一化
优化层 Cache Injector 在标准位置注入厂商缓存标记 针对厂商机制差异化策略
计算层 Hasher 计算前缀哈希和完整哈希,判断是否可共享 无状态,内存/Redis 皆可
出站转发 Outbound Adapter 把格式化后的 Block 还原为厂商原生格式 Raw 字段原样透传

三、关键模块设计

3.1 入站适配层

设计目标:新增一个入站协议的成本必须极低。

策略

  • 配置驱动:80% 的厂商 API 与 OpenAI 格式接近,通过 YAML 配置即可声明字段映射和路径匹配。
  • 编程式兜底:极少数差异过大的厂商,再手写代码实现 InboundAdapter 接口。

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

数据结构定义详见 CODE_WIKI.md


3.2 分块器(Chunker)

核心原则:只看 role type,不看 content。它决定「在哪里下刀」,而刀口的位置直接决定缓存前缀的范围。

数据结构定义详见 CODE_WIKI.md

MVP 静态分块规则

  1. req.Tools 直接归入 BlockTool
  2. 遍历 req.Messages
    • role=systemBlockSystem
    • role=assistant 且位于最后一轮 user 之前 → BlockHistory
    • role=tool/function 且位于最后一轮 user 之前 → BlockHistory
    • role=user 且位于最后一轮 user 之前 → BlockHistory
    • 最后一轮 role=user 以及其后的 assistant/tool/function 尾部 → BlockQuery

动态演进方向:当流量规模足够大时,通过 Observer 识别高频共现的 (System Prompt, Tool Subset) 组合,Chunker 将能够动态调整前缀边界。例如把高频工具提取到 Prefix Block,低频工具留在 Suffix Block,进一步扩大厂商侧 KV Cache 的命中范围。


3.3 排列器(Arranger / Formatter)

核心原则:只排列顺序,不改内容。最小操作单位是 Block。

排列后的 Block 顺序永远固定:

  1. System Block:多条 system 合并为一条(用 \n\n 拼接)
  2. Tool Block:工具定义按 Name 字段字母序排序
  3. History Block:保持原有顺序,但可截断(保留最近 N 轮)
  4. Query Block:原样后置

块内粗处理

块类型 处理 说明
System 合并 多条 system 合并为单条
Tool 字母排序 Name 字段排序,保持稳定性
History 截断 保留最近 maxHistoryTurns
Query 无处理 原样保留

关键边界:Arranger 不进入 Block 内部做字符级操作。它只移动 Block、合并同类型 Block、截断 Block 序列。内容的每一个字节都保持原样。


3.4 序列化规范器(Canonicalizer)

核心原则:把 Block 结构变成字节级确定性的标准形。这是反熵的最后一道防线。

如果两个请求经过 Chunker 和 Arranger 后已经逻辑等价,Canonicalizer 必须保证它们的字节输出完全一致。否则厂商侧的 KV Cache 会因为一个空白字符的差异而失效。

规范化操作(不碰语义,只做物理编码)

  • JSON key 的递归字母序排序
  • 空值的统一处理(omit 或显式 null
  • 数字的固定精度格式化
  • Unicode NFC 归一化
  • 空白字符与换行符的统一压缩

为什么必须独立成层:Arranger 的最小单位是 Block,而 Canonicalizer 的最小单位是字节。两者职责不同、抽象层级不同,必须分离。


3.5 缓存注入器

核心原则:在格式化后的固定位置,按目标厂商的机制注入缓存标记。

不同厂商的 prompt caching 规则差异极大:Anthropic 需要显式 cache_control 且对位置和 token 数有门槛限制;OpenAI 是自动缓存,无需标记;其他厂商可能有各自的折扣窗口和标记协议。

缓存策略引擎的职责:

  • 根据目标厂商决定哪些 Block 需要注入标记
  • 判断注入位置是否满足厂商的最小 token 门槛
  • 选择正确的标记类型(如 Anthropic 的 ephemeral

接口与示例详见 modules/cache-intelligence.md


3.6 跨请求共享机制

通过哈希实现两层共享:

层 1:KV Cache 前缀共享(核心商业价值)

只取 System + Tool 块计算前缀哈希:

func PrefixHash(blocks []block.Block) (string, error) {
    var prefix []block.Block
    for _, b := range blocks {
        if b.Type == block.BlockSystem || b.Type == block.BlockTool {
            prefix = append(prefix, b)
        }
    }
    data, err := canonicalizer.CanonicalJSON(prefix) // 必须是 Canonicalizer 的输出
    if err != nil {
        return "", fmt.Errorf("hasher: %w", err)
    }
    h := sha256.Sum256(data)
    return hex.EncodeToString(h[:]), nil
}

所有前缀哈希相同的请求,发送到上游时 System + Tool 部分完全一致,从而命中厂商侧 KV Cache。

层 2:完整请求去重(额外收益)

取全部 Block 计算完整哈希:

func FullHash(blocks []block.Block) (string, error) {
    data, err := canonicalizer.CanonicalJSON(blocks)
    if err != nil {
        return "", fmt.Errorf("hasher: %w", err)
    }
    h := sha256.Sum256(data)
    return hex.EncodeToString(h[:]), nil
}

如果上一个完全相同的请求还在处理中,后续请求可挂起等待并复用响应结果。

注:完整去重仅适用于非流式请求。流式请求因输出实时性要求,暂不挂起复用。


3.7 流量观测与聚类系统(Observer / Clusterer)

这是让 Chunker 从静态走向动态的数据引擎。(Phase 2 only)

作为单一超大客户,平台持续观测所有请求的结构特征(system prompt 签名、tool set 签名、出现频率)。当数据密度足够高时,自动识别出高分布区域,并输出「候选前缀模板」供 Chunker 使用。

观测目标

  • (system_signature, tool_set_signature) 的共现频率
  • 不同客户端/框架带来的结构收敛度
  • 高频前缀模板的覆盖率和命中率

产出

  • 高频前缀白名单,用于动态调整 Chunker 的切分策略
  • 预热调度建议,主动占住厂商缓存槽位

这不是 MVP 必需,但它是平台从「手工整合」走向「算法整合」的必由之路。


四、请求处理状态机

[接收请求]
    │
    ▼
[入站解析] ──错误──→ [返回错误]
    │
    ▼
[分块]  按 role/type 切分
    │
    ▼
[排列]  固定顺序排列 Block
    │
    ▼
[序列化规范]  生成字节级确定性输出
    │
    ▼
[缓存注入]  按厂商策略打标记
    │
    ▼
[计算完整哈希]
    │
    ├── 命中 ──→ [挂起复用] ──→ [返回成功]
    │
    └── 未命中 ──→ [计算前缀哈希]
                        │
                        ▼
                   [转发上游]
                        │
              ┌─────────┴─────────┐
         stream=true          stream=false
              │                    │
              ▼                    ▼
        [流式响应]            [同步响应]
              │                    │
              └────────┬───────────┘
                       ▼
                 [记录并返回]
                       │
                       ▼
                 [返回成功]
                       │
                       ▼
                      [*]

五、技术闭环论证

5.1 MVP 能否跑通?

可以。 Phase 1 只需要以下模块即可形成完整闭环:

  1. OpenAI 格式入站适配
  2. 静态 Chunker(四分块)
  3. Arranger(固定顺序 + 合并/排序/截断)
  4. Canonicalizer(基础 JSON 规范化)
  5. DeepSeek V3.2 出站适配(OpenAI 兼容)+ Cache Injector 透传策略。Anthropic 适配器和缓存注入器在接口层预留
  6. Prefix Hash 共享 + Full Hash 去重
  7. 双层计费模型

这个组合已经能够让一个请求从「客户端进入」到「厂商侧缓存命中」再到「成本降低」形成完整链路。每个模块的职责边界清晰,没有不可实现的黑箱。

5.2 规模化后如何增强?

当流量足够大时,系统按以下路径自然演进:

  1. Observer 上线 → 持续观测流量分布
  2. Clusterer 识别高频前缀 → 输出动态分块策略
  3. Chunker 升级 → 从静态四分块演进为动态边界分块
  4. 命中率提升 → 上游成本进一步下降
  5. 价格空间扩大 → 平台利润和客户返利同步提升

这是一个数据驱动的增强闭环:规模产生数据,数据优化结构,结构降低成本,成本优势吸引更多流量,流量又产生更多数据。

5.3 核心风险与应对

风险 影响 应对策略
Canonicalizer 不稳定 相同语义请求哈希分叉,缓存失效 独立成层,建立严格的规范化测试套件
Chunker 动态边界频繁变动 厂商侧缓存大面积失效 前缀模板采用渐进式更新,旧模板 sticky 保留
流式流量占比过高 Full Hash 去重收益被稀释 这是已知边界,MVP 明确不做流式去重
厂商收紧缓存政策 优化空间被压缩 出站适配器热插拔,快速切换或多厂商分散

六、降级策略

任何中间环节失败时,平台必须能降级为直接透传原始请求,不阻塞用户。

环节 失败行为
入站解析 返回 400,说明不支持的协议
分块 降级:直接透传原始 messages,不做任何处理
排列 降级:直接透传原始 messages,不做任何处理
序列化规范 降级:用普通 JSON 序列化替代 Canonical JSON
缓存注入 降级:透传排列后的 Block,但不注入缓存标记
上游转发 返回 502/504,带厂商错误详情

降级时,平台仅损失缓存优化收益,不影响请求基本可用性。这是整合者必须提供的可靠性承诺。


七、MVP 边界

Phase 1 必须实现

  • 入站:OpenAI 格式(/v1/chat/completions
  • 出站:DeepSeek V3.2(OpenAI 兼容接口)。Anthropic / OpenAI 适配器在接口层预留
  • Chunker + Arranger + Canonicalizer 跑通
  • 前缀哈希共享(结构收敛一致性验证,为后续 KV Cache 做准备)
  • 完整请求去重(非流式)
  • 双层计费模型(平台售价 vs 上游成本价)

Phase 1 明确不做

  • Anthropic/Google 原生入站接口
  • 语义模板替换
  • 对话历史托管(session 状态)
  • 复杂的能力协商
  • 流式请求的挂起复用
  • 自动成本最优路由
  • 动态分块(Observer / Clusterer 只做数据埋点,不做策略生效)

八、代码结构

计划中的目录结构(详见 CODE_WIKI.md):

tokenrouter/
├── cmd/server/main.go
├── internal/
│   ├── inbound/              # 入站适配层
│   ├── envelope/             # Envelope 定义
│   ├── block/                # Block 定义
│   ├── chunker/              # 分块器
│   ├── arranger/             # 排列器
│   ├── canonicalizer/        # 序列化规范器
│   ├── cacheinject/          # 缓存注入器
│   ├── hasher/               # 哈希计算
│   ├── dedup/                # 请求去重器
│   ├── observer/             # 流量观测(Phase 2 演进)
│   ├── outbound/             # 出站适配层
│   ├── proxy/                # HTTP/SSE 代理
│   ├── billing/              # 计费与配额
│   ├── middleware/           # 认证、限流
│   ├── monitor/              # Prometheus 指标
│   ├── model/                # 数据模型(GORM)
│   └── admin/                # 管理 API
├── pkg/                      # 公共工具包
├── tests/                    # 集成 / E2E 测试
├── migrations/               # 数据库迁移
├── deployments/              # Docker / 部署配置
└── docs/

九、关键设计约束

做什么(结构层,完全可控) 不做什么(语义层,绝不触碰)
role 把消息分成 System/Tool/History/Query 块 不解析 content 的语义
把 System 块合并、把 Tool 块排序 不替换 system prompt 的内容
按固定顺序拼接 Block 不用嵌入模型做语义匹配
做字节级序列化规范化(JSON key 排序、编码归一化) 不维护 session 状态
在标准位置注入 cache_control 不主动改写用户意图
相同结构哈希的请求共享缓存,作为单一超大客户运营 不把不同客户的语义混在一起处理

十、总结

TokenRouter 的核心竞争力不是某个复杂的 AI 算法,而是把请求当成标准化零件来组装

  • 对客户:用任意 SDK 调用任意模型,原生功能完整保留;同时吃到平台整理缓存后的返利。
  • 对厂商:请求格式高度标准化,KV Cache 前缀复用率最大化,吃到平台整理算力的红利。
  • 对平台:通过结构整合与跨用户缓存共享,把 N 个分散的调用成本收敛为接近 1 个调用的缓存成本,赚取整合层的合理差价。

流量在我这里只有一副面孔:一个超大号的、结构清晰的高质量客户。