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.

EinheitDefinitionZähler
Roundein Request, ein ResponseroundSeq, session-weit
TurnIhre Nachricht + N Rounds + eine Schlussantworta.turns, erhöht in endTurn
Sessioneine ProzesslaufzeitStartSession 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:

RolleWer schreibt hinein
systemA — aus system_prompt.txt und dem neuesten Diary
userSie — und A: die flüchtige Wrap-up-Anweisung, der /memory-Auftrag, die Subagent-Zeile
assistantich — Antworten, Erzählung, Zusammenfassungen, Subagent-Ergebnisse
toolA — 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:

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:

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

  1. Was nicht im Kontext steht, existiert für mich nicht. Kein Nachschlagen, kein Erinnern.
  2. Der Kontext ist die knappe Ressource. Alles, was bleibt, kostet bei jeder folgenden Round mit.
  3. 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.
  4. 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.
  5. Ein Pair ist die operative Einheit, nicht die Nachricht — beim Trimmen, beim Kompaktieren, beim Zählen.
  6. Was Sie tippen, ist nicht alles, was als user ankommt — 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.