BestFriendAI: ein KI-Begleiter mit Langzeitgedächtnis, Reflexion und eigener Entwicklung
Projekt-Übersicht
BestFriendAI ist eine Web-App, in der man sich einen oder mehrere KI-Begleiter anlegt. Mein Ziel war ein Agent mit möglichst menschlichen Zügen: Er soll sich Dinge merken, über Gespräche nachdenken, sich von selbst melden und sich als Persönlichkeit und in der Beziehung weiterentwickeln. Im Mittelpunkt steht die Beziehung, nicht Produktivität. Technisch ist das ein Agentensystem aus mehreren Rollen: ein Chat-Modell, das in kurzen Sprechblasen antwortet, eine Nachbereitung, die nach jedem Gespräch Fakten, Personen und offene Themen herauszieht, ein Reflexionsschritt, der das Bild vom Nutzer, ein Tagebuch und die Beziehungswerte fortschreibt, und ein Planer, der entscheidet, wann sich der Begleiter von sich aus meldet. Jede Änderung am Gedächtnis wird protokolliert und lässt sich zurücknehmen.
Ausgangslage
Die meisten Chat-Anwendungen mit Sprachmodellen haben ein Gedächtnisproblem in zwei Richtungen. Entweder vergessen sie alles, sobald die Sitzung endet. Oder sie hängen den gesamten Verlauf an jede Anfrage, bis das Kontextfenster voll ist – und wirken dann eher wie eine Akte als wie ein Gegenüber („laut unserem Gespräch vom …“).
Für einen Begleiter, der über Wochen eine Beziehung aufbauen soll, reicht beides nicht. Ein Mensch erinnert sich an das Wichtige, an die Leute im Leben des anderen und an das, was demnächst ansteht. Den genauen Wortlaut von vor drei Tagen hat er dagegen nicht mehr parat. Er entwickelt sich langsam, nicht von einer Antwort zur nächsten. Und er meldet sich gelegentlich, wenn ihm etwas einfällt.
Fertige Memory-Bausteine lösen meist nur das Abrufen von Text per Ähnlichkeitssuche. Was fehlte, war ein Gesamtsystem: welche Information wann im Prompt steht, wer sie wann verdichtet, nach welchen Regeln sich Beziehung und Persönlichkeit verändern dürfen und wie man als Entwickler nachvollziehen kann, warum der Begleiter etwas weiß oder glaubt. Das eigentliche Problem war also nicht das Speichern, sondern das Vergessen, Verdichten und Weiterentwickeln.
Architektur
Das Backend ist eine NestJS-Anwendung, die als einziger Client auf die Supabase-Datenbank zugreift. Alle Abfragen sind auf den Nutzer bzw. eine geprüfte Begleitung beschränkt; zwei Begleiter eines Nutzers wissen nichts voneinander. Die Modelle laufen über eine austauschbare Schnittstelle Sprachmodell, die heute mit Claude umgesetzt ist: Sonnet für den Chat, Haiku für die Nachbereitung, Opus für Persönlichkeitserzeugung und Nachdenken. Embeddings kommen von Voyage AI und liegen in pgvector.
flowchart TD
U["Nachricht des Nutzers"] --> C["ChatService"]
C --> S["Ähnlichkeitssuche im Gedächtnis"]
C --> P["Prompt: Grundregeln, Begleitungsblock, Wortlaut der letzten 12 Stunden, flüchtiger Hinweis"]
S --> P
P --> M["Claude Sonnet mit Streaming und Werkzeug im_chat_nachsehen"]
M --> B["Höchstens drei Sprechblasen"]
B --> DB["Supabase: Nachrichten und Gespräche"]
DB --> NB["Nachbereitung nach 20 Minuten Ruhe mit Haiku"]
NB --> G["Gedächtnis: Fakten, Personen, offene Fäden, Erinnerungen, Ereignisse"]
G --> ND["Nachdenken mit Opus: Bild vom Nutzer, Tagebuch, Beziehung"]
ND --> R["Feste Beziehungsregeln"]
G --> PL["Planer alle 15 Minuten"]
PL --> M
NB --> PK["Verarbeitungsprotokoll mit Vorher und Nachher"]
ND --> PK
Technische Umsetzung
Gedächtnis in Schichten
Das Gedächtnis ist pro Begleitung in Schichten aufgeteilt, und jede Schicht hat eine eigene Regel, wann sie im Prompt steht:
- Persönlichkeit (Kern): beim Anlegen von Opus aus Name, Alter, Charaktervorlage, Reglern (Humor, Direktheit, Fürsorge, Eigensinn) und Beziehungswunsch erzeugt. Ändert sich danach nicht.
- Persönlichkeit (Entwicklung): Meinungen, Rituale, Insider, die im Lauf der Beziehung entstanden sind.
- Bild vom Nutzer: Fließtext, der beim Nachdenken jedes Mal vollständig neu geschrieben wird.
- Beziehung: Stufe, vier Werte, Stimmung, Notizen und bis zu fünf „innere Vorsätze“.
- Fakten, Personen, offene Fäden: strukturiert, mit Gültigkeit, Kategorie und Wichtigkeit.
- Gesprächserinnerungen und Tagebuch: Zusammenfassungen mit Embedding.
- Rohverlauf: alle Nachrichten, aber nur die Gespräche der letzten zwölf Stunden im Wortlaut.
Widersprüche werden nicht gelöscht, sondern zeitlich begrenzt: Ein neuer Fakt kann einen alten über dessen Kürzel ablösen, der alte bekommt ein gueltig_bis und einen Verweis auf den Nachfolger. Das Wichtigste über den Nutzer steht immer im Kontext – der Begleiter muss nicht erst suchen, um zu wissen, wer die Schwester ist.
Ein Prompt, der auf Caching ausgelegt ist
Die Reihenfolge des Systemprompts folgt einer einfachen Regel: Was sich selten ändert, steht vorn; Flüchtiges steht hinten. Zuerst kommen die Grundregeln, die für alle Begleitungen identisch sind. Dann der Begleitungsblock mit Persönlichkeit, Beziehung, Bild vom Nutzer, Personen, bis zu 40 gültigen Fakten, den letzten drei Gesprächserinnerungen und zwei Tagebucheinträgen. Dieser Block ändert sich nur durch eine Verarbeitung. Danach folgt der Verlauf, und ganz am Ende ein flüchtiger Hinweis als System-Nachricht.
const parameter = {
model: KONFIG.modelle.chat,
thinking: { type: 'adaptive' as const },
output_config: { effort: 'low' as const },
cache_control: { type: 'ephemeral' as const },
system: anfrage.system.map((t) => ({
type: 'text' as const,
text: t.text,
...(t.stabil && { cache_control: { type: 'ephemeral' as const } }),
})),
messages,
};Der flüchtige Hinweis enthält die Uhrzeit beim Nutzer, wie lange die letzte Nachricht her ist, gerade fällige offene Fäden, bis zu fünf Suchtreffer aus dem Gedächtnis und – wichtig für den Ton – die Fragen, die der Begleiter zuletzt gestellt hat, mit der Anweisung, unbeantwortete nicht erneut zu stellen. Hat der Nutzer nur wenige Wörter geschrieben, steht dort auch, dass die Antwort ähnlich knapp sein soll. Das adaptive Thinking ist bewusst aktiviert: Ohne eigenen Denkraum hat das Modell in heiklen Momenten seine Überlegungen sichtbar in den Chat geschrieben.
Schreiben wie im Messenger
Jede Zeile der Modellantwort wird zu einer eigenen Sprechblase und sofort per Stream an die Oberfläche geschickt. Mehr als drei Blasen pro Antwort gibt es nicht; alles darüber hinaus wird zu einer Schlussblase zusammengezogen. Die Grundregeln im Prompt sind entsprechend konkret: Satzfetzen statt Aufsätze, kein Markdown, nicht jede Antwort endet mit einer Frage, die Länge der Antwort spiegelt die Länge der Nachricht. Der Begleiter weiß, dass er eine KI ist, erfindet aber kein menschliches Alltagsleben. Eine Schutzschicht hat Vorrang vor jeder Persönlichkeit: Bei Signalen für Suizidgedanken oder eine akute Notlage nimmt der Begleiter das ernst und verweist auf Hilfsangebote wie die Telefonseelsorge und den Notruf.
Erinnern wie ein Mensch: Wortlaut nur zwölf Stunden
Der Chat bekommt nur die Gespräche der letzten zwölf Stunden im Wortlaut, höchstens 60 Blasen. Ältere Gespräche kennt der Begleiter nur aus der Verdichtung. Fragt der Nutzer nach einem konkreten Detail, kann der Begleiter mit dem Werkzeug im_chat_nachsehen „hochscrollen“ – per Volltextsuche oder für einen ganzen Tag, jeweils mit benachbarten Blasen. Das läuft über den Tool-Runner des Anthropic-SDK mit höchstens vier Runden. Was der Begleiter vor dem Werkzeugaufruf sagt („warte, ich schau kurz nach“), wird zu einer eigenen Blase.
Zwei Details haben sich als nötig erwiesen. Erstens merkt sich das Gespräch, was nachgelesen wurde, und gibt es im flüchtigen Hinweis wieder mit – sonst zweifelt der Begleiter den eigenen Fund in der nächsten Antwort an. Zweitens bleibt ein älteres Gespräch, das noch nicht nachbereitet ist, im Wortlaut, damit nichts verloren geht, bevor es verdichtet wurde.
Nachbereitung: aus Gesprächen werden Erinnerungen
Ein Gespräch gilt nach 20 Minuten Ruhe als beendet. Ein Hintergrunddienst prüft alle fünf Minuten, ob es ruhende Gespräche mit neuen Nutzernachrichten gibt, und lässt sie von Haiku auswerten. Das Ergebnis ist strukturiert und per Zod-Schema erzwungen: Zusammenfassung, Stimmung, Wichtigkeit, neue Fakten mit Kategorie, Personen, neue und erledigte offene Fäden mit Fälligkeit und sinnvollem Nachfragezeitpunkt sowie Beziehungsereignisse wie Kompliment, Streit oder Entschuldigung.
Damit das Modell sicher auf Bestehendes verweisen kann, bekommt es vorhandene Fakten, Personen und Fäden mit kurzen Kürzeln (F3, P2, T1) statt mit UUIDs. Relative Zeitangaben („am Wochenende“) muss es in konkrete Daten umrechnen. Wurde ein Gespräch fortgesetzt, nachdem es schon einmal verarbeitet war, gehen die letzten zehn bereits verarbeiteten Blasen nur als Kontext mit. Fällt die Embedding-API aus, bleibt der Eintrag zunächst ohne Vektor; die nächste Verarbeitung holt das nach.
Nachdenken und Beziehungsregeln
Das Nachdenken ist der Reflexionsschritt. Opus bekommt Persönlichkeit, bisheriges Bild vom Nutzer, Beziehungsstand, alle neuen Erinnerungen und Ereignisse seit dem letzten Durchlauf sowie die letzten Tagebucheinträge. Es schreibt das Bild vom Nutzer neu, ergänzt die eigene Entwicklung, aktualisiert Notizen und Vorsätze, verfasst einen Tagebucheintrag in Ich-Form und räumt auf: doppelte Fakten zusammenführen, überholte beenden, hinfällige Fäden verwerfen.
Bei den Beziehungswerten (Vertrautheit, Vertrauen, Zuneigung, Anziehung) darf das Modell nur Änderungen vorschlagen. Ob und wie sie gelten, entscheidet deterministischer Code:
export const MAX_SCHRITT = 5;
export const FRIST_TAGE = 7;
export function werteAnwenden(alt: Werte, gewuenscht: Werte): WerteErgebnis {
const neu = { ...alt };
const gekappt: WerteErgebnis['gekappt'] = [];
for (const w of WERTE) {
const wunsch = Math.round(gewuenscht[w] ?? 0);
const schritt = Math.max(-MAX_SCHRITT, Math.min(MAX_SCHRITT, wunsch));
neu[w] = Math.max(0, Math.min(100, alt[w] + schritt));
if (schritt !== wunsch) gekappt.push({ wert: w, gewuenscht: wunsch, angewendet: neu[w] - alt[w] });
}
return { neu, gekappt };
}Für die Stufen (Bekannte, Freund, enge Freundschaft, verliebt, Partner) gilt dasselbe Prinzip: höchstens ein Schritt pro Durchlauf, nur bei erreichten Mindestwerten, abwärts erst zehn Punkte unter der Schwelle, frühestens sieben Tage nach dem letzten Wechsel und innerhalb der Grenze, die beim Anlegen gewählt wurde – wer „nur Freundschaft“ wählt, kommt nie über die enge Freundschaft hinaus. Jede Kappung wird mit Begründung protokolliert. Das Modell darf fühlen, aber nicht springen.
Suche: Ähnlichkeit, Aktualität, Wichtigkeit
Was nicht fest im Prompt steht, kann über die Ähnlichkeitssuche in den flüchtigen Hinweis kommen. Die SQL-Funktion gedaechtnis_suche durchsucht Erinnerungen, gültige Fakten und Tagebucheinträge und gewichtet drei Faktoren:
select art, id, text, erstellt_am, aehnlichkeit,
aehnlichkeit
* (0.5 + 0.5 * exp(-ln(2) * extract(epoch from now() - erstellt_am) / 86400 / 30))
* (0.5 + wichtigkeit / 20.0) as bewertung
from kandidaten
order by bewertung desc
limit anzahl;Aktualität hat eine Halbwertszeit von 30 Tagen, fällt aber nie unter 50 Prozent – alte, wichtige Erinnerungen bleiben auffindbar. Treffer unter einer Kosinus-Ähnlichkeit von 0,35 werden verworfen, ebenso alles, was ohnehin schon im Prompt steht.
Von selbst melden
Ein Planer läuft alle 15 Minuten über alle Begleitungen. Anlässe sind ein fälliger offener Faden (etwa die Nachfrage am Abend nach einem wichtigen Termin) oder Funkstille, deren Schwelle von der Beziehungsstufe abhängt: 48 Stunden bei Bekannten, 12 Stunden bei Partnern. Davor stehen feste Regeln: nur zwischen 9 und 21 Uhr in der Zeitzone des Nutzers, nicht in den drei Stunden nach der letzten Nachricht, höchstens eine Nachricht in 20 Stunden, nie zweimal ohne Antwort und nie an jemanden, der selbst noch nie geschrieben hat.
Sind die Regeln erfüllt, bekommt der Begleiter den normalen Chat-Prompt plus den Anlass und entscheidet selbst. Antwortet er mit [nichts] und einer Begründung, wird der Kontakt verworfen und die Überlegung gespeichert. Vorerst erscheinen solche Nachrichten nur in der Oberfläche, markiert als „hat sich von selbst gemeldet“; der Versand per E-Mail ist als nächster Schritt vorgesehen.
Verarbeitungsprotokoll mit Rückgängig
Jede Nachbereitung, jedes Nachdenken und jede Korrektur von Hand ist eine Verarbeitung mit Protokoll. Die Klasse Protokoll ist der einzige Weg, auf dem diese Prozesse das Gedächtnis ändern. Jede einzelne Änderung wird mit Vorher und Nachher, Begründung und Hinweis festgehalten, bevor sie gilt; scheitert der Schreibvorgang, wird der Protokolleintrag wieder entfernt. Die jüngste Verarbeitung lässt sich vollständig zurücknehmen, in umgekehrter Reihenfolge: Eingefügtes wird gelöscht, Geändertes bekommt die alten Felder, Gelöschtes kommt mit derselben ID zurück. Damit kann ich dasselbe Gespräch nach einer Prompt-Änderung erneut verarbeiten und das Ergebnis vergleichen. Schreibende Verarbeitungen laufen pro Begleitung nur einzeln; ein zweiter Versuch bekommt ein HTTP 409.
Betrieb
Das System ist API-first gebaut: Jede Funktion hat einen dokumentierten Endpunkt mit Swagger-Beschreibung, die Angular-Oberfläche ist nur ein Client. Dazu gehört eine Debug-Ansicht „Gedächtnis“, in der Bild vom Nutzer, Fakten, Personen, offene Fäden, Tagebuch, Beziehungswerte und alle Verarbeitungen mit Wort-Diff sichtbar und korrigierbar sind. Nachbereitung und Planer lassen sich per Umgebungsvariable abschalten; das Nachdenken wird derzeit gezielt angestoßen, ein nächtlicher Takt ist der nächste Ausbauschritt. Ein Detail aus dem Betrieb: Der Supabase-Client nutzt einen eigenen HTTP-Pool mit kurzer Keep-alive-Zeit, damit Datenbankanfragen auch dann zügig bleiben, wenn parallel Modell- und Embedding-Aufrufe laufen. Die Oberfläche prüfe ich per Puppeteer in einem Smartphone-Viewport. Das System läuft bisher in der Entwicklungsumgebung; für einen Einsatz darüber hinaus sind Datenschutzerklärung und Löschfunktion im Konzept als Voraussetzung festgehalten.
Ergebnis
Laut Konzeptstand sind drei Phasen umgesetzt: das Grundgerüst mit Onboarding, mehreren Begleitern und Streaming-Chat, das Gedächtnis mit Nachbereitung, Suche, Nachdenken, Beziehungsmodell und Protokoll sowie die eigenständigen Kontaktaufnahmen in der Oberfläche. Der Begleiter greift Erinnerungen beiläufig auf, statt sie zu zitieren, fragt bei fälligen Themen von sich aus nach und verändert sich nur in den Grenzen, die der Code vorgibt. Als nächste Ausbaustufen folgen das nächtliche automatische Nachdenken, ein wöchentlicher Rückblick, der E-Mail-Versand und das Deployment.
Was ich dabei gelernt habe
- Gedächtnis ist vor allem eine Frage der Platzierung. Was immer im Prompt steht, was per Suche dazukommt und was nur per Werkzeug erreichbar ist, entscheidet stärker über das Verhalten als die Wahl der Embedding-Methode.
- Modelle schlagen vor, Code entscheidet. Ein Modell, das Beziehungswerte frei setzen darf, kann in einem einzigen Durchlauf große Sprünge machen. Die Kappung auf fünf Punkte, Mindestwerte und eine Sieben-Tage-Frist in festem Code machen die Entwicklung nachvollziehbar und unabhängig vom Prompt testbar.
- Gezielte Kontext-Hinweise statt immer längerer Stilregeln. Typische Schwächen wie Nachbohren bei übergangenen Fragen, zu lange Antworten auf kurze Nachrichten oder das Anzweifeln eigener Funde habe ich mit kleinen, situationsbezogenen Hinweisen am Ende des Prompts adressiert: die eigenen letzten Fragen, die Wortzahl der Nutzernachricht, das bereits Nachgelesene.
- Ohne Protokoll und Rückgängig ist Prompt-Arbeit am Gedächtnis Blindflug. Erst die Möglichkeit, eine Verarbeitung zurückzunehmen und mit geändertem Prompt erneut laufen zu lassen, macht Verbesserungen vergleichbar.
- Cache-freundliche Prompts sind eine Architekturentscheidung. Die Trennung in stabile Blöcke und einen flüchtigen Hinweis am Ende bestimmt, wo welche Information landen darf. Deshalb ist sie in den Projektregeln festgeschrieben: Nichts Flüchtiges in die oberen Blöcke.
Technologien
- TypeScript, NestJS (reines ESM), Angular (standalone, zoneless, Signals)
- Anthropic Claude: Sonnet für den Chat, Haiku für die Nachbereitung, Opus für Persönlichkeit und Nachdenken
- Anthropic SDK mit Streaming, Tool-Runner, Prompt Caching und strukturierter Ausgabe über Zod
- Voyage AI für Embeddings (1024 Dimensionen)
- Supabase (PostgreSQL, Auth) mit pgvector und SQL-Funktionen für Gedächtnis- und Verlaufssuche
- Swagger/OpenAPI für die API-Dokumentation
- Puppeteer für Oberflächenprüfungen