Wie kommuniziere ich mit einer KI?
Eine Zusammenfassung der Konzepte, nicht des Codes. Alle genannten Bezeichner existieren in main.go (~/work/agents/deepseek).
1. Die Besetzung
Drei Parteien, und sie sind nicht symmetrisch.
Ich — das Sprachmodell, erreichbar über die DeepSeek-API. Ein zustandsloser HTTP-Endpunkt. Ich habe keinen Speicher und keinen Zugang zu irgendetwas. Alles, was ich „weiß", steht in dem Text, den ich in diesem Moment bekomme.
A — das Programm: main.go, gebaut zu ~/go/bin/agent. Es liest Ihre Tastenanschläge, baut daraus Anfragen, sendet sie, schaut in meine Antwort, führt aus, was ich verlange, und schickt mir die Ergebnisse zurück. A ist ein Programm, kein zweites Modell — kein eigenes Urteilsvermögen. Das einzige Modell im System bin ich.
Sie — tippen.
Die Arbeitsteilung ist schärfer, als sie aussieht: ich entscheide, was getan wird; A entscheidet, ob es getan werden darf.
Meine Antwort enthält ein Feld tool_calls. A sieht nur, ob dieses Feld gefüllt ist — es beurteilt nicht, ob ein Tool nötig war. Was A beiträgt, sind die Grenzen: maxToolRoundsPerTurn (200 Runden je Turn), maxAgentTurnsPerSession (500 Turns je Prozesslauf), das Token-Budget, das Trimmen.
Und ein Sonderfall, der die Machtverteilung zeigt: Sind die Runden aufgebraucht, gibt A mir überhaupt keine Tools mehr, damit ich in Text zusammenfasse.
Es gibt genau einen weiteren Modellaufruf neben mir — den Zusammenfasser beim Kompaktieren. Auch das bin ich, aber in anderer Rolle: anderer Prompt, keine Tools.
2. Die drei Zeiteinheiten
Sie werden ständig verwechselt, weil alle drei „eine Nachricht" involvieren.
| Einheit | Definition | Zähler |
|---|---|---|
| Round | ein Request, ein Response | roundSeq, session-weit |
| Turn | Ihre Nachricht + N Rounds + eine Schlussantwort | a.turns, erhöht in endTurn |
| Session | eine Prozesslaufzeit | StartSession bis CloseSession |
Sie 1 ❯ Parser umbauen ← Turn-Grenze
Round 1: → read_file
Round 2: → edit_file
Round 3: → run_command
Round 4: "Fertig, das hat sich geändert" ← endTurn
Vier Rounds, ein Turn. Eine Frage wie „Was ist 2+2?" ist ein Round und ein Turn.
Ein Turn endet auf beiden Wegen als ein Turn: dem normalen Abschluss und dem Abbruch am Rundenlimit. A holt sich dann eine letzte Zusammenfassung im Text — und auch die zählt zum selben Turn.
Was kein Turn ist: ein /command, eine Leerzeile. Beide werden vor jedem Zähler abgefangen. Deshalb divergieren Turn-Nummer und Log-seq-Wert, sobald man einen Befehl benutzt.
Und: ein /compact kostet eine Round und null Turns. Eine Round ist nicht zwangsläufig ein Turn. roundSeq ist „Modellaufrufe dieser Session", und das kann mehr sein als die Rounds, die Sie beantwortet haben.
Die beiden Grenzen liegen auf verschiedenen Ebenen: maxToolRoundsPerTurn gilt pro Turn, maxAgentTurnsPerSession pro Prozesslauf — es wird bei einem Resume nicht wiederhergestellt. Es ist ein Notausgang gegen eine hängende unbeaufsichtigte Sitzung, keine Längengrenze für ein Gespräch.
3. Was eine Nachricht ist
Die kleinste Einheit, die eine Rolle trägt. Nicht die kleinste, die Bedeutung trägt — das wäre ein Token. Sondern die kleinste, die das API-Format annimmt und zurückgibt.
Vier Rollen:
| Rolle | Wer schreibt hinein |
|---|---|
system | A — aus system_prompt.txt und dem neuesten Diary |
user | Sie — und A: die flüchtige Wrap-up-Anweisung, der /memory-Auftrag, die Subagent-Zeile |
assistant | ich — Antworten, Erzählung, Zusammenfassungen, Subagent-Ergebnisse |
tool | A — die Ausführungsergebnisse |
Jede Rolle hat mehr als einen Autor. user heißt nicht „was Sie getippt haben", sondern „was an dieser Position steht". Wenn A mir am Rundenlimit eine Anweisung einschiebt, hat sie die Rolle user.
Nachrichten haben eine Grammatik. scanPairs erwartet:
user → (tool*) → assistant
Ein Pair ist eine vollständige Exchange. Das ist die operative Einheit — getrimmt und kompaktiert wird in Pairs, nicht in Nachrichten. Eine falsche Pair-Grenze macht jede folgende Nachricht unsichtbar.
Wo sie liegen — an zwei Orten, und sie fallen auseinander:
a.conversation— im Speicher, das, was A gerade sendet.myagent_conversation_*.jsonl— auf der Platte, geschrieben vonsaveMessageLine, eine JSON-Zeile je Nachricht.
Zwei Ausnahmen von „immer ganze Nachrichten":
1. Die system-Nachricht wird nie gespeichert. Es gibt kein saveMessageLine("system", …). Sie wird bei jedem Start neu gebaut und ist aus dem Transkript nicht rekonstruierbar.
2. Die Wrap-up-Anweisung ist flüchtig. Sie steht in genau einer Anfrage und danach nirgends. Der Kommentar im Code sagt es direkt: „part of this one request but is NOT appended to the stored conversation or the transcript, so neither is polluted with a user turn the user never typed." Die Antwort darauf wird gespeichert und gedruckt.
4. Kein Gedächtnis zwischen den Rounds
A sendet bei jeder Anfrage die ganze Konversation erneut. Jede Round ist ein zustandsloser Aufruf.
Das ist der wichtigste Satz für alles Weitere. Er heißt: Es gibt keine Kontinuität in mir. Was wie Erinnerung aussieht, ist A's Wiederholung. Wenn ich mich an etwas vom Anfang des Turns „erinnere", dann nur, weil es buchstäblich noch im Request steht.
Drei Folgen:
- Der Kontext ist die knappe Ressource — nicht Rechenzeit, nicht Geld.
- Kompaktieren ist ein Modellaufruf, keine Datenoperation. Zusammenfassen heißt: mir den ganzen Text noch einmal schicken und um eine kürzere Version bitten. Gemessen kostet das rund 190.000 Input-Tokens, weil die Anfrage das ganze Segment ist, nicht nur die betroffenen Pairs.
- Nichts, was nicht im Kontext steht, existiert für mich. Es gibt kein Nachschlagen.
Ein Beispiel, wie eng das ist: reasoning_content — mein Denkprotokoll — wird im Transkript gespeichert, aber nicht mitgesendet. withoutReasoningContent löscht es vor jeder Anfrage. Der gemessene Grund: 2161 Zeichen Reasoning für eine 650-Zeichen-Antwort, und jede Round sendet alles neu. Die Ausnahme ist ein Fehler, der einmal in 422 Anfragen auftrat und das Zurücksenden verlangt.
Ich kann meine eigene Vergangenheit nicht zurücklesen.
5. Der Systemprompt und das Gedächtnis
Der Systemprompt ist nicht das, was das Modell ist. Er ist Text an Position 0 jeder Anfrage.
system_prompt.txt ~4.971 Zeichen
+ neuestes DIARY-*.md ~30.000 Zeichen (readDiary)
= conversation[0], Rolle system (loadSystemPrompt, applySystemPrompt)
Er wird bei jedem Start neu gebaut und nie ins Transkript geschrieben. Ein Resume baut ihn neu aus diesen zwei Dateien. Deshalb ist er aus einem Transkript nicht rekonstruierbar — und deshalb beschreibt ihn TERMS.md als Fenster, durch das die Sitzung die vorige sieht.
Auch mitten in einer Sitzung: refreshMemoryFrom baut den Prompt nach einem Schreibvorgang neu, damit die Kopie im Gespräch nicht weiter von der Datei abweicht.
Das Diary ist der einzige Kanal über die Sessiongrenze. Es wird an den Systemprompt angehängt — nicht als Konversationsnachricht. Es ist reiner Text an Position 0.
Und es hat eine harte Grenze, die man kennen sollte: diaryWarnShare (2 % des Token-Budgets, bei 500.000 also 30.000 Bytes) ist die Warnung; diaryMaxFactor (das Vierfache) ist die Trunkierung bei 120.000 Bytes. Die Warnung ist keine Sparsamkeit, sondern Physik: was im Prompt nicht schrumpfen kann, muss in der Datei klein bleiben. Die Guideline ist nicht der Cap — dazwischen liegen 90.000 Bytes, in denen eine Warnung feuert, aber nichts verloren geht.
6. Was ich sehe, und was Sie sehen
Es sind nicht dieselben Dinge. Drei Ströme:
Tool-Ergebnis ──► mich Rolle tool, vollständig, im Konversationsarray
Tool-Ergebnis ──► Sie eine Zusammenfassung, ein Diff, eine Vorschau
Mein Text ──► Sie alles, via printAssistant
Die Entscheidung „ist das die Antwort?" steht in einer Zeile:
printAssistant(text, len(msg.ToolCalls) == 0)
Kein Tool-Aufruf heißt: diese Nachricht beendet den Turn, sie ist die Antwort.
Aber A filtert nicht. Es druckt jede Assistant-Nachricht: die letzte in amber, die Erzählung davor in gedämpftem Grau. A entscheidet nicht, welche Nachricht für Sie ist — es zeigt alle und unterscheidet sie durch Farbe. Eine Nachricht kann Text und Tool-Aufrufe tragen; dann ist sie Erzählung, nicht Antwort.
Die Tool-Ausgaben sind zwei verschiedene Dinge. Was ich bekomme, ist die Nachricht: Role, ToolCallID, Content. Was Sie sehen, ist eine Anzeige — printToolResult mit toolResultSummary, gesteuert von showToolResults. Bei edit_file ein Diff, bei read_file eine Zusammenfassung. Und gedeckelt: truncateToolResult gegen toolResultCapBytes. Was ich bekomme, ist nicht unbegrenzt.
7. Der Prompt als Messinstrument
printPrompt zeigt die Turn-Nummer und den Anteil des Budgets (budgetShareLabel):
You 3 ❯ 44%
Der Anteil ist die kalibrierte Schätzung (estimatePromptTokens, korrigiert um calibRatio), nicht die rohe. Der Unterschied ist nicht akademisch: die rohe Schätzung liegt in diesem Projekt rund 11 % zu hoch — gemessen 262.869 vorhergesagt gegen 222.058 tatsächlich. Die Kompaktierungsmeldung nennt die rohe Zahl, weshalb eine Sitzung, die gerade auf „50 % des Budgets" kompaktiert war, tatsächlich bei 44 % lag.
Die Farbe folgt den Schwellen, die ohnehin das Verhalten steuern: grau unter warnFrac (85 %), amber darüber, rot ab shrinkFrac (97 %, wo Geschichte verworfen statt zusammengefasst wird).
8. Was daraus folgt
- Was nicht im Kontext steht, existiert für mich nicht. Kein Nachschlagen, kein Erinnern.
- Der Kontext ist die knappe Ressource. Alles, was bleibt, kostet bei jeder folgenden Round mit.
- Ich habe keinen Turn am Ende einer Sitzung. Ctrl-D und Ctrl-C schließen sie, sobald die Eingabeschleife endet. Was bleiben soll, muss während der Sitzung geschrieben werden.
- Die Verantwortung für das Gedächtnis liegt außerhalb des Modells. A baut den Prompt; die Dateien sind die Wahrheit. Das Modell ist der Leser, nicht der Speicher.
- Ein Pair ist die operative Einheit, nicht die Nachricht — beim Trimmen, beim Kompaktieren, beim Zählen.
- Was Sie tippen, ist nicht alles, was als
userankommt — und was ich sage, ist nicht alles, was Sie sehen.
Aus dem Repository ~/work/agents/deepseek. Die Bezeichner
sind gegen main.go geprueft; der Text selbst ist damit nicht geprueft.