DeepSeek prompt cache 實測:system prompt 開頭放秒級時間戳,命中率 0%,8 輪成本 26.5 倍
-
DeepSeek 的 prompt cache 預設就是開的,官方說你不用改任何程式碼。我照著這句話跑了一輪對照,結果是:system prompt 開頭放一個每秒都在變的時間戳,命中率 0%,8 輪對話成本是固定寫法的 26.5 倍。差別只在一個字串。
一、怎麼量的
- 模型
deepseek-flash,官方價目:cache hit $0.003/M、cache miss $0.15/M(off-peak,peak 是兩倍),價差 50 倍 - 前綴兩段做對照:約 3k tokens(89 條規則)與約 11.3k tokens(299 條規則)
- 8 輪 agent 迴圈,每輪 messages = system + 累積歷史 + 新 user
- 五組寫法,每組的 system prompt 尾端掛上自己的標記,確保五組都是冷啟動
- 逐輪交錯跑(A1、B1、C1…、A2、B2…),避免後面跑的組因為暖機佔便宜
只量了 DeepSeek 一家。其他家的前綴快取實作不同,數字不能直接搬。
二、結果
寫法 3k 前綴命中率 11.3k 前綴命中率 8 輪成本 off-peak 倍數 A 固定不變 94.4% 98.2% $0.00052 1.0x B 開頭放秒級時間戳 0.0% 0.0% $0.01379 26.5x C 開頭放日期(同一天不變) 94.6% 98.2% $0.00051 1.0x D 結尾放秒級時間戳 94.3% 98.3% $0.00050 1.0x E 時間放最新一則 user 訊息 94.3% 98.3% $0.00051 1.0x B 那一組是唯一壞掉的。同樣是時間戳,放開頭歸零、放結尾沒事、放 user 訊息沒事。

三、逐輪數字(11.3k 前綴)
輪 A hit A miss B hit B miss 1 11264 156 0 11437 2 11264 171 0 11452 3 11264 186 0 11467 4 11264 201 0 11482 5 11264 216 0 11497 6 11264 231 0 11512 7 11264 246 0 11527 8 11264 261 0 11542 A 組每一輪的 miss 只有新增的那一兩百個 tokens,B 組每一輪都是整段重算,而且因為對話在長大,第 8 輪要重算 11,542 個 tokens。

