上下文視窗和成本
把整份 httpx 文件塞進一次請求,看它花多少錢、找不找得到藏在中間的一句話,再看快取怎麼讓第二次呼叫便宜 30 多倍,最後寫一個按 usage 計費的函式。
- 約 40 分鐘
- 難度:入門
- 實測:2026-09-14 deepseek-flash
程式碼和執行結果保留原樣(簡體中文),與實際執行時完全一致。
現在的模型上下文視窗動輒幾十萬、上百萬詞元。很多人的第一反應是:那還做什麼 RAG,把所有資料一股腦塞進去不就行了?
有時候確實可以。但你得先知道這樣做每次要花多少錢、會慢多少、模型能不能在一大堆文字裡找到你要的那一句。這一課用 httpx 的全部文件做實驗,把這幾個問題都量一量。
上下文視窗是什麼
上下文視窗(context window)是模型一次能處理的詞元總數上限,輸入和輸出加在一起不能超過它。截至 2026 年 9 月,deepseek-flash 和 deepseek-v4-pro 的上下文視窗都是 100 萬詞元,單次輸出最多 38.4 萬詞元。
一次請求裡,下面這些東西都佔用上下文:
┌──────────────────────────────────────────────┐
│ system 消息:规则、人设 │
│ 之前的对话记录(user 和 assistant 轮流) │
│ 你塞进去的资料:文档、检索结果、工具返回的内容 │ 输入
│ 这一轮用户的问题 │
├──────────────────────────────────────────────┤
│ 模型的思考过程(如果开了思考模式) │ 输出
│ 模型的正式回答 │
└──────────────────────────────────────────────┘
做簡單的問答時,這些加起來可能只有幾百個詞元。但做多輪對話、帶資料的問答、讓模型呼叫工具時,它們會迅速增長。上下文用完了,請求會直接報錯。
實驗:把整份文件塞進去
httpx 的官方文件一共 20 多個 Markdown 檔案,合起來 116,933 個字元,放在課程倉庫的 data/httpx-docs/ 目錄下。下面的指令碼做兩個實驗:先把全部文件放進 system 訊息,同一個問題連問兩次;再在文件裡藏一句話,看模型能不能找到。
import os
import pathlib
from openai import OpenAI
client = OpenAI(
api_key=os.environ["LLM_API_KEY"],
base_url=os.environ.get("LLM_BASE_URL", "https://api.deepseek.com"),
)
MODEL = os.environ.get("LLM_MODEL", "deepseek-flash")
NO_THINKING = {"thinking": {"type": "disabled"}}
docs = pathlib.Path("data/httpx-docs")
text = "\n\n".join(p.read_text() for p in sorted(docs.rglob("*.md")) if p.name != "LICENSE.md")
print(f"文档共 {len(text)} 个字符")
# 实验一:同样的请求连发两次
system = "你是 httpx 的答疑助手。下面是 httpx 的全部文档:\n\n" + text
for i in range(2):
r = client.chat.completions.create(
model=MODEL,
messages=[
{"role": "system", "content": system},
{"role": "user", "content": "httpx 默认的超时时间是多少秒?一句话回答。"},
],
extra_body=NO_THINKING,
)
u = r.usage
print(f"第 {i + 1} 次:输入 {u.prompt_tokens},其中缓存命中 {u.prompt_cache_hit_tokens},"
f"输出 {u.completion_tokens} | {r.choices[0].message.content}")
# 实验二:把一句无关的话插到文档的不同位置
needle = "(备注:RepoBot 项目的内部口令是“蓝鲸四十二”。)"
paragraphs = text.split("\n\n")
for fraction in [0, 0.25, 0.5, 0.75, 1.0]:
k = int(len(paragraphs) * fraction)
haystack = "\n\n".join(paragraphs[:k] + [needle] + paragraphs[k:])
r = client.chat.completions.create(
model=MODEL,
messages=[{"role": "user", "content": haystack + "\n\n问题:上面的文字里提到的 RepoBot 内部口令是什么?只回答口令本身。"}],
extra_body=NO_THINKING,
)
print(f"口令放在 {fraction:>4.0%} 处:输入 {r.usage.prompt_tokens} 词元 → {r.choices[0].message.content}")
在課程目錄下執行,我得到的結果(回答措辭你會不一樣,詞元數應該接近):
文档共 116933 个字符
第 1 次:输入 29397,其中缓存命中 0,输出 12 | httpx 默认超时是 5 秒。
第 2 次:输入 29397,其中缓存命中 29184,输出 19 | httpx 默认的超时时间是 5 秒(指网络不活动的超时)。
口令放在 0% 处:输入 29404 词元 → 蓝鲸四十二
口令放在 25% 处:输入 29404 词元 → 蓝鲸四十二
口令放在 50% 处:输入 29404 词元 → 蓝鲸四十二
口令放在 75% 处:输入 29404 词元 → 蓝鲸四十二
口令放在 100% 处:输入 29404 词元 → 蓝鲸四十二
先看實驗一。回答是對的,httpx 文件原文寫著 "The default behavior is to raise a TimeoutException after 5 seconds of network inactivity"。整份文件大約 2.9 萬個詞元,對 100 萬的視窗來說只佔 3%。同樣的問題連問兩次,第二次的 29,397 個輸入詞元裡,有 29,184 個命中了快取。下面會講它有多省錢。
它能找到藏在中間的一句話嗎
實驗二是一個簡單的"大海撈針"測試。有一篇常被引用的論文,Lost in the Middle(Liu 等,2023),發現當時的模型在長輸入裡找資訊時,放在開頭和結尾的資訊找得最好,放在中間的最容易被忽略。
我在文件裡插入一句和 httpx 毫無關係的話,分別放在全文的開頭、25%、50%、75% 和結尾處,結果五個位置全部答對。在三萬詞元這個長度、找一句話這種簡單任務上,deepseek-flash 沒有表現出"中間遺忘"。那篇論文測的是 2023 年的模型,這幾年長上下文能力進步很大。
但別就此認為長上下文沒有問題。"找一句明顯的話"是最簡單的長上下文任務。需要把散落在文件各處的幾條資訊綜合起來、需要在幾十個相似的段落裡分辨細微差別時,模型還是更容易出錯,RULER(Hsieh 等,2024)這類評測專門測這些更難的情況。你的任務屬於哪一種,最好用自己的資料測一測,練習 3 就是這樣一個測試。
算錢:為什麼不總是全塞進去
能找到,不代表划算。寫一個根據 usage 算錢的函式:
PRICES = {
# 模型: (输入-缓存命中, 输入-缓存未命中, 输出),美元 / 每一百万词元,高峰价
"deepseek-flash": (0.006, 0.30, 1.20),
"deepseek-v4-pro": (0.044, 1.32, 3.96),
}
def cost_usd(usage, model, off_peak=False):
hit_price, miss_price, out_price = PRICES[model]
# DeepSeek 的 usage 里有 prompt_cache_hit_tokens,别家不一定有,没有就当全部未命中
hit = getattr(usage, "prompt_cache_hit_tokens", 0) or 0
miss = usage.prompt_tokens - hit
total = (hit * hit_price + miss * miss_price + usage.completion_tokens * out_price) / 1_000_000
return total / 2 if off_peak else total # 低谷时段五折
價格是截至 2026 年 9 月的 DeepSeek 官方價格,用之前請到價格頁面核對。把實驗一的兩組數字代進去,再估算一下每天 1000 次、一個月的費用(完整程式碼在 code/01-llm-basics/cost.py):
第一次:0.008833 美元
第二次:0.000262 美元
每月 3 万次,全部命中缓存:7.85 美元;全部不命中:265.00 美元
第 00 模組第 3 課那次簡單問答花了 0.00019 美元,這次把文件全塞進去的第一次呼叫花了 0.0088 美元,貴了 46 倍,而使用者看到的回答只有一句話。按每天 1000 次、一個月算,沒有快取要 265 美元。
另外還有速度。輸入越長,模型讀完它的時間越長,使用者等待第一個字出現的時間也越長。三萬詞元的差別還不明顯,文件一旦到了幾十萬詞元,就能明顯感覺到了。
所以,全部塞進去適合這些情況:資料本身不大,呼叫次數不多,或者能穩定命中快取。資料很大、呼叫頻繁、每次只需要其中一小部分時,先把相關的部分找出來再交給模型更划算。這就是 04 模組 RAG 要做的事。
快取:讓重複的開頭幾乎免費
第二次呼叫便宜了 30 多倍(0.000262 對 0.008833),靠的是 DeepSeek 的上下文快取:如果這次請求的開頭部分,和之前某次請求的開頭一模一樣,伺服器就可以複用之前算好的結果,這部分按"快取命中"的價格收費。deepseek-flash 快取命中的價格是未命中的五十分之一(0.006 對 0.30)。
根據 DeepSeek 的快取文件(截至 2026 年 9 月):
- 快取預設開啟,不需要改程式碼。
- 快取按"字首單元"儲存。伺服器會在每次請求的邊界、多個請求共有的開頭部分,以及長輸入中每隔固定的詞元數,建立這樣的單元。只有完整匹配了一個單元,這部分才算命中。
- 不再使用的快取,一般幾個小時到幾天內會被清除。
- 命中和沒命中的詞元數,分別在
usage的prompt_cache_hit_tokens和prompt_cache_miss_tokens裡。
實驗裡第二次還有 213 個詞元沒命中,就是因為快取是按單元匹配的:使用者問題所在的最後一段,以及不足以構成一個完整單元的部分,都按未命中計價。
這些規則決定了你應該怎麼安排訊息的順序:
- 不變的放前面:system 訊息、固定的資料、示例放在最前面。
- 變化的放後面:使用者的問題、每次不一樣的內容放在最後。
如果你把使用者的問題放在文件前面,每次問題不同,開頭就不同,後面的文件也就都命中不了。
輸出比輸入貴
回到價格表:deepseek-flash 的輸出是 1.20 美元每百萬詞元,未命中快取的輸入是 0.30,命中快取的輸入是 0.006。輸出比輸入貴 4 倍,比命中快取的輸入貴 200 倍。
幾個直接的推論:
- 讓模型別說廢話是在省錢。在提示詞裡要求"一句話回答"、"只輸出 JSON",效果立竿見影。
- 思考模式下,思考過程按輸出計費。簡單任務關掉思考,能省下大部分輸出費用。
- 長篇資料放在輸入裡,如果能命中快取,其實並不貴。真正花錢的,往往是沒命中快取的長輸入,以及冗長的輸出。
常見問題
報錯說上下文超長:先檢查是不是對話歷史越積越多。03 模組第 1 課會講怎麼截斷和壓縮歷史。
快取命中數一直是 0:檢查每次請求的開頭是不是真的完全一樣。常見的問題是在 system 訊息裡放了當前時間、隨機的 ID,或者每次調整了資料的順序。
別的服務商有快取嗎:大多數都有,但計價方式和規則不一樣,有的需要手動標記哪部分要快取。用之前查對方的文件。
練習
- 用
cost_usd算一算:同樣每月 3 萬次、不命中快取,全部放在低谷時段呼叫(傳off_peak=True),能省多少錢? - 把實驗一里的 system 訊息和 user 訊息順序換一下(問題放前面,文件放後面),連問兩次,看看第二次的快取命中數有什麼變化。
- 改一改實驗二:插入三句分散在不同位置的話,比如"口令的第一部分是藍鯨"、"第二部分是四十"、"第三部分是二",然後問完整的口令。關掉思考和開著思考各測幾次,結果和只找一句話時一樣嗎?
自測
1. 上下文視窗是 100 萬詞元,是不是意味著我可以輸入 100 萬詞元?
不是。上下文視窗是輸入和輸出加起來的上限。輸入用掉 99 萬,輸出最多就只剩 1 萬,而且開著思考時思考過程也算輸出。另外,能放進去不代表划算,長輸入又慢又貴。
2. 你的應用每次把一份固定的產品手冊和使用者問題一起發給模型。怎麼安排訊息順序最省錢?
把產品手冊放在最前面(比如放進 system 訊息),使用者問題放在最後。這樣每次請求的開頭都一樣,能命中快取,手冊部分按快取命中的價格收費,便宜幾十倍。如果問題放在前面,開頭每次都不同,快取永遠命中不了。
3. 你做了一次測試,模型在長文件裡準確找到了你藏的一句話。能不能說明這個模型的長上下文能力沒問題?
不能。"找一句話"是最簡單的長上下文任務。需要綜合多處資訊、在大量相似內容裡分辨細節時,模型更容易出錯。要判斷能不能滿足你的需求,得用接近真實任務的測試來驗證。