モジュール 01 · 第 4 回

コンテキストウィンドウとコスト

httpx のドキュメント全体を 1 回のリクエストに詰め込み、いくらかかるのか、途中に隠した一文を見つけられるのかを確かめます。さらにキャッシュで 2 回目の呼び出しが 30 倍以上安くなる仕組みを見て、最後に usage から料金を計算する関数を書きます。

  • 約 40 分
  • 難易度:入門
  • 検証:2026-09-14 deepseek-flash

コードと実行結果は実際に動かしたときのまま載せているため、コメントと出力は中国語です。

今のモデルのコンテキストウィンドウは、数十万から 100 万トークンを超えるのが当たり前になりました。多くの人の最初の反応はこうです。それなら RAG なんていらない、資料を全部まとめて詰め込めばいいのでは?

実際それで済むこともあります。ただしその前に、そうすると毎回いくらかかるのか、どれだけ遅くなるのか、モデルが大量の文章の中から目当ての一文を見つけられるのかを知っておく必要があります。この課では httpx のドキュメント全体を使って、これらを一つずつ測ってみます。

コンテキストウィンドウとは

コンテキストウィンドウ(context window)は、モデルが一度に処理できるトークン数の上限で、入力と出力を合わせてこれを超えることはできません。2026 年 9 月時点で、deepseek-flashdeepseek-v4-pro のコンテキストウィンドウはどちらも 100 万トークン、1 回の出力は最大 38.4 万トークンです。

1 回のリクエストでは、次のものがすべてコンテキストを占めます。

┌──────────────────────────────────────────────┐
│ system 消息:规则、人设                         │
│ 之前的对话记录(user 和 assistant 轮流)          │
│ 你塞进去的资料:文档、检索结果、工具返回的内容      │  输入
│ 这一轮用户的问题                                │
├──────────────────────────────────────────────┤
│ 模型的思考过程(如果开了思考模式)                  │  输出
│ 模型的正式回答                                  │
└──────────────────────────────────────────────┘

簡単な質疑応答なら、これらを合わせても数百トークン程度かもしれません。しかしマルチターンの会話、資料つきの質疑応答、モデルにツールを呼ばせる場合には、急速に増えていきます。コンテキストを使い切ると、リクエストはそのままエラーになります。

実験:ドキュメント全体を詰め込む

httpx の公式ドキュメントは 20 余りの Markdown ファイルで、合計 116,933 文字あり、コースのリポジトリの data/httpx-docs/ ディレクトリに置いてあります。次のスクリプトは二つの実験をします。まずドキュメント全体を system メッセージに入れて、同じ質問を 2 回続けて聞く。次にドキュメントの中に一文を隠し、モデルが見つけられるかを試す。

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% にすぎません。同じ質問を 2 回続けて聞くと、2 回目の入力 29,397 トークンのうち 29,184 トークンがキャッシュにヒットしました。どれだけ節約になるかは後で説明します。

途中に隠した一文を見つけられるか

実験二は簡単な「干し草の中の針」テストです。よく引用される論文に Lost in the Middle(Liu ら、2023)があり、当時のモデルは長い入力の中で情報を探すとき、冒頭と末尾の情報は最もよく見つけるが、途中の情報は最も見落としやすいことを示しました。

httpx とはまったく関係のない一文をドキュメントに挿入し、全体の冒頭、25%、50%、75%、末尾の位置にそれぞれ置いてみたところ、五つの位置すべてで正解しました。3 万トークン程度の長さで、一文を探すという簡単なタスクでは、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 公式料金です。使う前に料金ページで確認してください。実験一の二組の数字を代入し、さらに 1 日 1000 回、1 か月分の費用を見積もります(完全なコードは code/01-llm-basics/cost.py)。

第一次:0.008833 美元
第二次:0.000262 美元
每月 3 万次,全部命中缓存:7.85 美元;全部不命中:265.00 美元

モジュール 00 第 3 課の簡単な質疑応答は 0.00019 ドルでしたが、今回ドキュメントを全部詰め込んだ 1 回目の呼び出しは 0.0088 ドルで、46 倍高く、しかもユーザーに見える回答は一文だけです。1 日 1000 回、1 か月で計算すると、キャッシュなしでは 265 ドルになります。

速度の問題もあります。入力が長いほど、モデルがそれを読み終えるまでの時間も、ユーザーが最初の 1 文字を待つ時間も長くなります。3 万トークン程度ではまだ目立ちませんが、ドキュメントが数十万トークンになると、はっきり体感できるようになります。

ですから、全部詰め込むのは次のような場合に向いています。資料自体が大きくない、呼び出し回数が少ない、あるいは安定してキャッシュにヒットする。資料が大きく、呼び出しが頻繁で、毎回その一部しか必要ないなら、関係する部分を先に探し出してからモデルに渡すほうが割に合います。それがモジュール 04 の RAG の役目です。

キャッシュ:繰り返される冒頭をほぼ無料にする

2 回目の呼び出しが 30 倍以上安くなった(0.000262 対 0.008833)のは、DeepSeek のコンテキストキャッシュのおかげです。今回のリクエストの冒頭部分が以前のリクエストの冒頭とまったく同じなら、サーバーは以前の計算結果を再利用でき、その部分は「キャッシュヒット」の料金で課金されます。deepseek-flash のキャッシュヒットの料金はミスの 50 分の 1 です(0.006 対 0.30)。

