Content-addressed delta tunnel

把 AI Agent 的上行带宽砍掉 99.8%

Agent 每一次请求都要重传完整的上下文——system prompt、tools schema、全部多轮历史。本项目在隧道两端各放一个有状态压缩器:上一轮的请求体,就是这一轮的字典。相邻请求里 90% 以上的字节上一轮已经传过了,不必再传。

上行体积节省 99.9% 生产环境 215 条请求累计
单请求线宽 365B 279.8 MB → 659 KB,压缩比 0.225%
14 条连续请求累计 22.3MB → 20.9KB 生产日志,压缩比 0.094%
01 / 问题

Agent 的上行,远大于下行

上下文越长,每轮重传的重复字节越多。实测某生产 Agent 的真实负载:上行是下行的 14.8 倍,而相邻请求的体积几乎完全一样——这说明绝大部分流量是纯粹的重复。

自建 Agent 直连 LLM 网关时,每一个请求都是一个自包含的完整文档。多轮对话里,前 N 轮的每一句 user 与 assistant 消息,都会在第 N+1 轮被原样重新上传一遍。

真正变化的部分只有尾部追加的几十 KB——但线上传的是整个 1.3–2.2 MB。 相邻请求体积比 p50 为 1.003:体积几乎不变,内容也几乎不变。

gzip 救不了这个问题。它是无状态的,每个请求都从零开始找重复,永远看不到「上一轮刚传过的那 1.3 MB」。实测只能压到 32%。

日请求量22,000+
单请求上行1.3 – 2.2 MB
日上行20 – 44 GB
日下行3 GB
上行 / 下行14.8 : 1
相邻请求体积比 p501.003
02 / 原理

把上一轮请求当字典

客户端在历史请求里挑一个「块哈希命中率」最高的当基线,用 zstd 把新请求压成对基线的 delta。服务端按内容哈希取回字典、解压、校验哈希,再转发上游。全程只用一个 32 字节的哈希协商。

relay-client 本地 · 与 Agent 同机
  1. 1选基线:在历史请求里找块哈希命中率最高的一条
  2. 2zstd delta:把基线当字典压缩新请求
  3. 340 字节 dcz 头:magic + BLAKE3 哈希,后接 zstd 流
  4. 4经 QUIC / TCP 隧道发出
relay-server 远端 · 紧邻上游
  1. 1按哈希从 store 取回字典
  2. 2解 delta + 校验 ResultHash
  3. 3转发到上游(同机回环,不出网卡)
  4. 4存本次 body,供后续请求当基线

Agent 侧完全无感:只需把 base_url 指向 http://127.0.0.1:18899。

03 / 实测

14 条连续真实请求

从生产日志里取一段连续的请求(基线已就位、全部 200、全部走 dcz),逐条列出体积。所有数字都来自 client-*.jsonl,可用同仓库的代码复算。

14 条连续请求的压缩前后体积(11:04–11:06 采样窗口)
时间 请求体 线宽 压缩比 编码
11:04:001,528,7751,0570.069%dcz L19
11:04:161,531,2715270.034%dcz L19
11:04:301,535,5716250.041%dcz L19
11:04:312,178,1191,1830.054%dcz L12
11:04:421,537,4054930.032%dcz L19
11:04:472,179,8906610.030%dcz L12
11:05:051,539,3314620.030%dcz L19
11:05:231,545,4677730.050%dcz L19
11:05:331,548,2765580.036%dcz L19
11:05:352,182,2686140.028%dcz L12
11:05:512,190,5141,5570.071%dcz L12
11:05:521,552,7913900.025%dcz L19
11:06:061,556,8397160.046%dcz L19
11:06:192,191,6466250.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 列)。这是冷启动的真实代价。

原始请求体
22.3 MB
gzip -9
31.65%
zstd -19 · 无字典
22.65%
dcz delta · 本项目
0.094%

同一批 14 条真实请求体、本机实测:gzip -9 为 7,049,305 B,dcz 为 20,911 B —— 相差 337 倍。单条 1.35 MB 请求上差距更极端:gzip 471,780 B vs dcz 461 B,相差 1023 倍。柱长按体积线性绘制,最后一根为可视化起见已放大到可见宽度;数值以右侧标注为准。

04 / 快速开始

两条命令,Agent 无感接入

客户端与服务端都是单个 Go 二进制。Agent 侧只改一个 base_url,请求格式、鉴权、流式语义全部不变。

