路由:让同一会话落在同一缓存分片
OpenAI 系接口按
prompt_cache_key 分片。sidecar 用会话 id 派生这个键
(经长度钳制),通过 payload hook 在出站请求上注入——
会话不变,键就不变,缓存分片就不换。走网关 / 中转时,还需要让基础设施把同一会话路由到同一上游。
每个请求带
x-session-affinity 与 X-Session-Id 两个头。长缓存开关
压缩摘要请求必须和聊天请求”同形”
这是整个设计里最隐蔽、也最值钱的一条。 上下文压缩(compaction)本身要发一次模型请求来生成摘要。 如果这次请求的 system、tools、参数与聊天路径不一致, 服务端前缀缓存就命中不了任何东西——压缩会变成一笔全价重算。 线上实测过:摘要请求只命中 128 token(一个缓存块)就分叉。 所以摘要请求被刻意组装成与聊天完全同形:1
同一 system(messages[0])、同一 tools、同一亲和头与 prompt_cache_key
唯一的差别是尾部多一条摘要指令——前缀缓存能命中整段对话。
2
思考档位同参数
某些端点把参数算进缓存键(Anthropic 按 max_tokens 推导的 thinking 预算做键),
参数不一致同样会让”本该命中”的前缀失配。
3
同一长缓存开关
与聊天路径共用
PI_CACHE_RETENTION。出站载荷指纹:分叉了怎么定位
两条路径(聊天的 convertToLlm 链路、摘要的自有组装)分头组装请求体, 序列化不同源就会出现”看起来一样、字节不一样”。 为此对最可能分叉的三段各记一个短哈希 + 尺寸: tools(条数 + 指纹)、首条 system 消息(长度 + 指纹)、消息条数, 连同 reasoning / max_tokens 等参数一起打进 event 级日志。 两行日志一比对就知道差在哪一段——不用再猜。命中率怎么算:两套口径
单轮 input ≥ 2,000 且 ≥ prompt 的 5% 就算一次 miss。
问题在于分不清「本轮新增内容」(工具结果本来就是新的)
和「前缀被重算」,实测误报能占九成。
仅用于兼容旧面板的分母。
单轮重算量 = min(上一轮 prompt, 本轮 prompt) − cacheRead,地板 1024。
这个数才是「这一轮多付了多少 token」,
并带 lastMiss 归因:空闲超 TTL,还是切换了模型。
cacheRead / (input + cacheRead + cacheWrite),
面板上看的是近 10 轮窗口——累计值会被冷启动和重建轮稀释,短窗才看得到稳态。
两条刻意的豁免:冷启动首轮没有「本应命中」的前缀,不计;
provider 从未上报过缓存活动的会话整段不计重算——
否则会把「这家 provider 根本不报缓存」的会话全记成 miss。
和压缩的关系
压缩(compaction)和缓存是一对互相拉扯的机制: 压缩把历史换成摘要,前缀必然改变,压缩后的首轮预期整段重算, 统计里单列为rebuilds,不与真实 miss 混在一起。
压缩本身的设计约定(见 apps/sidecar/pi-agent/src/agent/context.ts 头注):
- 只发生在回合边界,摘要覆盖全部历史、不保留原始尾部消息;
- checkpoint 行持久化到 JSONL,全量消息行保留,UI 历史不受影响;
- 摘要生成失败走 fresh_window 兜底——不花模型请求,装填固定 rollover 标记, 保证会话能继续。
下一步
缓存账算清了,目标模式还有自己的一本账。
