Le problème de mémoire LangGraph
LangGraph est livré avec un checkpointer intégré appelé MemorySaver. Il stocke l'état du graphe pour que l'exécution puisse être reprise en cours de route. C'est utile pour les agents multi-étapes avec boucles humaines.
Mais il y a une limitation fondamentale qui piège presque toutes les équipes qui construisent des agents en production avec LangGraph : MemorySaver stocke l'état en mémoire process. Dès que votre processus Python redémarre — un nouveau déploiement, un événement de scaling — chaque checkpoint stocké est perdu.
La documentation LangGraph est claire à ce sujet, mais c'est facile à manquer. L'avertissement pertinent dit : "MemorySaver is an in-memory checkpointer. This means it will be lost when the process restarts. It is not suitable for production use."
L'étendue du problème : MemorySaver ne persiste l'état que dans un seul thread ID. Même si votre processus ne redémarre jamais, deux conversations séparées avec le même utilisateur — chacune utilisant un thread ID différent — ne peuvent pas partager la mémoire.
Cela affecte deux cas d'usage distincts souvent confondus. Le premier est le checkpointing : sauvegarder l'état d'exécution exact d'un graphe en cours. Le second est la mémoire à long terme : stocker des faits sémantiques sur un utilisateur pour que les conversations futures bénéficient des interactions passées.
Ce que "persistant" veut vraiment dire dans LangGraph
Quand les développeurs parlent de "mémoire persistante dans LangGraph", ils veulent généralement dire l'une de deux choses.
La persistance au niveau du thread signifie que l'état du graphe est durable dans un seul fil de conversation. Les checkpointers SqliteSaver et PostgresSaver de LangGraph fournissent cela.
La mémoire cross-session est différente. Elle signifie que l'agent peut rappeler des faits de conversations précédentes qui se sont déroulées dans différents thread IDs. "Cet utilisateur m'a dit qu'il préfère des réponses concises." Rien de tout cela ne rentre dans un checkpoint de graphe.
Le modèle deux couches : Les agents LangGraph en production ont généralement besoin des deux couches. Utilisez PostgresSaver pour la persistance des checkpoints, et un store de mémoire externe comme Kronvex pour la mémoire sémantique cross-session.
La partie déroutante est que la doc LangGraph parle des deux sous le terme "mémoire", mais ils servent des objectifs complètement différents. Un checkpoint de graphe est un snapshot binaire d'objets Python. Un store de mémoire sémantique est une base vectorielle interrogeable.
La solution : store de mémoire externe avec Kronvex
Kronvex est une API mémoire persistante pour agents IA. Elle expose trois endpoints qui s'alignent proprement sur le cycle de vie de la mémoire :
/remember— stocke un fait textuel avec un tag de type optionnel. Kronvex l'embarque avectext-embedding-3-smallet le persiste dans PostgreSQL + pgvector en UE./recall— récupère les top-k mémoires les plus pertinentes pour une requête par similarité cosinus, scorées parsimilarité × 0,6 + récence × 0,2 + fréquence × 0,2./inject-context— récupère les mémoires pré-formatées comme bloc contexte prêt à être inséré dans un prompt système. Gère la déduplication et le filtrage de pertinence automatiquement.
Chaque agent de votre système obtient son propre espace mémoire isolé, identifié par un UUID que vous assignez. Pour un SaaS multi-tenant, chacun de vos utilisateurs finaux devient un agent Kronvex séparé.
Le point d'intégration dans un graphe LangGraph est un nœud. Vous ajoutez un memory_node qui se déclenche avant l'appel LLM principal. Il interroge Kronvex pour des mémoires pertinentes basées sur le dernier message de l'utilisateur, puis injecte les résultats dans l'état comme contexte.
# async: pip install "kronvex[async]"
Code : agent LangGraph avec mémoire Kronvex
Avant : LangGraph avec MemorySaver (mémoire perdue au redémarrage)
Le quickstart standard LangGraph utilise MemorySaver comme checkpointer. L'état persiste dans un thread, mais disparaît avec le processus. Il est impossible de rappeler ce qui s'est passé en session 1 pendant la session 2.
from langgraph.graph import StateGraph, END
from langgraph.checkpoint.memory import MemorySaver
from typing import TypedDict, Annotated
from langchain_openai import ChatOpenAI
import operator
class AgentState(TypedDict):
messages: Annotated[list, operator.add]
llm = ChatOpenAI(model="gpt-4o-mini")
def call_model(state: AgentState):
response = llm.invoke(state["messages"])
return {"messages": [response]}
# Build graph
builder = StateGraph(AgentState)
builder.add_node("agent", call_model)
builder.set_entry_point("agent")
builder.add_edge("agent", END)
# MemorySaver: thread-level, in-process only
# All checkpoints vanish on process restart
memory = MemorySaver()
graph = builder.compile(checkpointer=memory)
# Session 1
config = {"configurable": {"thread_id": "user-001-session-1"}}
graph.invoke({"messages": [{"role": "user", "content": "I prefer Python over TypeScript"}]}, config)
# Session 2 — new thread ID, different process restart
# Agent has NO memory of session 1
config2 = {"configurable": {"thread_id": "user-001-session-2"}}
result = graph.invoke({"messages": [{"role": "user", "content": "What language should I use?"}]}, config2)
# Agent cannot recall the Python preference from session 1
Après : LangGraph + Kronvex (mémoire persistante cross-session)
Le pattern est simple : ajoutez deux nœuds à votre graphe. Un memory_recall_node se déclenche avant l'appel LLM pour injecter le contexte pertinent. Un memory_store_node se déclenche après pour persister les nouveaux faits.
from langgraph.graph import StateGraph, END
from langgraph.checkpoint.memory import MemorySaver
from typing import TypedDict, Annotated, Optional
from langchain_openai import ChatOpenAI
import operator
import httpx
KRONVEX_API_KEY = "kv-your-key"
KRONVEX_AGENT_ID = "your-agent-id"
class AgentState(TypedDict):
messages: Annotated[list, operator.add]
memory_context: Optional[str]
llm = ChatOpenAI(model="gpt-4o-mini")
# ── Kronvex helpers ──────────────────────────────────────────────
def remember(content: str, memory_type: str = "fact"):
httpx.post(
f"https://api.kronvex.io/api/v1/agents/{KRONVEX_AGENT_ID}/remember",
headers={"X-API-Key": KRONVEX_API_KEY},
json={"content": content, "memory_type": memory_type}
)
def recall(query: str, top_k: int = 5):
r = httpx.post(
f"https://api.kronvex.io/api/v1/agents/{KRONVEX_AGENT_ID}/recall",
headers={"X-API-Key": KRONVEX_API_KEY},
json={"query": query, "top_k": top_k, "threshold": 0.5}
)
return r.json()["results"]
# ── Graph nodes ──────────────────────────────────────────────────
def memory_recall_node(state: AgentState):
"""Recall relevant memories before the LLM responds."""
latest_message = state["messages"][-1]["content"]
memories = recall(latest_message)
if memories:
context = "\n".join(f"- {m['memory']['content']}" for m in memories)
return {"memory_context": f"Relevant context from past sessions:\n{context}"}
return {"memory_context": None}
def call_model(state: AgentState):
"""Call the LLM, injecting memory context into the system prompt."""
messages = state["messages"].copy()
if state.get("memory_context"):
system = {
"role": "system",
"content": f"You are a helpful assistant.\n\n{state['memory_context']}"
}
messages = [system] + messages
response = llm.invoke(messages)
return {"messages": [response]}
def memory_store_node(state: AgentState):
"""Extract and store new facts from the assistant's last response."""
last_response = state["messages"][-1].content
# In production: use an LLM to extract structured facts before storing.
# Here we store the full response as context (simplified example).
remember(f"Assistant said: {last_response[:500]}", memory_type="event")
return {}
# ── Build graph ──────────────────────────────────────────────────
builder = StateGraph(AgentState)
builder.add_node("recall", memory_recall_node)
builder.add_node("agent", call_model)
builder.add_node("store", memory_store_node)
builder.set_entry_point("recall")
builder.add_edge("recall", "agent")
builder.add_edge("agent", "store")
builder.add_edge("store", END)
graph = builder.compile(checkpointer=MemorySaver())
# Session 1
config = {"configurable": {"thread_id": "user-001-session-1"}}
graph.invoke({
"messages": [{"role": "user", "content": "I prefer Python over TypeScript for all my projects"}],
"memory_context": None
}, config)
# Session 2 — different thread, different process, different day
# Kronvex recalls the Python preference automatically
config2 = {"configurable": {"thread_id": "user-001-session-2"}}
result = graph.invoke({
"messages": [{"role": "user", "content": "What language should I use for the new microservice?"}],
"memory_context": None
}, config2)
# Agent now answers: "Based on your past preference for Python..."
Note de production : Dans le memory_store_node, remplacez l'approche simplifiée "stocker tout" par une étape d'extraction LLM. Promptez un modèle rapide (gpt-4o-mini) avec le tour de conversation et demandez-lui d'extraire des faits discrets à retenir. Stockez chaque fait comme une mémoire séparée.
Avancé : inject_context pour le rappel automatique
L'endpoint /recall renvoie des objets mémoire structurés avec des scores de confiance. Pour de nombreux cas d'usage, vous voulez une interface plus simple : donnez-moi un bloc contexte formaté que je peux insérer directement dans un prompt système.
L'endpoint /inject-context de Kronvex fait exactement cela. Vous passez une chaîne de requête et un budget de caractères maximum, et il renvoie une chaîne de contexte prête à l'emploi.
import httpx
KRONVEX_API_KEY = "kv-your-key"
KRONVEX_AGENT_ID = "your-agent-id"
def inject_context(query: str, max_tokens: int = 800) -> str:
"""Return a formatted memory block ready for a system prompt."""
r = httpx.post(
f"https://api.kronvex.io/api/v1/agents/{KRONVEX_AGENT_ID}/inject-context",
headers={"X-API-Key": KRONVEX_API_KEY},
json={"query": query, "max_tokens": max_tokens, "threshold": 0.45}
)
data = r.json()
return data.get("context", "") # Returns "" if no relevant memories
def memory_recall_node(state: AgentState):
"""Use inject_context to get a fully formatted memory block."""
latest_message = state["messages"][-1]["content"]
context = inject_context(latest_message)
return {"memory_context": context if context else None}
def call_model(state: AgentState):
"""System prompt now includes pre-formatted memory context."""
messages = list(state["messages"])
system_content = "You are a helpful assistant."
if state.get("memory_context"):
system_content += f"\n\n## Long-term memory\n{state['memory_context']}"
messages = [{"role": "system", "content": system_content}] + messages
response = llm.invoke(messages)
return {"messages": [response]}
L'endpoint inject_context gère la déduplication qui causerait autrement des problèmes quand plusieurs mémoires liées remontent pour la même requête.
Diagramme d'architecture
Voici comment les deux couches s'emboîtent dans un déploiement LangGraph en production.
User message
│
â–¼
┌────────────────────────────────────────────────â”
│ LangGraph Graph │
│ │
│ ┌──────────────────┠│
│ │ memory_recall │ ◄── Kronvex /recall │
│ │ node │ (semantic search, │
│ │ │ cross-session) │
│ └────────┬─────────┘ │
│ │ injects context into state │
│ ▼ │
│ ┌──────────────────┠│
│ │ agent node │ ◄── LLM (GPT-4o, etc.) │
│ │ (LLM call) │ system prompt = │
│ │ │ base + memories │
│ └────────┬─────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────┠│
│ │ memory_store │ ──► Kronvex /remember │
│ │ node │ (persist new facts) │
│ └────────┬─────────┘ │
│ │ │
│ ┌────────▼─────────┠│
│ │ PostgresSaver │ ↠Thread-level state │
│ │ checkpointer │ (resume on restart) │
│ └──────────────────┘ │
└────────────────────────────────────────────────┘
│
â–¼
Response to user
─────────────────────────────────────────────────
Kronvex (EU, pgvector) PostgresSaver (your DB)
Cross-session facts Graph execution state
Semantic search Exact checkpoint replay
L'insight clé est que ces deux couches de persistance sont orthogonales. PostgresSaver gère la question : "où en étais-je dans cette exécution ?" Kronvex gère la question : "que sais-je sur cet utilisateur ou ce domaine ?"
Comparaison : MemorySaver vs SqliteSaver vs Kronvex
| Fonctionnalité | MemorySaver | SqliteSaver | Kronvex |
|---|---|---|---|
| Survit au redémarrage du processus | ✗ | ✓ | ✓ |
| Mémoire cross-thread | ✗ | ✗ | ✓ |
| Recherche par similarité sémantique | ✗ | ✗ | ✓ |
| Aucune infrastructure à gérer | ✓ | Local file only | ✓ Hosted API |
| Isolation multi-tenant | ✗ | Manual | ✓ Agent ID scoped |
| Résidence des données UE (RGPD) | Wherever deployed | Wherever deployed | ✓ Frankfurt, EU |
| Score de confiance | ✗ | ✗ | ✓ Similarity + recency + frequency |
| Cas d'usage | Dev / prototyping | Single-machine apps | Production agents, B2B SaaS |
| Niveau gratuit | ✓ | ✓ | ✓ 1 agent, 100 memories |
Stack recommandé : Utilisez MemorySaver en développement local, passez à PostgresSaver pour la persistance des checkpoints en production, et ajoutez Kronvex pour la mémoire sémantique cross-session.
FAQ
Kronvex remplace-t-il le checkpointer de LangGraph ?
Quel est l'overhead de latence de l'ajout d'un nœud de rappel Kronvex ?
/recall typique se termine en 40–80ms (p50). C'est acceptable car l'appel LLM lui-même (la latence dominante) prend typiquement 500ms–3s.Comment gérer les agents multi-tenant dans LangGraph ?
/recall sur l'agent ID A ne renvoie jamais de mémoires de l'agent ID B.Puis-je utiliser Kronvex avec l'exécution de graphe async de LangGraph ?
httpx.post par await httpx.AsyncClient().post(...). LangGraph supporte nativement les fonctions de nœuds async. Définissez vos nœuds mémoire comme async def et utilisez await pour les appels API Kronvex.