Les prompts aussi se testent
Mettre les prompts dans des fichiers, préparer 30 cas de test, exécuter chacun 3 fois et comparer deux versions de prompt avec des données. On verra aussi que les résultats des tests doivent eux-mêmes être vérifiés – parfois, ce n'est pas le modèle qui se trompe, mais l'étiquette.
- 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.
La façon la plus courante de modifier un prompt est la suivante : on remarque une mauvaise réponse, on change une phrase du prompt, on réessaie cette question, c'est bon, terminé.
Le problème, c'est qu'on n'a vérifié que cette question-là. La modification l'a peut-être corrigée, mais elle a peut-être cassé trois autres questions qui marchaient, et vous ne l'apprendrez que par les plaintes des utilisateurs. C'est comme modifier du code sans lancer les tests.
Cette leçon construit un tout petit outil de test : les prompts dans des fichiers, les cas de test dans un fichier, et une seule commande qui exécute tous les cas et donne le taux de réussite, les erreurs et les cas instables.
Sortir les prompts du code
Première étape : enregistrer les prompts dans des fichiers texte séparés, au lieu de les écrire en dur dans le code Python :
code/02-prompting/
prompts/
classify_v1.txt 第一版提示词
classify_v2.txt 第二版提示词
cases.jsonl 测试用例
prompt_test.py 测试脚本
Avantages : on peut comparer deux versions côte à côte ; on voit avec git ce qui a changé à chaque fois ; des collègues qui ne codent pas peuvent aussi modifier les prompts.
classify_v1.txt est le prompt zero-shot de la leçon 2 :
把用户留言分成以下四类之一:缺陷、功能建议、使用问题、其他。
只输出类别名称。
classify_v2.txt est la version améliorée. D'après les deux erreurs du zero-shot à la leçon 2, chaque catégorie reçoit une définition, avec une précision particulière sur les frontières faciles à confondre, plus les 4 exemples de la leçon 2 :
把 httpx 项目收到的用户留言分成以下四类之一,只输出类别名称。
- 缺陷:httpx 库本身的行为不符合文档或者预期,比如报错、崩溃、结果不对。
- 功能建议:希望 httpx 增加目前没有的功能。
- 使用问题:问某个功能怎么用、某个行为是不是正常。哪怕看起来像在要新功能,只要 httpx 已经能做到,就算使用问题。拿不准是自己用错了还是库有问题的,也算使用问题。
- 其他:和 httpx 库本身无关的,比如文档网站、社区、招聘、感谢、和别的库比较。
例子:
(和第 2 课相同的 4 个例子)
Les cas de test
Chaque ligne de cases.jsonl est un cas, avec un message et sa bonne catégorie. En plus des 20 de la leçon 2, j'en ai ajouté 10 plus difficiles à classer, pour lesquels je dois moi-même réfléchir :
{"text": "httpx 支持 HTTP/3 吗?", "label": "使用问题"}
{"text": "文档里 Limits 那一节的示例代码跑不通,max_keepalive 这个参数名好像不对", "label": "其他"}
{"text": "response.elapsed 在流式请求里读出来一直是 0,这正常吗", "label": "使用问题"}
{"text": "同样的代码,requests 返回 200,httpx 返回 403", "label": "使用问题"}
{"text": "follow_redirects=True 时,301 跳转后 POST 变成了 GET", "label": "使用问题"}
……
Les bons cas de test viennent de plusieurs sources : les vraies entrées d'utilisateurs (le plus important) ; chaque erreur que vous avez corrigée, ajoutée une fois corrigée pour qu'elle ne revienne pas ; les cas limites que vous pouvez imaginer.
Le script de test
import json
import os
import sys
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
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")
RUNS = 3
HERE = Path(__file__).parent
cases = [json.loads(line) for line in (HERE / "prompts/cases.jsonl").read_text().splitlines() if line.strip()]
def classify(system, text):
response = client.chat.completions.create(
model=MODEL,
messages=[{"role": "system", "content": system}, {"role": "user", "content": f"留言:{text}\n类别:"}],
extra_body={"thinking": {"type": "disabled"}},
)
return response.choices[0].message.content.strip()
records = []
for prompt_path in sys.argv[1:]:
system = (HERE / prompt_path).read_text()
jobs = [case for case in cases for _ in range(RUNS)]
with ThreadPoolExecutor(10) as pool:
outputs = list(pool.map(lambda c: classify(system, c["text"]), jobs))
passed = sum(out == case["label"] for out, case in zip(outputs, jobs))
print(f"{prompt_path}:{passed}/{len(jobs)} 通过({passed / len(jobs):.0%})")
for i, case in enumerate(cases):
answers = outputs[i * RUNS:(i + 1) * RUNS]
right = sum(a == case["label"] for a in answers)
records.append({"prompt": prompt_path, "text": case["text"], "label": case["label"], "outputs": answers})
if right == 0:
print(f" 全错 {case['text']} 标注={case['label']} 模型={answers}")
elif right < RUNS:
print(f" 不稳 {case['text']} 标注={case['label']} 模型={answers}")
with open(HERE / "results.jsonl", "w") as f:
for r in records:
f.write(json.dumps(r, ensure_ascii=False) + "\n")
Deux choix de conception méritent une explication.
Chaque cas est exécuté 3 fois. Cette fois, je n'ai pas mis la température à 0 : j'ai gardé la température par défaut, comme en utilisation réelle. La leçon 3 du module 01 l'a montré : une même entrée peut donner des résultats différents. En n'exécutant qu'une fois, on ne distingue pas « juste de façon stable » de « juste par chance ». Avec 3 exécutions, les cas se répartissent en trois groupes : toujours justes, toujours faux, tantôt justes tantôt faux.
Les résultats sont enregistrés. La sortie brute de chaque exécution est écrite dans results.jsonl. Après une modification de prompt, on peut comparer ligne à ligne les anciens et les nouveaux résultats pour voir exactement quels cas se sont améliorés ou dégradés.
Exécution :
python prompt_test.py prompts/classify_v1.txt prompts/classify_v2.txt
Résultats
prompts/classify_v1.txt:71/90 通过(79%)
全错 怎么给单个请求设置不同的超时时间? 标注=使用问题 模型=['功能建议', '功能建议', '功能建议']
全错 你们的文档网站打不开了 标注=其他 模型=['缺陷', '缺陷', '缺陷']
全错 文档里 Limits 那一节的示例代码跑不通,max_keepalive 这个参数名好像不对 标注=其他 模型=['缺陷', '缺陷', '缺陷']
全错 response.elapsed 在流式请求里读出来一直是 0,这正常吗 标注=使用问题 模型=['缺陷', '缺陷', '缺陷']
不稳 同样的代码,requests 返回 200,httpx 返回 403 标注=使用问题 模型=['使用问题', '使用问题', '其他']
全错 follow_redirects=True 时,301 跳转后 POST 变成了 GET 标注=使用问题 模型=['缺陷', '缺陷', '缺陷']
全错 能不能出一个视频教程 标注=其他 模型=['功能建议', '功能建议', '功能建议']
prompts/classify_v2.txt:81/90 通过(90%)
不稳 httpx 支持 HTTP/3 吗? 标注=使用问题 模型=['功能建议', '功能建议', '使用问题']
不稳 文档里 Limits 那一节的示例代码跑不通,max_keepalive 这个参数名好像不对 标注=其他 模型=['其他', '缺陷', '其他']
全错 同样的代码,requests 返回 200,httpx 返回 403 标注=使用问题 模型=['缺陷', '缺陷', '缺陷']
全错 follow_redirects=True 时,301 跳转后 POST 变成了 GET 标注=使用问题 模型=['缺陷', '缺陷', '缺陷']
Le score global passe de 79 % à 90 %. Mais à ne regarder que le score global, on rate beaucoup de choses ; examinons les cas un par un.
Lire les résultats : ce qui est réparé, ce qui est cassé
Réparés. « Comment définir un délai différent pour une seule requête », « votre site de documentation ne s'ouvre plus », « pourriez-vous faire un tutoriel vidéo », « response.elapsed… est-ce normal », toujours faux en v1, sont justes en v2. Les définitions de v2 précisent justement « même si cela ressemble à une demande de fonctionnalité, si httpx le fait déjà, c'est une question d'utilisation » et « site de documentation, communauté… relèvent de autre ».
Cassé. « Le même code : requests renvoie 200, httpx renvoie 403 » était juste 2 fois sur 3 en v1, et devient faux 3 fois sur 3 en v2, toujours classé « bug ». C'est exactement ce qu'on rate en ne regardant que le score global : il a augmenté, mais un cas qui marchait à peu près s'est dégradé.
Nouvelle instabilité. « httpx prend-il en charge HTTP/3 ? » est classé « suggestion de fonctionnalité » 2 fois sur 3 en v2.
Toujours faux. « Après une redirection 301, le POST devient un GET » est faux dans les deux versions ; le modèle s'obstine à y voir un bug.
Soupçonner d'abord l'étiquette, ensuite le modèle
Face à un cas faux, la première chose à faire n'est pas de modifier le prompt, mais de vérifier que l'étiquette elle-même est correcte.
« Après une redirection 301, le POST devient un GET » : je l'ai étiqueté « question d'utilisation », car c'est le comportement normal de httpx. Mais il faut que je le vérifie. Dans le code source de httpx, httpx/_client.py, _redirect_method contient :
# If a POST is responded to with a 301, turn it into a GET.
# This bizarre behaviour is explained in 'requests' issue 1704.
if response.status_code == codes.MOVED_PERMANENTLY and method == "POST":
method = "GET"
C'est un choix délibéré, qui reprend le comportement des navigateurs et de requests ; l'étiquette est donc correcte, c'est le modèle qui ignore ce détail. Ce genre d'erreur se corrige difficilement en modifiant le prompt, car le problème vient des connaissances du modèle. On peut l'accepter, ou confier ce type de question sur « tel comportement est-il normal » à un système capable de consulter la documentation (c'est le RAG du module 04).
« requests renvoie 200, httpx renvoie 403 », c'est autre chose. Je l'avais étiqueté « question d'utilisation » parce que cela vient généralement d'une différence d'en-têtes (par exemple un User-Agent par défaut différent), qu'il suffit d'ajuster. Mais à la réflexion, rien dans le message ne permet d'en voir la cause, et le considérer comme « un comportement de la bibliothèque qui ne correspond pas aux attentes » se défend aussi. L'étiquette de ce cas est elle-même discutable. Que le modèle le classe « bug » 3 fois sur 3 ne signifie pas forcément qu'il se trompe.
Face à un tel cas, trois options : corriger l'étiquette ; réécrire le message pour le rendre plus explicite ; ou admettre qu'il est ambigu, le retirer du jeu de test ou accepter les deux réponses. Ne passez pas votre temps à ajuster le prompt pour que le modèle « réponde juste » à un cas discutable : ce ne serait qu'ajuster le modèle à une décision arbitraire de votre part.
Le rythme des itérations
Un rythme qui fonctionne :
- Lancer les tests, noter le score global et le résultat de chaque cas.
- Choisir une catégorie d'erreurs (pas un seul cas) et en comprendre la cause. Vérifier d'abord l'étiquette.
- Modifier le prompt, en ne visant que cette catégorie d'erreurs.
- Relancer, et comparer cas par cas avec l'exécution précédente : combien de cas réparés, combien de cas cassés.
- S'il y a plus de cas cassés que réparés, revenir en arrière.
Ne modifier qu'une chose à la fois permet de connaître l'effet de chaque changement. En modifiant cinq choses d'un coup, si le score change, vous ne savez pas laquelle a joué.
Les limites de cet outil
C'est une version légère, adaptée aux tâches à réponse unique comme la classification ou l'extraction. Elle a quelques faiblesses évidentes :
- 30 cas, c'est encore trop peu. L'écart entre 90 % et 79 % est assez crédible, mais entre 90 % et 88 %, ce peut être une simple fluctuation.
- Elle ne juge que la correspondance exacte. Quand la réponse est un texte (réponse de service client, résumé), on ne peut pas juger avec
==. - Elle n'enregistre ni coût ni durée.
Le module 06 l'étend en un système d'évaluation complet : un jeu d'évaluation plus grand, un modèle qui note les réponses ouvertes, des journaux avec le coût de chaque appel.
Exercices
- Lancez
prompt_test.pyet voyez en quoi vos résultats diffèrent des miens. Relancez deux fois : le score global est-il le même à chaque fois ? - Pour le cas discutable « requests renvoie 200, httpx renvoie 403 », prenez votre décision (corriger l'étiquette, réécrire le message ou le retirer) et justifiez-la.
- Écrivez un
classify_v3.txtqui tente de corriger l'instabilité de « httpx prend-il en charge HTTP/3 ? » sans dégrader les autres cas. Comparez v2 et v3 cas par cas avecresults.jsonl. - Ajoutez une fonction à
prompt_test.py: afficher le coût total de chaque version (avec lecost_usdde la leçon 4 du module 01). Le prompt de v2 est bien plus long : combien coûte-t-il de plus ?
Auto-test
1. Pourquoi exécuter chaque cas de test 3 fois plutôt qu'une ?
La sortie du modèle comporte une part d'aléatoire ; une même entrée peut donner des résultats différents. En n'exécutant qu'une fois, on ne distingue pas « juste de façon stable » de « juste par chance ». Plusieurs exécutions révèlent les cas instables, qui correspondent souvent aux zones floues que le prompt ne précise pas.
2. Le nouveau prompt obtient un meilleur score global que l'ancien. Peut-on l'adopter directement ?
Il faut encore examiner les cas un par un. Le score global peut augmenter alors que des cas qui marchaient se dégradent, comme « requests renvoie 200, httpx renvoie 403 » dans cette leçon. Il faut vérifier si les cas dégradés concernent des situations importantes, et si le problème vient du modèle ou de l'étiquette, avant de décider.
3. Un cas est toujours faux avec les deux versions du prompt. Que faire ?
Vérifier d'abord que l'étiquette elle-même est correcte, en consultant si nécessaire la documentation ou le code source. Si l'étiquette est discutable, la corriger ou retirer le cas. Si l'étiquette est bien correcte et que l'erreur vient d'un manque de connaissances du modèle, modifier le prompt ne suffit souvent pas ; on peut accepter l'erreur, ou fournir de la documentation au modèle avec une méthode comme le RAG.
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…