模組 06 · 第 4 課

省錢和提速

五個小實驗:把固定資料放在前面省了 64% 的錢,要求簡短讓輸出少了 92%,簡單問題關掉思考,自己做結果快取,併發把 10 個請求從 9.5 秒壓到 2.3 秒。

  • 約 35 分鐘
  • 難度:進階
  • 實測:2026-09-14 deepseek-flash

程式碼和執行結果保留原樣(簡體中文),與實際執行時完全一致。

一個 AI 應用上線後,每天被呼叫成千上萬次,每次省下一點點,加起來就很可觀;每次快一點點,使用者的感受就會好很多。

這一課做五個小實驗,每一個都是可以直接用到專案裡的辦法,每一個都有測出來的數字。實驗都用 deepseek-flash,價格按 2026 年 9 月的高峰價計算。完整程式碼在 code/06-production/cost_latency.py

1. 固定的資料放在前面

第 01 模組第 4 課講過 DeepSeek 的上下文快取:請求的開頭和之前某次請求一樣,這部分就按快取命中的價格收費,是未命中的五十分之一。

實驗:三個不同的問題,每次都附上同一份 httpx 文件(大約 1.5 萬詞元)。一種寫法把問題放在前面、文件放在後面;另一種把文件放進 system 訊息、問題放在後面。

for label, build in [
    ("问题在前", lambda q: [{"role": "user", "content": f"问题:{q}\n\n参考文档:\n{DOCS}\n\n一句话回答。"}]),
    ("文档在前", lambda q: [{"role": "system", "content": f"参考文档:\n{DOCS}"}, {"role": "user", "content": q + "一句话回答。"}]),
]:
== 1. 缓存:固定的资料放在前面,还是问题放在前面
  问题在前:三次的缓存命中 ['0/15168', '0/15169', '0/15167'],共 0.01379 美元
  文档在前:三次的缓存命中 ['0/15167', '14976/15168', '14976/15166'],共 0.00500 美元

問題在前,三次一次都沒命中:每個問題不同,請求的開頭就不同,後面的文件再一樣也沒用。文件在前,第一次沒有快取,後兩次各命中了 14976 個詞元。總花費從 0.01379 美元降到 0.00500 美元,省了 64%。

這裡只問了三次,第一次的"冷啟動"佔了大頭。問的次數越多,文件在前的寫法越接近"每次只為問題本身付費"。

只是調換了一下順序,一行程式碼都不用多寫。檢查一下你的提示詞:system 訊息裡有沒有放當前時間、使用者名稱、隨機編號這類每次都會變的東西?把它們挪到最後。

2. 讓模型少說點

輸出的價格是輸入的 4 倍,而且生成速度直接決定使用者要等多久。

q = "httpx 和 requests 有什么区别?"
for label, prompt in [("不做要求", q), ("要求简短", q + "用三句话以内回答。")]:
== 2. 输出长度:不做要求 vs 要求简短
  不做要求:输出 650 词元,3.3 秒,0.00078 美元
  要求简短:输出 52 词元,0.8 秒,0.00007 美元

只加了一句"用三句話以內回答",輸出從 650 個詞元降到 52 個,費用降到十分之一,等待時間從 3.3 秒降到 0.8 秒。

模型預設傾向於寫得很全面,這對一些場景是好事,對另一些場景是浪費。想清楚你的使用者真正需要多少資訊,在提示詞裡寫明。還可以設定 max_tokens 作為硬性上限,防止個別情況下寫個沒完,但要記住它會直接截斷回答(第 00 模組第 3 課),所以主要還是靠提示詞。

3. 簡單問題別開思考

== 3. 简单问题开不开思考
  不思考:输出 26 词元,0.7 秒,0.00004 美元 | '用 `params` 参数传字典,如 `httpx.get(url, param'
  思考:输出 146 词元,1.9 秒,0.00019 美元 | '用 `params` 参数传字典或元组列表,如 `httpx.get(url, '

"怎麼傳查詢參數"這種問題,兩種模式的回答幾乎一樣,開思考多花了將近 5 倍的錢、多等了 1.2 秒。第 02 模組第 3 課講過,思考適合多步推理的問題。一個應用裡的問題往往有難有易,可以按型別分開處理:簡單的查詢關掉思考,複雜的分析才打開。怎麼判斷難易,可以用下一課的分類器順便做。

4. 相同的問題,直接返回上次的答案

很多應用裡,使用者會反覆問同樣的問題。與其每次都呼叫模型,不如把答案存起來:

cache = {}


def cached_ask(question):
    key = hashlib.sha256(" ".join(question.lower().split()).encode()).hexdigest()  # 忽略大小写和多余空格
    if key in cache:
        return cache[key], 0.0
    text, u, _ = ask([{"role": "user", "content": question}])
    cache[key] = text
    return text, cost(u)

