Retrospektive: Was würden wir heute anders machen?
TL;DR: Ein Jahr Wintermute hat drei Sorten Lessons produziert. Erstens: Architektur-Entscheidungen, die sich bewährt haben — und die wir Anfängern ohne zu zögern wieder empfehlen würden. Zweitens: Entscheidungen, die wir zu spät oder zu früh getroffen haben — wo nicht die Richtung falsch war, sondern das Timing. Drittens: strukturelle Probleme, für die wir keine Lösung haben und für die der LLM-Stack als Ganzes auch noch keine hat. Dieses Kapitel ist die ehrliche Aufstellung — kein Schönwetter, aber auch kein Reinreiten in den Negativ-Befund. Am Ende steht die Antwort auf die einzige Frage, die wirklich zählt: würden wir's nochmal so machen?
Das Format
Eine Retrospektive ist keine Architektur-Doku. Sie ist die Anerkennung, dass man manche Dinge erst im Rückblick versteht — und dass das Verstehen oft mehr wert ist als die ursprüngliche Entscheidung selbst. Wir gliedern in drei Teile, jeweils mit einer konkreten Antwort statt eines Wischiwaschi-Befunds:
- Was sich bewährt hat — und warum.
- Was wir anders machen würden — wobei das spannender Teil meistens das Timing ist, nicht die Richtung.
- Was uns noch schmerzt — die ehrlichen offenen Lücken, die wir nicht aus eigener Kraft schließen können.
Plus die Meta-Frage zum Schluss.
Was sich bewährt hat
Die Container-Architektur — und der Schnitt der Container
Wintermute ist in sechs Container aufgeteilt: agent, mcp-gateway,
telegram, qdrant, ollama, ollama-init. Diese Aufteilung
sieht im ersten Moment nach Overhead aus — sechs Compose-
Services für „nur" einen Personal Assistant. Im Alltag war sie
die wertvollste einzelne Architektur-Entscheidung.
Warum: jeder Schnitt zwischen den Containern ist gleichzeitig drei Sachen auf einmal. Eine funktionale Grenze (was gehört zur Tool-Surface, was zur Agent-Loop, was zum Channel- Adapter), eine Sicherheits-Grenze (welcher Container darf welchen Mount sehen, welche Env-Variablen, welche Sockets), und eine Iterations-Grenze (was kann ich neu bauen, ohne den Rest mitzureißen). Diese drei Eigenschaften zusammen machen den Schnitt zur eigentlichen Investition.
Konkret heißt das: wenn wir am Memory-Stack arbeiten, fassen wir den Agent-Container nicht an. Wenn wir neue Tools bauen, fassen wir den Telegram-Adapter nicht an. Wenn wir den Telegram-Adapter aktualisieren, läuft Memory durch. Modularität, Sicherheit, Flexibilität — gewahrt, ohne dass einer die anderen zwei kostet.
Das ist nicht „Microservices als Selbstzweck". Das ist Schnitt entlang von Eigenschaften, die in der Praxis auseinanderdriften würden. Ein Monolith mit denselben sechs Verantwortungen würde in den ersten Wochen genau so funktionieren — aber jede folgende Änderung würde mehr Querbezüge anfassen müssen, und die Sicherheits-Grenze gäbe es schlicht nicht.
MCP als Tool-Surface — auch wenn's wie „Nachbau" aussieht
Die zweite Entscheidung, die im Rückblick im konkreten Alltag am meisten Wert geliefert hat, war die Anbindung des Tool- Protokolls über MCP (Model Context Protocol) als eigenen Service. Das fühlte sich phasenweise wie ein Nachbau dessen an, was Frameworks wie OpenClaw out-of-the-box mitbringen — nur eben selbstgemacht.
War es trotzdem die richtige Entscheidung? Ja, und zwar eindeutig. Drei Gründe:
- Tool-Surface ist Sicherheits-Surface. Wenn Tools im selben Prozess wie die Agent-Loop laufen, hat ein kompromittierter Agent jeden Tool-Aufruf zur Verfügung. Bei separatem Gateway ist das ein HTTP-Call durch eine kontrollierbare Grenze — mit Allow-Lists, Auth-Headern, Audit-Logs.
- Tools können einzeln evolvieren. Ein neues IMAP-Tool ist eine PR am Gateway, ohne dass die Agent-Loop überhaupt weiß, dass sich was geändert hat. Hot-Reload des Tool-Inventars bei laufender Konversation.
- Tools sind nicht an einen LLM-Anbieter gekoppelt. Weil LiteLLM die Tool-Call-Normalisierung macht und der Gateway die Tool-Definitionen liefert, könnte derselbe Tool-Stack morgen einen anderen Agenten bedienen — gleichgültig, ob der mit Anthropic-, OpenAI- oder Moonshot-Tool-Format arbeitet.
Der „fühlt sich an wie Nachbau"-Reflex ist verständlich, aber falsch. Standard-Frameworks haben den Schnitt nicht da, wo deine Anforderungen ihn hinlegen würden — und der Wert liegt genau in diesem Schnitt-Detail.
Dass die Tool-Surface ein eigener Service mit eigener Grenze
ist, hat sich zuletzt noch einmal bestätigt: die neuen
Workspace-Read-Tools (workspace_read_file,
workspace_list_directory, Juni 2026) konnten als reine
Gateway-Erweiterung landen — mit einem doppelt abgesicherten
Read-only-Mount (:ro auf Compose-Ebene, Path-Traversal-Check
im Tool), ohne dass die Agent-Loop eine Zeile Änderung sah.
Der dreischichtige Memory-Mechanismus
Wenn jemand käme und sagte „du musst eines deiner Patterns aufgeben, welches wählst du?" — die Antwort wäre eindeutig: alles andere ist verhandelbar, der Memory-Mechanismus nicht.
Wintermutes Memory besteht aus drei Schichten:
- Curated Markdown-Files (
MEMORY.md,memory/<section>.md,memory/YYYY-MM-DD.md): das was ein Mensch als „journal" bezeichnen würde. Wird vom Agenten selbst geschrieben, manuell pflegbar, versioniert. - Vector-DB (Qdrant) mit semantischer Recall über embedded Memory-Inhalte: damit der Agent „weiß was er weiß" auch dann findet, wenn die Query nicht wörtlich im Memory steht.
- Lokales Embedding-Modell (Ollama): damit die Embeddings keine Provider-Kosten erzeugen und keine privaten Memory-Texte zu einem externen Anbieter wandern.
Alles andere am Stack ist technisch notwendig, damit Wintermute überhaupt läuft. Der Memory-Mechanismus dagegen liefert den tatsächlichen Nutzen im täglichen Arbeiten. Ohne ihn ist Wintermute eine Tool-Sammlung mit Chat-Interface. Mit ihm ist er ein Kollege, der weiß was letzten Dienstag passiert ist — und der bei „du erinnerst dich an die Pi-Hole-Sache aus April?" nicht ratlos guckt.
Das ist auch der ehrlichste Maßstab für „lohnt sich ein eigener Agent?". Wenn der Memory-Layer den Pain nicht wert wäre, würde sich das ganze Projekt nicht rentieren. Er war es, jederzeit.
Operative Regeln, die strukturell wirken
Drei kleinere Dinge, die im Cluster „bewährt" stehen und zusammen einen Effekt haben, der größer ist als jeder einzelne Punkt:
- Identity in
.env, nicht in Source. Ein Fork von Wintermute ändert exakt eine Zeile und der ganze Self-Reference- Mechanismus passt zur neuen Realität (LESSONS §18). - Per-Repo-PAT-Mapping ab Tag 1.
(repos, token)-Liste inGITHUB_TOKENS_JSONstatt ein-Token-für-alles. War am Anfang Overhead, hat aber die erste Berührung mit einem fremden Repo problemlos überstanden (LESSONS §19). - „Verbatim or nothing" als operative Regel. Identifier in Replies müssen aus diesem Turns Tool-Trace kommen. Codifiziert 2026-05-03, geschärft 2026-05-04. Mehr in Kapitel 07.
Diese Regeln sind keine Killer-Features. Sie sind die kleinen Hebel, die später spürbar werden — nicht in „wow, was für ein Feature", sondern in „komisch, dass das nie weh tat".
Was wir anders machen würden
Hier kommen die Lessons, in denen die Richtung stimmt, aber das Timing falsch war.
Self-Modification: früher abschalten
Wintermute durfte sechs Monate lang an seinem eigenen Source- Code mitschreiben. Pull-Requests gegen das eigene Repo, mit Branch-Protection und Persona-Prompt-Disziplin. Ende 2026-05-06 abgeschaltet, nachdem ein Nachmittag mit sechs gefangenen Halluzinationen klargemacht hatte, dass das Form-Factor falsch war (mehr in Kapitel 07).
Was wir anders machen würden: früher abschalten, nicht gar nicht erst zulassen. Die Risiken waren von Anfang an bekannt — Prompt-Injection plus Code-Schreib-Rechte ist ein Sicherheits-Albtraum mit Ansage. Trotzdem haben wir es laufen lassen, weil der „richtige Weg" (Self-Modification deaktivieren, Code-Änderungen über Issues + separater Agent) zwar nicht viel mehr Aufwand war, aber eben etwas mehr Aufwand. Faulheit hat über Sorgfalt gesiegt.
Das ist keine Code-Lesson, das ist eine Steuermanns-Lesson: wenn ich als Architekt ein Risiko sehe und mich trotzdem für den bequemen Weg entscheide, dann ist der Tag, an dem das Risiko schlagend wird, ein vorhersehbarer Tag — kein Schicksal.
Würden wir Self-Modification gar nicht mehr einbauen, wenn
wir nochmal anfangen? Nicht zwingend. Das Experiment „wohin
entwickelt sich ein Agent, der sich selbst modifizieren kann,
nur durch tägliches Usage-Pattern?" ist eine inhaltlich
interessante Frage. Einer der konkreten Stolpersteine ist
inzwischen übrigens umgesetzt: der gh_put_file-Content-Drop
bei großen Bodies (das Modell „vergisst" das Pflichtfeld
content) wird seit Juni 2026 framework-seitig abgefangen —
die Tool-Registry validiert Pflicht-Argumente client-seitig
gegen das Tool-Schema und lehnt den Call mit einem
strukturierten Fehler ab, statt ihn stillschweigend
durchzulassen. Auf der LLM-Seite bleibt das Verhalten aber
ungelöst; wenn die strukturellen Halluzinations-Gaps (Kap 07)
etwas zugehen, könnte man dem Experiment wieder eine Phase
geben — mit klaren Limits und einer expliziten Auswertungs-
Frage, nicht als Default-Mode.
Aber eingebaut wäre es dann nicht von Tag 1, sondern als abgegrenzte, terminierte Experimentierphase. Das ist der Unterschied: Self-Modification als Forschungswerkzeug ist spannend. Als Default-Capability ist es ein offener Sicherheitskanal.
Updater-Service als Host-Daemon: erst später bauen
Spiegelbildlich zu Self-Modification ist die Updater-Migration ein Beispiel für das umgekehrte Timing-Problem: zu früh.
Lesson §9 stand sechs Monate offen — „self-update inside the
container leaves a dead container behind". Wir haben uns
durchgewurschtelt mit --force-recreate, mit connection drop is expected, mit allerlei kleinen Pflastern. Die saubere
Lösung (separater wintermute-updater als systemd-Service auf
dem Host) war architektonisch sofort sichtbar — wir haben sie
nur nicht gebaut, weil sie nicht gerade jetzt dringend war.
Was wir anders machen würden: Den Host-Updater erst
ganz am Ende einbauen, nicht früher. In der Entwicklungs-
Phase war er wirklich nicht notwendig. Ich (Jürgen) hing
sowieso ständig per SSH auf dem Server, jede Code-Änderung
war git pull && docker compose up mit zwei Tabs Abstand.
Der Updater war keine echte Ersparnis, sondern eine
Infrastruktur-Investition für eine Welt, in der ich nicht
mehr ständig auf dem Server bin.
Diese Welt ist eingetreten — aber erst, als Wintermute stabil genug war, dass ich ihn ohne permanente SSH-Aufsicht laufen lassen konnte. Vorher war der Updater overengineering, nicht Engineering-Hygiene.
Die Lesson hier ist nicht „du brauchst keinen Host-Updater", sondern: der richtige Zeitpunkt für eine Sicherheits- Investition ist der Tag, an dem die Sicherheits-Eigenschaft gebraucht wird — nicht der Tag, an dem du sie theoretisch zum ersten Mal denken könntest. Tag 1 ist der Bootstrap; der Updater gehört zu Tag 90.
Stille Bugs leben lange: die Historie, die sich selbst zerschnitt
Die unangenehmste Entdeckung des Frühsommers war kein
Sicherheits-Loch, sondern ein Parser (LESSONS §29). Die
Konversations-Historie wurde als ### {role}-Blöcke
serialisiert und mit einem naiven split("### ") wieder
zerlegt. Nur: die Antworten des Agenten enthalten selbst
Markdown-Überschriften — ### Zusammenfassung, ### Schritt 1,
der Stil der eigenen Persona. Jede dieser Überschriften wurde
als Nachrichten-Grenze interpretiert. Ergebnis: das Modell hat
über Wochen in jedem Turn eine zerschnittene Kopie seines
eigenen vorherigen Turns gelesen — abgeschnitten an der ersten
Überschrift, der Rest verworfen.
Das Tückische: die Symptome waren diffus. Kein Crash, kein Fehler-Log, nur gelegentlich ein Agent, der seinen eigenen letzten Turn seltsam unvollständig referenzierte. Genau die Sorte Symptom, die man einem LLM zuschreibt statt dem eigenen Code.
Was wir anders machen würden: Serialisierungs-Formate ab
Tag 1 mit Round-Trip-Tests absichern — schreiben, parsen,
vergleichen, mit Payloads, die den Delimiter enthalten. Und
die allgemeine Regel aus der Lesson: ein Record-Format nie an
einem Substring splitten, den der Payload selbst enthalten
kann. Entweder den Marker escapen oder den Split verankern
(Zeilenanfang plus geschlossenes Token-Set). Ein nacktes
split(marker) über Freitext ist ein schlafender
Korruptions-Bug.
Hardening als Welle, nicht als Tropf
Im Juni 2026 lief eine gebündelte Hardening-Runde durchs Repo:
ein SSRF-Guard in web_fetch (private, Loopback- und
Metadata-Adressen sind per Default geblockt, jeder Redirect-Hop
einzeln geprüft), Container-Healthchecks, atomare
Datei-Schreibvorgänge im Memory-Layer, Bounds-Checks auf
Config-Werten, dazu der bereits erwähnte Read-only-Workspace.
Im gleichen Zug bekam der Post-Turn-Validator
Conversation-Awareness: er prüft Identifier jetzt gegen die
ganze Konversation statt nur gegen die aktuelle User-Nachricht,
was eine Klasse von False-Positive-Selbstkorrekturen beendet
hat.
Zwei Erkenntnisse daraus. Erstens: Hardening als konzentrierte Welle nach der Feature-Phase funktioniert — besser als der Versuch, jede Feature-PR gleichzeitig zum Security-Audit zu machen. Man sieht Querbezüge (derselbe fehlende Bounds-Check an drei Stellen), die in Einzel-PRs unsichtbar bleiben. Das ist dieselbe Timing-These wie beim Updater, nur positiv gewendet: die Investition kam, als die Eigenschaft gebraucht wurde, und dann vollständig.
Zweitens: auch der Fix kann den schlimmeren Failure-Mode
bauen (LESSONS §31). Compose-Healthchecks sind die richtige
Antwort auf „Service startet vor seiner Dependency" — aber der
naive curl-Healthcheck schlägt auf Slim-Images, die gar kein
curl mitbringen, immer fehl. Und ein Healthcheck, der nie
grün wird, macht aus depends_on: service_healthy einen
Deadlock, der den ganzen Stack am Starten hindert. Die Regel:
der Probe-Befehl muss aus dem bestehen, was das Image
tatsächlich enthält — und wo es kein brauchbares Werkzeug gibt,
lieber gar kein Healthcheck und ein toleranter Client als eine
Probe, die man nicht verifizieren kann.
Multi-Provider und die Web-UI
Multi-Provider haben wir früh eingebaut (LiteLLM-Abstraktion, drei Modell-Slots, Slash-Commands zur Modell-Wahl). Im Rückblick: konkret ausgezahlt hat es sich, als wir parallel eine eigene Web-UI gebaut haben (finn), durch die auch Speech-to-Text mit OpenAI-Whisper dazukam.
Diese Anbindung wäre ohne die Multi-Provider-Architektur nicht in ein-zwei Tagen machbar gewesen — sie wäre eine Architektur-Migration gewesen. So war sie eine neue ENV- Variable plus ein neuer Slot-Eintrag.
Der Effekt hat sich seitdem wiederholt: der /local-
Slash-Command (Juni 2026) routet einen Turn an eine
selbst-gehostete Ollama-Instanz im eigenen Tailscale-Netz —
strukturell identisch zu /think und /fast, nur mit einem
Alias-Mapping (LOCAL_MODELS) und einem api_base-Override.
Wieder kein Architektur-Umbau, wieder nur Konfiguration.
Hätten wir mit Anthropic-only angefangen, hätten wir die Migration zum richtigen Zeitpunkt sowieso machen müssen. Multi-Provider war keine Investition in eine ferne Zukunft, sondern in eine Realität, die innerhalb von Monaten eintrat.
Telegram als primärer Channel
Telegram würden wir wieder als Erstes wählen. Nicht weil das Bot-API besonders schön ist, sondern weil der Channel leicht vom Telefon erreichbar sein muss. Ob Telegram oder WhatsApp — solange diese eine Eigenschaft erfüllt ist, ist die Wahl sekundär. Web-UI kam dazu (siehe finn), aber als zweite Surface, nicht als erste.
Der Channel trägt inzwischen auch die Gegenrichtung: seit Juni 2026 kann Wintermute selbst Nachrichten anstoßen (Proactive Push) — eine Queue im Agenten, die der Telegram-Adapter periodisch leert und nur an allowgelistete Chats ausliefert. Damit wird aus „vom Bett aus pingen" auch „vom Agenten geweckt werden, wenn's wichtig ist".
Die Lesson hier ist breiter: für einen Personal Assistant ist Erreichbarkeit der wichtigste Faktor. Alle Architektur-Schönheit nützt nichts, wenn du den Agenten nicht vom Bett aus pingen kannst.
Was uns noch schmerzt
Gap 3 — strukturelles Tool-Result-Quoting
Von allen offenen Halluzinations-Gaps in HALLUCINATIONS.md
ist Gap 3 der schmerzhafteste, weil er gleichzeitig der
einzige strukturell saubere Fix wäre — und gleichzeitig
außerhalb dessen, was wir bauen können.
Die Idee von Gap 3: das Modell darf Tool-Ergebnisse nicht
mehr in freier Prosa zitieren. Stattdessen schreibt es
{{tool_result.commit_sha}} und das Framework substituiert
beim Render-Schritt den echten Wert. Das Modell kann nicht
zitieren, was es nicht aufgerufen hat — strukturell, nicht
disziplinarisch.
Die anderen Gaps (2, 4, 5, 6) sind ohne sehr großen Aufwand nicht „einfangbar" — sie sind Patterns, die wir mit zusätzlichen Layern erkennen aber nicht verhindern können. Gap 3 wäre der eine Eingriff, der das Halluzinations-Problem an der Wurzel adressiert.
Warum bauen wir Gap 3 nicht? Weil es nicht auf unserer Seite des Stacks zu lösen ist. Eine vernünftige Implementierung verlangt, dass jeder LLM-Anbieter (oder zumindest der, den wir gerade benutzen) ein Render-Protokoll unterstützt, in dem strukturierte Werte vom Server substituiert werden. Heute gibt es das nicht — Tool-Calls geben dem Modell Text zurück, und was das Modell aus dem Text macht, ist sein eigenes Pattern-Completion-Spiel.
Das ist die unangenehme Wahrheit über LLM-Agenten in 2026: das Halluzinations-Problem ist gelöst lösbar, aber nicht auf der Anwendungsebene. Es braucht eine Architektur- Entscheidung in den LLM-Stack-Schichten unter uns. Bis dahin sind wir bei Schadensbegrenzung und Disziplin (Kapitel 07).
Generell: weniger Halluzinationen wären schön
Wenn die Frage „was hat Wintermute nicht geliefert, was du dir versprochen hattest?" ist, dann ist die ehrliche Antwort: eigentlich nichts — aber weniger Halluzinationen wären schön gewesen. Nicht weil sie häufig sind. Sondern weil sie leise sind und du nie ganz sicher weißt, in welcher Reply-Variante du gerade steckst (siehe Kapitel 07, Eingangsbeispiel mit den vier Varianten).
Das ist keine Wintermute-Lücke, das ist eine Eigenschaft des heutigen LLM-Stacks. Aber sie fühlt sich an wie eine Wintermute-Lücke, weil sie an unserem Agenten sichtbar wird.
Würden wir nochmal anfangen?
Ja. Und zwar ohne Zögern.
Die Antwort lebt nicht primär in rationalem Kosten-Nutzen- Kalkül. Sie lebt in einer Disposition: wer einmal eine Borland- Pascal-CRT-Unit in Assembler neu geschrieben hat, weil ihm die ausgelieferte zu langsam war, der schreibt sich beim nächsten Mal auch wieder seinen Agenten selbst. Übung macht den Meister. Verstehen entsteht beim Bauen, nicht beim Lesen über die Sachen, die andere gebaut haben.
Das gilt auch für die Frage „bauen oder fertige Lösung nehmen?". ChatGPT mit MCP, Claude mit Computer-Use, OpenClaw als Framework — alles legitime Wege, alle mit der Eigenschaft, dass jemand anders die Entscheidungen für dich getroffen hat. Wer die Entscheidungen verstehen will (warum so geschnitten, warum diese Defense-Layer, warum dieser Memory-Stack), muss sie selbst treffen. Und dafür ist die teuerste Lehrer-Stunde oft die selbstgebaute.
Was wir am Anfang nicht hatten — und worauf wir am Ende gelandet sind — ist eine ehrlich kleine, aber sehr konkrete Liste von Dingen, die wir wieder so machen würden:
- Container-Schnitt entlang funktionaler + Sicherheits- + Iterations-Grenzen
- MCP als separater Service, auch wenn's nach Nachbau aussieht
- Memory in drei Schichten, weil hier der eigentliche Nutzen lebt
- Identity und Tokens als ENV-Konfiguration, nie als Source- Konstante
- Multi-Provider von Tag 1, weil die zweite Surface schneller kommt als gedacht
- Erreichbarkeit vom Telefon als nicht-verhandelbar
- Neue Capabilities read-only und allowgelistet starten (Workspace-Tools, Standing Instructions, Proactive Push) — Schreibrechte erst, wenn ein konkreter Bedarf sie rechtfertigt
- Verbatim-or-nothing als operative Disziplin, nicht als technischer Mechanismus
Und eine Liste von Dingen, die wir später, kleiner, oder gar nicht mehr so machen würden:
- Self-Modification nur als terminiertes Experiment, nicht als Default
- Host-Updater erst am Ende, nicht in der Entwicklungs-Phase
- Serialisierungs-Formate ohne Round-Trip-Test — nie wieder (die Delimiter-Lesson)
- Bequemlichkeits-Entscheidungen, deren Risiken man vorher sieht, nicht treffen (das war die Steuermanns-Lesson)
Wintermute ist nicht fertig — was er nie sein wird, weil ein Personal Assistant kein abgeschlossener Zustand ist, sondern ein laufender Dialog. Aber er ist gut genug, dass die nächsten Wochen wieder vom Bauen handeln und nicht vom Reparieren. Das ist nach einem Jahr der wichtigste Befund.
Und der einzige, der dich wirklich nochmal anfangen lässt.
📚 Quellen im Wintermute-Repo
LESSONS.md— chronologisches Lessons-Log (31 Einträge, Stand 2026-06); für dieses Kapitel besonders §29 (Delimiter-Parsing) und §31 (Healthcheck-Deadlock)docs/HALLUCINATIONS.md— die offenen Gaps 2-6 ausführlichdocs/UPDATER.md— die Architektur, die wir später hätten bauen sollendocs/WORKSPACE-TOOLS.md— die Workspace-Read-Tools und ihr Read-only-Modelldocs/SECURITY.md— Stand nach der Hardening-Welle vom Juni 2026- Issue-Tracker juergenvh/wintermute — der aktuelle Stand der offenen Punkte