memory — Conversation history strategies (Buffer, Window, Summary, KG, Vector)
Conversation history strategies (Buffer, Window, Summary, KG, Vector)
Module langchain_classic/memory/ — Conversation history strategies (Buffer, Window, Summary, KG, Vector). Part of the State & Tools group.
🎯 Why This Module Exists
This module is documented from the memory_deep.md Phase 1B research artifact. See sections below for design decisions, algorithms, error philosophy, performance, and evolution clues.
BaseMemory Token-aware Pluggable backend
Architecture
flowchart LR
subgraph Core["langchain_classic/memory/"]
BASE["Base abstraction"]
IMPLS["Concrete implementations"]
BASE --> IMPLS
end
subgraph Up["Upstream callers"]
U1["chains/"]
U2["agents/"]
end
subgraph Down["Downstream"]
D1["langchain_core"]
D2["External providers"]
end
Up --> Core
Core --> Down
Data Flow
sequenceDiagram participant Caller as Caller participant Memory as memory participant Core as langchain_core participant Ext as External Caller->>Memory: invoke(input) Note over Memory: Validate & route Memory->>Core: delegate primitive Core->>Ext: provider call Ext-->>Core: response Core-->>Memory: typed output Memory-->>Caller: result
Existence rationale
memory/ provides the conversational state primitives that turn a stateless chain into something that remembers prior turns. The contract is small and uniform: load_memory_variables(inputs) -> dict and save_context(inputs, outputs) -> None. The interesting variation is what state is kept and how it is compacted: raw buffer, sliding window, running summary, knowledge-graph triples, salient entity facts, vectorstore-indexed turns, or a token-budgeted hybrid. These are distinct algorithms — each is a deliberate trade between recall, fidelity, prompt cost, and latency.
Design decisions visible in the code
| Decision | Choice made | Plausible alternatives | Inferred rationale |
|---|---|---|---|
| Memory contract | BaseMemory.load/save_context | Direct attribute access; observer | Two-method contract is small enough to subclass in a notebook; orthogonal to Chain |
| Sync write, lazy read | save_context mutates state synchronously; load_memory_variables reads on demand | Always lazy; always eager | Saving must be deterministic (next turn must see this turn); reading happens once per chain call |
| Compaction strategies as classes | Buffer / Window / Summary / SummaryBuffer / TokenBuffer / KG / Entity / VectorStore | Single class with config flags | Each algorithm has its own state shape (string vs list vs graph) — subclassing keeps state types clean |
| Summary memory uses an LLM | _get_summary calls an LLM with prior summary + new turns | Heuristic truncation | LLM summaries preserve semantic content much better; cost is the trade |
| Token-buffer eviction | FIFO drop until token count ≤ max_token_limit | Random / LRU | FIFO matches conversational recency intuition |
| ChatMessageHistory abstraction | Memory stores delegate to a BaseChatMessageHistory | Direct list | Lets memory be backed by Redis, file, Postgres, … without changing the algorithm |
| Entity extraction | Lightweight LLM call extracts entity names + summaries | NER + heuristic | LLM extraction handles cross-domain entities a static NER misses |
| Read-only / combined wrappers | ReadOnlySharedMemory, CombinedMemory | Inline composition | Allow agents to expose memory to a sub-chain without letting that sub-chain write to it |
Algorithm deep-dives
1. ConversationSummaryBufferMemory
Problem. Pure buffer overflows context; pure summary loses recent detail. Combine: keep the last N turns verbatim, and an LLM summary of everything older.
Trace. On save_context(input, output):
- Append
(HumanMessage(input), AIMessage(output))tochat_memory.messages. - Compute
total_tokens = sum(token_count(m) for m in messages). - While
total_tokens > max_token_limit:
- Pop the oldest message (or pair).
moving_summary = llm.invoke(summary_prompt(moving_summary, popped_msg)).
- Persist
(moving_summary, remaining messages).
load_memory_variables returns: system_summary + the recent messages (in messages mode) or a flattened string (in string mode).
Why this works. Recent turns are kept verbatim — high fidelity for clarification turns. Older turns are folded into a running summary that preserves names/decisions/intents. The summary cost is amortised: only one LLM call when eviction triggers.
Edge cases.
- LLM call fails during eviction → state remains over budget; logged via callbacks.
max_token_limitis approximate (token counter is an estimate for non-OpenAI models).
2. ConversationKGMemory
Problem. A multi-turn chat about Alice's preferences should let the agent answer "what does Alice like?" turns later. Summaries blur this; raw buffers exceed limits. Build a knowledge graph.
Trace.
- After each turn, the LLM is asked to extract
(subject, predicate, object)triples from the latest exchange. - Triples are added to
NetworkxEntityGraph(in-memory by default). - On
load_memory_variables, the LLM is asked which entities the new input mentions. - The retrieved entities' triples are formatted as text and added to the prompt.
Why a KG. Triples deduplicate naturally (("Alice", "likes", "tea") is the same regardless of phrasing) and support directed lookup (give me everything about Alice). Cost is two extra LLM calls per turn — acceptable for high-fidelity assistants.
3. VectorStoreRetrieverMemory
Problem. Long-running agents (weeks of conversation) blow past summary capacity. Index every turn into a vectorstore and retrieve only the relevant ones.
Trace.
save_context(inputs, outputs): formatf"input: {…}\noutput: {…}"→ embed → upsert into vectorstore.load_memory_variables({"input": new_q}): vectorstore.similarity_search(new_q, k=4) → format messages → return.
Why. Effectively unbounded memory at the cost of context-relevance precision. Works well for "support agent looks up past tickets from the same user" scenarios.
Error philosophy
Best-effort, never block the next turn. Memory failures are logged but never raised (an agent can survive without long-term recall, but mid-turn crash loses context). LLM-driven memories (Summary, KG, Entity) catch parser errors and skip the problematic turn — the buffer still grows, just without the LLM-derived overlay.
Performance characteristics
- Buffer / Window: O(1) save, O(N) read (N = window size). No LLM cost. Cheapest.
- Summary / SummaryBuffer: O(1) save (O(LLM) when eviction triggers), O(1) read. One LLM call amortised per K turns.
- KG: O(LLM) save (extract triples) + O(LLM) read (entity detection). Two LLM calls per turn.
- VectorStore: O(embed) save, O(embed + search) read. Two embedding calls per turn, no LLM.
- Entity: between Summary and KG.
Evolution clues
- Most memory classes pre-date
BaseChatMessageHistory;chat_memory.pyshows the bolt-on:BaseChatMemory(BaseMemory)adds achat_memory: BaseChatMessageHistoryattribute. motorhead_memory.pyandzep_memory.pyare external-service variants that delegate the entire algorithm to a server — kept for backwards compatibility but largely superseded by community packages.- The newer LCEL pattern is
RunnableWithMessageHistory(runnable, get_session_history=...)— explicit history rather than implicit memory. The classicMemoryAPI persists for users on the legacyChainsurface.
Step-by-Step Walkthrough
Follow a typical operation through the memory/ module:
-
Caller invokes the public surface — usually
.invoke(input)on a Runnable, or a legacy__call__(inputs_dict)on aChainsubclass. - Pydantic validation — every entry point is a BaseModel; constructor args, input dicts, and tool args_schema are validated before any expensive work runs. Fail-fast on type errors.
-
CallbackManager.configure — a fresh manager is built per call, fans
on_*_startevents to all subscribers (LangSmith tracer, stdout handler, custom logging), and stamps a UUID4run_idfor tree reconstruction. - memory-specific logic — the module's core method runs: prompt rendering, retrieval, tool execution, memory load, document split, etc. See the deep-dive sections above.
- Provider call — typically a single HTTP request to OpenAI/Anthropic/etc., or a local SDK call. Retries are handled by tenacity with exponential backoff and a configurable max-attempts knob.
-
Streaming fan-out — if the provider streams tokens, each chunk fires
on_llm_new_tokenthrough the callback manager so downstream observers (UI, log, tracer) see partial output. -
OutputParser pass — if a parser is attached, the raw provider output is parsed into a structured Python object;
OutputParserExceptionmay trigger a retry chain (RetryWithErrorOutputParser). -
Memory write-back — for chains/agents that have a
BaseMemoryattached,save_context(inputs, outputs)persists the turn before returning to the caller. -
on_*_end fan-out — the callback manager closes the run span; tracers serialise the
Runtree and ship it to LangSmith asynchronously. - Result returned — caller receives a typed object (AIMessage, dict, BaseModel, list[Document], ...) matching the module's declared output schema.
What Could Go Wrong
| Failure Mode | Trigger | Detection | Recovery |
|---|---|---|---|
| Provider 5xx / rate-limit | OpenAI/Anthropic/etc. transient error | HTTP status from SDK | tenacity retries with exponential backoff up to max_retries; then surface to caller |
| Output parser failure | LLM returned malformed JSON / wrong schema |
OutputParserException in parser |
RetryWithErrorOutputParser re-prompts with the parser error included; or surface |
| Pydantic validation | Caller passes wrong-shaped dict |
ValidationError at __init__ / invoke
|
Surface immediately — no fallback is correct here |
| Callback handler exception | User-supplied tracer raises | Try/except around each fan-out | Logged and swallowed by default; raise_error=True to opt in to propagation |
| Provider auth missing |
OPENAI_API_KEY unset |
SDK AuthenticationError
|
Surface with a helpful message; no retry (would just fail again) |
Performance Profile
Hot Paths
- Provider HTTP round-trip dominates wall-clock time (100ms–30s depending on model and prompt length).
-
Pydantic validation is repeated on every
invoke; cached schema parsing keeps it under 1ms for typical inputs. - Callback fan-out is synchronous by default — a slow handler will slow every call. Async tracers (LangSmith) push work to a background thread to avoid blocking.
Resource Usage
-
Memory: token-history buffers (in
memory/) and document caches (in retrieval pipelines) are the largest steady-state allocations. - I/O: bounded by provider rate limits; vector store I/O for retrieval-flavored modules.
-
Concurrency: async support via
a*methods; agents and parallel chains useasyncio.gatherfor concurrent tool calls.
Directory Map
Source tree for memory
memory/
├── __init__.py
├── buffer.py
├── buffer_window.py
├── chat_memory.py
├── chat_message_histories/
├── combined.py
├── entity.py
├── kg.py
├── motorhead_memory.py
├── prompt.py
├── readonly.py
├── simple.py
├── summary.py
├── summary_buffer.py
├── token_buffer.py
├── utils.py
├── vectorstore.py
├── vectorstore_token_buffer_memory.py
└── zep_memory.py