問 4 次,寫法略有不同:"httpx 怎麼設定代理?"、同樣一句再問一次、"HTTPX 怎麼設定代理?"(大寫,多了一個空格)、"httpx 怎麼設定代理"(沒有問號)。

== 4. 自己做结果缓存:同样的问题直接返回上次的答案
  问了 4 次(写法略有不同),实际调用 2 次模型,共 0.00239 美元

前三次被識別為同一個問題,只調用了一次模型。最後一次因為少了個問號,被當成了新問題。這說明規範化做得還不夠:標點也應該去掉。練習 2 會讓你改進它。

更進一步,還可以用第 01 模組第 5 課的向量嵌入做"語義快取":意思相近的問題("怎麼配代理"和"如何設定代理伺服器")也命中同一個快取。但要小心:意思相近不等於答案相同,"httpx 怎麼設定超時"和"requests 怎麼設定超時"的向量就非常接近。

什麼時候不能用結果快取:

  • 答案會變。依賴即時資料、依賴使用者個人資訊的回答,不能跨使用者、跨時間共享。
  • 多輪對話。"那非同步呢"這種問題,答案取決於上文,只看這一句不能快取。
  • 需要多樣性。寫文案、起名字這種任務,使用者本來就想要不一樣的結果。

快取要設過期時間。文件更新了,舊的答案就可能過時。

5. 併發

== 5. 串行 vs 并发
  10 个请求:一个一个来 9.5 秒,5 个并发 2.3 秒

10 個互相獨立的請求,一個接一個發,要 9.5 秒;5 個同時發,只要 2.3 秒。模型呼叫的時間大部分花在等待伺服器上,這段時間你的程式什麼都沒做,完全可以同時發出別的請求。

第 03 模組第 4 課講過怎麼用執行緒池加訊號量控制併發數。併發不是越多越好:服務商有併發限制,超過了會返回 429;併發太多,你自己的機器和網路也可能扛不住。

其他辦法

  • 換更小、更便宜的模型。第 01 模組第 6 課的方法:在你的評估集上比較,便宜的模型能達標就用便宜的。也可以按問題難度分流:簡單的交給小模型,難的才交給大模型。
  • 避開高峰時段。DeepSeek 低谷時段的價格是高峰時段的一半(截至 2026 年 9 月,北京時間工作日 9:00~12:00 和 14:00~18:00 是高峰)。不著急的批次任務,比如夜裡跑評估、處理積壓的資料,放到低谷時段。
  • 減少不必要的上下文。RAG 取 5 塊還是 3 塊?對話歷史保留 20 條還是 10 條?智慧體的工具返回能不能再短一點?每一項都用評估集驗證,確認效果不降再改。
  • 流式輸出。它不省錢,但能讓使用者感覺快得多(第 03 模組第 2 課)。

先量,再改

這一課的每一個最佳化,都應該在你自己的應用裡先量一量:現在的錢花在哪裡、時間花在哪裡?上一課的日誌正好能回答這個問題。按費用排序,找出最貴的那一類請求,從它開始最佳化。最佳化之後,用評估集確認效果沒有變差。省了錢卻答錯了,不划算。

練習

  1. 改進 cached_ask 的規範化:去掉所有標點和空白,統一全形半形。再跑一次實驗,4 次提問是不是隻呼叫一次模型了?
  2. 用第 3 課的日誌(traces.jsonl),算一算智慧體回答一個問題時,輸入詞元裡有多大比例命中了快取。想一想怎麼調整訊息的順序能讓命中率更高。
  3. 在第 2 個實驗裡,把"用三句話以內回答"換成 max_tokens=60,不改提示詞。回答會是什麼樣子?哪種辦法更好?

自測

1. 同樣的文件和問題,為什麼"文件在前"比"問題在前"便宜那麼多?

DeepSeek 的快取只認請求開頭相同的部分。問題在前時,每次問題不同,開頭就不同,後面的文件無法命中快取,每次都按全價收費。文件在前時,開頭的文件每次都一樣,第一次之後都能命中快取,按五十分之一的價格收費。

2. 什麼情況下不能用結果快取?

答案會隨時間變化(依賴即時資料、文件會更新)、答案依賴使用者個人資訊、問題依賴對話上下文(比如"那非同步呢")、以及使用者本來就希望每次得到不同結果的任務(寫文案、起名字)。另外快取要設過期時間,避免返回過時的答案。

3. 併發請求為什麼能大幅縮短總耗時?併發數是不是越大越好?

一次模型呼叫的大部分時間都在等伺服器返回,程式在等待時是空閒的,同時發出多個請求就能把這些等待重疊起來。併發數不是越大越好:超過服務商的併發限制會被限流(429),自己的機器和網路也有承受上限,所以要用訊號量之類的辦法控制併發數。