多 Provider 负载均衡会把同一会话的请求路由到不同账号/节点,导致缓存命中率极低;通过会话 sticky routing 可将缓存命中率从"随机"提升到 80-95%。
TL;DR — 如今按 token 计价已成行业默认,coding agent 账单里最大的开销往往不是"更聪明的模型",而是以全价反复购买的重复内容。现代提供商(Anthropic、DeepSeek、OpenAI)对重复输入前缀的缓存命中只收取 10-20% 的费用。制胜之道不是让 prompt 变得更小——而是确保你的多轮会话永远不会离开缓存已经热起来的那个上游节点。在我们 150 req/s 的长会话压测中,使用 sticky routing 将会话固定后,缓存命中率从"靠运气"提升到了 80-95%,账单费用降低了 50-70%(自测,非第三方审计)。
这里有个反直觉的地方。如果你运行一个 coding agent(Claude Code、Cursor、OpenClaw——任何具有长多轮上下文的工具),并且配置了多个 API key 或提供商以保证韧性,你的网关很可能在每一轮都在杀死你的缓存。
Prompt 缓存是按上游节点隔离的——按账号、按物理节点。当你的负载均衡器把请求 #1 轮询到账号 A、#2 到账号 B、#3 又回到 A 时,对每个账号来说每一轮看起来都像是一个全新的会话。什么都命中不了。所有内容都按全价计费,包括前面轮次中已经计算过的 90% 的 token。
更糟的是:经典的网关调度算法——平滑加权轮询、贪婪配额平衡——的设计目标恰好是均匀分散流量。缓存局部性想要的恰恰相反:流量应该被固定。在长会话中,"公平"和"便宜"是互相排斥的。
"让 agent 更便宜"这个领域有两条主流路径,它们基于对上游计费方式的不同假设:
本地压缩(如 OpenProxy 的 RTK):假设上游是无状态的,所有内容都按全价计费,所以缩小 payload——重新编码表格、注入简短输出指令、裁剪工具 schema。声称能节省 20-40%。
缓存亲和性保护(VMR):假设上游是有状态的——即长会话成本的 90% 是由 prompt 缓存决定的。所以什么都不动,保持字节前缀完全一致将会话固定在一个节点上,让重复前缀持续命中折扣层。
冲突是结构性的:每一次压缩"优化"都会破坏缓存。动态重编码、注入指令、schema 裁剪——每一种改动都会从 token 0 开始使前缀哈希失效。在一个 30 轮的会话中,压缩 30% 的 token 不足以补偿 29 轮累积上下文重新按全价计费的损失。机制层面的结论:在启用缓存的 2026 年,压缩反而可能让你花更多钱。
(公平地说:OpenProxy 是一个真正更广泛意义上的工具——为 ChatGPT/Codex/Gemini 提供 OAuth 订阅池聚合、真正的 Web UI、对冲和融合调度、MCP/多模态扩展。上述批评仅针对长会话中的 RTK 压缩,不针对整个项目。)
VMR 的 sticky registry(internal/sticky/sticky.go)是一个内存中的映射表,从会话指纹映射到上一次为其提供服务的节点。指纹是 system prompt + 第一条用户消息的哈希值——特意不采用客户端会话 ID,因为客户端(尤其是 agent 框架)在重启时会重新生成 ID,而缓存前缀是由内容决定的。
路由有两层,按优先级从高到低:
Sticky Pin——如果指纹在 registry 中且节点健康,强制将请求发送回该节点。这优先于所有配额调度。即使该账号的配额用完了,我们仍会保持会话粘性并承担超出配额的部分,因为中途切换会话会使缓存归零,而数十轮的重计算成本比短暂的超出配额要高出一个数量级。
New-session spread——只有没有粘性绑定的会话才参与调度。优先级排序后平分的情况下,按配额余量贪婪分配。值得注意的是,我们没有使用 SWRR:它需要一个持久化的累加器,而分散流量本身就是在破坏缓存局部性。一个新会话会一次性选择一个节点;第一层会把它固定在那里。
两个值得借鉴的工程细节:
TTL 是分层的。全局 sticky_ttl 默认 10m(对 Anthropic/OpenAI 的内存缓存足够),由内存驱逐兜底硬性上限 24h,防止 registry 无限增长。像 DeepSeek 这样使用磁盘缓存的提供商会将缓存保留数小时到数天——按节点覆盖(sticky_ttl: 2h)以匹配缓存生命周期。
过期清理是机会性的,不是 ticker goroutine——由事件触发并节流,避免在每次调用时都执行带全局锁的遍历。
models:
coding:
capabilities: [text, tools]
max_context_tokens: 256000
endpoints:
- protocol: openai
provider: deepseek
models: [deepseek-v4-flash]
sticky_ttl: 2h # disk cache lives hours-to-days; match it
- protocol: anthropic
provider: anthropic
models: [claude-sonnet-5]
# sticky_ttl: 10m (default)
客户端指向 http://localhost:8080/v1(或者对于 Anthropic 协议用 /v1/messages),模型名称是你的虚拟模型。同一个会话、同一节点、每一轮。
同一会话修复前 → 修复后的追踪(示意性的微观数字,在合理范围内):
before req#0127 endpoint=deepseek-01 cache_hit=0 input=185K cost=$0.41
before req#0128 endpoint=anthropic-02 cache_hit=0 input=199K cost=$1.86
before req#0129 endpoint=deepseek-01 cache_hit=0 input=213K cost=$0.47
after req#0127 endpoint=deepseek-01 cache_hit=182K input=185K cost=$0.03
after req#0128 endpoint=deepseek-01 cache_hit=195K input=199K cost=$0.04
after req#0129 endpoint=deepseek-01 cache_hit=208K input=213K cost=$0.04
将会话固定后,累积的上下文就不再按全价重新购买了。
无法实现全局配额最优调度。理论上可能存在一个既保留缓存又在会话之间随机分配配额的完美调度器,但需要持久化状态和预测能力。对于个人开发者而言,复杂度与 bug 的比值不值得。会话本地锁 + 会话间贪婪是务实的最优解。
无法实现对冲。同时向两个提供商发送相同请求并取更快的结果会增加延迟保险,但会使全价消耗翻倍并破坏缓存。对个人使用来说预期价值为负。
TTL 粒度较粗。10m 对超长会话来说偏保守;我们提供按节点覆盖而非自适应 TTL。已知的后续工作。
先检查你的缓存命中率。看账单/状态输出中的 cache_read 与 fresh。长会话缓存命中率低于 ~50% 意味着你的配置在漏钱——先解决这个问题再做其他优化。
停止在多个 key 之间分散会话。如果你的工具支持会话亲和性,开启它。如果不支持,手动为一个长会话分配一个 key,让故障转移只处理真正的宕机。
保持 system prompt 字节稳定。动态注入(时间戳、随机变量)会使每条请求的前缀失效。而且如果你使用"压缩"代理,要知道它是在用缓存命中换取更短的 payload——在长会话中通常是一笔坏交易。
零数据库、纯自托管、一个静态二进制文件(~15MB,Go)。如果这听起来像是你想要的工具,VMR 开源于 https://github.com/bigfatsea/vmr。
数据声明:50-70% 这个数字来自我们自己的长会话压测和项目记录,非第三方审计。实际节省取决于提供商的缓存定价——DeepSeek 级磁盘缓存能带来最大的收益。