Module 01 · Leçon 4

Fenêtre de contexte et coût

Mettre toute la documentation de httpx dans une seule requête, voir combien cela coûte et si le modèle retrouve une phrase cachée au milieu, puis voir comment le cache rend le deuxième appel plus de 30 fois moins cher, et enfin écrire une fonction qui calcule le coût à partir de usage.

  • Environ 40 minutes
  • Niveau : Débutant
  • Testé : 2026-09-14 deepseek-flash

Le code et les sorties des programmes sont reproduits tels qu’ils ont tourné : commentaires et sorties sont donc en chinois.

Les modèles actuels ont des fenêtres de contexte de centaines de milliers, voire de millions de tokens. Première réaction de beaucoup de gens : à quoi bon le RAG, il suffit de tout mettre dedans ?

Parfois, c'est vrai. Mais il faut d'abord savoir combien cela coûte à chaque fois, de combien cela ralentit, et si le modèle retrouve la phrase que vous cherchez dans une montagne de texte. Cette leçon mesure tout cela avec l'intégralité de la documentation de httpx.

Qu'est-ce que la fenêtre de contexte

La fenêtre de contexte (context window) est le nombre total maximal de tokens que le modèle peut traiter en une fois : entrée et sortie additionnées ne peuvent pas la dépasser. En septembre 2026, deepseek-flash et deepseek-v4-pro ont tous deux une fenêtre d'un million de tokens, avec au plus 384 000 tokens de sortie par appel.

Dans une requête, tout ceci occupe le contexte :

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

Pour une simple question-réponse, cela ne fait peut-être que quelques centaines de tokens. Mais avec une conversation à plusieurs tours, des questions sur des documents ou des appels d'outils, le total grimpe vite. Quand le contexte est épuisé, la requête échoue directement.

Expérience : tout mettre dans la requête

La documentation officielle de httpx compte une vingtaine de fichiers Markdown, 116 933 caractères au total, rangés dans data/httpx-docs/ du dépôt du cours. Le script ci-dessous fait deux expériences : d'abord mettre toute la documentation dans le message system et poser deux fois la même question ; puis cacher une phrase dans la documentation et voir si le modèle la retrouve.

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}")

Exécuté depuis le dossier du cours, voici ce que j'obtiens (la formulation de la réponse sera différente chez vous, le nombre de tokens devrait être proche) :

文档共 116933 个字符
第 1 次:输入 29397,其中缓存命中 0,输出 12 | httpx 默认超时是 5 秒。
第 2 次:输入 29397,其中缓存命中 29184,输出 19 | httpx 默认的超时时间是 5 秒(指网络不活动的超时)。
口令放在   0% 处:输入 29404 词元 → 蓝鲸四十二
口令放在  25% 处:输入 29404 词元 → 蓝鲸四十二
口令放在  50% 处:输入 29404 词元 → 蓝鲸四十二
口令放在  75% 处:输入 29404 词元 → 蓝鲸四十二
口令放在 100% 处:输入 29404 词元 → 蓝鲸四十二

D'abord l'expérience 1. La réponse est juste : la documentation de httpx dit bien « The default behavior is to raise a TimeoutException after 5 seconds of network inactivity ». Toute la documentation fait environ 29 000 tokens, soit 3 % d'une fenêtre d'un million. La même question posée deux fois de suite : sur les 29 397 tokens d'entrée du second appel, 29 184 ont été servis depuis le cache. Nous verrons plus bas combien cela fait économiser.

Retrouve-t-il une phrase cachée au milieu ?

L'expérience 2 est un simple test de « l'aiguille dans la botte de foin ». Un article souvent cité, Lost in the Middle (Liu et al., 2023), a montré que les modèles de l'époque trouvaient le mieux l'information placée au début et à la fin d'une longue entrée, et négligeaient le plus facilement celle placée au milieu.

