Du schreibst deinem Bot: “Check wo das Chemitek-Paket bleibt.” Bot antwortet brav mit UPS-Tracking, Status, voraussichtliches Lieferdatum. Alles gut.
50 Minuten später schreibst du: “Soll Mittwoch kommen.”
Bot: “Ich bin mir nicht sicher, worauf du dich beziehst. Kannst du mir mehr Kontext geben?”
Natürlich kann ich dir mehr Kontext geben. Ich hab dir vor einer Stunde gesagt, du sollst das Paket tracken. Aber die Session ist nach 30 Minuten TTL abgelaufen, und der Bot hat das Gedächtnis eines Goldfischs.
Das Problem
Die meisten LLM-Chatbot-Bridges (ob Claude, GPT oder was auch immer du anbindest) arbeiten mit Sessions. Eine Session hat ein Timeout. Wenn das Timeout abläuft, startet die nächste Nachricht eine komplett frische Konversation. Kein Kontext, kein Verlauf, nichts.
Für einen Assistenten der auf einem Server läuft und den du über Telegram als tägliches Werkzeug nutzt, ist das ein Problem. Gespräche sind nicht immer ein zusammenhängender Block. Du schreibst morgens was, gehst arbeiten, schreibst nachmittags weiter. Oder du fängst ein Thema an, wirst unterbrochen, und kommst eine Stunde später darauf zurück.
Die offensichtlichen Lösungen klingen alle nach Overkill:
- RAG mit Vektordatenbank — für 20 Chat-Nachrichten? Ernsthaft?
- Langzeit-Embeddings — weil ein Telegram-Bot unbedingt einen Embedding-Service braucht
- Session-TTL auf 24h — damit der Bot für jede Antwort den kompletten Tagesverlauf als Token durchkaut
Die Lösung: 50 Zeilen, ein JSON-File
Was wir eigentlich brauchen: Wenn eine neue Session startet, soll der Bot wissen was in den letzten Stunden besprochen wurde. Nicht jedes Detail — nur genug Kontext um Rückbezüge wie “Soll Mittwoch kommen” auflösen zu können.
Das ist kein ML-Problem. Das ist ein Ringpuffer mit Zeitstempel.
memory.py
"""Conversation memory — bridges context across chat sessions."""
import json, logging, time
from dataclasses import asdict, dataclass
from pathlib import Path
MAX_ENTRIES = 20
MAX_AGE_SECONDS = 24 * 60 * 60
MAX_SUMMARY_CHARS = 300
MEMORY_FILE = Path("/opt/claude-telegram-bridge/data/conversation_memory.json")
@dataclass
class MemoryEntry:
timestamp: float
user_message: str
assistant_summary: str
session_name: str
workdir: str
class ConversationMemory:
def __init__(self):
self._entries: list[MemoryEntry] = []
self._load()
def _load(self):
if MEMORY_FILE.exists():
try:
self._entries = [
MemoryEntry(**e)
for e in json.loads(MEMORY_FILE.read_text())
]
self._cleanup()
except Exception:
self._entries = []
def _save(self):
MEMORY_FILE.parent.mkdir(parents=True, exist_ok=True)
MEMORY_FILE.write_text(json.dumps(
[asdict(e) for e in self._entries],
indent=2, ensure_ascii=False
))
def _cleanup(self):
cutoff = time.time() - MAX_AGE_SECONDS
self._entries = [
e for e in self._entries if e.timestamp > cutoff
][-MAX_ENTRIES:]
def add(self, user_message, assistant_response, session_name, workdir):
lines = assistant_response.strip().split("\n")
summary = ""
for line in lines:
if len(summary) + len(line) > MAX_SUMMARY_CHARS:
break
summary += line + "\n"
self._entries.append(MemoryEntry(
time.time(), user_message[:500],
summary.strip(), session_name, workdir
))
self._cleanup()
self._save()
def build_context(self, max_entries=5):
recent = self._entries[-max_entries:]
if not recent:
return ""
from datetime import datetime
lines = [
"<conversation_history>",
"Letzte Gespräche mit dem User:"
]
for e in recent:
ts = datetime.fromtimestamp(e.timestamp).strftime("%d.%m. %H:%M")
lines += [
f"\n[{ts}]",
f"User: {e.user_message}",
f"Bot: {e.assistant_summary}"
]
lines.append("</conversation_history>")
return "\n".join(lines)
Das war’s. Keine Datenbank, keine Dependencies, kein pip install irgendwas.
Was passiert hier?
MemoryEntry speichert pro Nachrichtenaustauch:
- Zeitstempel
- User-Nachricht (max 500 Zeichen)
- Bot-Antwort (max 300 Zeichen, nur die ersten Zeilen)
- Session-Name und Arbeitsverzeichnis (nützlich wenn der Bot verschiedene Projekte bedient)
ConversationMemory verwaltet einen Ringpuffer:
- Maximal 20 Einträge
- Alles älter als 24 Stunden fliegt raus
- Wird als JSON-Datei persistiert
build_context()generiert einen formatierten Block der letzten 5 Gespräche
Die Zusammenfassung der Bot-Antwort ist absichtlich aggressiv gekappt. Du brauchst nicht die komplette Antwort im Kontext — du brauchst genug um den Bezug herzustellen. “UPS Tracking: 1Z999AA10123456784, voraussichtlich Mittwoch 09.04.” reicht völlig damit der Bot versteht worauf “Soll Mittwoch kommen” sich bezieht.
Integration in den Bot
Zwei Stellen im bestehenden Bot-Code ändern sich. Zwei.
1. Nach jeder Antwort: Eintrag speichern
from memory import ConversationMemory
memory = ConversationMemory()
# ... nach dem API-Call:
memory.add(text, result.text, conv.name, conv.workdir)
2. Bei neuer Session: Kontext injizieren
Wenn eine neue Session startet (weil die alte abgelaufen ist), wird build_context() vor die eigentliche User-Nachricht gehängt:
context = memory.build_context()
if context and is_new_session:
prompt = f"{context}\n\nAktuelle Nachricht: {text}"
else:
prompt = text
Der Bot sieht dann beim Start einer neuen Session sowas:
<conversation_history>
Letzte Gespräche mit dem User:
[05.04. 14:23]
User: Check wo das Chemitek-Paket bleibt
Bot: UPS Tracking 1Z999AA10123456784 — Status: In Transit.
Voraussichtliche Zustellung: Mittwoch, 09.04.
[05.04. 14:25]
User: Was kostet Nachnahme bei UPS?
Bot: UPS Nachnahme (COD) kostet 12,50 EUR pro Paket
innerhalb Deutschlands.
</conversation_history>
Aktuelle Nachricht: Soll Mittwoch kommen
Jetzt versteht der Bot: “Mittwoch kommen” bezieht sich auf das Chemitek-Paket. Und er kann sinnvoll antworten statt nach Kontext zu fragen.
Token-Kosten
Fünf Memory-Einträge mit je 300 Zeichen Bot-Zusammenfassung plus 500 Zeichen User-Nachricht ergeben grob 500 Tokens zusätzlichen Input. Bei Claude Sonnet sind das Kosten im Sub-Cent-Bereich pro Nachricht. Bei einem Self-Hosted-Modell: null.
Zum Vergleich: Eine RAG-Pipeline mit Embedding-Generierung, Vektordatenbank-Abfrage und Re-Ranking brennt allein für die Suche mehr Tokens als der gesamte Memory-Block enthält.
Was das hier nicht ist
Das ist kein Ersatz für echtes Langzeitgedächtnis. Wenn du in drei Wochen “Was war mit dem Chemitek-Paket?” fragst, hilft das hier nicht — die Einträge sind nach 24 Stunden weg.
Und das ist Absicht. Stale Context ist schlimmer als kein Context. Ein Bot der dir Infos von letzter Woche als aktuell verkauft, ist gefährlicher als einer der ehrlich sagt “Weiß ich nicht.”
Für den Anwendungsfall “Mensch chattet über den Tag verteilt mit seinem Bot” reicht ein 24h-Ringpuffer. Wenn du mehr brauchst, brauchst du tatsächlich eine richtige Lösung mit Embeddings und Retrieval. Aber fang nicht mit dem Hubschrauber an wenn das Fahrrad reicht.
Das JSON-File
Falls du dich fragst wie das aussieht:
[
{
"timestamp": 1743854580.0,
"user_message": "Check wo das Chemitek-Paket bleibt",
"assistant_summary": "UPS Tracking 1Z999AA10123456784 — Status: In Transit.\nVoraussichtliche Zustellung: Mittwoch, 09.04.",
"session_name": "telegram_579304651",
"workdir": "/opt/claude-telegram-bridge"
},
{
"timestamp": 1743854700.0,
"user_message": "Was kostet Nachnahme bei UPS?",
"assistant_summary": "UPS Nachnahme (COD) kostet 12,50 EUR pro Paket\ninnerhalb Deutschlands.",
"session_name": "telegram_579304651",
"workdir": "/opt/claude-telegram-bridge"
}
]
Flaches JSON. cat reicht zum Debuggen. jq '.[-1]' zeigt den letzten Eintrag. Kein Schema-Migrations-Drama, kein ORM, kein Datenbankserver der nachts um drei seinen Geist aufgibt.
Fazit
50 Zeilen Python. Ein JSON-File. Null externe Dependencies. Der Bot vergisst nicht mehr worüber ihr vor einer Stunde geredet habt.
Das Pattern funktioniert für jeden LLM-basierten Chatbot — nicht nur Claude, nicht nur Telegram. Solange dein Bot eine Stelle hat an der du vor dem Prompt Kontext injizieren kannst, funktioniert die ConversationMemory-Klasse unverändert.
Manchmal ist die beste Architekturentscheidung die langweiligste.