供应商漂移:默认路由如何抬高 LLM 成本
你开启了 prompt 缓存,命中计数器时不时跳动一下,但账单几乎没变。在归咎于你的 prompt 结构之前,先看看仪表盘没有展示的东西:到底是哪个上游真正处理了每一个请求。
多供应商网关会把单一模型分散到多个上游供应商,并为每个请求挑选其中一个。Prompt 缓存是按供应商划分的(在某个供应商内部往往还是按节点划分的)。所以当你第二个完全相同的请求落到与第一个不同的上游时,那就是一次缓存未命中——即便你的 prompt 一个字节都没改。这就是供应商漂移(provider drift),在按 token 付费的模式下,它会悄无声息地让你的成本翻倍。
触发它的两个条件
这并不是你主动选择的错误配置。这是开箱即得的默认行为:
- 默认自动路由。 请求发送给模型时没有锁定某个上游,于是网关会为每次调用选择一个。
- 默认供应商排序 = “default (balanced)”。 网关会在符合条件的上游之间做负载均衡,而不是固定使用某一个。
这两者都是出厂默认值。你什么都不用动就会产生漂移;要避免它,你反而得去改设置。
20 个相同请求长什么样
我们在上述默认设置下,向一个流行的多供应商网关连续发送了 同一个 约 8K token 的前缀 20 次,每次都索取上游自己上报的供应商和缓存字段。对于 DeepSeek 系列中的一个磁盘缓存模型:
- 这 20 次调用由 9 个不同的上游 处理:
N***a、S***w、M***h、D***a、A***L、P***l、S***e、V***e、A***d。 - 缓存命中率:4/20(20%)。 只有当某次调用恰好落到已经缓存过你前缀的上游时,你才会命中。
把同样的 20 次调用发给一个 单后端 网关(一个模型、一个上游、不做均衡),在完全相同的工作负载下命中率是 19/20(95%)。相同的模型、相同的 prompt、相同的调用次数。唯一的变量就是路由会不会漂移。
作为对比,在同一个多供应商网关上,一个 GPT 类模型的全部 20 次调用都被路由到了同一个上游(A***e),命中 19/20。漂移并不是均匀发生的;它会咬住网关恰好分散的那个模型,而在这次运行中,那就是 DeepSeek 系列的模型。
结论 A:你预期的成本 vs 你实际付出的成本
漂移模型上每次调用的成本,按缓存结果清晰地分成两类:
| 调用类型 | 每次调用的中位成本 |
|---|---|
| 缓存命中 | ~$0.00015 |
| 缓存未命中 | ~$0.00062 |
在这个模型上,一次未命中的成本约为命中的 4 倍(就原始输入 token 而言,官方公布的差距还要更大,大约 50 倍)。现在把 20 次调用加起来:
| 场景 | 命中率 | 20 次相同调用的成本 |
|---|---|---|
| 预期(缓存可触达) | 95% | $0.0026 |
| 实际(默认漂移) | 20% | $0.0102 |
相同的模型、相同的 prompt、相同的 20 个请求。供应商漂移让这次运行的成本高出 约 3.9 倍。缓存全程都是”开着”的;只是路由层把你的大部分 token 按未命中价格计费了。把这扩展到一个整天重放大型稳定前缀的生产端点,这个差距就会成为你输入开销的大头。
结论 B:没有缓存也意味着没有延迟收益
缓存不仅仅是个成本杠杆。一次热预填充能更快返回第一个 token。当漂移让你触达不到缓存时,你也就放弃了这份加速。我们在重复的相同调用上测量了首 token 时间(TTFT):
GPT 类模型(路由到一个一致的上游,缓存可触达):
| 调用 | TTFT |
|---|---|
| 第 1 次(冷启动,未命中) | ~1760 ms |
| 后续(热启动,命中) | ~1130 ms |
缓存大约换来 36% 更快的首 token,而且很稳定:每一次热调用都落在一个很窄的区间内。
DeepSeek 系列模型(默认漂移,缓存很少能触达):
- 在 10 次重复调用中的缓存命中:0。
- TTFT 在每次调用之间从 ~1000 ms 到 ~4500 ms 大幅波动,偶尔还出现空响应。
因为几乎每个请求都落到一个全新的上游,你始终停留在冷预填充延迟,并继承了无论哪个供应商应答时的方差。GPT 模型因为缓存可触达而获得了 36% 的 TTFT 改善;漂移的模型一点都没拿到,外加最快与最慢调用之间 4.5 倍的波动。
五分钟审计你自己的配置
不要轻信这些数字,也不要轻信任何人的。把同一个长前缀发送几次,观察两个字段。不要硬编码任何域名;用环境变量把它指向你自己的网关。
import os, uuid
from openai import OpenAI
client = OpenAI(api_key=os.environ["GW_KEY"], base_url=os.environ["GW_BASE"])
SYS = f"[probe {uuid.uuid4().hex}]\n\n" + ("You are a support assistant. " * 300)
seen, hits = {}, 0
for i in range(20):
r = client.chat.completions.create(
model=os.environ["GW_MODEL"], max_tokens=16,
messages=[{"role": "system", "content": SYS},
{"role": "user", "content": f"q{i}"}],
extra_body={"usage": {"include": True}})
d = r.model_dump()
det = r.usage.prompt_tokens_details
cached = (getattr(det, "cached_tokens", 0) or 0) if det else 0
seen[d.get("provider")] = seen.get(d.get("provider"), 0) + 1 # populated when exposed
hits += 1 if cached else 0
print(f"hit rate {hits}/20; upstreams seen: {len(seen)}")
同一个模型出现多于一个的上游就意味着漂移。命中率远低于你的 prompt 稳定度,就说明它在向你征税。更完整的方法见 你的 LLM 网关在缓存问题上撒谎了吗?。
该关注什么
治理漂移的方法是结构性的:把某个模型路由到一个一致的后端,让下一个请求真正能触达到热缓存,而不是把每次调用都负载均衡到一个从未见过你前缀的全新上游。当你评估一个网关时,把同一个前缀发送 20 次,数一数上游数量。一个,是你想要的。九个,就是一笔税。
一个公正的说明:prompt 缓存在任何地方都是尽力而为的,而在磁盘缓存模型上,即便是单后端,长时间空闲后命中率也会软化。消除漂移并不会给你一个无限的缓存。它消除的是最大、最浪费的那一类未命中——那种你从未同意、也看不见的未命中。
结语
“支持 prompt 缓存”和”你的缓存可触达”是两个不同的说法。一个把单一模型分散到不断轮换的一群上游的网关,可以如实地报告它支持缓存,同时却交付 20% 的命中率、约 4 倍的账单,以及波动 4.5 倍的首 token 延迟。要关注的数字不是是否宣称支持缓存,而是你实测的命中率,以及你的相同请求触及了多少个上游。跑一下探针,让数据来定论。
更全面的审计方法见 你的 LLM 网关在缓存问题上撒谎了吗?;关于缓存为何存在,见 KV Cache 与 TTL 的工作原理。
常见问题
这是我这边的配置错误吗? 不是。它发生在出厂默认值下:自动路由,外加供应商排序保持为 “default (balanced)“。避免漂移需要你主动锁定一个上游,而不是反过来。
锁定一个上游能解决吗? 它能消除跨供应商的漂移,但单个上游往往运行多个没有前缀亲和性的副本,所以命中仍可能反复跳变。锁定之后要去测量,而不是想当然。
为什么 GPT 类模型没有漂移? 在这次运行中,网关恰好把它路由到了单个上游。漂移是按模型而定的,取决于网关在多少个符合条件的上游之间做均衡;它并不均匀。
成本差距真的有约 4 倍吗? 就我们测到的每次调用总成本而言,未命中约为命中的 4 倍;就这个模型类别的原始输入 token 定价而言,官方公布的命中与未命中差距更接近 50 倍。无论哪种算法,把预期的命中变成未命中都是最烧钱的部分。
我应该监控哪一个单一指标? 随时间变化的每个模型的缓存命中率,配合每个模型的不同上游数量。如果命中率下降或上游数量上升,你的实际 token 成本刚刚就上去了。