他的代理人一跑就是兩三個小時:每往前一步就要呼叫一次 API,而且每次都得重新帶上同一份很長的作業規則、二十幾個工具的說明書,還有前面所有的對話紀錄。
當你用 GPT-6 打造 AI 代理人能自己規劃步驟、呼叫工具、連續工作很久的 AI 程式,不是問一句答一句的聊天機器人,它每往前做一步,就要透過 API應用程式介面,程式用來呼叫別人服務的標準入口,這裡指你的程式跟 OpenAI 溝通的管道 送出一次請求。OpenAI 在說明裡點出一個關鍵現象:這些請求是一環扣一環的,每一次都常常重複帶著同樣的指示、工具定義用文字寫給 AI 的「工具說明書」,告訴它有哪些工具可用、每個工具的參數要怎麼填,還有前面幾輪的內容。
既然每次的開頭都差不多,何必每次從頭算一遍?這就是 提示詞快取把多次請求共有的開頭內容「算過的結果」記起來,下次遇到一樣的開頭就直接重用,不必重算 的概念。OpenAI 表示,快取能重用計算、縮短回應時間,而且對被快取的輸入 tokenAI 處理文字的最小計量單位,大約是一個字或半個字,計費就是按 token 數量算 提供最高 90% 的折扣。
GPT-6 家族推出了改良版快取:預設就有更高的 命中率送出的請求裡,有多少比例成功找到之前記過的內容而重用;找不到就叫沒命中,英文是 cache miss。符合資格的「共用前綴」(前綴一段內容從第一個字開始算起的開頭部分,必須從頭就一模一樣才算共用),只要在 30 分鐘的視窗內再次被用到,就能拿到快取折扣。請抓住那個關鍵字:是「前綴」。從官方要你「保持工具定義與順序穩定、把新指示加在後面」的建議可以推得,快取認的是「從開頭起完全一樣的那一段」,一旦前面某處變了,後面就對不上了。
除了提高命中率,這次還給開發者幾樣新工具:能看命中率走勢的 Prompt Caching Dashboard(儀表板)、能回答「為什麼沒命中」的診斷工具、能自己決定「哪一段要被快取」的明確快取斷點(explicit cache breakpoints),以及讓你在不弄壞快取的前提下調整 推理力道reasoning effort,讓模型決定「想多深」的設定:越高,代表模型願意花越多力氣思考 的做法。這堂課要教你的,就是把它們組合成一套「省錢又省時間」的工程習慣。
| 你想做的事 | 可能弄壞快取的做法 | 官方建議、能保住快取的做法 |
|---|---|---|
| 這一輪用不到某些工具 | 把那幾個工具的定義從清單裡刪掉 | 定義原封不動;用 allowed_tools 只開放相關工具,或設 tool_choice 為 none 表示這輪不用工具 |
| 想新增或推翻一條指示 | 回頭去改前面的舊指示 | 用新的 developer 訊息追加在最後面,蓋過舊指示 |
| 想調高或調低推理力道 | 直接改請求層級的推理力道設定 | 在後面附加 configuration_update,請求層級的設定保持不變 |
| 想整理工具清單 | 調換順序,或修改參數格式(schema) | 保持工具定義、參數格式、順序都穩定 |
這是 cache_lab.py 的核心:diagnose 函式。它模擬「拿新請求跟上一個請求比開頭」,找出第一個對不上的地方,再用跟官方診斷相似的 JSON 格式回報。注意:這是教學用的簡化模擬器,比對邏輯只為了幫你理解概念,並不是 OpenAI 的真實實作。
def diagnose(prev, curr, minutes_gap): """比對「上一個請求」與「這一個請求」,輸出仿官方診斷的 JSON 結構。 請求 = [(種類, 內容), ...];種類沿用新聞稿提到的 model / settings / tools / input。""" if minutes_gap > WINDOW_MINUTES: return {"type": "cache_miss", "reason": "window_expired"} reusable = 0 for i, (old, new) in enumerate(zip(prev, curr)): if old != new: # 第一個對不上的區塊:從這裡起,後面全部跟著失效 missed = sum(tokens(text) for _, text in curr[i:]) return {"type": "cache_miss", "reason": new[0] + "_changed", "comparison_reusable_tokens": reusable, "cache_missed_tokens": missed} reusable += tokens(new[1]) fresh = sum(tokens(text) for _, text in curr[len(prev):]) return {"type": "cache_hit", "comparison_reusable_tokens": reusable, "new_tokens": fresh}這是一個真實小專案:下載(或複製)檔案,照步驟在你電腦上跑起來。
# cache_lab.py — 提示詞快取「前綴比對」模擬器
# 純 Python 標準庫,不連網、不需要 API 金鑰;只用來體會「改了前面,後面全部對不上」。
import json
WINDOW_MINUTES = 30 # 新聞稿:符合資格的共用前綴,30 分鐘視窗內再用才有折扣
MAX_DISCOUNT = 0.9 # 新聞稿寫「最高 90%」;實際折扣請以官方定價頁為準
def tokens(text):
# 教學用粗估(約 4 個字元算 1 個 token);真實用量請看 API 回傳的數字
return max(1, len(text) // 4)
def diagnose(prev, curr, minutes_gap):
"""比對「上一個請求」與「這一個請求」,輸出仿官方診斷的 JSON 結構。
請求 = [(種類, 內容), ...];種類沿用新聞稿提到的 model / settings / tools / input。"""
if minutes_gap > WINDOW_MINUTES:
return {"type": "cache_miss", "reason": "window_expired"}
reusable = 0
for i, (old, new) in enumerate(zip(prev, curr)):
if old != new: # 第一個對不上的區塊:從這裡起,後面全部跟著失效
missed = sum(tokens(text) for _, text in curr[i:])
return {"type": "cache_miss", "reason": new[0] + "_changed",
"comparison_reusable_tokens": reusable,
"cache_missed_tokens": missed}
reusable += tokens(new[1])
fresh = sum(tokens(text) for _, text in curr[len(prev):])
return {"type": "cache_hit", "comparison_reusable_tokens": reusable,
"new_tokens": fresh}
def relative_cost(curr, result):
"""相對成本:沒命中的 token 算 1,命中快取的 token 依折扣打折。"""
total = sum(tokens(text) for _, text in curr)
reusable = result.get("comparison_reusable_tokens", 0)
return round(total - reusable * MAX_DISCOUNT)
def swap(request, index, block):
# 複製一份再改,避免動到原本的請求
copy = list(request)
copy[index] = block
return copy
TOOLS = "search_code: 搜尋程式碼\nread_file: 讀取檔案\nrun_tests: 執行測試\n" * 40
RULES = "你是重構代理人:小步修改、每步都跑測試、不可刪除公開函式。\n" * 30
# 模擬器假設的排列順序是 model → settings → tools → input(真實系統以官方文件為準),
# 只是為了讓你體會「越前面的東西一變,損失越大」。
turn1 = [("model", "gpt-6"), ("settings", "reasoning_effort=medium"),
("tools", TOOLS), ("input", RULES), ("input", "請重構 utils.py")]
tail = [("input", "已完成 utils.py"), ("input", "接著重構 db.py")]
SCENARIOS = [
("A 正常做法:只在最後追加新問題", turn1 + tail, 5),
("B 這輪用不到 run_tests,所以把它從工具清單刪掉",
swap(turn1, 2, ("tools", TOOLS.replace("run_tests: 執行測試\n", "", 1))) + tail, 5),
("C 回頭去改前面的舊指示",
swap(turn1, 3, ("input", RULES + "請改用繁體中文回覆。\n")) + tail, 5),
("D 在最後追加一則 developer 訊息來蓋過舊指示",
turn1 + tail + [("input", "[developer] 從現在起請改用繁體中文回覆。")], 5),
("E 直接把推理力道從 medium 改成 high(模擬:設定屬於被比對的前面內容)",
swap(turn1, 1, ("settings", "reasoning_effort=high")) + tail, 5),
("F 改用 configuration_update 附加在最後來調高推理力道",
turn1 + tail + [("configuration_update", "reasoning_effort=high")], 5),
("G 一切照 A,但距離上次請求已過 45 分鐘",
turn1 + tail, 45),
]
if __name__ == "__main__":
for name, request, gap in SCENARIOS:
result = diagnose(turn1, request, gap)
print("=" * 50)
print(name)
print(json.dumps({"prompt_cache_diagnostics": result}, ensure_ascii=False, indent=2))
no_cache = sum(tokens(text) for _, text in request)
print("相對成本:{}(完全沒快取會是 {})".format(relative_cost(request, result), no_cache))