Le modèle ne fait qu'une chose : prédire le token suivant
Faire afficher au modèle ses tokens candidats et leurs probabilités à chaque étape, pour voir comment une réponse est générée token par token ; puis comment il est passé de machine à continuer du texte à assistant, et d'où viennent les hallucinations.
- Environ 35 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.
Vous avez sans doute entendu dire : « Au fond, un grand modèle ne fait que prédire le mot suivant. » C'est vrai, mais avec cette seule phrase, on imagine mal comment un programme qui « prédit le mot suivant » peut écrire du code, résoudre des problèmes de maths ou répondre à des questions. Surtout, cette phrase explique beaucoup de phénomènes que vous rencontrerez plus tard : pourquoi il invente avec le plus grand sérieux, pourquoi la même question reçoit tantôt une bonne tantôt une mauvaise réponse, pourquoi la formulation du prompt influence tant le résultat.
Dans cette leçon, nous demandons au modèle d'afficher sa « pensée » à chaque étape.
Le regarder choisir pas à pas
La leçon précédente mentionnait le paramètre logprobs de l'API : activé, le modèle vous donne la probabilité de chaque token produit, et peut y joindre les candidats les plus probables à la même position. DeepSeek le prend en charge en mode sans réflexion :
import math
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")
def show_candidates(prompt, max_tokens=6):
response = client.chat.completions.create(
model=MODEL,
messages=[{"role": "user", "content": prompt}],
max_tokens=max_tokens,
logprobs=True, # 返回每个输出词元的概率
top_logprobs=5, # 同时返回每一步概率最高的 5 个候选
extra_body={"thinking": {"type": "disabled"}},
)
print(f"问:{prompt}")
print(f"答:{response.choices[0].message.content}")
for step, item in enumerate(response.choices[0].logprobs.content, 1):
# 接口返回的是概率的自然对数,用 exp 变回 0~1 之间的概率
options = " ".join(f"{c.token}({math.exp(c.logprob):.1%})" for c in item.top_logprobs)
print(f" 第 {step} 步选了 {item.token!r:8} 候选:{options}")
print()
show_candidates("床前明月光,下一句是什么?只回答下一句。")
show_candidates("请续写这句话,只写接下来的几个字:周末我打算去")
Mon résultat (quelques candidats étaient des sauts de ligne qui cassaient l'affichage ; je les ai retirés) :
问:床前明月光,下一句是什么?只回答下一句。
答:疑是地上霜。
第 1 步选了 '疑' 候选:疑(100.0%) 疑似(0.0%) 下一(0.0%) 低头(0.0%) 举(0.0%)
第 2 步选了 '是' 候选:是(100.0%) 是中(0.0%) 是全(0.0%) <||end▁of▁sentence||>(0.0%) 是高(0.0%)
第 3 步选了 '地上' 候选:地上(100.0%) 地上的(0.0%) 明月(0.0%) 银河(0.0%) 地下(0.0%)
第 4 步选了 '霜' 候选:霜(100.0%) <||end▁of▁sentence||>(0.0%) 妆(0.0%) 箱(0.0%) 光(0.0%)
第 5 步选了 '。' 候选:<||end▁of▁sentence||>(96.4%) 。(3.6%)
问:请续写这句话,只写接下来的几个字:周末我打算去
答:爬山,顺便看看日出。
第 1 步选了 '爬山' 候选:爬山(74.0%) 图书馆(12.7%) 公园(5.9%) 山里(3.6%) 超市(1.1%)
第 2 步选了 ',' 候选:,(67.0%) 。(31.9%) <||end▁of▁sentence||>(0.7%) 放松(0.3%) /(0.1%)
第 3 步选了 '顺便' 候选:顺便(48.8%) 呼吸(44.9%) 放松(3.3%) 亲近(1.1%) 或者(0.8%)
第 4 步选了 '看看' 候选:看看(51.6%) 看(20.3%) 拍(19.0%) 透(3.9%) 呼吸(3.4%)
第 5 步选了 '日出' 候选:日出(86.1%) 山(4.7%) 日落(4.7%) 秋天的(2.7%) 春天的(0.3%)
第 6 步选了 '。' 候选:。(100.0%) <||end▁of▁sentence||>(0.0%)
Pour « 疑是地上霜 » (on dirait du givre sur le sol), chaque étape est à 100 % (après arrondi) : le modèle a vu ce vers d'innombrables fois pendant l'entraînement, aucun suspense.
La suite de « 周末我打算去 » (ce week-end, je compte aller…) est différente. À l'étape 3, « 顺便 » (au passage) et « 呼吸 » (respirer) sont presque à égalité, 48,8 % contre 44,9 %. Cette fois, le tirage a donné « 顺便 », d'où « 顺便看看日出 » (et au passage voir le lever du soleil) ; si la prochaine fois le tirage donne « 呼吸 », la suite sera peut-être « 呼吸新鲜空气 » (respirer l'air frais). Une réponse contient beaucoup de bifurcations de ce genre, et le chemin choisi à chacune décide de la direction de toute la réponse. C'est pourquoi la même question reçoit des réponses différentes ; la leçon 3 détaille cela.
Autre détail : à l'étape 5 de la première question, le candidat le plus probable est <||end▁of▁sentence||> (96,4 %), le point n'a que 3,6 % de probabilité, et c'est pourtant le point qui a été tiré. Ce end▁of▁sentence est un token spécial qui signifie « j'ai fini ». Le modèle ne sait pas quand il doit s'arrêter ; il prédit simplement, à une certaine étape, ce token de « fin », et le programme arrête la génération en le voyant. Le finish_reason: stop de la leçon précédente vient de là.
Comment une réponse est générée
En reliant ce que nous venons de voir, la génération d'une réponse par le modèle est une boucle :
输入:"床前明月光,下一句是什么?"
→ 模型算出下一个词元的概率分布 → 抽中"疑" → 把"疑"接到输入后面
输入:"床前明月光,下一句是什么?疑"
→ 模型算出下一个词元的概率分布 → 抽中"是" → 接到后面
输入:"床前明月光,下一句是什么?疑是"
→ ……
一直重复,直到抽中"结束"词元,或者达到 max_tokens 的上限
À chaque étape, le modèle prend tout le contenu jusque-là (votre question plus ce qu'il a déjà écrit) comme entrée, et ne prédit qu'un seul token. Chaque caractère qu'il écrit devient l'entrée de l'étape suivante.
Ce mécanisme a plusieurs conséquences directes :
- Plus la sortie est longue, plus c'est lent et cher. Générer 100 tokens demande 100 étapes. C'est aussi l'une des raisons pour lesquelles la sortie coûte plus cher que l'entrée.
- Il ne peut pas revenir en arrière. S'il a écrit un mot faux, il ne peut que continuer dans le sillage de cette erreur, ou tenter de se rattraper plus loin. Le mode réflexion est utile en partie parce qu'il offre au modèle un « brouillon » où il peut se tromper puis se corriger, la réponse officielle ne contenant que la conclusion finale.
- Ce qui précède influence ce qui suit. Si vous demandez au modèle « d'abord la conclusion, puis les raisons », il écrit d'abord la conclusion, puis cherche des raisons pour la justifier ; à l'inverse, avec « d'abord l'analyse, puis la conclusion », la conclusion s'appuie sur l'analyse. Ces deux formulations peuvent avoir des taux de réussite très différents ; la leçon 3 du module 02 en fait l'expérience.
De la machine à continuer du texte à l'assistant
Un modèle qui ne fait que « prédire le mot suivant » ne savait, au départ, vraiment que continuer du texte. Donnez-lui « 床前明月光 » (devant mon lit, la clarté de la lune), il continue le poème ; donnez-lui « Comment apprendre Python ? », il écrira peut-être dix autres questions semblables, parce que les listes de questions sur le web ressemblent souvent à cela.
Pour devenir un assistant qui répond aux questions, il est passé grosso modo par trois étapes :
- Pré-entraînement : s'exercer à « prédire le token suivant » sur d'immenses quantités de texte (pages web, livres, code). Pour bien prédire, le modèle est obligé d'apprendre la grammaire, des faits et des schémas de raisonnement. Cette étape détermine ce que le modèle « sait » et consomme l'essentiel de la puissance de calcul.
- Fine-tuning sur instructions : poursuivre l'entraînement sur de nombreux dialogues « question → bonne réponse », pour qu'il apprenne à « répondre à une question quand il en voit une ».
- Alignement sur les préférences : faire noter les réponses par des humains ou d'autres modèles, et l'entraîner à préférer des réponses utiles, honnêtes et sûres.
La leçon 7 du module 09 détaille ces trois étapes. Pour l'instant, retenez que le comportement de l'assistant est le fruit de l'entraînement, mais que le mécanisme sous-jacent n'a jamais changé : il prédit toujours le token suivant.
Mais alors, comment le system, le user et l'assistant d'une conversation deviennent-ils « un seul texte » donné au modèle ? La configuration du tokenizer publiée par DeepSeek contient un « modèle de conversation » qui fixe la manière d'assembler les messages. Sans les détails, une conversation simple est assemblée ainsi :
<|begin▁of▁sentence|>你是一个说话简短的助手。<|User|>Python 里的列表和元组有什么区别?<|Assistant|>
<|User|> et <|Assistant|> sont des tokens spéciaux du vocabulaire. Une fois l'assemblage fait, le modèle continue simplement le texte : il prédit la suite après <|Assistant|>. La leçon précédente notait que « les tokens d'entrée sont plus nombreux que les caractères des messages » ; le surplus, ce sont ces balises de format. (C'est le modèle fourni avec l'archive du tokenizer ; ceux du mode réflexion et de l'appel d'outils sont plus complexes, mais le principe est le même.)
D'où viennent les hallucinations
Une fois compris le principe de « prédire le token suivant », on comprend pourquoi le modèle invente, ce qu'on appelle couramment une hallucination (hallucination).
À chaque étape, le modèle choisit « ce qui ressemble le plus à ce qui va venir », et non « ce qui est vrai ». La plupart du temps, les deux coïncident, parce que dans les données d'entraînement, le vrai est le plus fréquent. Mais quand le modèle connaît mal un sujet, ou que la question suppose une prémisse inexistante, « ce qui ressemble le plus » et « ce qui est vrai » divergent. Le modèle n'a pas d'interrupteur « je ne sais pas », sauf si l'entraînement lui a appris à le dire dans ces cas-là.
J'ai posé à deepseek-flash une question précise sur httpx (réflexion désactivée) :
问:httpx 0.27.2 这个版本的更新日志里具体修复了哪些 bug?逐条列出。
答:httpx 0.27.2 的更新日志中,没有列出任何 bug 修复。
该版本(发布于 2024 年 9 月 25 日)是一个纯安全修复版本,只包含一条内容:
- 修复了一个安全漏洞:在特定条件下,之前已重定向的请求在后续重定向中会错误地保留
Authorization 头,可能导致凭证泄露到非同源主机。该问题通过 CVE-2024-46319
相关补丁修复,行为改为在跨源重定向时剥离 Authorization 头。
(La réponse d'origine continuait sur un paragraphe, omis ici.)
Le ton est très assuré, avec une date, un numéro de CVE et des détails techniques ; cela paraît tout à fait crédible. Je suis allé voir CHANGELOG.md dans le dépôt de httpx ; l'entrée 0.27.2 dit en réalité :
## 0.27.2 (27th August, 2024)
### Fixed
* Reintroduced supposedly-private `URLTypes` shortcut. (#2673)
La date est fausse, le contenu est faux, et le numéro de CVE comme l'idée d'une « version purement de sécurité » sont inventés. Le modèle a vu beaucoup de journaux de modifications du type « telle version de telle bibliothèque corrige la fuite de l'en-tête Authorization lors des redirections », et il a produit un texte qui « ressemble à un journal de modifications de httpx ».
Pour être juste, le modèle s'en sort bien face aux pièges évidents. Quand je lui ai demandé « le Client de httpx a un paramètre retry_on_status, comment l'utiliser ? » (ce paramètre n'existe pas), il a directement signalé que « ce paramètre n'existe pas » et donné une alternative correcte (j'ai vérifié la documentation de httpx et le code source de la bibliothèque tierce qu'il recommandait ; tout concordait). Quand je lui ai demandé trois articles de recherche sur le pool de connexions de httpx, il a dit n'avoir trouvé aucun article de ce genre et m'a prévenu de ne pas lui demander d'en inventer. Le problème vient des questions qui ont l'air parfaitement normales : un numéro de version précis, un point d'histoire précis. Plus c'est précis et pointu, plus il invente facilement.
Retenez donc :
- Fluidité et assurance ne veulent pas dire exactitude. Ce que le modèle invente se lit aussi facilement que ce qui est vrai.
- Plus c'est précis, plus il faut vérifier. Numéros de version, dates, chiffres, noms de personnes, paramètres d'API, références : ce sont les zones à risque.
- Le moyen le plus efficace est de donner au modèle des sources. Mettre les vrais documents dans l'entrée et le faire répondre d'après eux, c'est le RAG du module 04 ; ou lui permettre de chercher lui-même, ce sont les agents du module 05. Le projet fil rouge de la première partie, RepoBot, garde volontairement ce problème dans sa version v1, pour que vous le voyiez se tromper, avant de le résoudre en v2.
Exercices
- Essayez
show_candidatesavec plusieurs débuts : « 我最喜欢的编程语言是 » (mon langage de programmation préféré est), « 1+1= », « 从前有座山 » (il était une fois une montagne). Lesquels ont les candidats les plus concentrés, lesquels les plus dispersés ? - Passez
top_logprobsà 20 et regardez s'il y a des candidats inattendus pour « 周末我打算去 ». - Posez au modèle une question précise sur un sujet que vous connaissez très bien mais qui est assez pointu (un détail d'un produit de votre entreprise, une petite rue de votre ville natale, une version d'une bibliothèque open source que vous connaissez), réflexion désactivée, et voyez s'il invente. Reposez-la avec la réflexion activée : y a-t-il une différence ? Notez vos observations.
Auto-test
1. Comment le modèle sait-il quand une réponse doit se terminer ?
Il ne le « sait » pas. Le vocabulaire contient un token spécial de « fin » ; quand le modèle, à une étape, prédit ce token avec une forte probabilité et qu'il est tiré, le programme arrête la génération. L'autre cas est l'atteinte de la limite max_tokens, qui coupe la réponse de force.
2. Pourquoi est-il difficile pour le modèle de se corriger quand il s'aperçoit en cours de route qu'il s'est trompé ?
À chaque étape, le modèle prend tout ce qu'il a déjà écrit comme entrée et ne prédit qu'un token de plus, sans mécanisme pour revenir en arrière. L'erreur passée devient l'entrée de la suite et influence les prédictions suivantes. Le mode réflexion atténue en partie le problème : le modèle peut faire un brouillon et se corriger dans sa réflexion, puis n'écrire que la conclusion dans la réponse officielle.
3. Pourquoi le modèle invente-t-il un numéro de CVE inexistant ?
À chaque étape, le modèle choisit « ce qui ressemble le plus à ce qui va venir », et non « ce qui est vrai ». Il a vu énormément de textes du type « telle version corrige tel problème de sécurité, numéro CVE-xxxx » ; quand il ne connaît pas le fait précis, il produit selon ce schéma un contenu qui paraît plausible. Plus un fait est précis et pointu, plus il risque d'être inventé ; ce genre d'information doit toujours être vérifié.
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…