J'ai inséré dans la documentation une phrase sans aucun rapport avec httpx, au début, à 25 %, 50 %, 75 % et à la fin du texte : les cinq positions ont toutes donné une bonne réponse. Sur cette longueur de trente mille tokens et une tâche aussi simple que retrouver une phrase, deepseek-flash ne montre pas « d'oubli du milieu ». L'article testait des modèles de 2023, et les capacités sur les longs contextes ont beaucoup progressé depuis.

Mais n'en concluez pas que les longs contextes ne posent aucun problème. « Retrouver une phrase évidente » est la tâche de long contexte la plus simple. Quand il faut combiner plusieurs informations dispersées dans le document, ou distinguer des nuances entre des dizaines de paragraphes semblables, le modèle se trompe plus facilement ; des évaluations comme RULER (Hsieh et al., 2024) testent justement ces cas plus difficiles. Pour savoir dans quelle catégorie tombe votre tâche, le mieux est de tester avec vos propres données ; l'exercice 3 propose un tel test.

Le coût : pourquoi ne pas toujours tout mettre

Trouver ne veut pas dire rentable. Écrivons une fonction qui calcule le coût à partir de 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  # 低谷时段五折

Ce sont les prix officiels de DeepSeek en septembre 2026 ; vérifiez-les sur la page des tarifs avant de les utiliser. En y mettant les deux séries de chiffres de l'expérience 1, et en estimant le coût mensuel pour 1000 appels par jour (code complet dans code/01-llm-basics/cost.py) :

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

La simple question-réponse de la leçon 3 du module 00 coûtait 0,00019 dollar ; ce premier appel avec toute la documentation coûte 0,0088 dollar, 46 fois plus, alors que l'utilisateur ne voit qu'une phrase de réponse. À 1000 appels par jour pendant un mois, sans cache, cela fait 265 dollars.

Il y a aussi la vitesse. Plus l'entrée est longue, plus le modèle met de temps à la lire, et plus l'utilisateur attend le premier caractère. Avec trente mille tokens, la différence reste discrète ; avec une documentation de plusieurs centaines de milliers de tokens, elle se sent nettement.

Tout mettre dans la requête convient donc quand les documents sont petits, que les appels sont peu nombreux, ou que le cache est touché de façon fiable. Quand les documents sont volumineux, les appels fréquents et qu'une petite partie seulement sert à chaque fois, il est plus rentable de trouver d'abord les passages pertinents et de ne transmettre qu'eux au modèle. C'est ce que fait le RAG du module 04.

Le cache : un début répété presque gratuit

Le second appel est plus de 30 fois moins cher (0,000262 contre 0,008833) grâce au cache de contexte de DeepSeek : si le début de la requête est exactement identique au début d'une requête précédente, le serveur peut réutiliser le résultat déjà calculé, et cette partie est facturée au prix « avec cache ». Pour deepseek-flash, ce prix vaut un cinquantième du prix sans cache (0,006 contre 0,30).

D'après la documentation du cache de DeepSeek (septembre 2026) :

  • Le cache est activé par défaut, sans modifier le code.
  • Le cache est stocké par « unités de préfixe ». Le serveur en crée à la frontière de chaque requête, sur le début commun à plusieurs requêtes, et à intervalles fixes de tokens dans les longues entrées. Une partie n'est comptée comme touchée que si elle correspond entièrement à une unité.
  • Un cache qui ne sert plus est généralement effacé au bout de quelques heures à quelques jours.
  • Le nombre de tokens touchés et non touchés figure respectivement dans prompt_cache_hit_tokens et prompt_cache_miss_tokens de usage.

Dans l'expérience, 213 tokens n'ont pas été touchés au second appel, justement parce que le cache fonctionne par unités : le dernier segment, qui contient la question de l'utilisateur, et la partie trop courte pour former une unité complète sont facturés sans cache.

Ces règles dictent l'ordre dans lequel ranger les messages :

  • Ce qui ne change pas en premier : message system, documents fixes, exemples.
  • Ce qui change à la fin : la question de l'utilisateur, tout ce qui varie d'une fois sur l'autre.

Si vous mettez la question de l'utilisateur avant la documentation, le début change à chaque question, et la documentation qui suit n'est jamais servie depuis le cache.

