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.

39
Source files
0
Sub-directories
State & Tools
Category
Python
Language

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

DecisionChoice madePlausible alternativesInferred rationale
Memory contractBaseMemory.load/save_contextDirect attribute access; observerTwo-method contract is small enough to subclass in a notebook; orthogonal to Chain
Sync write, lazy readsave_context mutates state synchronously; load_memory_variables reads on demandAlways lazy; always eagerSaving must be deterministic (next turn must see this turn); reading happens once per chain call
Compaction strategies as classesBuffer / Window / Summary / SummaryBuffer / TokenBuffer / KG / Entity / VectorStoreSingle class with config flagsEach 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 turnsHeuristic truncationLLM summaries preserve semantic content much better; cost is the trade
Token-buffer evictionFIFO drop until token count ≤ max_token_limitRandom / LRUFIFO matches conversational recency intuition
ChatMessageHistory abstractionMemory stores delegate to a BaseChatMessageHistoryDirect listLets memory be backed by Redis, file, Postgres, … without changing the algorithm
Entity extractionLightweight LLM call extracts entity names + summariesNER + heuristicLLM extraction handles cross-domain entities a static NER misses
Read-only / combined wrappersReadOnlySharedMemory, CombinedMemoryInline compositionAllow 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):

  1. Append (HumanMessage(input), AIMessage(output)) to chat_memory.messages.
  2. Compute total_tokens = sum(token_count(m) for m in messages).
  3. While total_tokens > max_token_limit:
  1. 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.

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.

  1. After each turn, the LLM is asked to extract (subject, predicate, object) triples from the latest exchange.
  2. Triples are added to NetworkxEntityGraph (in-memory by default).
  3. On load_memory_variables, the LLM is asked which entities the new input mentions.
  4. 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.

  1. save_context(inputs, outputs): format f"input: {…}\noutput: {…}" → embed → upsert into vectorstore.
  2. 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

Evolution clues

Step-by-Step Walkthrough

Follow a typical operation through the memory/ module:

  1. Caller invokes the public surface — usually .invoke(input) on a Runnable, or a legacy __call__(inputs_dict) on a Chain subclass.
  2. 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.
  3. CallbackManager.configure — a fresh manager is built per call, fans on_*_start events to all subscribers (LangSmith tracer, stdout handler, custom logging), and stamps a UUID4 run_id for tree reconstruction.
  4. 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.
  5. 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.
  6. Streaming fan-out — if the provider streams tokens, each chunk fires on_llm_new_token through the callback manager so downstream observers (UI, log, tracer) see partial output.
  7. OutputParser pass — if a parser is attached, the raw provider output is parsed into a structured Python object; OutputParserException may trigger a retry chain (RetryWithErrorOutputParser).
  8. Memory write-back — for chains/agents that have a BaseMemory attached, save_context(inputs, outputs) persists the turn before returning to the caller.
  9. on_*_end fan-out — the callback manager closes the run span; tracers serialise the Run tree and ship it to LangSmith asynchronously.
  10. 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

Resource Usage

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