Module 00 · Leçon 3

Premier appel à un grand modèle

Écrire un programme d'une quinzaine de lignes qui appelle DeepSeek, et comprendre champ par champ la requête et la réponse – rôles des messages, raison de fin, consommation de tokens et coût –, puis voir avec curl qu'au fond ce n'est qu'une requête HTTP.

  • Environ 30 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.

Une simple recherche sur le web suffit pour trouver du code d'exemple qui appelle un grand modèle ; on le copie, on change la question, et ça marche. Mais beaucoup de gens s'arrêtent là : le programme tourne, mais ils ne savent pas ce que contient le gros objet renvoyé. Si bien que plus tard, face à des questions comme « pourquoi la réponse s'arrête-t-elle soudain au milieu d'une phrase ? » ou « pourquoi la facture de ce mois est-elle si élevée ? », ils ne savent absolument pas par où commencer.

Cette leçon n'écrit qu'un programme très court, mais explique chaque champ de la requête et de la réponse.

L'appel minimal

Dans le dossier ai-course créé à la leçon précédente, créez first_call.py :

import os

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

response = client.chat.completions.create(
    model=MODEL,
    messages=[
        {"role": "system", "content": "你是一个说话简短的助手,每次回答不超过两句话。"},
        {"role": "user", "content": "Python 里的列表和元组有什么区别?"},
    ],
)

message = response.choices[0].message
print("回答:", message.content)
print("结束原因:", response.choices[0].finish_reason)
print("实际使用的模型:", response.model)
print("输入词元:", response.usage.prompt_tokens)
print("输出词元:", response.usage.completion_tokens)

# DeepSeek 的模型默认先思考再回答,思考过程放在 reasoning_content 里。
# 别的服务商没有这个字段,所以用 getattr 取,取不到就是 None。
reasoning = getattr(message, "reasoning_content", None)
if reasoning:
    print("思考过程(前 100 字):", reasoning[:100])

Exécution :

uv run python first_call.py

Mon résultat :

回答: 列表可变、用 `[]`,元组不可变、用 `()`。因此列表适合频繁修改的数据,元组更适合固定不变的数据。
结束原因: stop
实际使用的模型: deepseek-flash
输入词元: 52
输出词元: 145
思考过程(前 100 字): 我们需要回答中文。用户要求:你是一个说话简短的助手,每次回答不超过两句话。问题:Python 里的列表和元组有什么区别?需要不超过两句话。要准确。可以一句或两句。核心区别:列表可变,用方括号;元组不可

La formulation de votre réponse sera différente, et le nombre de tokens un peu aussi ; c'est normal.

Le programme ne fait que trois choses : créer un client, appeler chat.completions.create, extraire des éléments de la valeur renvoyée. Décortiquons.

Le client

OpenAI(...) crée un objet client, chargé d'envoyer votre requête au serveur indiqué par base_url en ajoutant api_key dans l'en-tête. La classe s'appelle OpenAI, mais elle peut se connecter à n'importe quel service compatible avec l'interface d'OpenAI. En pointant base_url vers DeepSeek, c'est DeepSeek qu'elle contacte.

Le nom chat.completions vient de l'interface de « complétion de conversation » conçue à l'origine par OpenAI. Elle est depuis devenue le standard de fait du secteur, et la plupart des services de modèles, en Chine comme ailleurs, proposent une interface au même format. Une fois celle-ci maîtrisée, vous pouvez utiliser presque n'importe quel fournisseur.

La requête : model et messages

La requête n'a que deux paramètres obligatoires.

model est le nom du modèle. Le serveur s'en sert pour décider quel modèle répond.

messages est une liste dont chaque élément est un message, avec deux champs : role et content (le contenu). Il y a trois rôles :

Rôle Qui parle À quoi il sert
system Le développeur Fixer les règles du modèle : quel personnage jouer, quel ton adopter, quelles limites respecter
user L'utilisateur La question ou l'instruction de l'utilisateur
assistant Le modèle Les réponses précédentes du modèle. Dans une conversation à plusieurs tours, on y remet ce qu'il a déjà dit

Dans l'exemple ci-dessus, le message system demande « pas plus de deux phrases par réponse », et le modèle ne répond effectivement qu'en deux phrases. L'utilisateur ne voit pas le message system, mais celui-ci influence chaque réponse du modèle. Dans une application, le « personnage » et les règles du produit s'écrivent en général ici.

