增量上下文打包(Context Pack)
这个 Skill 解决什么问题
多轮对话 / 会议纪要 / 检索式问答里,服务端每轮都会返回一份完整的 prepare 正文。客户端上一轮已经拿到过同样的内容,如果每轮都把整份再传一遍,代价随轮次线性累积。
Context Pack 把完整正文表示成可校验的分块包(pack),增量则只携带保留的前缀 + 新增后缀(delta):
- 首次请求:发送完整 pack;
- 后续请求:发送 delta("我留了前 N 块,你再补上这些块");
- 接收端:拿手上已有的 base pack + delta → 校验 → 重建出与完整正文逐字节一致的结果。
关键点是重建只依赖序列化数据,不依赖发送端的进程内缓存、不共享任何 Python 对象,因此可以跨进程、跨语言、跨网络实现。
适用场景
| 场景 | 收益来源 | | --- | --- | | 会议纪要 / 长对话逐轮追加 | 前面轮次的内容字节完全不变,直接复用 | | 检索式 prepare:引用顺序变化、正文尾部追加 | 中间内容不变时只重发尾部 | | 客户端已缓存整份正文(304 风格协商) | 用摘要判断是否变化,变化时才走增量 | | 弱网 / 计费敏感的网关转发 | 同样的语义,少传很多字节 |
快速开始
本 Skill 自带一份零依赖的参考实现 scripts/ctxpack.py(Python 3.9+,只用标准库)。
# 0) 先跑一遍内置自检,确认环境正常
python scripts/ctxpack.py selftest
# 1) 把正文打成完整 pack
python scripts/ctxpack.py pack --input body.txt --out pack.json
# 2) 下一轮正文变了,算出增量(base = 上一轮 pack,target = 本轮 pack)
python scripts/ctxpack.py pack --input body2.txt --out pack2.json
python scripts/ctxpack.py delta --base pack.json --target pack2.json --out delta.json
# 3) 接收端:只凭 base + delta 重建,并和期望正文逐字节比对
python scripts/ctxpack.py apply --base pack.json --delta delta.json --out rebuilt.json --check body2.txt
# 4) 校验任意一个包的摘要与字节数
python scripts/ctxpack.py verify --pack pack.json
作为库使用:
from ctxpack import ContextPack, ContextPackDelta, apply_delta
base = ContextPack.from_content(previous_text) # 默认每块上限 512 字节
target = ContextPack.from_content(current_text)
delta = ContextPackDelta.between(base, target) # 只带差异
wire = delta.to_bytes() # 规范 JSON,可直接发网络
# 接收端:手上只有 base 和 wire
rebuilt = apply_delta(base, ContextPackDelta.from_bytes(wire))
assert rebuilt.content == current_text # 与完整 prepare 完全一致
工作原理
分块:以行为锚点
split_content() 把正文按行边界贪心聚合成块,单块不超过 max_chunk_bytes(默认 512)字节;超长单行退化为按字符边界切分。
- 每块都是合法 UTF-8,单独解码也不会乱码;
- 切分点永远落在字符边界上;
- 同一输入永远得到同一组分块(确定性);
- 因为锚在行边界上,正文只在尾部追加时,前面各块的字节保持不变 —— 这正是前缀能复用的原因。若按固定容量切块,追加内容会改动最后一块并连带打乱边界,收益会明显变差。
摘要与字节数
digest= 对{"chunk_bytes": [...], "schema": "ctxpack.pack.v1"}的规范 JSON 求 SHA-256,写成sha256:<hex>;摘要只覆盖正文内容,不覆盖自身。byte_count= 序列化结果的真实字节数,包含该字段自身(用不动点迭代算出),因此拿到包就知道线上要传多少字节。
增量
ContextPackDelta.between(base, target) 求两块之间最长公共分块前缀:
{
"base_digest": "sha256:...",
"kept_chunks": 12,
"new_chunks": ["BASE64", "..."],
"schema": "ctxpack.delta.v1",
"target_digest": "sha256:..."
}
接收端 apply_delta(base, delta) 的校验顺序:
delta.base_digest必须等于base.digest(防止用错 base);delta.kept_chunks不得超过 base 的块数;- 重建后摘要必须等于
delta.target_digest(防止传输损坏或伪造)。
任何一步不通过都抛 ContextPackError,不会返回一个"看起来能用"的结果。
协议速查
| 对象 | 字段 | 说明 |
| --- | --- | --- |
| Pack | schema | 固定 ctxpack.pack.v1 |
| | chunk_bytes | Base64 字符串数组,内存中是 bytes 元组 |
| | digest | sha256:<hex>,对 {schema, chunk_bytes} 的规范 JSON 求 SHA-256 |
| | total_bytes | 正文原始字节数(不含协议开销) |
| | byte_count | 序列化后真实字节数,含该字段自身 |
| Delta | schema | 固定 ctxpack.delta.v1 |
| | base_digest / target_digest | 增量两端的内容摘要 |
| | kept_chunks | 接收端保留 base 前缀的块数(非负整数) |
| | new_chunks | 追加的目标后缀块 |
JSON 一律 UTF-8、键排序、分隔符 (',', ':')、无多余空白。完整字段定义与字节级示例见 references/protocol.md。
校验规则与失败处理
ContextPack.from_bytes() / ContextPackDelta.from_bytes() 会拒绝以下输入:
- 缺少字段或多出字段(字段集合必须精确匹配);
schema不匹配;- Base64 非法或不是标准规范编码;
- 摘要格式错误,或重算摘要与携带摘要不一致(篡改检测);
byte_count/total_bytes与实际不一致;- JSON 不是规范形式(键未排序或有多余空白),可用
strict=False放宽。
什么时候不值得用增量
- 正文很小(几百字节以内):delta 的两个摘要字段本身就有固定开销,此时直接发完整正文更省;
- 每一轮正文都被完全重写(例如每轮换检索词导致正文整体重排):公共前缀为空,增量反而比完整正文大;
- gzip / brotli 已经在链路上压缩过:增量解决的是"重复内容",压缩解决的是"冗余编码",两者可叠加但别指望叠加后还有同等比例的收益。
实测参考(本 Skill 自带 measure 命令,40 轮追加式会议记录,每轮追加一段发言):
| 指标 | 字节数 | 相对原始正文 | | --- | --- | --- | | 原始正文累计 | 114,652 | — | | 每轮都发完整 pack | 160,550 | -40.0% | | 增量协议(首次 pack + 后续 delta) | 24,454 | +78.7% | | gzip(9) 逐轮压缩对照 | 12,735 | +88.9% |
可以看到:增量协议明显优于"每轮重发完整正文",但仍弱于 gzip —— 因为两者解决的问题不同(重复内容 vs 冗余编码)。如果链路已经全局 gzip,直接上 gzip 即可;增量协议的价值在于"客户端已经持有上一轮"这一前提。
参考资料
references/protocol.md—— 字节级协议定义、字段表、错误码、兼容性说明references/integration.md—— HTTP / 缓存协商 / 存储选型等接入配方scripts/ctxpack.py—— 参考实现(零依赖,含selftest)
许可
MIT,见 LICENSE。
Scan to join WeChat group