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 :

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.

INSTALL
pip install kronvex
# 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.

Python — Standard LangGraph (in-memory, non-persistent)
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.

Python — LangGraph + Kronvex persistent memory
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.

Python — Using inject_context in a LangGraph node
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 ?
Non. Ils résolvent des problèmes différents. Un checkpointer LangGraph sauvegarde l'état d'exécution du graphe. Kronvex stocke des mémoires sémantiques : faits, préférences et événements à rappeler dans de futures conversations. Vous utilisez les deux ensemble.
Quel est l'overhead de latence de l'ajout d'un nœud de rappel Kronvex ?
Un appel /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 ?
Assignez un ID agent Kronvex par utilisateur final. Chaque agent Kronvex a un store mémoire complètement isolé — /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 ?
Oui. Remplacez les appels 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.