La sortie coûte plus cher que l'entrée

Retour au tableau des prix : pour deepseek-flash, la sortie coûte 1,20 dollar par million de tokens, l'entrée sans cache 0,30, l'entrée avec cache 0,006. La sortie coûte 4 fois plus que l'entrée, et 200 fois plus que l'entrée servie depuis le cache.

Quelques conséquences directes :

  • Faire moins bavarder le modèle, c'est économiser. Exiger dans le prompt « réponds en une phrase » ou « ne produis que du JSON » a un effet immédiat.
  • En mode réflexion, la réflexion est facturée comme de la sortie. Désactiver la réflexion pour les tâches simples économise l'essentiel des frais de sortie.
  • De longs documents dans l'entrée ne coûtent pas si cher s'ils sont servis depuis le cache. Ce qui coûte vraiment, ce sont souvent les longues entrées hors cache et les sorties interminables.

Problèmes courants

Erreur de contexte trop long : vérifiez d'abord si l'historique de conversation ne s'accumule pas. La leçon 1 du module 03 montre comment tronquer et compresser l'historique.

Le nombre de tokens servis depuis le cache reste à 0 : vérifiez que le début de chaque requête est vraiment identique. Problème fréquent : l'heure actuelle ou un identifiant aléatoire dans le message system, ou un ordre des documents qui change à chaque fois.

Les autres fournisseurs ont-ils un cache ? La plupart oui, mais la tarification et les règles diffèrent ; certains exigent de marquer à la main ce qui doit être mis en cache. Consultez leur documentation.

Exercices

  1. Avec cost_usd, calculez : pour les mêmes 30 000 appels par mois sans cache, combien économiseriez-vous en appelant uniquement en heures creuses (off_peak=True) ?
  2. Inversez l'ordre du message system et du message user dans l'expérience 1 (la question d'abord, la documentation ensuite), posez deux fois la question et regardez comment évolue le nombre de tokens servis depuis le cache au second appel.
  3. Modifiez l'expérience 2 : insérez trois phrases à des endroits différents, par exemple « la première partie du mot de passe est baleine bleue », « la deuxième partie est quarante », « la troisième partie est deux », puis demandez le mot de passe complet. Testez plusieurs fois avec et sans réflexion : les résultats sont-ils les mêmes que pour une seule phrase ?

Auto-test

1. Une fenêtre de contexte d'un million de tokens signifie-t-elle que je peux envoyer un million de tokens en entrée ?

Non. La fenêtre de contexte est la limite de l'entrée et de la sortie additionnées. Si l'entrée en utilise 990 000, il ne reste que 10 000 pour la sortie, et avec la réflexion activée, la réflexion compte aussi comme sortie. En outre, pouvoir tout y mettre ne veut pas dire que c'est rentable : les longues entrées sont lentes et chères.

2. Votre application envoie à chaque fois un manuel produit fixe avec la question de l'utilisateur. Dans quel ordre ranger les messages pour payer le moins ?

Le manuel en premier (par exemple dans le message system), la question de l'utilisateur à la fin. Ainsi, le début de chaque requête est identique, le cache est touché et la partie manuel est facturée au prix avec cache, des dizaines de fois moins cher. Si la question est au début, le début change à chaque fois et le cache n'est jamais touché.

3. Lors d'un test, le modèle a retrouvé exactement la phrase que vous aviez cachée dans un long document. Cela prouve-t-il que ses capacités sur les longs contextes sont sans problème ?

Non. « Retrouver une phrase » est la tâche de long contexte la plus simple. Quand il faut combiner des informations de plusieurs endroits ou distinguer des détails parmi beaucoup de contenus semblables, le modèle se trompe plus facilement. Pour savoir s'il répond à vos besoins, il faut le vérifier avec un test proche de la tâche réelle.

Questions et discussion

Bloqué sur cette leçon ? Posez votre question ici. Et si vous pouvez répondre à quelqu'un, n'hésitez pas.

Une question rapporte 3 points, une réponse 6. Les messages paraissent après vérification.

Chargement de la discussion…