Mehrstufige Gespräche: Das Modell merkt sich eigentlich nichts
Ein Chatprogramm für die Kommandozeile schreiben und selbst sehen, dass das Modell kein Gedächtnis hat und „Erinnerung“ nur heißt, den Verlauf jedes Mal neu zu senden. Dann, wie gut Kürzen und Zusammenfassen funktionieren, wenn der Verlauf zu lang wird.
- Etwa 35 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.
Wer schon einmal einen Chatbot benutzt hat, hat ein Gefühl: Er erinnert sich an das, was ich vorher gesagt habe. Sagst du ihm „Ich heiße Xiao Wang“ und fragst ein paar Sätze später „Wie heiße ich?“, kann er antworten.
Dieses Gefühl täuscht, zumindest bei der API. Das Modell selbst merkt sich nichts; jeder Aufruf ist völlig neu. Dass ein Chatbot sich „erinnert“, liegt daran, dass das Programm dem Modell jedes Mal das gesamte bisherige Gespräch erneut schickt. In dieser Lektion schreiben wir selbst ein Chatprogramm, um das klar zu sehen.
Erst sehen, dass das Modell sich nichts merkt
code/03-llm-apps/conversation.py ist ein Chatprogramm für die Kommandozeile; mit der Option --stateless sendet es jedes Mal nur den aktuellen Satz, ohne Verlauf. Ich füttere es per Pipe mit zwei Sätzen:
printf '%s\n' "我叫小王,最近在用 httpx 写一个爬虫。" "我叫什么名字?" | python conversation.py --stateless
你:我叫小王,最近在用 httpx 写一个爬虫。
助手:好的,httpx 很适合爬虫。有什么具体问题需要帮忙吗? (本轮输入 32 词元)
你:我叫什么名字?
助手:你没有告诉我你的名字。 (本轮输入 23 词元)
Aus Sicht des Modells enthält der zweite Aufruf nur den Satz „我叫什么名字?“ (wie heiße ich?); natürlich weiß es das nicht.
Gedächtnis ist die Nachrichtenliste
Damit es sich „erinnert“, muss man nur das bisherige Gespräch in messages packen. Der Kern des Programms sind diese paar Zeilen:
SYSTEM = {"role": "system", "content": "你是一个说话简短的编程助手,每次回答不超过三句话。"}
history = [] # 只放 user 和 assistant 消息,system 每次单独加在最前面
while True:
user_text = input("你:").strip()
if not user_text:
break
messages = [SYSTEM] + history + [{"role": "user", "content": user_text}]
response = client.chat.completions.create(model=MODEL, messages=messages, extra_body=NO_THINKING)
answer = response.choices[0].message.content
print(f"助手:{answer} (本轮输入 {response.usage.prompt_tokens} 词元)")
history += [{"role": "user", "content": user_text}, {"role": "assistant", "content": answer}]
In jeder Runde wird das, was der Nutzer sagt, als user-Nachricht und die Antwort des Modells als assistant-Nachricht an history angehängt. Beim nächsten Aufruf wird der ganze history zusammen mit der neuen Frage gesendet.
Ich füttere es mit sechs Sätzen:
你:我叫小王,最近在用 httpx 写一个爬虫。
助手:好的,httpx 支持同步和异步请求,写爬虫时注意设置 timeout 和重试。有什么具体问题吗? (本轮输入 32 词元)
你:我叫什么名字?
助手:你叫小王。 (本轮输入 68 词元)
你:我在用什么库做什么?
助手:你在用 httpx 写爬虫。 (本轮输入 81 词元)
你:推荐一个解析 HTML 的库。
助手:推荐 parsel 或 BeautifulSoup,异步场景可选 selectolax。 (本轮输入 102 词元)
你:它和 httpx 怎么配合用?一句话。
助手:把 httpx 返回的 `response.text` 传给解析库(如 `BeautifulSoup(response.text, "lxml")`)即可。 (本轮输入 132 词元)
你:我叫什么名字?
助手:小王。 (本轮输入 170 词元)
Jetzt kennt es den Namen und versteht auch, dass mit „es“ in „它和 httpx 怎么配合用“ (wie benutzt man es zusammen mit httpx) die in der Runde zuvor empfohlene Parser-Bibliothek gemeint ist.
Achte auf die Zahl der Eingabe-Tokens am Ende jeder Zeile: 32, 68, 81, 102, 132, 170, von Runde zu Runde mehr. Denn in jeder Runde wird das gesamte bisherige Gespräch erneut gesendet.
Der Verlauf wird immer länger
Daraus folgt das Kernproblem mehrstufiger Gespräche: Der Verlauf wächst nur.
- Immer teurer. Die Eingabe der sechsten Runde ist über fünfmal so groß wie die der ersten. In einem Gespräch mit hundert Runden zahlt jede Runde für die neunundneunzig davor. Zum Glück ist der Anfang des Verlaufs fest und trifft den Cache aus Modul 01, Lektion 4, was viel billiger ist, aber es wächst trotzdem.
- Immer langsamer. Je länger die Eingabe, desto länger braucht das Modell, um sie zu lesen.
- Irgendwann wird das Kontextfenster überschritten. Dann schlägt die Anfrage direkt fehl.
Jedes Chatprogramm, das wirklich live geht, muss die Länge des Verlaufs begrenzen. Zwei Methoden sind üblich.
Methode eins: kürzen
Am einfachsten: nur die letzten paar Nachrichten behalten und die älteren verwerfen. Das tut die Option --max-messages 4 von conversation.py: Wird der Verlauf länger als 4 Nachrichten, bleiben nur die letzten 2.
def trim(history):
if len(history) <= args.max_messages:
return history
keep = history[-(args.max_messages // 2):]
dropped = history[: len(history) - len(keep)]
print(f" [丢掉了最早的 {len(dropped)} 条消息]")
return keep
Dieselben sechs Sätze:
你:我叫小王,最近在用 httpx 写一个爬虫。
助手:好的,小王。请问在 httpx 爬虫过程中遇到了什么问题? (本轮输入 32 词元)
你:我叫什么名字?
助手:你叫小王。 (本轮输入 55 词元)
你:我在用什么库做什么?
助手:你在用 httpx 写爬虫。 (本轮输入 68 词元)
[丢掉了最早的 4 条消息]
你:推荐一个解析 HTML 的库。
助手:推荐 BeautifulSoup,配合 httpx 用很方便。 (本轮输入 45 词元)
你:它和 httpx 怎么配合用?一句话。
助手:用 `BeautifulSoup(httpx.get(url).text, "html.parser")` 就能解析。 (本轮输入 71 词元)
[丢掉了最早的 4 条消息]
你:我叫什么名字?
助手:我不知道你的名字,你还没告诉我。 (本轮输入 60 词元)
Die Eingabe-Tokens bleiben bei ein paar Dutzend und wachsen nicht mehr. Der Preis: Bei der letzten Frage hat es vergessen, dass der Nutzer Xiao Wang heißt, denn dieser Satz wurde längst verworfen.
Kürzen passt zu Situationen, in denen „die letzten Runden am wichtigsten sind und Früheres vergessen werden darf“, etwa Plaudern oder einmalige einfache Fragen. In echten Projekten kürzt man meist nach Tokens statt nach Nachrichten; hier nutze ich der Einfachheit halber die Zahl der Nachrichten.
Eines ist zu beachten: Beim Kürzen immer „Frage und Antwort“ paarweise verwerfen und nicht nur eine user-Nachricht löschen, während die dazugehörige assistant-Nachricht bleibt; sonst sieht das Modell ein unvollständiges Gespräch. Der Code oben behält immer eine gerade Zahl, daher tritt das nicht auf.
Methode zwei: zusammenfassen
Etwas besser ist es, alte Nachrichten nicht einfach zu verwerfen, sondern vom Modell zu einer Zusammenfassung verdichten zu lassen:
def summarize(messages):
text = "\n".join(f"{m['role']}: {m['content']}" for m in messages)
response = client.chat.completions.create(
model=MODEL,
messages=[{"role": "user", "content": "把下面这段对话压缩成几句话的摘要,保留人名、项目、偏好等以后可能用到的事实:\n\n" + text}],
extra_body=NO_THINKING,
)
return response.choices[0].message.content
Beim Kürzen geht der verworfene Teil an summarize, und die entstandene Zusammenfassung kommt als Nachrichtenpaar an den Anfang des Verlaufs:
summary = summarize(dropped)
return [{"role": "user", "content": f"(之前对话的摘要:{summary})"},
{"role": "assistant", "content": "好的,我记住了。"}] + keep
Mit der Option --summarize noch einmal:
你:我叫小王,最近在用 httpx 写一个爬虫。
助手:好的小王,httpx 适合爬虫,支持异步和 HTTP/2。有具体问题可以问我。 (本轮输入 32 词元)
你:我叫什么名字?
助手:你叫小王。 (本轮输入 63 词元)
你:我在用什么库做什么?
助手:你在用 httpx 写爬虫。 (本轮输入 76 词元)
[把最早的 4 条消息压缩成了摘要:小王在用 httpx 写爬虫;httpx 支持异步和 HTTP/2,适合爬虫。]
你:推荐一个解析 HTML 的库。
助手:推荐 BeautifulSoup 或 lxml,前者易用,后者更快。 (本轮输入 83 词元)
[把最早的 4 条消息压缩成了摘要:小王在用 httpx 写爬虫;httpx 支持异步和 HTTP/2,适合爬虫。]
你:它和 httpx 怎么配合用?一句话。
助手:用 httpx 获取响应后,把 `resp.text` 传给 BeautifulSoup 解析即可。 (本轮输入 95 词元)
[把最早的 4 条消息压缩成了摘要:小王在用 httpx 写爬虫,需要 HTML 解析库;推荐了 BeautifulSoup(易用)和 lxml(更快)。]
你:我叫什么名字?
助手:你叫小王。 (本轮输入 104 词元)
[把最早的 4 条消息压缩成了摘要:小王在用 httpx 写爬虫,需 HTML 解析库;推荐了 BeautifulSoup(易用)和 lxml(更快)。与 httpx 配合时,把 `resp.text` 传给 BeautifulSoup 解析即可。]
Bei der letzten Frage erinnert es sich. Die Zusammenfassung hat Kernfakten wie „Xiao Wang“, „httpx“ und „Crawler“ behalten. Die Eingabe-Tokens sind geringer als ohne Kürzen (104 gegen 170), aber mehr als beim bloßen Kürzen, weil auch die Zusammenfassung Platz braucht.
Auch Zusammenfassen hat seinen Preis:
- Jedes Verdichten braucht einen zusätzlichen Modellaufruf, der Geld kostet und diese Runde langsamer macht. Man kann es im Hintergrund asynchron tun oder erst verdichten, wenn der Verlauf eine gewisse Länge erreicht, statt in jeder Runde.
- Zusammenfassungen verlieren Details. Sie behalten nur, was das Modell für wichtig hält. Erwähnt der Nutzer in Runde 3 nebenbei „mein Server steht in Hongkong“, steht das in der Zusammenfassung vielleicht nicht mehr.
- Zusammenfassungen können falsch sein. Das Modell kann Fakten falsch zusammenfassen.
Schau genau auf die Ausgabe oben: Jede Verdichtung fasst „alte Zusammenfassung + neu verworfene Nachrichten“ erneut zusammen; die Zusammenfassung wird laufend fortgeschrieben.
Welche Methode
| Situation | Vorgehen |
|---|---|
| Gespräche sind meist kurz, etwa Kundenservice-Fragen | nichts tun oder eine großzügige Grenze setzen und einfach kürzen |
| Lange Gespräche, aber nur das Neueste zählt | kürzen |
| Lange Gespräche, und frühere Informationen werden später gebraucht | zusammenfassen, oder kürzen plus zusammenfassen |
| Informationen über viele Sitzungen hinweg merken, etwa Vorlieben des Nutzers | wichtige Informationen separat speichern und bei Bedarf in den Kontext holen; das ist das „Langzeitgedächtnis“ aus Modul 05, Lektion 5 |
Häufige Probleme
Die system-Nachricht landete im history und wurde mitgekürzt: Die system-Nachricht gehört jedes Mal separat an den Anfang und wird nie gekürzt. Deshalb speichert der Code oben sie getrennt von history.
Die Verläufe mehrerer Nutzer vermischen sich: Als Webdienst braucht jeder Nutzer und jede Sitzung einen eigenen history, meist in einer Datenbank oder einem Cache, unterschieden durch eine Sitzungs-ID. Eine globale Variable history taugt nur für ein Kommandozeilenprogramm mit einem einzigen Nutzer.
Übungen
- Führ
conversation.pyaus, chatte zehn Runden oder mehr und beobachte die Eingabe-Tokens pro Runde. Chatte dann mit--max-messages 6und finde den Moment, in dem es etwas vergisst. - Schreib
trimso um, dass nach Tokens statt nach Nachrichten gekürzt wird: Übersteigt der Verlauf insgesamt 500 Tokens, werden von den ältesten an paarweise Nachrichten verworfen. Dafür kannst duusage.prompt_tokensder vorigen Runde nutzen. - Erwähne in Runde 3 nebenbei ein Detail (etwa „mein Server steht in Hongkong“) und frag in Runde 8 danach. Lass es einmal mit Kürzen und einmal mit Zusammenfassen laufen und schau, ob die Zusammenfassung das Detail bewahrt. Falls nicht, versuch den Prompt von
summarizezu verbessern.
Selbsttest
1. Wie „merkt“ sich das Modell, was du vorher gesagt hast?
Das Modell selbst merkt sich nichts. Das Programm schickt bei jedem Aufruf alle bisherigen user- und assistant-Nachrichten zusammen mit der neuen Frage an das Modell, und das Modell „liest“ den früheren Inhalt aus diesen Nachrichten.
2. Warum ist in einem mehrstufigen Gespräch jede Runde teurer als die vorige?
In jeder Runde wird der gesamte bisherige Gesprächsverlauf erneut als Eingabe gesendet; je länger der Verlauf, desto mehr Eingabe-Tokens und desto höher die Kosten. Der gleiche Anfang des Verlaufs kann den Cache treffen und ist viel billiger, aber die gesamte Eingabe wächst weiter.
3. Welche Nachteile haben Kürzen und Zusammenfassen jeweils?
Kürzen verwirft frühere Informationen vollständig, etwa den Namen des Nutzers, sodass spätere Fragen danach nicht beantwortet werden können. Zusammenfassen bewahrt Kernfakten, braucht aber für jedes Verdichten einen zusätzlichen Modellaufruf, verliert Details, die das Modell für unwichtig hält, und kann auch falsch zusammenfassen.
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…