Modern language models are stateless. Each inference starts from scratch, with no memory of previous interactions. For a single-turn Q&A system, this is fine. For agents that must learn, adapt, and accumulate knowledge over hours, days, or weeks of operation, it's a fundamental limitation.
This is where long-term memory architectures enter the picture. By layering different memory systems atop your agent—working memory for immediate context, episodic memory for session history, semantic memory for facts and relationships, and procedural memory for learned strategies—you can build agents that genuinely remember and learn.
In this article, I'll walk through the four pillars of agent memory, the implementation patterns that make each work at scale, and how to integrate them into a unified architecture. This is core material from Building Agentic AI Systems, grounded in production experience.
Why Context Windows Aren't Memory
The first misconception to dispel: a large context window is not the same as memory. Claude 3 supports 200K tokens; GPT-4 supports 128K. But context windows are:
- Ephemeral: The context exists only for a single request and is discarded after the response.
- Expensive: Every token in the context window costs money and increases latency. Stuffing everything into every request is wasteful.
- Sequential: You can't efficiently query or search a context window; you must read linearly from the beginning.
- Lossy: Information at the edges of large contexts tends to be deprioritized by attention mechanisms ("lost in the middle").
A context window is a working memory buffer—useful for immediate reasoning but not a substitute for persistent storage and retrieval.
The Four Memory Systems: A Human Analogy
Human memory isn't monolithic. We have four distinct systems that work in concert:
1. Working Memory
Capacity: 5–9 items, seconds to minutes. Function: Active manipulation of information. Example: Holding a phone number while dialing it.
Agent equivalent: The context window of the current LLM request. It's where the agent reasons, plans, and makes decisions. Limited but immediate.
2. Episodic Memory
Capacity: Unbounded. Duration: Years. Function: Autobiographical memory—recall specific events, conversations, and experiences. Example: "I remember the client call on Tuesday when they asked about deployment latency."
Agent equivalent: Session logs, conversation history, experience records. Indexed for fast retrieval, compressed for efficiency.
3. Semantic Memory
Capacity: Unbounded. Duration: Lifetime. Function: Facts, concepts, relationships. No context attached. Example: "Paris is the capital of France" or "recursion is a key algorithmic pattern."
Agent equivalent: Knowledge graphs, vector databases, fact stores. Structured relationships that enable reasoning across domains.
4. Procedural Memory
Capacity: Hundreds of procedures. Duration: Lifetime. Function: Skills and strategies. Implicit, not explicit. Example: How to ride a bike, or how to debug a performance issue.
Agent equivalent: Tool strategies, learned execution patterns, heuristics distilled from experience.
Agents with only working memory are reactive: they respond to the current query with no history. Agents with episodic memory can learn from experience. Agents with semantic and procedural memory can reason deeply and adapt strategies. The combination creates genuine intelligence.
Implementation Pattern 1: Vector Store Memory (RAG-Based Recall)
The most common implementation of episodic and semantic memory is a vector database (Pinecone, Weaviate, Chroma, Milvus). The pattern is Retrieval-Augmented Generation (RAG):
- Embedding: Convert text (experiences, facts) into dense vectors using an embedding model (OpenAI text-embedding-3, Anthropic's embeddings, etc.).
- Storage: Index vectors in a database with metadata (timestamp, source, confidence).
- Retrieval: When the agent needs context, embed the query and retrieve the k nearest neighbors (most relevant items).
- Augmentation: Prepend retrieved items to the LLM prompt, so the agent reasons with relevant history.
Here's a skeleton implementation:
import openai
import pinecone
# Initialize
pinecone.init(api_key="YOUR_KEY", environment="us-west1-gcp")
index = pinecone.Index("agent-memory")
def store_experience(text: str, agent_id: str, timestamp: str):
"""Embed and store an experience."""
embedding = openai.Embedding.create(
input=text,
model="text-embedding-3-small"
)["data"][0]["embedding"]
index.upsert([(
f"{agent_id}-{timestamp}",
embedding,
{"text": text, "agent_id": agent_id, "timestamp": timestamp}
)])
def retrieve_memories(query: str, agent_id: str, k: int = 5):
"""Retrieve relevant memories for a query."""
query_embedding = openai.Embedding.create(
input=query,
model="text-embedding-3-small"
)["data"][0]["embedding"]
results = index.query(
query_embedding,
top_k=k,
filter={"agent_id": {"$eq": agent_id}}
)
return [
result.metadata["text"]
for result in results.matches
]
def agent_with_memory(query: str, agent_id: str):
"""Query the agent with memory augmentation."""
# Retrieve relevant memories
memories = retrieve_memories(query, agent_id)
# Augment the prompt
context = "\n".join([f"- {m}" for m in memories])
augmented_query = f"""Relevant past experiences:
{context}
Current query: {query}"""
# Call LLM
response = openai.ChatCompletion.create(
model="gpt-4",
messages=[{"role": "user", "content": augmented_query}]
)
# Store this interaction as a new experience
store_experience(f"Q: {query}\nA: {response.choices[0].message.content}", agent_id, timestamp=str(datetime.now()))
return response.choices[0].message.content
This pattern is production-proven. The key insights:
- Semantic search is better than keyword search. Vector embeddings capture meaning, so similar experiences are retrieved even if they don't share keywords.
- Filter by agent_id. In multi-agent systems, memory must be partitioned. A filter prevents hallucinations across agent contexts.
- Store with metadata. Timestamps, sources, and confidence scores enable ranking and debugging.
Implementation Pattern 2: Knowledge Graph Memory (Structured Relationships)
Vector stores excel at recall by similarity. But they don't capture relationships between concepts. For that, you need a knowledge graph—a graph database (Neo4j, AWS Neptune) that stores facts as nodes and relationships.
Example: Instead of storing "Client X uses technology Y," you store:
CREATE (client:Client {name: "Acme Corp"})
CREATE (tech:Technology {name: "Kubernetes"})
CREATE (client)-[:USES]->(tech)
Now the agent can query:
MATCH (client:Client {name: "Acme Corp"})-[:USES]->(tech)
RETURN tech.name
And reason: "Acme Corp uses Kubernetes, so they likely care about container orchestration reliability."
For agents, knowledge graphs are particularly powerful because:
- Reasoning over multiple hops: Find second- and third-degree relationships. "Who else uses the technologies that Acme uses?"
- Structured facts: No ambiguity—each edge is typed and can be queried precisely.
- Explainability: You can trace exactly why the agent made a recommendation by walking the graph.
A hybrid approach combines both: use the knowledge graph for reasoning and structured queries, and a vector store for fuzzy recall and semantic search.
Implementation Pattern 3: Episodic Memory (Conversation Summaries and Experience Logs)
Raw conversation logs are verbose and expensive to index. Instead, compress and summarize them:
def summarize_session(messages: list[dict]) -> str:
"""Summarize a session into a compact experience record."""
conversation_text = "\n".join([
f"{m['role']}: {m['content']}"
for m in messages
])
summary_prompt = f"""Summarize this conversation in 2-3 sentences.
Focus on:
- What the user asked
- What the agent did
- What the outcome was
Conversation:
{conversation_text}"""
summary = openai.ChatCompletion.create(
model="gpt-4",
messages=[{"role": "user", "content": summary_prompt}]
).choices[0].message.content
return summary
def store_session_memory(session_id: str, messages: list[dict], agent_id: str):
"""Compress and store a session."""
summary = summarize_session(messages)
# Store both the summary (for retrieval) and metadata
metadata = {
"session_id": session_id,
"agent_id": agent_id,
"timestamp": datetime.now().isoformat(),
"message_count": len(messages),
"first_message": messages[0]["content"][:100],
"outcome": "success" # or "failure", from agent logs
}
# Index in vector store
store_experience(summary, agent_id, str(datetime.now()))
The benefit: a 50-message conversation becomes a 2-sentence summary, reducing storage and retrieval costs by 20x while retaining the essential information.
Implementation Pattern 4: Procedural Memory (Tool Strategies and Learned Heuristics)
As an agent executes tasks repeatedly, it learns which tools work best in which contexts. Procedural memory captures these learned strategies.
Example: An agent tasked with debugging performance issues learns:
- Start with CPU profile, then memory trace, then database query log.
- If response time is >1s, check network latency first.
- Certain tools (flame graphs, APM dashboards) provide faster insight than others (raw logs).
Store this as a procedural policy:
class ProcedurePolicy:
"""Learn and store tool execution strategies."""
def __init__(self, agent_id: str):
self.agent_id = agent_id
self.strategies = {} # task_type -> [sequence of tools]
self.success_rates = {} # tool -> success rate
self.db = None # Persist to DB
def record_execution(self, task_type: str, tools_used: list[str], success: bool):
"""Record the outcome of a tool sequence."""
if task_type not in self.strategies:
self.strategies[task_type] = []
self.strategies[task_type].append(tools_used)
for tool in tools_used:
if tool not in self.success_rates:
self.success_rates[tool] = {"successes": 0, "attempts": 0}
self.success_rates[tool]["attempts"] += 1
if success:
self.success_rates[tool]["successes"] += 1
def get_recommended_tools(self, task_type: str) -> list[str]:
"""Return tools ranked by learned success."""
if task_type not in self.strategies:
return []
# Most common successful sequence
sequences = self.strategies[task_type]
return sequences[-1] if sequences else []
def save(self):
"""Persist to database."""
# Serialize to JSON and store
pass
Over time, the agent's tool selection becomes more efficient—it learns to reach for the right tools first, reducing wasted steps and token consumption.
Memory Management: Forgetting and Consolidation
Unlimited memory is a liability. Agents need to forget strategically:
1. Time-Decay Forgetting
Older memories matter less. Decay their weight:
def decay_memory_relevance(timestamp: str, decay_rate: float = 0.01):
"""Compute relevance decay based on age."""
age_days = (datetime.now() - datetime.fromisoformat(timestamp)).days
relevance = math.exp(-decay_rate * age_days)
return relevance
When retrieving memories, multiply by this decay factor. Old experiences matter, but less than recent ones.
2. Consolidation (Merging Similar Memories)
After many similar experiences, consolidate them into a single higher-level memory:
- Before: 100 debug sessions stored as individual memories, each 2-3 sentences.
- After: One consolidated memory: "Performance debugging typically starts with CPU profiling, then memory, then database queries. Success rate: 87%."
This reduces retrieval cost and prevents redundancy clutter.
3. Relevance-Based Pruning
Delete memories that the agent never retrieves:
def prune_memories(agent_id: str, retrieval_threshold: int = 2):
"""Delete memories that haven't been retrieved in N months."""
old_unused = index.query(
filter={
"agent_id": {"$eq": agent_id},
"retrieval_count": {"$lt": retrieval_threshold},
"age_days": {"$gt": 90}
}
)
for memory in old_unused:
index.delete(memory.id)
Hybrid Architecture: Bringing It All Together
In production, you use all four memory systems simultaneously:
| Memory Type | Storage | Use Case | Query Pattern |
|---|---|---|---|
| Working | LLM context window | Immediate reasoning | All (in every request) |
| Episodic | Vector store + DB | Recall past sessions | Semantic search |
| Semantic | Knowledge graph | Fact lookup, reasoning | Cypher/SPARQL |
| Procedural | Policy DB | Tool selection, strategies | Direct lookup |
The architecture looks like this:
┌─────────────────────────────────────────────────┐
│ User Query │
└────────────────┬────────────────────────────────┘
│
┌────────▼────────┐
│ Working Memory │ (LLM Context)
└────────┬────────┘
│
┌──────────┼──────────┐
│ │ │
┌──▼──┐ ┌───▼────┐ ┌──▼───┐
│ RAG │ │ Knowledge Graph │ │Policy│
│(Vec)│ │(Semantic Memory) │ │ DB │
└──┬──┘ └───┬────┘ └──┬───┘
│ │ │
└─────────┼──────────┘
│
┌─────▼──────┐
│ LLM │
│ Reasoning │
└─────┬──────┘
│
┌─────▼──────────┐
│ Tool Execution │
└─────┬──────────┘
│
┌─────▼──────┐
│ Store │
│ Outcome │ (Update Memory Systems)
└────────────┘
Code Example: A Complete Agentic Memory System
Here's a production-ready sketch of all four systems integrated:
from datetime import datetime
from typing import List, Dict, Any
import json
class AgentMemorySystem:
"""Unified agent memory architecture."""
def __init__(self, agent_id: str):
self.agent_id = agent_id
self.vector_store = VectorStore() # Episodic + Semantic
self.knowledge_graph = KnowledgeGraph()
self.policy_db = PolicyDatabase()
self.working_memory = [] # Current context
def observe(self, event: Dict[str, Any]):
"""Record an observation (tool call, result, etc.)."""
# Add to working memory
self.working_memory.append({
"timestamp": datetime.now(),
"event": event
})
# Limit working memory size
if len(self.working_memory) > 20:
# Consolidate oldest items
self._consolidate_working_memory()
def _consolidate_working_memory(self):
"""Compress working memory into episodic memory."""
if len(self.working_memory) < 5:
return
# Summarize oldest 5 items
old_items = self.working_memory[:5]
summary = self._summarize_events(old_items)
# Store in episodic memory
self.vector_store.store(
text=summary,
agent_id=self.agent_id,
timestamp=datetime.now()
)
# Remove from working memory
self.working_memory = self.working_memory[5:]
def recall_episodic(self, query: str, k: int = 3) -> List[str]:
"""Retrieve relevant episodic memories."""
return self.vector_store.retrieve(
query=query,
agent_id=self.agent_id,
k=k
)
def recall_semantic(self, query: str) -> List[Dict]:
"""Query the knowledge graph."""
return self.knowledge_graph.query(query)
def get_tool_strategy(self, task_type: str) -> List[str]:
"""Retrieve learned tool sequence."""
return self.policy_db.get_strategy(self.agent_id, task_type)
def record_tool_outcome(self, task_type: str, tools_used: List[str], success: bool):
"""Update procedural memory."""
self.policy_db.record(
agent_id=self.agent_id,
task_type=task_type,
tools_used=tools_used,
success=success
)
def augment_prompt(self, base_query: str) -> str:
"""Augment the LLM prompt with all relevant memories."""
episodic = self.recall_episodic(base_query)
semantic = self.recall_semantic(base_query)
working = [e["event"] for e in self.working_memory[-3:]]
augmented = f"""Working Context:
{json.dumps(working, default=str)}
Relevant Past Experiences:
{json.dumps(episodic)}
Knowledge Base:
{json.dumps(semantic)}
User Query: {base_query}"""
return augmented
Key Takeaways
1. Four memory systems are essential: Working memory (context), episodic (experiences), semantic (facts), procedural (strategies).
2. Vector stores enable semantic search: Use them for fast retrieval of relevant memories by meaning, not just keywords.
3. Knowledge graphs encode relationships: They enable multi-hop reasoning and are essential for structured domains.
4. Compression is critical: Summarize sessions, consolidate memories, and prune old unused data to keep costs manageable.
5. Integrate all systems: The agent's reasoning power comes from having multiple sources of truth (working + episodic + semantic + procedural) to draw from.
6. Memory management matters: Implement time-decay, consolidation, and pruning to prevent memory bloat and hallucination.
7. This is from production systems: All patterns here come from Chapter 5 of Building Agentic AI Systems and real deployed agents at scale.
Next Steps
Start with episodic memory (vector store + retrieval). It's the highest-impact, easiest-to-implement first step. Once you have working retrieval, add the knowledge graph for structured reasoning, then procedural memory for tool optimization.
For deeper exploration—implementation details, benchmarks, failure modes, and advanced architectures—see Building Agentic AI Systems, Chapter 5: "Memory and Learning."