|
| 1 | +--- |
| 2 | +title: "Adding Long-Term Memory to LangGraph and LangChain Agents" |
| 3 | +description: Learn how to add long-term memory to LangGraph and LangChain agents using three integration patterns — tools, nodes, and BaseStore — with per-user memory banks and semantic recall. |
| 4 | +authors: [DK09876] |
| 5 | +date: 2026-03-17 |
| 6 | +tags: [langgraph, langchain, integrations, agents, memory] |
| 7 | +image: /img/blog/langgraph-longterm-memory.png |
| 8 | +hide_table_of_contents: true |
| 9 | +--- |
| 10 | + |
| 11 | +LangGraph agents are stateful by design — checkpointers save graph state between steps, and the Store API persists data across threads. But neither gives agents true long-term memory: the ability to extract meaning from conversations, build up knowledge over time, and recall it semantically when relevant. |
| 12 | + |
| 13 | +That's what Hindsight adds. Hindsight is a memory layer for LLM applications that automatically extracts facts from conversations, builds entity graphs, and retrieves relevant context using four parallel recall strategies. The `hindsight-langgraph` package brings that to LangGraph — and since the memory tools are standard LangChain `@tool` functions, they work with plain LangChain too. |
| 14 | + |
| 15 | +<!-- truncate --> |
| 16 | + |
| 17 | +## The problem |
| 18 | + |
| 19 | +LangGraph's built-in persistence is designed for graph state — checkpoints, intermediate values, cross-thread key-value storage. It's good at "what did this graph do last time?" but not at "what does this agent know about this user?" |
| 20 | + |
| 21 | +Consider a support agent that talks to the same customer across dozens of sessions. With checkpointers alone, each new thread starts cold. With `InMemoryStore` or `PostgresStore`, you can manually store and retrieve facts, but you're responsible for: |
| 22 | + |
| 23 | +- Deciding what to store (fact extraction) |
| 24 | +- Deciding what's relevant (semantic retrieval) |
| 25 | +- Handling contradictions and updates |
| 26 | +- Building knowledge graphs from raw conversations |
| 27 | + |
| 28 | +Hindsight does all of this automatically. You retain conversations, and it extracts facts, builds entity graphs, and retrieves relevant memories using four parallel strategies: **semantic** (embedding similarity), **BM25** (keyword overlap), **graph traversal** (entity relationships), and **temporal** (recency weighting). Each strategy catches different things — semantic recall finds conceptually similar memories, graph traversal finds memories linked through shared entities, and temporal weighting surfaces recent context before older facts. Together they substantially outperform single-strategy retrieval. |
| 29 | + |
| 30 | +## Three integration patterns |
| 31 | + |
| 32 | +We built three ways to add Hindsight memory to LangGraph, at different abstraction levels. |
| 33 | + |
| 34 | +### 1. Tools — the agent decides (LangChain & LangGraph) |
| 35 | + |
| 36 | +Give the agent retain/recall/reflect tools and let it decide when to use memory. These are standard LangChain `@tool` functions, so they work with both LangGraph (via `create_react_agent`) and plain LangChain (via `bind_tools()`). |
| 37 | + |
| 38 | +```python |
| 39 | +from hindsight_client import Hindsight |
| 40 | +from hindsight_langgraph import create_hindsight_tools |
| 41 | +from langchain_openai import ChatOpenAI |
| 42 | +from langgraph.prebuilt import create_react_agent |
| 43 | + |
| 44 | +client = Hindsight(base_url="http://localhost:8888") |
| 45 | +tools = create_hindsight_tools(client=client, bank_id="user-123") |
| 46 | + |
| 47 | +# With LangGraph |
| 48 | +agent = create_react_agent(ChatOpenAI(model="gpt-4o"), tools=tools) |
| 49 | + |
| 50 | +# Or with plain LangChain |
| 51 | +model = ChatOpenAI(model="gpt-4o").bind_tools(tools) |
| 52 | +``` |
| 53 | + |
| 54 | +The agent gets three tools: |
| 55 | + |
| 56 | +- **`hindsight_retain`** — stores the conversation and extracts facts from it |
| 57 | +- **`hindsight_recall`** — searches the memory bank for relevant context |
| 58 | +- **`hindsight_reflect`** — synthesizes across multiple memories to produce a summary or answer a question about what the agent knows (useful for questions like "what has this user told me about their stack?") |
| 59 | + |
| 60 | +The agent calls these based on conversation context — storing facts when the user shares something important, recalling when asked about past context, and reflecting when it needs to synthesize accumulated knowledge. |
| 61 | + |
| 62 | +**Best for**: ReAct agents that need to reason about when memory is relevant. Works with LangGraph for automatic tool execution loops or with plain LangChain if you manage the loop yourself. |
| 63 | + |
| 64 | +### 2. Nodes — memory as graph steps |
| 65 | + |
| 66 | +Add recall and retain as automatic nodes in your graph. No tool-calling required — memory runs on every turn. |
| 67 | + |
| 68 | +```python |
| 69 | +from hindsight_langgraph import create_recall_node, create_retain_node |
| 70 | +from langgraph.graph import StateGraph, MessagesState, START, END |
| 71 | + |
| 72 | +recall = create_recall_node(client=client, bank_id_from_config="user_id") |
| 73 | +retain = create_retain_node(client=client, bank_id_from_config="user_id") |
| 74 | + |
| 75 | +builder = StateGraph(MessagesState) |
| 76 | +builder.add_node("recall", recall) |
| 77 | +builder.add_node("agent", agent_node) |
| 78 | +builder.add_node("retain", retain) |
| 79 | +builder.add_edge(START, "recall") |
| 80 | +builder.add_edge("recall", "agent") |
| 81 | +builder.add_edge("agent", "retain") |
| 82 | +builder.add_edge("retain", END) |
| 83 | +``` |
| 84 | + |
| 85 | +The recall node runs before the LLM, searches Hindsight for memories relevant to the user's message, and injects them as a `SystemMessage`. The retain node runs after, storing the conversation. Both resolve per-user bank IDs from `RunnableConfig` at runtime. |
| 86 | + |
| 87 | +**Best for**: Agents where you always want memory context injected automatically, without relying on the LLM to decide when to use memory tools. |
| 88 | + |
| 89 | +### 3. BaseStore — drop-in backend |
| 90 | + |
| 91 | +Replace LangGraph's `InMemoryStore` with Hindsight as the storage backend. If your team already uses LangGraph's store patterns, this is the lowest-friction path. |
| 92 | + |
| 93 | +```python |
| 94 | +from hindsight_langgraph import HindsightStore |
| 95 | + |
| 96 | +store = HindsightStore(client=client) |
| 97 | +graph = builder.compile(checkpointer=checkpointer, store=store) |
| 98 | +``` |
| 99 | + |
| 100 | +Namespace tuples map to Hindsight bank IDs (`("user", "123")` → bank `user.123`), banks are auto-created, and `search()` uses Hindsight's full semantic recall instead of basic vector similarity. |
| 101 | + |
| 102 | +**Best for**: Teams already using LangGraph's `store` patterns who want better retrieval without restructuring their graph. |
| 103 | + |
| 104 | +--- |
| 105 | + |
| 106 | +### Which pattern fits your use case? |
| 107 | + |
| 108 | +| | Tools | Nodes | BaseStore | |
| 109 | +|---|---|---|---| |
| 110 | +| Works with plain LangChain | Yes | No | No | |
| 111 | +| Memory runs automatically | No (LLM decides) | Yes | Yes | |
| 112 | +| Uses existing store interface | No | No | Yes | |
| 113 | +| LLM controls when to remember | Yes | No | No | |
| 114 | +| Lowest migration cost | — | Low | Lowest | |
| 115 | + |
| 116 | +--- |
| 117 | + |
| 118 | +## Complete working example |
| 119 | + |
| 120 | +Here's a full support agent that remembers each user across sessions using the nodes pattern. This is copy-pasteable and runnable against either self-hosted Hindsight or Hindsight Cloud. |
| 121 | + |
| 122 | +```python |
| 123 | +import asyncio |
| 124 | +from hindsight_client import Hindsight |
| 125 | +from hindsight_langgraph import create_recall_node, create_retain_node |
| 126 | +from langchain_openai import ChatOpenAI |
| 127 | +from langchain_core.messages import HumanMessage, SystemMessage |
| 128 | +from langgraph.graph import StateGraph, MessagesState, START, END |
| 129 | +from langgraph.checkpoint.memory import MemorySaver |
| 130 | + |
| 131 | +# --- Setup --- |
| 132 | + |
| 133 | +client = Hindsight(base_url="http://localhost:8888") |
| 134 | +# For Hindsight Cloud: |
| 135 | +# client = Hindsight(base_url="https://api.hindsight.vectorize.io", api_key="...") |
| 136 | + |
| 137 | +llm = ChatOpenAI(model="gpt-4o") |
| 138 | +checkpointer = MemorySaver() |
| 139 | + |
| 140 | +# --- Memory nodes --- |
| 141 | +# bank_id_from_config pulls the user ID from RunnableConfig at runtime, |
| 142 | +# so one graph definition serves all users with isolated memory banks. |
| 143 | + |
| 144 | +recall = create_recall_node(client=client, bank_id_from_config="user_id") |
| 145 | +retain = create_retain_node(client=client, bank_id_from_config="user_id") |
| 146 | + |
| 147 | +# --- Agent node --- |
| 148 | + |
| 149 | +async def agent_node(state: MessagesState): |
| 150 | + system = SystemMessage(content=( |
| 151 | + "You are a helpful support agent. " |
| 152 | + "Relevant memories about this user have been injected above. " |
| 153 | + "Use them to personalize your response." |
| 154 | + )) |
| 155 | + response = await llm.ainvoke([system] + state["messages"]) |
| 156 | + return {"messages": [response]} |
| 157 | + |
| 158 | +# --- Graph --- |
| 159 | + |
| 160 | +builder = StateGraph(MessagesState) |
| 161 | +builder.add_node("recall", recall) |
| 162 | +builder.add_node("agent", agent_node) |
| 163 | +builder.add_node("retain", retain) |
| 164 | +builder.add_edge(START, "recall") |
| 165 | +builder.add_edge("recall", "agent") |
| 166 | +builder.add_edge("agent", "retain") |
| 167 | +builder.add_edge("retain", END) |
| 168 | + |
| 169 | +graph = builder.compile(checkpointer=checkpointer) |
| 170 | + |
| 171 | +# --- Run --- |
| 172 | + |
| 173 | +async def chat(user_id: str, thread_id: str, message: str): |
| 174 | + config = { |
| 175 | + "configurable": { |
| 176 | + "user_id": user_id, |
| 177 | + "thread_id": thread_id, |
| 178 | + } |
| 179 | + } |
| 180 | + result = await graph.ainvoke( |
| 181 | + {"messages": [HumanMessage(content=message)]}, |
| 182 | + config=config, |
| 183 | + ) |
| 184 | + return result["messages"][-1].content |
| 185 | + |
| 186 | + |
| 187 | +async def main(): |
| 188 | + # Session 1: user shares context |
| 189 | + print("Session 1") |
| 190 | + print(await chat("user-42", "thread-1", "Hi! I'm running into issues with our Postgres connection pool. We're on SQLAlchemy 2.0.")) |
| 191 | + print(await chat("user-42", "thread-1", "We're using async sessions with asyncpg. The pool keeps exhausting under load.")) |
| 192 | + |
| 193 | + # Session 2: new thread, same user — agent remembers |
| 194 | + print("\nSession 2 (new thread)") |
| 195 | + print(await chat("user-42", "thread-2", "Hey, back again. Still fighting the connection pool issue.")) |
| 196 | + # Agent recalls SQLAlchemy 2.0, asyncpg, and the pool exhaustion context |
| 197 | + # without the user having to repeat themselves. |
| 198 | + |
| 199 | + |
| 200 | +asyncio.run(main()) |
| 201 | +``` |
| 202 | + |
| 203 | +What Hindsight extracts from Session 1 and stores in `user-42`'s memory bank: |
| 204 | + |
| 205 | +``` |
| 206 | +- Uses SQLAlchemy 2.0 with async sessions |
| 207 | +- Uses asyncpg driver |
| 208 | +- Experiencing connection pool exhaustion under load |
| 209 | +- Running Postgres |
| 210 | +``` |
| 211 | + |
| 212 | +When Session 2 starts on a fresh thread, the recall node searches the memory bank for context relevant to "Still fighting the connection pool issue" and injects those facts as a `SystemMessage` before the LLM responds. The agent picks up exactly where the last session ended. |
| 213 | + |
| 214 | +--- |
| 215 | + |
| 216 | +## Per-user memory in one line |
| 217 | + |
| 218 | +All three patterns support dynamic bank IDs. Instead of hardcoding a bank, resolve it from the graph's config at runtime: |
| 219 | + |
| 220 | +```python |
| 221 | +recall = create_recall_node(client=client, bank_id_from_config="user_id") |
| 222 | + |
| 223 | +# Each invocation gets its own isolated memory bank |
| 224 | +await graph.ainvoke( |
| 225 | + {"messages": [...]}, |
| 226 | + config={"configurable": {"user_id": "user-456"}}, |
| 227 | +) |
| 228 | +``` |
| 229 | + |
| 230 | +One graph definition serves all users. Memory banks are created automatically and kept fully isolated. |
| 231 | + |
| 232 | +## Getting started |
| 233 | + |
| 234 | +```bash |
| 235 | +pip install hindsight-langgraph |
| 236 | +``` |
| 237 | + |
| 238 | +Works with both self-hosted Hindsight and [Hindsight Cloud](https://ui.hindsight.vectorize.io/signup). For cloud, pass your API key when creating the client: |
| 239 | + |
| 240 | +```python |
| 241 | +client = Hindsight(base_url="https://api.hindsight.vectorize.io", api_key="your-key") |
| 242 | +# or |
| 243 | +from hindsight_client import configure |
| 244 | +configure(api_key="your-key") # defaults to the cloud URL |
| 245 | +``` |
| 246 | + |
| 247 | +## What to build with this |
| 248 | + |
| 249 | +Long-term memory unlocks a different class of agent behavior. A few patterns we've seen work well: |
| 250 | + |
| 251 | +- **Support agents** that remember each customer's history, preferences, and past issues across sessions |
| 252 | +- **Sales assistants** that accumulate context about prospects over multiple touchpoints |
| 253 | +- **Personal productivity agents** that build up a model of a user's work style, priorities, and decisions |
| 254 | + |
| 255 | +In all three cases, the agent gets meaningfully better the longer it runs — not just because of a longer context window, but because Hindsight distills conversations into structured knowledge it can retrieve precisely when relevant. |
| 256 | + |
| 257 | +Full docs: [LangGraph integration](/docs/sdks/integrations/langgraph) | [GitHub](https://github.com/vectorize-io/hindsight/tree/main/hindsight-integrations/langgraph) |
0 commit comments