Le rôle assistant ne sert pas encore dans cette leçon. Vous pourriez croire que le modèle se souvient de ce que vous lui avez demandé la dernière fois ; ce n'est pas le cas, chaque appel est indépendant. Pour qu'il « se souvienne » de la conversation précédente, il faut remettre les questions et réponses précédentes, une par une, sous forme de messages user et assistant dans messages et tout renvoyer. La leçon 1 du module 03 y est consacrée.

La réponse : choices, finish_reason, usage

Dans l'objet response renvoyé, voici ce qui sert le plus :

response.choices[0].message.content : la réponse du modèle. choices est une liste parce que l'interface permet de demander plusieurs réponses candidates à la fois, mais il n'y en a presque toujours qu'une, d'où l'index 0.

response.choices[0].finish_reason : pourquoi le modèle s'est arrêté. Valeurs courantes :

Valeur Signification
stop Le modèle estime avoir fini ; fin normale
length La limite de longueur est atteinte, la réponse est coupée de force. Elle est très probablement incomplète
tool_calls Le modèle veut appeler un outil (leçon 3 du module 03)
content_filter Le contenu a été bloqué par le filtre de sécurité du fournisseur

Votre programme doit le vérifier. Si c'est length, vous avez peut-être une demi-phrase ; l'afficher telle quelle à l'utilisateur ou l'analyser comme du JSON posera problème.

response.model : le modèle qui vous a réellement répondu. La plupart du temps, c'est celui que vous avez demandé, mais il y a des exceptions : un ancien nom de modèle peut être redirigé par le fournisseur vers un modèle plus récent. En septembre 2026 par exemple, une requête avec l'ancien nom deepseek-chat est traitée par deepseek-flash en mode sans réflexion. Pour diagnostiquer un problème, ce champ est donc plus fiable que le nom que vous avez vous-même écrit.

response.usage : combien de tokens cet appel a utilisés. prompt_tokens pour l'entrée, completion_tokens pour la sortie. Le token est l'unité de base avec laquelle le modèle traite le texte : un caractère, une moitié de mot anglais, ou un groupe de quelques caractères. Le nombre de tokens d'un texte varie d'un modèle à l'autre ; la leçon 1 du module suivant vous montrera un vrai découpage. Les fournisseurs facturent au token : usage, c'est votre facture.

Combien a coûté cet appel

En septembre 2026, le prix de deepseek-flash est (en dollars, par million de tokens) :

Heures pleines Heures creuses
Entrée (sans cache) 0,30 0,15
Entrée (avec cache) 0,006 0,003
Sortie 1,20 0,60

Les heures pleines vont de 01:00 à 04:00 et de 06:00 à 10:00 UTC, du lundi au vendredi, soit de 9:00 à 12:00 et de 14:00 à 18:00, heure de Pékin, les jours ouvrés ; le reste du temps s'applique le tarif creux, à moitié prix. « Avec cache » signifie que le début de l'entrée est identique à celui d'une requête précédente et que le serveur peut réutiliser un calcul déjà fait ; le module 06 explique comment en profiter pour économiser.

Au tarif plein, l'appel ci-dessus coûte :

input_cost = 52 * 0.30 / 1_000_000
output_cost = 145 * 1.20 / 1_000_000
print(f"{input_cost + output_cost:.6f} 美元")
0.000190 美元

Avec un dollar, on peut poser plus de cinq mille questions de ce genre. Cela paraît très bon marché, mais notez deux choses. D'abord, la sortie coûte quatre fois plus que l'entrée : faire moins bavarder le modèle, c'est économiser. Ensuite, quand votre programme met à chaque fois tout un document dans l'entrée et qu'il est appelé des dizaines de milliers de fois par jour, ce chiffre grimpe très vite.

D'où viennent ces 145 tokens de sortie

La réponse ne fait que deux phrases, une quarantaine de caractères chinois, et pourtant la sortie compte 145 tokens. Le surplus, c'est la réflexion.

deepseek-flash active par défaut le mode réflexion : il réfléchit d'abord dans reasoning_content, puis donne sa réponse officielle dans content. On voit sa réflexion dans la sortie ci-dessus : il reformule d'abord la consigne « pas plus de deux phrases », puis organise sa réponse. Les tokens de réflexion sont comptés dans completion_tokens et facturés au prix de la sortie. J'ai lancé ce programme trois fois de suite : la réflexion a utilisé 137, 110 et 189 tokens, plusieurs fois plus que la réponse officielle.

