AI Agent Memory Architecture: Redis Vector Store Guide

AI Agent Memory Architecture: Redis Vector Store Guide
Photo by Google DeepMind on Pexels
Quick Answer: AI agent memory in 2026 is best implemented as a three-tier architecture: (1) Short-term memory — conversation history in Redis Streams (last N turns, TTL: 1 hour), (2) Episodic memory — past interactions stored as vector embeddings in Redis Stack (searchable by semantic similarity), (3) Semantic memory — long-term knowledge graph in RedisGraph (facts, preferences, relationships). Redis Vector Store (part of Redis Stack) handles all three: 5ms vector similarity search at 10K vectors, 100K ops/sec write throughput, native TTL for automatic memory expiration. This pattern beats Pinecone/Weaviate for latency-critical agent applications because Redis keeps everything in-memory and supports hybrid (vector + metadata filter) search without separate infrastructure.
Why Agent Memory Needs a Three-Tier Architecture
The Problem with Single Memory
LLMs are stateless — they don't remember past conversations. Most agents solve this by dumping ALL conversation history into context, which:
- Grows unbounded — every turn costs token $$$
- Hit context window — even 128K fills up fast
- Recency bias — LLM attends more to recent messages
- No structured retrieval — can't find "that time the user mentioned their dog's name"
Three-Tier Memory Model
┌────────────────────────────────────────────────────┐
│ AI Agent │
├────────────────────────────────────────────────────┤
│ │
│ ┌────────────────┐ ┌─────────────┐ ┌────────┐ │
│ │ Short-Term │ │ Episodic │ │Semantic│ │
│ │ Memory │ │ Memory │ │Memory │ │
│ ├────────────────┤ ├─────────────┤ ├────────┤ │
│ │Current session │ │Past sessions│ │Facts │ │
│ │Buffer │ │Vector search│ │User │ │
│ │TTL: 1 hour │ │TTL: 30 days │ │prefs │ │
│ │Redis Streams │ │Redis VSS │ │Graph │ │
│ └────────────────┘ └─────────────┘ └────────┘ │
│ │
└────────────────────────────────────────────────────┘
Each Tier Solves a Specific Problem
| Tier | Storage | Query | TTL | Purpose |
|---|---|---|---|---|
| Short-term | Redis Streams | FIFO, last N | 1 hour | Current conversation flow |
| Episodic | Redis Vector Store | Semantic similarity | 30 days | "What did we discuss about X?" |
| Semantic | RedisGraph | SPARQL-like match | Permanent | "User lives in Tokyo" |
Redis Stack for Memory: What You Need
Components
| Redis Module | Purpose in Agent Memory | Data Type Used |
|---|---|---|
| Redis Stack | Core + modules | Strings, Hashes, Streams |
| RediSearch | Full-text indexing, hybrid search | Index on Hash/JSON |
| RedisJSON | Store agent state, conversation metadata | JSON documents |
| RedisGraph | Semantic memory (knowledge graph) | Nodes + edges |
| RedisTimeSeries | Agent performance metrics | Timestamped data |
Minimum Setup
# Option 1: Docker (recommended)
docker run -d -p 6379:6379 -p 8001:8001 redis/redis-stack
# Option 2: Production (Kubernetes)
helm repo add redis https://charts.bitnami.com/bitnami
helm install redis-stack redis/bitnami/redis \
--set image.tag=7.4.0 \
--set architecture=standalone \
--set master.persistence.enabled=true
Installation (Python)
pip install redis redisvl openai # redisvl = Redis Vector Library
Tier 1: Short-Term Memory (Conversation Buffer)
Architecture
┌─────────────────────────────────────┐
│ Agent Session (Per User) │
├─────────────────────────────────────┤
│ │
│ Redis Stream: agent:session:{id} │
│ │
│ Message 1: user "Hello" │
│ Message 2: assis "Hi, how can I?" │
│ Message 3: user "I need help..." │
│ ... │
│ Message N: (trimmed to last 50) │
│ │
└─────────────────────────────────────┘
Implementation
import redis
import json
from datetime import datetime
class ShortTermMemory:
def __init__(self, redis_client: redis.Redis):
self.redis = redis_client
self.MAX_STREAM_LEN = 50 # Keep last 50 messages per session
self.TTL_SECONDS = 3600 # 1 hour
def add_message(self, session_id: str, role: str, content: str):
"""Add message to short-term memory stream."""
stream_key = f"agent:session:{session_id}"
message = {
"role": role,
"content": content,
"timestamp": datetime.utcnow().isoformat()
}
# Add to stream and trim to max length
self.redis.xadd(stream_key, message, maxlen=self.MAX_STREAM_LEN)
# Set TTL on first message (extends each time)
self.redis.expire(stream_key, self.TTL_SECONDS)
def get_recent_messages(self, session_id: str, count: int = 20) -> list:
"""Get last N messages from stream."""
stream_key = f"agent:session:{session_id}"
# Get all messages, keep only last `count`
all_msgs = self.redis.xrevrange(stream_key, max="+", min="-", count=count)
messages = []
for msg_id, msg_data in reversed(all_msgs):
messages.append({
"role": msg_data[b"role"].decode(),
"content": msg_data[b"content"].decode(),
"timestamp": msg_data.get(b"timestamp", b"").decode()
})
return messages
def clear_session(self, session_id: str):
"""Clear session memory."""
self.redis.delete(f"agent:session:{session_id}")
def format_for_llm(self, session_id: str, max_tokens: int = 4000) -> str:
"""Format recent conversation for LLM context window."""
messages = self.get_recent_messages(session_id)
formatted = []
token_count = 0
for msg in reversed(messages): # Process newest first
line = f"{msg['role'].upper()}: {msg['content']}"
tokens_approx = len(line.split()) * 1.3
if token_count + tokens_approx > max_tokens:
break
formatted.insert(0, line)
token_count += tokens_approx
return "
".join(formatted)
Trimming Strategy
Stream trimming ensures bounded memory:
Strategy 1: Fixed length (recommended)
Keep last N messages (e.g., 50)
Max 50 messages × ~100 tokens each = 5K tokens
Fits in most context windows with room for other memory
Strategy 2: Token-budget
Calculate token count per message
Trim until total fits in budget (e.g., 4K tokens)
More precise but slightly more complex
Strategy 3: Time-based (TTL)
Auto-expire after 1 hour
Short enough for short-term, long enough for conversation
Combined with stream maxlen for safety
Tier 2: Episodic Memory (Vector Search)
Architecture
┌─────────────────────────────────────────────────────┐
│ Episodic Memory Index │
├─────────────────────────────────────────────────────┤
│ │
│ Index: idx:episodic │
│ │
│ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ │
│ │Embed │ │Embed │ │Embed │ │Embed │ │
│ │conv #1│ │conv #2│ │conv #3│ │conv #4│ ... │
│ └──┬───┘ └──┬───┘ └──┬───┘ └──┬───┘ │
│ │ │ │ │ │
│ └───────────┴───────────┴───────────┘ │
│ │ │
│ Similarity Search │
│ │ │
│ "When did we discuss │
│ vector databases?" │
└─────────────────────────────────────────────────────┘
Implementation with RedisVL
import numpy as np
from redisvl.index import SearchIndex
from redisvl.query import VectorQuery
from sentence_transformers import SentenceTransformer
class EpisodicMemory:
def __init__(self, redis_client: redis.Redis):
self.redis = redis_client
self.embedder = SentenceTransformer("all-MiniLM-L6-v2")
self._setup_index()
def _setup_index(self):
"""Create or connect to vector search index."""
schema = {
"index": {"name": "idx:episodic", "prefix": "ep:memory:"},
"fields": [
{"name": "user_id", "type": "tag"},
{"name": "session_id", "type": "tag"},
{"name": "timestamp", "type": "numeric"},
{"name": "summary", "type": "text"},
{"name": "embedding", "type": "vector",
"attrs": {
"algorithm": "FLAT",
"dims": 384,
"distance_metric": "cosine",
"datatype": "float32"
}}
]
}
self.index = SearchIndex(self.redis, schema)
self.index.create(overwrite=False)
def store_session(self, user_id: str, session_id: str, conversation: str):
"""Store conversation summary as vector memory."""
# Generate embedding
embedding = self.embedder.encode(conversation).astype(np.float32).tobytes()
# Generate summary (could use LLM for better quality)
summary = conversation[:500] # Simple truncation
# Store in Redis
key = f"ep:memory:{user_id}:{session_id}"
self.redis.hset(key, mapping={
"user_id": user_id,
"session_id": session_id,
"timestamp": datetime.utcnow().timestamp(),
"summary": summary,
"embedding": embedding
})
# Expire after 30 days
self.redis.expire(key, 30 * 24 * 60 * 60)
def search_memory(self, user_id: str, query: str, k: int = 5) -> list:
"""Search episodic memory by semantic similarity."""
query_embedding = self.embedder.encode(query).astype(np.float32).tobytes()
vector_query = VectorQuery(
vector=query_embedding,
vector_field_name="embedding",
return_fields=["user_id", "session_id", "summary", "timestamp"],
num_results=k,
filters={"user_id": user_id} # Filter by user
)
results = self.index.query(vector_query)
return [
{
"session_id": r["session_id"],
"summary": r["summary"],
"timestamp": r["timestamp"],
"score": r.get("vector_distance", 0)
}
for r in results
]
def format_for_llm(self, memories: list) -> str:
"""Format retrieved memories for LLM context."""
if not memories:
return ""
lines = ["Relevant past conversations:"]
for mem in memories:
timestamp = datetime.fromtimestamp(float(mem["timestamp"]))
lines.append(
f"[{timestamp.strftime('%Y-%m-%d %H:%M')}] "
f"(similarity: {1 - float(mem['score']):.2f}): "
f"{mem['summary']}"
)
return "
".join(lines)
Vector Index Configuration
| Parameter | Value | Why |
|---|---|---|
| Algorithm | FLAT (brute force) | For <100K vectors, flat search is simpler and more accurate |
| Metric | COSINE | Best for sentence embedding similarity |
| Dimensions | 384 (MiniLM) or 768 (instructor) | Match your embedding model |
| Data type | FLOAT32 | Standard precision for embeddings |
Hybrid Search (Vector + Metadata)
# Search with both semantic AND time filter
vector_query = VectorQuery(
vector=query_embedding,
vector_field_name="embedding",
return_fields=["summary", "timestamp"],
num_results=10,
filters={
"user_id": user_id,
# Only last 7 days
"timestamp": (timestamp_7days_ago, float('inf'))
}
)
Photo by Steve A Johnson on Pexels
Tier 3: Semantic Memory (Knowledge Graph)
Architecture
┌──────────────────────────────────────────┐
│ Semantic Knowledge Graph │
├──────────────────────────────────────────┤
│ │
│ [User: Alice] ──lives_in──→ [City: Tokyo]│
│ │ │
│ ├──prefers──→ [Topic: Python] │
│ │ │
│ ├──has_account──→ [Exchange: Binance] │
│ │ │
│ └──mentioned──→ [Interest: AI Agents] │
│ │
│ [User: Alice] ──asked_about──→ [Query: │
│ "What's the best GPU for ML?"] │
│ │
└──────────────────────────────────────────┘
Implementation with RedisGraph
from redisgraph import Node, Edge, Graph
class SemanticMemory:
def __init__(self, redis_client: redis.Redis):
self.graph = Graph("agent:semantic", redis_client)
self._init_schema()
def _init_schema(self):
"""Create constraints for graph schema."""
# Note: RedisGraph supports schema-less, but labeling is useful
pass
def store_fact(self, user_id: str, subject: str, relation: str, obj: str):
"""Store a fact in the knowledge graph."""
query = f"""
MERGE (s:Entity {{name: $subject, user_id: $user_id}})
MERGE (o:Entity {{name: $obj, user_id: $user_id}})
MERGE (s)-[r:{relation}]->(o)
SET r.created_at = timestamp()
RETURN s.name, type(r), o.name
"""
params = {
"subject": subject.replace(" ", "_"),
"obj": obj.replace(" ", "_"),
"user_id": user_id
}
self.graph.query(query, params)
def get_user_facts(self, user_id: str) -> list:
"""Get all known facts about a user."""
query = """
MATCH (s:Entity {user_id: $user_id})-[r]->(o:Entity {user_id: $user_id})
RETURN s.name, type(r) as relation, o.name
"""
result = self.graph.query(query, {"user_id": user_id})
return [
{"subject": r[0], "relation": r[1], "object": r[2]}
for r in result.result_set
]
def query_fact(self, user_id: str, query_text: str) -> list:
"""Query knowledge graph by relationship pattern."""
# Example: "who lives in Tokyo"
query = """
MATCH (s:Entity {user_id: $user_id})-[r]->(o:Entity)
WHERE o.name CONTAINS $query
OR s.name CONTAINS $query
RETURN s.name, type(r), o.name
"""
result = self.graph.query(query, {
"user_id": user_id,
"query": query_text
})
return [
{"subject": r[0], "relation": r[1], "object": r[2]}
for r in result.result_set
]
def extract_and_store(self, user_id: str, conversation: str, llm_client):
"""Use LLM to extract facts from conversation and store them."""
prompt = f"""
Extract factual statements about the user from this conversation.
Return as JSON array: [{{"subject": "...", "relation": "...", "object": "..."}}]
Use relations like: lives_in, prefers, has_account, owns, works_at, interested_in
Conversation: {conversation}
"""
response = llm_client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": prompt}],
response_format={"type": "json_object"}
)
import json
facts = json.loads(response.choices[0].message.content)
for fact in facts.get("facts", facts if isinstance(facts, list) else []):
self.store_fact(
user_id,
fact.get("subject", "User"),
fact.get("relation", "mentioned"),
fact.get("object", "")
)
Putting It Together: Agent Memory Loop
Full Agent Memory Pipeline
class AgentMemory:
def __init__(self, redis_client: redis.Redis):
self.short_term = ShortTermMemory(redis_client)
self.episodic = EpisodicMemory(redis_client)
self.semantic = SemanticMemory(redis_client)
def before_llm_call(self, user_id: str, session_id: str, query: str) -> dict:
"""Build memory context before LLM call."""
# 1. Get short-term (recent conversation)
conversation_history = self.short_term.format_for_llm(session_id)
# 2. Search episodic memory for relevant past sessions
episodic_memories = self.episodic.search_memory(user_id, query)
episodic_context = self.episodic.format_for_llm(episodic_memories)
# 3. Get relevant semantic facts
facts = self.semantic.get_user_facts(user_id)
fact_context = "User facts:
" + "
".join(
f"- {f['subject']} {f['relation']} {f['object']}"
for f in facts
)
return {
"short_term": conversation_history,
"episodic": episodic_context,
"semantic": fact_context
}
def after_llm_call(self, user_id: str, session_id: str,
query: str, response: str):
"""Store memories after LLM response."""
# 1. Store in short term
self.short_term.add_message(session_id, "user", query)
self.short_term.add_message(session_id, "assistant", response)
def end_session(self, user_id: str, session_id: str):
"""Archive session to episodic memory."""
# 1. Get full conversation
messages = self.short_term.get_recent_messages(session_id, count=100)
conversation = "
".join(
f"{m['role']}: {m['content']}" for m in messages
)
# 2. Store as episodic memory
self.episodic.store_session(user_id, session_id, conversation)
# 3. Extract semantic facts (optional, could use async job)
# self.semantic.extract_and_store(user_id, conversation, llm_client)
# 4. Clear short-term (optional — TTL handles this)
self.short_term.clear_session(session_id)
Memory Injection to LLM Prompt
System Prompt + Memory:
────────────────────────
You are an AI assistant with memory of the user.
[SHORT-TERM MEMORY — Recent conversation]
USER: Hi, I need help with Redis.
ASSISTANT: I can help! What specifically about Redis?
USER: I want to use it for vector search.
[EPISODIC MEMORY — Relevant past sessions]
2026-06-15 (similarity: 0.92):
User asked about vector databases and FAISS vs Pinecone.
Discussed HNSW index parameters.
[SEMANTIC MEMORY — Known facts]
- User lives_in Tokyo
- User prefers Python
- User works_at AI Startup
CURRENT USER QUERY: How do I set up Redis vector search?
────────────────────────
Memory Retrieval Strategies
When to Use Each Tier
| Situation | Memory Tier | Why |
|---|---|---|
| "As I was saying…" | Short-term | Current session context |
| "Last time we discussed X…" | Episodic | Past session retrieval |
| "You mentioned you live in…" | Semantic | Permanent user facts |
| "What did I ask about Y?" | Episodic + Short | Cross-session context |
| "Can you summarize our conversation?" | Short-term | Current session only |
| "Remember my preferences?" | Semantic | Long-term facts |
Retrieval Order
1. ALWAYS inject short-term memory (last N turns)
- Maximum: 4K tokens or 20 messages
- Priority: recency
2. Search episodic memory if query length > 10 chars
- Retrieve top 3-5 most similar past sessions
- Filter: same user, last 30 days
- Priority: similarity score
3. Query semantic memory for known facts
- Always retrieve user facts if user identified
- Priority: by relation type (preferences > facts > history)
4. Merge and trim to context window
- Short-term: 30% of budget
- Episodic: 40% of budget
- Semantic: 10% of budget
- Remaining 20% for system prompt + current query
Performance Tuning
| Strategy | Latency | Memory Usage | Recall Quality | Best For |
|---|---|---|---|---|
| Always retrieve all tiers | ~20ms | High | Excellent | Production agents |
| Cache episodic results (TTL: 5min) | ~5ms | Medium | Very Good | High traffic agents |
| Only short-term + semantic | ~2ms | Low | Good | Simple chatbots |
| Async episodic refresh | ~2ms sync | Low + background | Very Good | Latency-sensitive apps |
| LLM-decides retrieval (function call) | ~500ms (extra LLM call) | Low | Excellent (only when needed) | Cost-insensitive |
Production Patterns & Scaling
Horizontal Scaling
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Agent Node 1 │ │ Agent Node 2 │ │ Agent Node 3 │
└──────┬───────┘ └──────┬───────┘ └──────┬───────┘
│ │ │
└─────────────────┼─────────────────┘
│
┌──────────▼──────────┐
│ Redis Cluster │
│ (Shared Memory) │
│ │
│ • Agent session │
│ • Episodic index │
│ • Semantic graph │
└──────────────────────┘
Redis Cluster Configuration for Agent Memory
# Minimal production cluster (3 nodes)
redis-cli --cluster create \
10.0.0.1:6379 10.0.0.2:6379 10.0.0.3:6379 \
--cluster-replicas 1
# Memory limits per node
maxmemory 8gb
maxmemory-policy allkeys-lru # Ephemeral memory eviction
Production Checklist
□ Redis Stack 7.4+ deployed (with RediSearch + RedisJSON)
□ Vector index on SSD-backed persistent storage (AOF + RDB)
□ Memory limit per node (prevent OOM)
□ TTL on all temporal data (short-term: 1h, episodic: 30d)
□ Cluster mode for >50K concurrent users
□ Read replicas for episodic search (vector queries are read-heavy)
□ Backup schedule: RDB every 6h, AOF every 1s
□ Monitoring: latency P95 <20ms for vector search
□ Rate limiting: per-user memory writes (prevent abuse)
Redis vs Other Vector Databases for Agent Memory
| Factor | Redis Stack | Pinecone | Weaviate | Chroma | Qdrant |
|---|---|---|---|---|---|
| Latency (P50) | 2-5ms | 10-20ms | 5-15ms | 5-10ms | 3-8ms |
| Throughput (writes/sec) | 100K+ | 10K+ | 5K+ | 1K+ | 10K+ |
| Vector + metadata filter | ✅ Native | ✅ | ✅ | ✅ (limited) | ✅ |
| Multi-modal vectors | ✅ | ✅ | ✅ | ❌ | ❌ |
| TTL/Expiration | ✅ Native | ❌ (custom) | ❌ (custom) | ❌ | ❌ |
| Graph/Knowledge | ✅ RedisGraph | ❌ | ✅ (suboptimal) | ❌ | ❌ |
| Streaming | ✅ Redis Streams | ❌ | ❌ | ❌ | ❌ |
| Persistence | AOF + RDB | Auto | Auto | File-based | WAL |
| Self-hosted | ✅ | ❌ | ✅ | ✅ | ✅ |
| Pricing (1GB RAM) | ~$50/mo (self) | ~$70/mo | ~$150/mo | $0 (self) | $0 (self) |
| Best for | Full-stack agent | Pure vector search | Hybrid search | Dev prototyping | Vector search |
Why Redis for Agent Memory Specifically
Redis wins for agent memory because agents need:
1. Multiple data types in ONE system (not just vectors)
2. Sub-5ms latency (agents can't wait 50ms+ for memory)
3. TTL-based memory expiration (critical for short-term)
4. Streams for conversation history
5. ACID transactions for critical memory updates
6. Battle-tested deployment (90%+ of infra already has Redis)
Related Reads
- Multi-Agent AI Systems: Architecture and Coordination
- vLLM PagedAttention Explained Simply (with Visuals)
- Large-Scale Embedding Serving: Architecture, Indexing, and Retrieval
Key Takeaways
- Implement a three-tier memory architecture for AI agents: short-term (Redis Streams, TTL 1h), episodic (Redis Vector Store, TTL 30d), and semantic (RedisGraph) to balance recency, relevance, and permanence while controlling token costs.
- Use Redis Streams for short-term memory with fixed-length trimming (e.g., last 50 messages) and TTL to ensure bounded context windows (max ~5K tokens) without manual cleanup.
- Configure Redis Vector Store with FLAT algorithm (for <100K vectors), COSINE distance metric, and hybrid search (vector + metadata filters) to enable sub-5ms semantic retrieval of past interactions.
- Store semantic memory in RedisGraph with MERGE queries to avoid duplicates, and use LLM extraction (e.g., GPT-4o-mini) to auto-populate facts like 'User lives_in Tokyo' from conversations.
- Allocate LLM context budget strategically: 30% short-term, 40% episodic, 10% semantic, and 20% for system prompt/query to maximize relevance without exceeding token limits.
- Deploy Redis Stack in production with persistence (AOF + RDB), memory limits (e.g., 8GB/node), and cluster mode for >50K users to ensure low-latency (<20ms P95) and scalability.
Frequently Asked Questions
all-MiniLM-L6-v2
(384 dims) for most agents — fast, small, good quality. BGE-small or instructor-xl (768 dims) if you need higher accuracy. Match the model dimension in your Redis index. For multilingual agents, use intfloat/multilingual-e5-small.
FLAT
(brute force) for <100K vectors — simpler, no training, no recall loss. **HNSW** for >100K vectors — better scaling, but needs parameter tuning (EF construction, M). Keep FLAT for most agent use cases (<10K distinct sessions per user is typical).
How much memory does agent memory need?
For 10,000 users with 100 sessions each (1M sessions), with typical 384-dim embeddings: ~5-10GB for episodic memory, ~2-5GB for short-term streams, ~500MB for semantic graph. Total: ~8-16GB of RAM. Scale by number of users × sessions per user.
What embedding model should I use for agent memory?
all-MiniLM-L6-v2 (384 dims) for most agents — fast, small, good quality. BGE-small or instructor-xl (768 dims) if you need higher accuracy. Match the model dimension in your Redis index. For multilingual agents, use intfloat/multilingual-e5-small.
How do I prevent memory from growing unbounded?
TTL on short-term (1 hour) and episodic (30 days). Maxlen on streams (50 messages). Semantic memory should be curated — don't auto-store everything, use LLM to extract only important facts. Implement memory compaction: summarize old sessions into a single vector instead of keeping every turn.
Can I use Redis for agent memory in production at scale?
Yes — Redis Stack is production-proven at millions of operations per second for companies like Discord ($200M ARR), GitHub, and Twitter. The key is: configure persistence (AOF + RDB), use cluster mode, set memory limits, and always use TTLs.
How does memory retrieval affect latency?
With Redis: 2-5ms for vector search (10K vectors, FLAT), 1-2ms for short-term memory, 1-3ms for graph queries. Total retrieval adds ~10ms to agent latency. Compare to Pinecone (30-50ms) or external HTTP APIs (50-200ms). Redis's in-memory architecture wins.
What's the best vector index algorithm for agent memory?
FLAT (brute force) for <100K vectors — simpler, no training, no recall loss. **HNSW** for >100K vectors — better scaling, but needs parameter tuning (EF construction, M). Keep FLAT for most agent use cases (<10K distinct sessions per user is typical).


Comments
Sign in to join the conversation
No comments yet. Be the first to share your thoughts!