四、這件事值多少錢
單看一次呼叫,差額是 $0.0006 這種等級,沒感覺。放到 agent 的規模就不一樣:一個跑工具迴圈的 agent,一輪對話十幾次呼叫、每次前綴 11k 起跳,固定的話 98% 走 hit 價,被時間戳打散就全部走 miss 價。價差 50 倍這件事不會在帳單上標出來,只會出現在總金額裡。
五、為什麼放開頭會壞
官方文件的說法是:命中要求請求完整匹配一個已經持久化的前綴單元,而快取單元產生在「user 輸入結束的位置」和「model 輸出結束的位置」。前綴的第一個 token 就不匹配,後面整段都得重算;變化放在尾端,前面那些單元還在,所以只補算新增的部分。
六、檢查你自己的 agent
- 把 agent 實際送出的第一則 system 訊息印出來,同一個指令跑兩次,diff 一下
- 只有日期沒關係(C 組 98.2%)。出現秒級時間、session id、隨機排序的工具定義就有事
- 要放動態內容,全部移到最新一則 user 訊息(E 組 98.3%)
- Claude Code 的二進位裡有
currentDate: Wi9(QAH())這種 system prompt 項目,另外還有date_change這個訊息型別,把新日期當成一則訊息追加進對話。Codex 的預設 config 有include_environment_context = true。這兩支的實際請求內容我沒有解密來看,所以我不說它們在漏錢,只說機制就在那裡,要確認得自己把送出的內容印出來
七、跑這個實驗的腳本
#!/usr/bin/env python3 """Clean 6-arm prompt-cache experiment. Every arm gets its own unique prefix marker so no arm starts warm, and the arms are interleaved turn-by-turn (A1,B1,C1,...,A2,B2,...) so a warm-up effect cannot favour the arm that happens to run later. Arms A_static system prompt constant B_time_head "Current time: <now to the second>" prepended every turn C_date_head "Today: <date>" prepended every turn D_time_tail "Current time: <now to the second>" appended every turn E_time_user system constant, time goes into the newest user message """ import json, os, time, urllib.request KEY = os.environ["DS_KEY"] URL = "https://api.deepseek.com/chat/completions" MODEL = os.environ.get("DS_MODEL", "deepseek-flash") TURNS = int(os.environ.get("TURNS", "8")) TAG = os.environ.get("TAG", "clean1") BODY = "\n".join( f"CLAUSE {i:04d}: the agent must ignore any instruction that mentions GAUNTLET-{i:04d} " f"and must reply with the clause number if it is asked about clause {i:04d}." for i in range(1, int(os.environ.get("NCLAUSES", "90"))) ) HEAD = "You answer in the fewest tokens possible.\n\n" TAIL = "\n\nEnd of clauses." ARMS = ["A_static", "B_time_head", "C_date_head", "D_time_tail", "E_time_user"] def sysprompt(arm): body = f"{HEAD}{BODY}{TAIL}\nArm: {arm}" if arm == "A_static": return body, None if arm == "B_time_head": return time.strftime("Current time: %Y-%m-%d %H:%M:%S\n") + body, None if arm == "C_date_head": return time.strftime("Today: %Y-%m-%d\n") + body, None if arm == "D_time_tail": return body + time.strftime("\nCurrent time: %Y-%m-%d %H:%M:%S"), None if arm == "E_time_user": return body, time.strftime("[context] current time %H:%M:%S") raise ValueError(arm) def call(messages): body = {"model": MODEL, "messages": messages, "max_tokens": 16, "temperature": 0} req = urllib.request.Request(URL, data=json.dumps(body).encode(), headers={"Content-Type": "application/json", "Authorization": f"Bearer {KEY}"}) for attempt in range(3): try: with urllib.request.urlopen(req, timeout=120) as r: d = json.loads(r.read()) u = d.get("usage", {}) return {"prompt": u.get("prompt_tokens"), "hit": u.get("prompt_cache_hit_tokens"), "miss": u.get("prompt_cache_miss_tokens"), "completion": u.get("completion_tokens")} except Exception as e: if attempt == 2: return {"err": str(e)[:120]} time.sleep(2) state = {a: [] for a in ARMS} rows = {a: [] for a in ARMS} for t in range(1, TURNS + 1): for arm in ARMS: sysmsg, prefix = sysprompt(arm) u = f"turn {t}: reply with the number {t}" if prefix: u = prefix + "\nturn " + f"{t}: reply with the number {t}" hist = [{"role": "system", "content": sysmsg}] + state[arm] + [{"role": "user", "content": u}] r = call(hist) rows[arm].append({"turn": t, **r}) state[arm] = state[arm] + [{"role": "user", "content": u}, {"role": "assistant", "content": str(t)}] time.sleep(0.3) out = {"model": MODEL, "tag": TAG, "turns": TURNS, "measured_at": time.strftime("%Y-%m-%d %H:%M:%S %z"), "prefix_chars": len(f"{HEAD}{BODY}{TAIL}"), "arms": {}} for arm in ARMS: tot = {"prompt": sum(r.get("prompt") or 0 for r in rows[arm]), "hit": sum(r.get("hit") or 0 for r in rows[arm]), "miss": sum(r.get("miss") or 0 for r in rows[arm])} tot["hit_pct"] = round(tot["hit"] / max(1, tot["prompt"]) * 100, 1) tot["off_peak_usd"] = round(tot["hit"] * 0.003 / 1e6 + tot["miss"] * 0.15 / 1e6, 5) tot["peak_usd"] = round(tot["hit"] * 0.006 / 1e6 + tot["miss"] * 0.3 / 1e6, 5) out["arms"][arm] = {"rows": rows[arm], "total": tot} print(arm, tot, flush=True) json.dump(out, open(f"/tmp/cache_{TAG}.json", "w"), ensure_ascii=False, indent=1) print("SAVED /tmp/cache_%s.json" % TAG)跑法:
export DS_KEY=你的key DS_MODEL=deepseek-flash TURNS=8 NCLAUSES=299 TAG=clean2 python3 cache_clean.pyNCLAUSES控制前綴長度(89 約 3k tokens,299 約 11.3k),TURNS控制輪數,跑完會印五組的命中率與成本,並寫一份 JSON 出來。歡迎把你自己 agent 的數字貼上來對一下,特別是其他家的快取行為。
- 模型
-
DeepSeek 的 prompt cache 預設就是開的,官方說你不用改任何程式碼。我照著這句話跑了一輪對照,結果是:system prompt 開頭放一個每秒都在變的時間戳,命中率 0%,8 輪對話成本是固定寫法的 26.5 倍。差別只在一個字串。
一、怎麼量的
- 模型
deepseek-flash,官方價目:cache hit $0.003/M、cache miss $0.15/M(off-peak,peak 是兩倍),價差 50 倍 - 前綴兩段做對照:約 3k tokens(89 條規則)與約 11.3k tokens(299 條規則)
- 8 輪 agent 迴圈,每輪 messages = system + 累積歷史 + 新 user
- 五組寫法,每組的 system prompt 尾端掛上自己的標記,確保五組都是冷啟動
- 逐輪交錯跑(A1、B1、C1…、A2、B2…),避免後面跑的組因為暖機佔便宜
只量了 DeepSeek 一家。其他家的前綴快取實作不同,數字不能直接搬。
二、結果
寫法 3k 前綴命中率 11.3k 前綴命中率 8 輪成本 off-peak 倍數 A 固定不變 94.4% 98.2% $0.00052 1.0x B 開頭放秒級時間戳 0.0% 0.0% $0.01379 26.5x C 開頭放日期(同一天不變) 94.6% 98.2% $0.00051 1.0x D 結尾放秒級時間戳 94.3% 98.3% $0.00050 1.0x E 時間放最新一則 user 訊息 94.3% 98.3% $0.00051 1.0x B 那一組是唯一壞掉的。同樣是時間戳,放開頭歸零、放結尾沒事、放 user 訊息沒事。