La réflexion rend le modèle plus précis sur les questions complexes (la leçon 3 du module 02 fait une expérience comparative), mais sur les questions simples, c'est de l'argent et du temps perdus. DeepSeek permet de la désactiver :

response = client.chat.completions.create(
    model=MODEL,
    messages=[...],
    extra_body={"thinking": {"type": "disabled"}},
)

extra_body est une porte que le SDK d'OpenAI laisse ouverte pour transmettre des paramètres propres à chaque fournisseur. thinking est un paramètre de DeepSeek que les autres ne reconnaissent pas forcément. En changeant de fournisseur, retirez-le ou regardez quel paramètre celui-ci utilise pour contrôler la réflexion.

Les tokens d'entrée ont aussi un petit détail. Avec les deux mêmes messages (43 caractères chinois en tout), prompt_tokens vaut 27 en mode sans réflexion et 52 avec la réflexion activée. Le contenu des messages est identique ; les 25 tokens supplémentaires sont des balises de format ajoutées par le serveur : il indique autour des messages quelle partie est system, quelle partie est user et où commence la réflexion, avant de transmettre le tout au modèle, et ces balises comptent aussi comme tokens d'entrée. Que 43 caractères ne donnent qu'une vingtaine de tokens montre que le tokenizer de DeepSeek regroupe souvent deux ou trois caractères chinois en un seul token ; la leçon 1 du module suivant regarde cela de près.

Un piège : la réflexion épuise le quota

Le paramètre max_tokens limite le nombre maximal de tokens en sortie ; on s'en sert souvent pour maîtriser les coûts et empêcher le modèle de parler sans fin. Mais en mode réflexion, la réflexion consomme aussi ce quota.

J'ai fixé max_tokens à 30 et demandé « présente les compréhensions de liste de Python », une fois sans réflexion et une fois avec :

== 非思考: finish_reason=length completion_tokens=30 reasoning_tokens=None
content: '## Python 列表推导式(List Comprehension)\n\n列表推导式是 Python 中一种**简洁优雅**的创建列表的方式,可以用一行代码'
reasoning: ''
== 思考: finish_reason=length completion_tokens=30 reasoning_tokens=30
content: ''
reasoning: 'We need answer in Chinese. User asks "介绍一下 Python 的列表推导式。" Need introduce Python'

En mode sans réflexion, la réponse est coupée au milieu d'une phrase, ce qui était attendu. En mode réflexion, les 30 tokens ont tous servi à réfléchir, et la réponse officielle content est une chaîne vide. Si votre programme ne regarde que content, il croira que le modèle n'a rien dit.

Deux leçons, donc : avec la réflexion activée, soyez généreux avec max_tokens ; et vérifiez toujours finish_reason dans votre programme : length signifie que le résultat est incomplet.

Voir son vrai visage avec curl

Ce que le SDK fait pour vous, c'est simplement envoyer une requête HTTP. On peut aussi appeler le modèle sans Python, avec curl en ligne de commande :

curl https://api.deepseek.com/chat/completions \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $LLM_API_KEY" \
  -d '{
    "model": "deepseek-flash",
    "messages": [{"role": "user", "content": "用五个字形容秋天"}],
    "thinking": {"type": "disabled"}
  }'

PowerShell sous Windows traite les guillemets différemment, et cette commande risque de ne pas fonctionner ; lancez-la dans Git Bash ou WSL, ou sautez cette étape, cela ne gêne pas la suite.

La réponse est un morceau de JSON, que j'ai mis en forme :

{
    "id": "10bef209-3437-4efc-906b-dcb18fe30f7f",
    "object": "chat.completion",
    "created": 1789444049,
    "model": "deepseek-flash",
    "choices": [
        {
            "index": 0,
            "message": {
                "role": "assistant",
                "content": "**金风送爽凉**\n\n如果不局限于这五个字,还有其他不同角度的五字形容:\n\n- **秋高气爽天** — 天高云淡,气候宜人\n- **霜叶红于花** — 枫叶经霜比花还红\n- **硕果满枝头** — 丰收的景象\n- **一叶知秋来** — 落叶预示着秋天到来\n- **寒蝉鸣凄切** — 秋蝉叫声悲凉\n- **天凉好个秋** — 辛弃疾词句,凉爽舒适"
            },
            "logprobs": null,
            "finish_reason": "stop"
        }
    ],
    "usage": {
        "prompt_tokens": 9,
        "completion_tokens": 121,
        "total_tokens": 130,
        "prompt_tokens_details": {
            "cached_tokens": 0
        },
        "prompt_cache_hit_tokens": 0,
        "prompt_cache_miss_tokens": 9
    },
    "system_fingerprint": "aeb56401ca74e127821c4f9126dcb669"
}

