最后更新:2026-04-16 | 定位:LLM 调用层的结构整合者
接口定义见 CODE_WIKI.md,部署细节见 modules/system-implementation.md
TokenRouter 不是网关,不是套利层,而是 LLM 调用层的结构整合者。
- 无状态:平台内部不维护任何 session,每次请求独立处理。状态是敌人,它会破坏结构收敛。
- 只分块,不细处理:我们只按结构(role/type)把请求切成几块,然后按固定顺序拼好。不看内容、不改语义、不维护对话状态。语义属于用户,结构属于平台。
- 三方共赢的结构价值:
- 上游厂商吃到了我整理算力的红利——因为请求结构被标准化,厂商侧 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 字段原样透传 |
设计目标:新增一个入站协议的成本必须极低。
策略:
- 配置驱动:80% 的厂商 API 与 OpenAI 格式接近,通过 YAML 配置即可声明字段映射和路径匹配。
- 编程式兜底:极少数差异过大的厂商,再手写代码实现
InboundAdapter接口。
Envelope:内部最小公共格式,只包含后续路由和处理必需的字段,其余全部放入 Raw。
数据结构定义详见 CODE_WIKI.md。
核心原则:只看 role 和 type,不看 content。它决定「在哪里下刀」,而刀口的位置直接决定缓存前缀的范围。
数据结构定义详见 CODE_WIKI.md。
MVP 静态分块规则:
req.Tools直接归入BlockTool。- 遍历
req.Messages:role=system→BlockSystemrole=assistant且位于最后一轮 user 之前 →BlockHistoryrole=tool/function且位于最后一轮 user 之前 →BlockHistoryrole=user且位于最后一轮 user 之前 →BlockHistory- 最后一轮
role=user以及其后的assistant/tool/function尾部 →BlockQuery
动态演进方向:当流量规模足够大时,通过 Observer 识别高频共现的 (System Prompt, Tool Subset) 组合,Chunker 将能够动态调整前缀边界。例如把高频工具提取到 Prefix Block,低频工具留在 Suffix Block,进一步扩大厂商侧 KV Cache 的命中范围。
核心原则:只排列顺序,不改内容。最小操作单位是 Block。
排列后的 Block 顺序永远固定:
- System Block:多条 system 合并为一条(用
\n\n拼接) - Tool Block:工具定义按
Name字段字母序排序 - History Block:保持原有顺序,但可截断(保留最近 N 轮)
- Query Block:原样后置
块内粗处理:
| 块类型 | 处理 | 说明 |
|---|---|---|
| System | 合并 | 多条 system 合并为单条 |
| Tool | 字母排序 | 按 Name 字段排序,保持稳定性 |
| History | 截断 | 保留最近 maxHistoryTurns 轮 |
| Query | 无处理 | 原样保留 |
关键边界:Arranger 不进入 Block 内部做字符级操作。它只移动 Block、合并同类型 Block、截断 Block 序列。内容的每一个字节都保持原样。
核心原则:把 Block 结构变成字节级确定性的标准形。这是反熵的最后一道防线。
如果两个请求经过 Chunker 和 Arranger 后已经逻辑等价,Canonicalizer 必须保证它们的字节输出完全一致。否则厂商侧的 KV Cache 会因为一个空白字符的差异而失效。
规范化操作(不碰语义,只做物理编码):
- JSON key 的递归字母序排序
- 空值的统一处理(omit 或显式
null) - 数字的固定精度格式化
- Unicode NFC 归一化
- 空白字符与换行符的统一压缩
为什么必须独立成层:Arranger 的最小单位是 Block,而 Canonicalizer 的最小单位是字节。两者职责不同、抽象层级不同,必须分离。
核心原则:在格式化后的固定位置,按目标厂商的机制注入缓存标记。
不同厂商的 prompt caching 规则差异极大:Anthropic 需要显式 cache_control 且对位置和 token 数有门槛限制;OpenAI 是自动缓存,无需标记;其他厂商可能有各自的折扣窗口和标记协议。
缓存策略引擎的职责:
- 根据目标厂商决定哪些 Block 需要注入标记
- 判断注入位置是否满足厂商的最小 token 门槛
- 选择正确的标记类型(如 Anthropic 的
ephemeral)
接口与示例详见 modules/cache-intelligence.md。
通过哈希实现两层共享:
只取 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。
取全部 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
}如果上一个完全相同的请求还在处理中,后续请求可挂起等待并复用响应结果。
注:完整去重仅适用于非流式请求。流式请求因输出实时性要求,暂不挂起复用。
这是让 Chunker 从静态走向动态的数据引擎。(Phase 2 only)
作为单一超大客户,平台持续观测所有请求的结构特征(system prompt 签名、tool set 签名、出现频率)。当数据密度足够高时,自动识别出高分布区域,并输出「候选前缀模板」供 Chunker 使用。
观测目标:
(system_signature, tool_set_signature)的共现频率- 不同客户端/框架带来的结构收敛度
- 高频前缀模板的覆盖率和命中率
产出:
- 高频前缀白名单,用于动态调整 Chunker 的切分策略
- 预热调度建议,主动占住厂商缓存槽位
这不是 MVP 必需,但它是平台从「手工整合」走向「算法整合」的必由之路。
[接收请求]
│
▼
[入站解析] ──错误──→ [返回错误]
│
▼
[分块] 按 role/type 切分
│
▼
[排列] 固定顺序排列 Block
│
▼
[序列化规范] 生成字节级确定性输出
│
▼
[缓存注入] 按厂商策略打标记
│
▼
[计算完整哈希]
│
├── 命中 ──→ [挂起复用] ──→ [返回成功]
│
└── 未命中 ──→ [计算前缀哈希]
│
▼
[转发上游]
│
┌─────────┴─────────┐
stream=true stream=false
│ │
▼ ▼
[流式响应] [同步响应]
│ │
└────────┬───────────┘
▼
[记录并返回]
│
▼
[返回成功]
│
▼
[*]
可以。 Phase 1 只需要以下模块即可形成完整闭环:
- OpenAI 格式入站适配
- 静态 Chunker(四分块)
- Arranger(固定顺序 + 合并/排序/截断)
- Canonicalizer(基础 JSON 规范化)
- DeepSeek V3.2 出站适配(OpenAI 兼容)+ Cache Injector 透传策略。Anthropic 适配器和缓存注入器在接口层预留
- Prefix Hash 共享 + Full Hash 去重
- 双层计费模型
这个组合已经能够让一个请求从「客户端进入」到「厂商侧缓存命中」再到「成本降低」形成完整链路。每个模块的职责边界清晰,没有不可实现的黑箱。
当流量足够大时,系统按以下路径自然演进:
- Observer 上线 → 持续观测流量分布
- Clusterer 识别高频前缀 → 输出动态分块策略
- Chunker 升级 → 从静态四分块演进为动态边界分块
- 命中率提升 → 上游成本进一步下降
- 价格空间扩大 → 平台利润和客户返利同步提升
这是一个数据驱动的增强闭环:规模产生数据,数据优化结构,结构降低成本,成本优势吸引更多流量,流量又产生更多数据。
| 风险 | 影响 | 应对策略 |
|---|---|---|
| Canonicalizer 不稳定 | 相同语义请求哈希分叉,缓存失效 | 独立成层,建立严格的规范化测试套件 |
| Chunker 动态边界频繁变动 | 厂商侧缓存大面积失效 | 前缀模板采用渐进式更新,旧模板 sticky 保留 |
| 流式流量占比过高 | Full Hash 去重收益被稀释 | 这是已知边界,MVP 明确不做流式去重 |
| 厂商收紧缓存政策 | 优化空间被压缩 | 出站适配器热插拔,快速切换或多厂商分散 |
任何中间环节失败时,平台必须能降级为直接透传原始请求,不阻塞用户。
| 环节 | 失败行为 |
|---|---|
| 入站解析 | 返回 400,说明不支持的协议 |
| 分块 | 降级:直接透传原始 messages,不做任何处理 |
| 排列 | 降级:直接透传原始 messages,不做任何处理 |
| 序列化规范 | 降级:用普通 JSON 序列化替代 Canonical JSON |
| 缓存注入 | 降级:透传排列后的 Block,但不注入缓存标记 |
| 上游转发 | 返回 502/504,带厂商错误详情 |
降级时,平台仅损失缓存优化收益,不影响请求基本可用性。这是整合者必须提供的可靠性承诺。
- 入站:OpenAI 格式(
/v1/chat/completions) - 出站:DeepSeek V3.2(OpenAI 兼容接口)。Anthropic / OpenAI 适配器在接口层预留
- Chunker + Arranger + Canonicalizer 跑通
- 前缀哈希共享(结构收敛一致性验证,为后续 KV Cache 做准备)
- 完整请求去重(非流式)
- 双层计费模型(平台售价 vs 上游成本价)
- 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 个调用的缓存成本,赚取整合层的合理差价。
流量在我这里只有一副面孔:一个超大号的、结构清晰的高质量客户。