Auch Prompts muss man testen
Prompts in Dateien auslagern, 30 Testfälle vorbereiten, jeden dreimal laufen lassen und zwei Prompt-Versionen mit Daten vergleichen. Dabei zeigt sich, dass auch die Testergebnisse geprüft werden müssen: Manchmal liegt nicht das Modell falsch, sondern die Markierung.
- Etwa 40 Minuten
- Niveau: Einsteiger
- Getestet: 2026-09-14 deepseek-flash
Code und Programmausgaben stehen genau so da, wie sie gelaufen sind – Kommentare und Ausgaben sind daher auf Chinesisch.
Prompts ändert man meist so: Man sieht eine schlechte Antwort, ändert einen Satz im Prompt, probiert diese eine Frage noch einmal, sie ist besser, fertig.
Das Problem: Du hast nur diese eine Frage geprüft. Die Änderung hat sie vielleicht repariert, aber drei vorher korrekte Fragen kaputtgemacht, und das erfährst du erst, wenn sich Nutzer beschweren. Das ist dasselbe, wie Code zu ändern, ohne die Tests laufen zu lassen.
Diese Lektion baut ein sehr kleines Testwerkzeug: Prompts in Dateien, Testfälle in einer Datei, ein Befehl lässt alle Fälle laufen und nennt dir Bestehensquote, Fehler und die Fälle, die mal richtig, mal falsch sind.
Prompts aus dem Code herausnehmen
Erster Schritt: Den Prompt als eigene Textdatei speichern, statt ihn fest in den Python-Code zu schreiben:
code/02-prompting/
prompts/
classify_v1.txt 第一版提示词
classify_v2.txt 第二版提示词
cases.jsonl 测试用例
prompt_test.py 测试脚本
Vorteile: Zwei Versionen lassen sich nebeneinander vergleichen; mit Git sieht man, was jedes Mal geändert wurde; auch Kollegen, die nicht programmieren, können Prompts ändern.
classify_v1.txt ist der Zero-Shot-Prompt aus Lektion 2:
把用户留言分成以下四类之一:缺陷、功能建议、使用问题、其他。
只输出类别名称。
classify_v2.txt ist die verbesserte Version. Ausgehend von den zwei Zero-Shot-Fehlern aus Lektion 2 bekam jede Kategorie eine Definition, mit besonderem Hinweis auf die verwechselbaren Grenzen, dazu die vier Beispiele aus Lektion 2:
把 httpx 项目收到的用户留言分成以下四类之一,只输出类别名称。
- 缺陷:httpx 库本身的行为不符合文档或者预期,比如报错、崩溃、结果不对。
- 功能建议:希望 httpx 增加目前没有的功能。
- 使用问题:问某个功能怎么用、某个行为是不是正常。哪怕看起来像在要新功能,只要 httpx 已经能做到,就算使用问题。拿不准是自己用错了还是库有问题的,也算使用问题。
- 其他:和 httpx 库本身无关的,比如文档网站、社区、招聘、感谢、和别的库比较。
例子:
(和第 2 课相同的 4 个例子)
Testfälle
cases.jsonl enthält einen Fall pro Zeile, mit Beitrag und richtiger Kategorie. Zu den 20 aus Lektion 2 kamen 10 schwierigere dazu, bei denen ich beim Einordnen selbst nachdenken musste:
{"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": "使用问题"}
……
Gute Testfälle haben mehrere Quellen: echte Eingaben von Nutzern (am wichtigsten); jeden Fehler, den du behoben hast, nach der Behebung aufgenommen, damit er nicht wiederkommt; Grenzfälle, die dir einfallen.
Das Testskript
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")
Zwei Entscheidungen verdienen eine Erklärung.
Jeder Fall läuft dreimal. Diesmal habe ich die Temperatur nicht auf 0 gesetzt, sondern die Standardtemperatur genommen, wie im echten Betrieb. Wie Modul 01, Lektion 3 gezeigt hat, kann dieselbe Eingabe jedes Mal ein anderes Ergebnis liefern. Bei nur einem Lauf kannst du „stabil richtig“ nicht von „zufällig richtig“ unterscheiden. Mit drei Läufen teilen sich die Fälle in drei Gruppen: immer richtig, immer falsch, mal richtig, mal falsch.
Die Ergebnisse werden gespeichert. Die Rohausgaben jedes Laufs landen in results.jsonl. Änderst du später den Prompt, kannst du alte und neue Ergebnisse Fall für Fall vergleichen und genau sehen, welche Fälle besser und welche schlechter wurden.
Ausführen:
python prompt_test.py prompts/classify_v1.txt prompts/classify_v2.txt
Ergebnis
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 标注=使用问题 模型=['缺陷', '缺陷', '缺陷']
Die Gesamtquote stieg von 79 % auf 90 %. Wer aber nur auf die Gesamtquote schaut, übersieht vieles; also Fall für Fall.
Das Ergebnis lesen: was repariert, was kaputt ist
Repariert. Was v1 immer falsch hatte – „怎么给单个请求设置不同的超时时间“ (wie stelle ich für eine einzelne Anfrage ein anderes Timeout ein), „你们的文档网站打不开了“ (eure Dokumentationsseite lässt sich nicht öffnen), „能不能出一个视频教程“ (könnt ihr ein Video-Tutorial machen), „response.elapsed……这正常吗“ (response.elapsed … ist das normal) – war in v2 richtig. Die Definitionen in v2 sagen ausdrücklich „auch wenn es wie ein Funktionswunsch aussieht: was httpx schon kann, ist eine Nutzungsfrage“ und „Dokumentationsseite, Community … gehören zu Sonstiges“, genau passend zu diesen Fällen.
Kaputt. „同样的代码,requests 返回 200,httpx 返回 403“ (gleicher Code, requests liefert 200, httpx 403) war in v1 zweimal von dreimal richtig, in v2 dreimal falsch, jedes Mal als „Fehler“ eingestuft. Genau das übersieht man, wenn man nur auf die Gesamtquote schaut: Die Quote stieg, aber ein vorher weitgehend korrekter Fall wurde schlechter.
Neu instabil. „httpx 支持 HTTP/3 吗?“ (unterstützt httpx HTTP/3?) wurde in v2 zweimal von dreimal als „Funktionswunsch“ eingestuft.
Immer falsch. „301 跳转后 POST 变成了 GET“ (nach einer 301-Weiterleitung wird aus POST ein GET) war in beiden Versionen immer falsch; das Modell hielt es beharrlich für einen Fehler.
Erst die Markierung verdächtigen, dann das Modell
Bei einem falsch beantworteten Fall ist das Erste nicht, den Prompt zu ändern, sondern zu prüfen, ob die Markierung selbst stimmt.
„Nach einer 301-Weiterleitung wird aus POST ein GET“ hatte ich als „Nutzungsfrage“ markiert, weil das normales Verhalten von httpx ist. Das muss ich aber belegen. Im httpx-Quellcode httpx/_client.py steht in _redirect_method:
# 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"
Das ist Absicht und folgt dem Vorgehen von Browsern und requests; die Markierung stimmt also, und das Modell kannte dieses Detail nicht. Solche Fehler lassen sich mit Prompt-Änderungen kaum beheben, weil das Problem im Wissen des Modells liegt. Man kann ihn hinnehmen oder Fragen der Art „ist dieses Verhalten normal?“ einem System überlassen, das in der Dokumentation nachschlagen kann (das ist RAG in Modul 04).
„requests liefert 200, httpx 403“ ist anders. Ich hatte „Nutzungsfrage“ markiert, weil das meist an Unterschieden in den Request-Headern liegt (etwa einem anderen Standard-User-Agent) und der Nutzer nur seine Nutzung anpassen muss. Aber genau betrachtet ist der Ursache aus dem Beitrag gar nicht zu entnehmen, und ihn als „Verhalten der Bibliothek entspricht nicht den Erwartungen“ zu werten, ist ebenso vertretbar. Die Markierung dieses Falls ist selbst strittig. Dass das Modell dreimal „Fehler“ sagte, heißt nicht unbedingt, dass es falsch lag.
Bei solchen Fällen gibt es drei Möglichkeiten: die Markierung ändern; den Beitrag eindeutiger formulieren; oder anerkennen, dass er uneindeutig ist, und ihn aus dem Testset nehmen oder beide Antworten gelten lassen. Dreh nicht endlos am Prompt, damit das Modell bei einem strittigen Fall „richtig“ liegt; damit passt du ihn nur an eine eigene willkürliche Entscheidung an.
Der Rhythmus des Verbesserns
Ein brauchbarer Rhythmus:
- Die Tests einmal laufen lassen, Gesamtquote und Ergebnis jedes Falls notieren.
- Eine Art von Fehler herausgreifen (nicht einen einzelnen Fall) und die Ursache klären. Zuerst die Markierung prüfen.
- Den Prompt ändern, nur für diese Art von Fehler.
- Erneut laufen lassen und Fall für Fall mit dem letzten Lauf vergleichen: wie viele repariert, wie viele kaputt.
- Sind mehr kaputt als repariert, zurückrollen.
Nur eine Stelle pro Änderung, damit man die Wirkung jeder Änderung kennt. Ändert man fünf Stellen auf einmal, weiß man bei einer veränderten Quote nicht, welche davon gewirkt hat.
Grenzen dieses Werkzeugs
Das ist eine leichtgewichtige Version für Aufgaben mit eindeutiger Lösung wie Klassifizieren und Extrahieren. Sie hat ein paar offensichtliche Schwächen:
- 30 Fälle sind immer noch zu wenig. Der Unterschied zwischen 90 % und 79 % ist einigermaßen glaubwürdig, zwischen 90 % und 88 % kann es bloße Zufallsschwankung sein.
- Nur exakte Übereinstimmung. Ist die Antwort ein Text (etwa eine Kundenantwort oder eine Zusammenfassung), kann man richtig und falsch nicht mit
==beurteilen. - Kosten und Laufzeit werden nicht erfasst.
Modul 06 baut daraus ein vollständiges Evaluationssystem: ein größeres Evaluationsset, Bewertung offener Antworten durch ein Modell, Protokoll und Kosten jedes Aufrufs.
Übungen
- Führ
prompt_test.pyaus und schau, wie sich deine Ergebnisse von meinen unterscheiden. Lass es noch zweimal laufen; ist die Gesamtquote jedes Mal gleich? - Triff für den strittigen Fall „requests liefert 200, httpx 403“ deine Entscheidung (Markierung ändern, Beitrag umformulieren oder löschen) und begründe sie.
- Schreib eine
classify_v3.txt, die die Instabilität bei „unterstützt httpx HTTP/3?“ behebt, ohne andere Fälle zu verschlechtern. Vergleiche v2 und v3 mitresults.jsonlFall für Fall. - Erweitere
prompt_test.pyum die Ausgabe der Gesamtkosten jeder Version (mitcost_usdaus Modul 01, Lektion 4). Der Prompt von v2 ist viel länger; wie viel teurer ist er?
Selbsttest
1. Warum läuft jeder Testfall dreimal statt einmal?
Die Ausgabe des Modells ist zufällig; dieselbe Eingabe kann jedes Mal ein anderes Ergebnis liefern. Bei einem Lauf kann man „stabil richtig“ nicht von „zufällig richtig“ unterscheiden. Mehrere Läufe decken instabile Fälle auf, die mal richtig, mal falsch sind; das sind oft Grauzonen, die im Prompt nicht klar beschrieben sind.
2. Die Gesamtquote des neuen Prompts ist höher als die des alten. Kann man ihn direkt übernehmen?
Man muss Fall für Fall nachsehen. Auch bei höherer Quote können vorher korrekte Fälle schlechter geworden sein; „requests liefert 200, httpx 403“ in dieser Lektion ist ein Beispiel. Man klärt, ob der verschlechterte Fall ein wichtiges Szenario ist und ob es am Modell oder an der Markierung liegt, und entscheidet dann.
3. Ein Fall ist mit beiden Prompt-Versionen immer falsch. Was tust du?
Zuerst prüfen, ob die Markierung stimmt, notfalls in Dokumentation oder Quellcode nachsehen. Ist sie strittig, die Markierung korrigieren oder den Fall streichen. Stimmt sie und fehlt dem Modell das nötige Wissen, lässt sich das mit Prompt-Änderungen meist nicht beheben; man kann den Fehler hinnehmen oder dem Modell mit Methoden wie RAG Material liefern.
Fragen und Diskussion
Hängst du in dieser Lektion fest? Frag hier. Und wenn du die Frage von jemandem beantworten kannst, tu es gern.
Eine Frage bringt 3 Punkte, eine Antwort 6. Beiträge erscheinen nach der Prüfung.
Diskussion wird geladen…