不做块级去重,做整文档 delta
FastCDC 块级去重(min 2KB / avg 8KB / max 64KB,292 KB 请求)产出 17 KB;整文档 zstd delta 产出 1,297 B。原因:LLM 会话以尾部追加为主,LZ77 的变长 match 对「未变的前缀」只编码 2–3 字节,而块级去重把未变的 12.7 KB 当成不可分原子。
整文档 delta 更快 13 倍Content-addressed delta tunnel
Agent 每一次请求都要重传完整的上下文——system prompt、tools schema、全部多轮历史。本项目在隧道两端各放一个有状态压缩器:上一轮的请求体,就是这一轮的字典。相邻请求里 90% 以上的字节上一轮已经传过了,不必再传。
上下文越长,每轮重传的重复字节越多。实测某生产 Agent 的真实负载:上行是下行的 14.8 倍,而相邻请求的体积几乎完全一样——这说明绝大部分流量是纯粹的重复。
自建 Agent 直连 LLM 网关时,每一个请求都是一个自包含的完整文档。多轮对话里,前 N 轮的每一句 user 与 assistant 消息,都会在第 N+1 轮被原样重新上传一遍。
真正变化的部分只有尾部追加的几十 KB——但线上传的是整个 1.3–2.2 MB。 相邻请求体积比 p50 为 1.003:体积几乎不变,内容也几乎不变。
gzip 救不了这个问题。它是无状态的,每个请求都从零开始找重复,永远看不到「上一轮刚传过的那 1.3 MB」。实测只能压到 32%。
客户端在历史请求里挑一个「块哈希命中率」最高的当基线,用 zstd 把新请求压成对基线的 delta。服务端按内容哈希取回字典、解压、校验哈希,再转发上游。全程只用一个 32 字节的哈希协商。
Agent 侧完全无感:只需把 base_url 指向 http://127.0.0.1:18899。
从生产日志里取一段连续的请求(基线已就位、全部 200、全部走 dcz),逐条列出体积。所有数字都来自 client-*.jsonl,可用同仓库的代码复算。
| 时间 | 请求体 | 线宽 | 压缩比 | 编码 |
|---|---|---|---|---|
| 11:04:00 | 1,528,775 | 1,057 | 0.069% | dcz L19 |
| 11:04:16 | 1,531,271 | 527 | 0.034% | dcz L19 |
| 11:04:30 | 1,535,571 | 625 | 0.041% | dcz L19 |
| 11:04:31 | 2,178,119 | 1,183 | 0.054% | dcz L12 |
| 11:04:42 | 1,537,405 | 493 | 0.032% | dcz L19 |
| 11:04:47 | 2,179,890 | 661 | 0.030% | dcz L12 |
| 11:05:05 | 1,539,331 | 462 | 0.030% | dcz L19 |
| 11:05:23 | 1,545,467 | 773 | 0.050% | dcz L19 |
| 11:05:33 | 1,548,276 | 558 | 0.036% | dcz L19 |
| 11:05:35 | 2,182,268 | 614 | 0.028% | dcz L12 |
| 11:05:51 | 2,190,514 | 1,557 | 0.071% | dcz L12 |
| 11:05:52 | 1,552,791 | 390 | 0.025% | dcz L19 |
| 11:06:06 | 1,556,839 | 716 | 0.046% | dcz L19 |
| 11:06:19 | 2,191,646 | 625 | 0.029% | dcz L12 |
| 合计 | 24,798,163 | 10,241 | 0.041% | 省 99.96% |
另一段包含冷启动的 14 条请求(基线尚未就位)为
22,273,268 B → 20,911 B = 0.094%,其中 2 条首次 delta 未命中、触发了全量重发(约 762 KB,未计入该统计的 up_wire 列)。这是冷启动的真实代价。
同一批 14 条真实请求体、本机实测:gzip -9 为 7,049,305 B,dcz 为 20,911 B —— 相差 337 倍。单条 1.35 MB 请求上差距更极端:gzip 471,780 B vs dcz 461 B,相差 1023 倍。柱长按体积线性绘制,最后一根为可视化起见已放大到可见宽度;数值以右侧标注为准。
客户端与服务端都是单个 Go 二进制。Agent 侧只改一个 base_url,请求格式、鉴权、流式语义全部不变。
# 编译 go build -o relay-server ./cmd/relay-server # 启动:QUIC 与 TCP/TLS 共用 28443 ./relay-server \ -listen 0.0.0.0:28443 \ -upstream http://127.0.0.1:3431 \ -uphost magpie.aiweb.xin \ -cert /path/to/fullchain.pem \ -key /path/to/privkey.pem
# 编译 go build -o relay-client ./cmd/relay-client # 启动 ./relay-client \ -listen 127.0.0.1:18899 \ -server 47.82.76.102:28443 \ -server-name magpie.aiweb.xin \ -transport auto
# 原来的上游 ANTHROPIC_BASE_URL=https://magpie.example.com # 指向本地隧道入口即可,其余全部不变 ANTHROPIC_BASE_URL=http://127.0.0.1:18899
-upstream 必须填 127.0.0.1 或同机内网地址。若填公网域名,流量会绕一整圈公网(实测某云主机公网 IP 根本不在网卡上,真实内网地址是 172.17.61.35)。服务端默认基版本缓存 256 MB、spool 活跃区 512 MB / 幂等区 2048 MB,均按字节预算淘汰。
这套方案看起来可以更复杂——块级去重、多候选基线、字典目录。三个方向都做了实测,都被数据否决了。
FastCDC 块级去重(min 2KB / avg 8KB / max 64KB,292 KB 请求)产出 17 KB;整文档 zstd delta 产出 1,297 B。原因:LLM 会话以尾部追加为主,LZ77 的变长 match 对「未变的前缀」只编码 2–3 字节,而块级去重把未变的 12.7 KB 当成不可分原子。
整文档 delta 更快 13 倍
真实请求从第 45 字节起就不同(max_tokens 的数值字段变了),平均公共前缀只有 45 字节,但第 64 字节后有 93% 逐字节相同。「从头算前缀」永远只看得到前 45 字节,于是误判「不值得 delta」,白扔 1.3 MB。
BaseHash = blake3(上一轮 body)。服务端 store 命中就解码、不命中就回 DeltaMiss 让客户端重发全量。没有目录、没有版本号、没有会话 ID,服务端完全无状态。协商成本固定 32 字节,不随缓存规模增长。
第三条与 RFC 9842 §2.1.3 一致:服务端分配的 ID 不得用于保证字典内容,使用前必须校验哈希。历史上的 SDCH 正是因为「客户端块缓存 + 服务端块缓存 + 一致性协议」的字典老化问题被 Chrome 移除;「上一轮 body 当字典」天然随会话推进而更新,不存在老化。
有状态压缩最危险的失败模式不是报错,而是「解出来了一个语义合理但内容错乱的请求」。模型不会报警,只会给奇怪回答,几天后才发现。
1. zstd frame CRC —— 帧级 checksum(WithEncoderCRC)。关闭它就等于放弃第一道防线。
2. dcz 头里的 BLAKE3 哈希 —— 解压前与提供的字典比对,字典错配立刻拒绝,绝不尝试「解解看」。
3. ResultHash —— 客户端声明「解码后应当得到的 body 哈希」,服务端解压完再验一次。任一失败都返回错误帧,客户端重发全量。
上游请求用 context.WithoutCancel 派生——客户端断开不影响上游继续接收。响应流边收边落盘,磁盘双区域:tmp/ 活跃、idem/ 幂等,各自按字节预算淘汰。
客户端重连带 IdemKey + ResumeFrom(字节偏移)或 SSE event id,从断点接着收。终止帧 FrameDone 让「上游正常完成」与「连接断了」在客户端视角第一次可区分。
实测:60 个 SSE 事件的响应在断开后重连,拼接连贯无重复。
写清楚边界比多列几个优点有用。
up_wire,因此含冷启动的窗口,其真实占比会高于表格数字。PickLevel 按体积降级(≤2 MB 用 L19,≤8 MB 用 L12,更大用 L9)。线格式直接沿用 Compression Dictionary Transport 的 dcz 语义,只是把「按需下载字典」换成了「隧道两端各自持有历史请求」。