服务端 · 紧邻上游relay-server
# 编译
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
客户端 · 与 Agent 同机relay-client
# 编译
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
Agent 侧只改一行
# 原来的上游
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,均按字节预算淘汰。

05 / 设计决策

三个被实测数据钉住的取舍

这套方案看起来可以更复杂——块级去重、多候选基线、字典目录。三个方向都做了实测,都被数据否决了。

决策 01

不做块级去重,做整文档 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 倍
决策 02

基线判定用块哈希命中率,不用公共前缀

真实请求从第 45 字节起就不同(max_tokens 的数值字段变了),平均公共前缀只有 45 字节,但第 64 字节后有 93% 逐字节相同。「从头算前缀」永远只看得到前 45 字节,于是误判「不值得 delta」,白扔 1.3 MB。

区分度 0% → 91.5%
决策 03

一致性靠内容哈希,不要字典目录

BaseHash = blake3(上一轮 body)。服务端 store 命中就解码、不命中就回 DeltaMiss 让客户端重发全量。没有目录、没有版本号、没有会话 ID,服务端完全无状态。协商成本固定 32 字节,不随缓存规模增长。

协商成本恒定 32 字节

第三条与 RFC 9842 §2.1.3 一致:服务端分配的 ID 不得用于保证字典内容,使用前必须校验哈希。历史上的 SDCH 正是因为「客户端块缓存 + 服务端块缓存 + 一致性协议」的字典老化问题被 Chrome 移除;「上一轮 body 当字典」天然随会话推进而更新,不存在老化。

06 / 正确性

防静默损坏 + 断线续传

有状态压缩最危险的失败模式不是报错,而是「解出来了一个语义合理但内容错乱的请求」。模型不会报警,只会给奇怪回答,几天后才发现。

三道防线,全部强制开启,没有关闭开关

1. zstd frame CRC —— 帧级 checksum(WithEncoderCRC)。关闭它就等于放弃第一道防线。

2. dcz 头里的 BLAKE3 哈希 —— 解压前与提供的字典比对,字典错配立刻拒绝,绝不尝试「解解看」。

3. ResultHash —— 客户端声明「解码后应当得到的 body 哈希」,服务端解压完再验一次。任一失败都返回错误帧,客户端重发全量。

断线不该让上游白跑

上游请求用 context.WithoutCancel 派生——客户端断开不影响上游继续接收。响应流边收边落盘,磁盘双区域:tmp/ 活跃、idem/ 幂等,各自按字节预算淘汰。

客户端重连带 IdemKey + ResumeFrom(字节偏移)或 SSE event id,从断点接着收。终止帧 FrameDone 让「上游正常完成」与「连接断了」在客户端视角第一次可区分。

实测:60 个 SSE 事件的响应在断开后重连,拼接连贯无重复。

07 / 边界

已知限制

写清楚边界比多列几个优点有用。

  • 01冷启动无收益。服务端首次见到某基线时必然 miss,第一次重发是全量成本(约 20–35%)。稳态才是 0.04% 那一档。
  • 02响应不压缩。当前只优化上行;下行 p50 约 18 KB,尚未处理。
  • 03线宽统计只计首次尝试。DeltaMiss 后的全量重发不计入该请求的 up_wire,因此含冷启动的窗口,其真实占比会高于表格数字。
  • 04QUIC 生产未启用。回环端到端测试通过,但生产链路仍走 TCP/TLS;启用需放通 UDP 并验证中间设备。
  • 05大文档内存。30 MB 级请求的 delta 需要显著内存,靠 PickLevel 按体积降级(≤2 MB 用 L19,≤8 MB 用 L12,更大用 L9)。
  • 06单请求体全量缓冲。客户端与服务端都会把 body 读进内存(为了统计与压缩)。
  • 07跨用户字典隔离。若将来共享字典,必须按租户隔离——混合公私数据的字典存在 CRIME/BREACH 类攻击面(RFC 9842 §9.2)。
08 / 规范

站在 RFC 9842 的肩上

线格式直接沿用 Compression Dictionary Transport 的 dcz 语义,只是把「按需下载字典」换成了「隧道两端各自持有历史请求」。

RFC 9842 Compression Dictionary Transport dcz 线格式、内容哈希协商。线格式一致;请求方向为自研扩展。
RFC 8878 Zstandard raw content dictionary,直接用。
RFC 9530 Repr-Digest 表示身份摘要,语义参考。
RFC 7694 Client-Initiated Content-Encoding 请求方向编码协商,语义参考。