DeepSeek のキャッシュのドキュメント(2026 年 9 月時点)によると、次のとおりです。

  • キャッシュは既定で有効で、コードを変える必要はありません。
  • キャッシュは「プレフィックス単位」で保存されます。サーバーは各リクエストの境界、複数のリクエストに共通する冒頭部分、そして長い入力では一定のトークン数ごとに、この単位を作ります。一つの単位に完全に一致した部分だけがヒットになります。
  • 使われなくなったキャッシュは、通常数時間から数日で消去されます。
  • ヒットしたトークン数とミスしたトークン数は、それぞれ usageprompt_cache_hit_tokensprompt_cache_miss_tokens に入っています。

実験の 2 回目でも 213 トークンがミスになったのは、キャッシュが単位ごとに照合されるからです。ユーザーの質問がある最後の部分と、一つの完全な単位にならない部分は、ミスとして課金されます。

このルールから、メッセージをどんな順番で並べるべきかが決まります。

  • 変わらないものを前に:system メッセージ、固定の資料、例は一番前に置く。
  • 変わるものを後に:ユーザーの質問や、毎回違う内容は最後に置く。

ユーザーの質問をドキュメントの前に置くと、質問が毎回違うので冒頭も毎回違い、後ろのドキュメントもまったくヒットしなくなります。

出力は入力より高い

料金表に戻りましょう。deepseek-flash の出力は 100 万トークンあたり 1.20 ドル、キャッシュミスの入力は 0.30 ドル、キャッシュヒットの入力は 0.006 ドルです。出力は入力の 4 倍、キャッシュヒットの入力の 200 倍高いのです。

ここからいくつかのことが直接導けます。

  • モデルに無駄話をさせないことが節約になります。プロンプトで「一文で答えて」「JSON だけを出力して」と求めれば、すぐに効果が出ます。
  • 思考モードでは、思考過程が出力として課金されます。簡単なタスクで思考をオフにすれば、出力費用の大部分を節約できます。
  • 長い資料を入力に入れても、キャッシュにヒットするなら実はそれほど高くありません。本当にお金がかかるのは、たいていキャッシュにヒットしない長い入力と、冗長な出力です。

よくある問題

コンテキストが長すぎるというエラーが出る:まず会話の履歴がどんどん積み上がっていないか確認してください。履歴の切り詰めと圧縮の方法はモジュール 03 第 1 課で扱います。

キャッシュのヒット数がずっと 0:各リクエストの冒頭が本当に完全に同じか確認してください。よくある原因は、system メッセージに現在時刻やランダムな ID を入れている、あるいは毎回資料の順番を変えていることです。

他のサービスにもキャッシュはあるか:たいていありますが、課金方法やルールが違い、どの部分をキャッシュするか手動で指定する必要があるものもあります。使う前に相手のドキュメントを確認してください。

練習問題

  1. cost_usd で計算してみてください。同じく月 3 万回、キャッシュにヒットしない場合で、すべてオフピーク時間帯に呼び出すと(off_peak=True を渡す)、いくら節約できますか。
  2. 実験一の system メッセージと user メッセージの順番を入れ替え(質問を前に、ドキュメントを後に)、2 回続けて聞いて、2 回目のキャッシュヒット数がどう変わるか見てください。
  3. 実験二を改造してください。別々の位置に散らばった 3 つの文を挿入します。たとえば「合言葉の第 1 部分は蓝鲸(シロナガスクジラ)」「第 2 部分は四十」「第 3 部分は二」とし、完全な合言葉を聞きます。思考オフと思考オンでそれぞれ何回か試して、一文を探すときと同じ結果になるか確かめてください。

確認テスト

1. コンテキストウィンドウが 100 万トークンなら、100 万トークンを入力できるということですか?

違います。コンテキストウィンドウは入力と出力を合わせた上限です。入力で 99 万使えば、出力に残るのは最大 1 万で、思考をオンにしているなら思考過程も出力に数えられます。それに、入れられることと割に合うことは別で、長い入力は遅くて高くつきます。

2. あなたのアプリは毎回、固定の製品マニュアルとユーザーの質問を一緒にモデルに送っています。最も安くなるメッセージの順番は?

製品マニュアルを一番前(たとえば system メッセージ)に置き、ユーザーの質問を最後に置きます。そうすれば毎回のリクエストの冒頭が同じになってキャッシュにヒットし、マニュアルの部分はキャッシュヒットの料金で課金されるので、数十倍安くなります。質問を前に置くと、冒頭が毎回違うので、キャッシュには決してヒットしません。

3. テストしたところ、モデルは長いドキュメントに隠した一文を正確に見つけました。これでこのモデルの長コンテキスト能力に問題はないと言えますか?

言えません。「一文を探す」のは最も簡単な長コンテキストのタスクです。複数の箇所の情報を統合する必要があるとき、大量の似た内容の中で細部を見分ける必要があるときは、モデルはより間違えやすくなります。あなたの要件を満たせるかどうかは、実際のタスクに近いテストで確かめる必要があります。

質問と議論

このレッスンでつまずいたところは、ここで質問してください。他の人の質問に答えるのも歓迎です。

質問で 3 ポイント、回答で 6 ポイント。審査を通過すると公開されます。

議論を読み込んでいます…