供應商漂移:預設路由如何推高 LLM 成本
你開啟了 prompt caching,命中計數器偶爾跳動一下,但帳單幾乎沒變。在怪罪你的提示結構之前,先看看儀表板隱藏起來的東西:實際上是哪個上游服務了每一筆請求。
多供應商閘道會把單一模型分散到數個上游供應商,並為每筆請求挑選其中一個。Prompt 快取是各供應商獨立的(在供應商內部通常還是各節點獨立)。因此當你第二筆相同的請求落到與第一筆不同的上游時,這就是一次快取未命中,即便你的提示一個位元組都沒改。這就是供應商漂移,而在按權杖計費的模式下,它會悄悄把你的成本翻好幾倍。
觸發它的兩個條件
這不是你主動選擇的設定錯誤,而是開箱即得的結果:
- 預設自動路由。 請求被送往模型時並未釘選某個上游,所以閘道會為每次呼叫挑選一個。
- 預設供應商排序 = 「default(balanced)」。 閘道會在符合條件的上游之間做負載平衡,而非固定使用其中一個。
兩者都是出廠預設值。你不必動任何設定就會發生漂移;反而是要動設定才能避免它。
20 筆相同請求長什麼樣
我們在上述預設值下,對一個熱門的多供應商閘道連續送了 20 次相同的約 8K 權杖前綴,每次都向上游索取它自己回報的供應商與快取欄位。對於 DeepSeek 家族的某個磁碟快取模型:
- 9 個不同的上游服務了這 20 次呼叫:
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%)。相同模型、相同提示、相同呼叫次數。唯一的變數就是路由會不會漂移。
作為對比,在同一個多供應商閘道上,某個 GPT 等級的模型在這 20 次呼叫中全部被路由到同一個上游(A***e),命中 19/20。漂移並不一致;它會咬住閘道剛好拿去分散的那個模型,而在這次執行中那是 DeepSeek 家族的模型。
結論 A:你預期的成本 vs 你實際付的成本
漂移模型上的每次呼叫成本,依快取結果清楚地分成兩類:
| 呼叫類型 | 中位成本 / 次呼叫 |
|---|---|
| 快取命中 | ~$0.00015 |
| 快取未命中 | ~$0.00062 |
在這個模型上,一次未命中的成本約為一次命中的 4 倍(就原始輸入權杖而言,公布的差距更大,約 50 倍)。現在把這 20 次呼叫加總起來:
| 情境 | 命中率 | 20 次相同呼叫的成本 |
|---|---|---|
| 預期(快取可觸及) | 95% | $0.0026 |
| 實際(預設漂移) | 20% | $0.0102 |
相同模型、相同提示、相同的 20 筆請求。供應商漂移讓這次執行的成本變成約 3.9 倍。快取自始至終都「開著」;只是路由層把你大部分的權杖以未命中費率計費了。把這個情況放大到一個整天重播大型穩定前綴的生產端點,這個差距就是你輸入花費的大宗。
結論 B:沒有快取也意味著沒有延遲優勢
快取不只是成本槓桿。一個預熱過的 prefill 會更快回傳第一個權杖。當漂移讓你拿不到快取時,你也一併放棄了這個提速。我們在重複的相同呼叫上量測了首個權杖時間(TTFT):
GPT 等級模型(路由到單一一致的上游,快取可觸及):
| 呼叫 | TTFT |
|---|---|
| 第 1 次(冷、未命中) | ~1760 ms |
| 後續(熱、命中) | ~1130 ms |
快取大約買到快 36% 的首個權杖,而且很穩定:每次熱呼叫都落在一個緊密的區間內。
DeepSeek 家族模型(預設漂移,快取很少可觸及):
- 在 10 次重複呼叫中的快取命中數:0。
- TTFT 在各呼叫之間從 ~1000 ms 擺盪到 ~4500 ms,偶爾還出現空回應。
因為幾乎每筆請求都打到一個全新的上游,你會一直停留在冷 prefill 的延遲,並承受是哪個供應商回應就帶來的變異。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)}")
同一個模型出現超過一個上游就代表漂移。命中率遠低於你的提示穩定度,就代表它正在向你課稅。更完整的方法在 Does Your LLM Gateway Lie About Cache?。
該注意什麼
對付漂移的解法是結構性的:把某個模型路由到一致的後端,讓預熱過的快取在下一筆請求時真的可觸及,而不是把每次呼叫負載平衡到一個從未見過你前綴的全新上游。當你評估閘道時,把相同的前綴送 20 次並計算上游數量。一個才是你要的。九個就是課稅。
公允地補充一點:prompt caching 在任何地方都是盡力而為,而在磁碟快取的模型上,即便是單一後端,命中率在長時間閒置後仍會逐漸軟化。消除漂移不會給你一個無限的快取。它只是移除了最大、最浪費的未命中來源——那個你從未同意、也看不見的來源。
結語
「支援 prompt caching」與「你的快取可觸及」是兩個不同的主張。一個把單一模型分散到輪番上陣的上游陣容的閘道,可以如實回報快取支援,卻交付 20% 的命中率、約 4 倍的帳單,以及擺盪達 4.5 倍的首個權杖延遲。該盯的數字不是有沒有宣稱支援快取,而是你實測的命中率,以及你的相同請求觸及了多少個上游。跑一下探測程式,讓資料來定奪。
更廣泛的稽核方法見 Does Your LLM Gateway Lie About Cache?;要了解快取存在的根本原因,見 How KV Cache & TTL Work。
FAQ
這是我這邊的設定錯誤嗎? 不是。它在出廠預設值下就會發生:自動路由加上供應商排序維持在「default(balanced)」。要避免漂移得主動釘選一個上游,而非反過來。
釘選一個上游就能修好嗎? 它能移除跨供應商的漂移,但單一上游往往會跑多個沒有前綴親和性的副本,所以命中仍可能反覆不定。釘選之後要量測,別只是假設。
為什麼那個 GPT 等級的模型沒有漂移? 在這次執行中,閘道剛好把它路由到單一上游。漂移是各模型獨立的,取決於閘道在多少符合條件的上游之間做平衡;它並不一致。
成本差距真的是約 4 倍嗎? 就我們量測的每次呼叫總額而言,一次未命中約為一次命中的 4 倍;就這類模型公布的原始輸入權杖定價而言,命中與未命中的差距更接近 50 倍。無論如何,把預期的命中變成未命中才是昂貴的部分。
我該監控哪個單一指標? 隨時間追蹤每個模型的快取命中率,並搭配每個模型的不同上游數量。如果命中率下降或上游數量上升,你的實質權杖成本就剛剛上去了。