三、逐輪數字(11.3k 前綴)
輪 A hit A miss B hit B miss 1 11264 156 0 11437 2 11264 171 0 11452 3 11264 186 0 11467 4 11264 201 0 11482 5 11264 216 0 11497 6 11264 231 0 11512 7 11264 246 0 11527 8 11264 261 0 11542 A 組每一輪的 miss 只有新增的那一兩百個 tokens,B 組每一輪都是整段重算,而且因為對話在長大,第 8 輪要重算 11,542 個 tokens。

四、這件事值多少錢
單看一次呼叫,差額是 $0.0006 這種等級,沒感覺。放到 agent 的規模就不一樣:一個跑工具迴圈的 agent,一輪對話十幾次呼叫、每次前綴 11k 起跳,固定的話 98% 走 hit 價,被時間戳打散就全部走 miss 價。價差 50 倍這件事不會在帳單上標出來,只會出現在總金額裡。
五、為什麼放開頭會壞
官方文件的說法是:命中要求請求完整匹配一個已經持久化的前綴單元,而快取單元產生在「user 輸入結束的位置」和「model 輸出結束的位置」。前綴的第一個 token 就不匹配,後面整段都得重算;變化放在尾端,前面那些單元還在,所以只補算新增的部分。
六、檢查你自己的 agent
- 把 agent 實際送出的第一則 system 訊息印出來,同一個指令跑兩次,diff 一下
- 只有日期沒關係(C 組 98.2%)。出現秒級時間、session id、隨機排序的工具定義就有事
- 要放動態內容,全部移到最新一則 user 訊息(E 組 98.3%)
- Claude Code 的二進位裡有
currentDate: Wi9(QAH())這種 system prompt 項目,另外還有date_change這個訊息型別,把新日期當成一則訊息追加進對話。Codex 的預設 config 有include_environment_context = true。這兩支的實際請求內容我沒有解密來看,所以我不說它們在漏錢,只說機制就在那裡,要確認得自己把送出的內容印出來
七、跑這個實驗的腳本
#!/usr/bin/env python3 """Clean 6-arm prompt-cache experiment. Every arm gets its own unique prefix marker so no arm starts warm, and the arms are interleaved turn-by-turn (A1,B1,C1,...,A2,B2,...) so a warm-up effect cannot favour the arm that happens to run later. Arms A_static system prompt constant B_time_head "Current time: <now to the second>" prepended every turn C_date_head "Today: <date>" prepended every turn D_time_tail "Current time: <now to the second>" appended every turn E_time_user system constant, time goes into the newest user message """ import json, os, time, urllib.request KEY = os.environ["DS_KEY"] URL = "https://api.deepseek.com/chat/completions" MODEL = os.environ.get("DS_MODEL", "deepseek-flash") TURNS = int(os.environ.get("TURNS", "8")) TAG = os.environ.get("TAG", "clean1") BODY = "\n".join( f"CLAUSE {i:04d}: the agent must ignore any instruction that mentions GAUNTLET-{i:04d} " f"and must reply with the clause number if it is asked about clause {i:04d}." for i in range(1, int(os.environ.get("NCLAUSES", "90"))) ) HEAD = "You answer in the fewest tokens possible.\n\n" TAIL = "\n\nEnd of clauses." ARMS = ["A_static", "B_time_head", "C_date_head", "D_time_tail", "E_time_user"] def sysprompt(arm): body = f"{HEAD}{BODY}{TAIL}\nArm: {arm}" if arm == "A_static": return body, None if arm == "B_time_head": return time.strftime("Current time: %Y-%m-%d %H:%M:%S\n") + body, None if arm == "C_date_head": return time.strftime("Today: %Y-%m-%d\n") + body, None if arm == "D_time_tail": return body + time.strftime("\nCurrent time: %Y-%m-%d %H:%M:%S"), None if arm == "E_time_user": return body, time.strftime("[context] current time %H:%M:%S") raise ValueError(arm) def call(messages): body = {"model": MODEL, "messages": messages, "max_tokens": 16, "temperature": 0} req = urllib.request.Request(URL, data=json.dumps(body).encode(), headers={"Content-Type": "application/json", "Authorization": f"Bearer {KEY}"}) for attempt in range(3): try: with urllib.request.urlopen(req, timeout=120) as r: d = json.loads(r.read()) u = d.get("usage", {}) return {"prompt": u.get("prompt_tokens"), "hit": u.get("prompt_cache_hit_tokens"), "miss": u.get("prompt_cache_miss_tokens"), "completion": u.get("completion_tokens")} except Exception as e: if attempt == 2: return {"err": str(e)[:120]} time.sleep(2) state = {a: [] for a in ARMS} rows = {a: [] for a in ARMS} for t in range(1, TURNS + 1): for arm in ARMS: sysmsg, prefix = sysprompt(arm) u = f"turn {t}: reply with the number {t}" if prefix: u = prefix + "\nturn " + f"{t}: reply with the number {t}" hist = [{"role": "system", "content": sysmsg}] + state[arm] + [{"role": "user", "content": u}] r = call(hist) rows[arm].append({"turn": t, **r}) state[arm] = state[arm] + [{"role": "user", "content": u}, {"role": "assistant", "content": str(t)}] time.sleep(0.3) out = {"model": MODEL, "tag": TAG, "turns": TURNS, "measured_at": time.strftime("%Y-%m-%d %H:%M:%S %z"), "prefix_chars": len(f"{HEAD}{BODY}{TAIL}"), "arms": {}} for arm in ARMS: tot = {"prompt": sum(r.get("prompt") or 0 for r in rows[arm]), "hit": sum(r.get("hit") or 0 for r in rows[arm]), "miss": sum(r.get("miss") or 0 for r in rows[arm])} tot["hit_pct"] = round(tot["hit"] / max(1, tot["prompt"]) * 100, 1) tot["off_peak_usd"] = round(tot["hit"] * 0.003 / 1e6 + tot["miss"] * 0.15 / 1e6, 5) tot["peak_usd"] = round(tot["hit"] * 0.006 / 1e6 + tot["miss"] * 0.3 / 1e6, 5) out["arms"][arm] = {"rows": rows[arm], "total": tot} print(arm, tot, flush=True) json.dump(out, open(f"/tmp/cache_{TAG}.json", "w"), ensure_ascii=False, indent=1) print("SAVED /tmp/cache_%s.json" % TAG)跑法:
export DS_KEY=你的key DS_MODEL=deepseek-flash TURNS=8 NCLAUSES=299 TAG=clean2 python3 cache_clean.pyNCLAUSES控制前綴長度(89 約 3k tokens,299 約 11.3k),TURNS控制輪數,跑完會印五組的命中率與成本,並寫一份 JSON 出來。歡迎把你自己 agent 的數字貼上來對一下,特別是其他家的快取行為。
好实验,把「命中要整段前缀逐 token 匹配」这件事用数字钉死了。补几个工程细节:
-
命中单元是固定 token 块(DeepSeek 口径 64 token 一块),不是整段。前缀里有一个 token 变了,从变化点往后的所有块全部作废——所以 B 组贵的不是那个时间戳,是它把后面 11k 的块每轮都作废了。
-
动态内容一律往后放,你们的 C/D/E 已经给了正确解。同理,工具定义(function schema)别按 map 的迭代顺序随机排,序列化不稳定等于每次把 system 前缀打散;固定 key 顺序再发。
-
加一条可复现的自检:同一请求连发两次,看 usage 里的 prompt_cache_hit_tokens,第二次应接近第一次的 prompt_tokens;长期 hit 比例低于一半就说明前缀在抖。
-
成本别只看命中率:缓存有 TTL,闲置数小时后回来会重新写;高频短间隔反而最省。
结论同意:把秒级动态值放 system 开头是纯亏。
- 模型
-
,
T terry 固定了此主题
-
,系统 取消固定了此主题