Les champs correspondent un à un à ceux vus en Python : choices[0].message.content, finish_reason, usage. Le SDK ne fait que transformer ce JSON en objet Python, et s'occupe au passage des nouvelles tentatives, des délais et autres détails. Une fois cela compris, vous pouvez appeler un grand modèle depuis n'importe quel langage, et en cas de problème, utiliser directement curl pour voir si le problème vient de votre code ou du serveur.

Remarquez que dans curl, thinking s'écrit directement au premier niveau du JSON. En Python, on le passe via extra_body, et le SDK finit par le fusionner dans ce même JSON.

Un autre détail mérite l'attention : j'ai demandé de « décrire l'automne en cinq caractères », le modèle a donné cinq caractères, puis en a ajouté six de sa propre initiative. Les modèles en disent souvent plus que demandé ; le module 02 s'en occupe en parlant des prompts.

Problèmes courants

content vaut None ou une chaîne vide : regardez d'abord finish_reason. Si c'est length, max_tokens est trop petit et la réflexion l'a épuisé. Si c'est tool_calls, le modèle veut appeler un outil, et la réponse se trouve dans un autre champ.

Erreur 429 : trop de requêtes, vous êtes limité. Attendez quelques secondes et réessayez. La leçon 4 du module 03 montre comment réessayer automatiquement.

Le programme reste bloqué longtemps sans réagir : en mode réflexion, la réflexion sur une question complexe peut durer des dizaines de secondes. Essayez d'abord une question simple pour vérifier que le programme lui-même fonctionne. La leçon 2 du module 03 présente la sortie en streaming, qui affiche la réponse au fur et à mesure qu'elle est générée.

Exercices

  1. Remplacez le message system par « Tu es un vieux lettré qui ne répond qu'en chinois classique », reposez la même question et observez comment la réponse change.
  2. Ajoutez extra_body={"thinking": {"type": "disabled"}} à l'appel et comparez completion_tokens et le temps d'exécution avec et sans réflexion.
  3. Supposez que votre application soit appelée dix mille fois par jour, avec à chaque fois 2000 tokens en entrée et 500 en sortie (réflexion désactivée), le tout au tarif plein. Combien coûte un mois (30 jours) ? Calculez-le en Python.
  4. Ajoutez à la main deux messages dans messages pour simuler une conversation déjà passée : d'abord user demande « Je m'appelle Xiao Wang, retiens-le », puis assistant répond « D'accord, Xiao Wang », et enfin user demande « Comment je m'appelle ? ». Le modèle répond-il juste ? Réfléchissez à pourquoi.

Auto-test

1. Que signifie un finish_reason égal à length ? Comment le programme doit-il réagir ?

Cela signifie que la sortie du modèle a atteint la limite max_tokens et a été coupée de force ; la réponse est très probablement incomplète. Le programme ne doit pas l'utiliser comme un résultat normal : il peut relancer la requête avec un max_tokens plus élevé, ou au moins signaler à l'utilisateur que la réponse est incomplète. Si la réponse devait être analysée comme du JSON, un JSON tronqué échouera à coup sûr.

2. Avec la réflexion activée et max_tokens fixé à 50, content est vide. Pourquoi ?

La réflexion consomme aussi le quota de max_tokens. Les 50 tokens ont tous servi à réfléchir, et la limite a été atteinte avant que le modèle ait pu écrire sa réponse officielle. Avec la réflexion activée, il faut être généreux avec max_tokens, ou désactiver la réflexion pour les tâches simples.

3. Vous demandez le modèle deepseek-chat, mais response.model affiche deepseek-flash. Est-ce normal ?

Oui. Les fournisseurs redirigent les anciens noms de modèles vers des modèles plus récents pour continuer à les servir. response.model vous dit quel modèle a réellement répondu ; c'est lui qui fait foi pour diagnostiquer un problème ou vérifier une facture.

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…