AI Agent Memory Management: Building Multi-Turn Conversation Memory Systems
Summary
- AI Agent memory systems are divided into 3 layers: working memory (context window), short-term memory (session-level), and long-term memory (cross-session persistence)
- Conversation context compression is essential for long conversation scenarios; LLM summary compression can compress 100 turns of conversation into under 2K tokens
- Three storage modes for long-term memory: vector retrieval memory, knowledge graph memory, and structured key-value memory
- RAG approach for memory retrieval: embed historical conversations into a vector database and recall key segments by relevance
- This article provides a complete solution from memory architecture design to production deployment, including Redis+Milvus persistence implementation
Table of Contents
- 3-Layer Architecture of AI Agent Memory Systems
- Working Memory: Context Window Management
- Short-Term Memory: Session-Level State Persistence
- Long-Term Memory: Cross-Session Knowledge Accumulation
- Conversation Compression: Essential for Long Conversation Scenarios
- Production Deployment: Redis+Milvus Memory Service
- Summary and Further Reading
3-Layer Architecture of AI Agent Memory Systems
┌──────────────────────────────────────────────────────────────┐ │ AI Agent 3-Layer Memory Architecture │ │ │ │ ┌──────────────────────────────────────────────────────┐ │ │ │ Layer 1: Working Memory │ │ │ │ Conversation history within context window │ │ │ │ Capacity: 4K-128K tokens │ │ │ │ Latency: 0ms (already in memory) │ │ │ │ Lifecycle: Single request │ │ │ └──────────────────────────────────────────────────────┘ │ │ ↓ Overflow compression │ │ ┌──────────────────────────────────────────────────────┐ │ │ │ Layer 2: Short-Term Memory │ │ │ │ Complete conversation state for current session │ │ │ │ Storage: Redis / In-memory cache │ │ │ │ Capacity: Unlimited │ │ │ │ Lifecycle: Single session (expires after browser close)│ │ │ └──────────────────────────────────────────────────────┘ │ │ ↓ Key information extraction │ │ ┌──────────────────────────────────────────────────────┐ │ │ │ Layer 3: Long-Term Memory │ │ │ │ Cross-session user preferences, knowledge, history │ │ │ │ Storage: Milvus vector DB + PostgreSQL structured │ │ │ │ Capacity: Unlimited │ │ │ │ Lifecycle: Permanent │ │ │ └──────────────────────────────────────────────────────┘ │ └──────────────────────────────────────────────────────────────┘
3-Layer Memory Comparison
| Dimension | Working Memory | Short-Term Memory | Long-Term Memory |
|---|---|---|---|
| Storage | LLM context window | Redis/Memory | Milvus+PostgreSQL |
| Capacity | 4K-128K tokens | Unlimited | Unlimited |
| Latency | 0ms | 1-5ms | 10-30ms |
| Lifecycle | Single request | Single session | Permanent |
| Retrieval | Direct access | Key-Value | Vector+Structured |
| Typical Content | Current conversation | Session history | User preferences/knowledge |
Working Memory: Context Window Management
Token Budget Allocation
┌──────────────────────────────────────────────────────────┐ │ Token Budget Allocation for 8K Context Window│ │ │ │ System Prompt: 500 tokens (6%) │ │ Long-term memory recall: 1000 tokens (12%) │ │ Conversation history: 5000 tokens (62%) │ │ Tool call results: 1000 tokens (12%) │ │ Reserved output space: 500 tokens (6%) │ │ ───────────────────────────────── │ │ Total: 8000 tokens │ └──────────────────────────────────────────────────────────┘
Context Window Manager
`python from dataclasses import dataclass, field from typing import Optional
@dataclass class Message: role: str content: str token_count: int = 0 metadata: dict = field(default_factory=dict)
class ContextWindowManager: def init( self, max_tokens: int = 8192, system_prompt_tokens: int = 500, reserved_output_tokens: int = 500, memory_recall_tokens: int = 1000, ): self.max_tokens = max_tokens self.system_prompt_tokens = system_prompt_tokens self.reserved_output_tokens = reserved_output_tokens self.memory_recall_tokens = memory_recall_tokens self.available_for_history = ( max_tokens - system_prompt_tokens - reserved_output_tokens - memory_recall_tokens )
def build_context(
self,
system_prompt: str,
memory_facts: list[str],
conversation_history: list[Message],
tool_results: list[str] = None,
) -> list[dict]:
messages = [{"role": "system", "content": system_prompt}]
if memory_facts:
memory_text = "User-related memory:\n" + "\n".join(f"- {f}" for f in memory_facts)
messages.append({"role": "system", "content": memory_text})
budget = self.available_for_history
selected_history = []
for msg in reversed(conversation_history):
if budget <= 0:
break
if msg.token_count <= budget:
selected_history.insert(0, msg)
budget -= msg.token_count
else:
break
for msg in selected_history:
messages.append({"role": msg.role, "content": msg.content})
if tool_results:
for result in tool_results:
messages.append({"role": "tool", "content": result})
return messages
`
Short-Term Memory: Session-Level State Persistence
Redis Session Storage
`python import json import redis from datetime import timedelta
class SessionMemory: def init(self, redis_url: str = "redis://localhost:6379"): self.redis = redis.from_url(redis_url) self.session_ttl = timedelta(hours=24)
def _key(self, session_id: str) -> str:
return f"session:{session_id}"
async def save_message(self, session_id: str, role: str, content: str):
key = self._key(session_id)
message = {"role": role, "content": content, "timestamp": int(time.time())}
await self.redis.rpush(key, json.dumps(message, ensure_ascii=False))
await self.redis.expire(key, self.session_ttl)
async def get_history(self, session_id: str, limit: int = 50) -> list[dict]:
key = self._key(session_id)
raw_messages = await self.redis.lrange(key, -limit, -1)
return [json.loads(m) for m in raw_messages]
async def get_session_summary(self, session_id: str) -> Optional[str]:
key = f"session_summary:{session_id}"
summary = await self.redis.get(key)
return summary.decode() if summary else None
async def save_session_summary(self, session_id: str, summary: str):
key = f"session_summary:{session_id}"
await self.redis.set(key, summary, ex=self.session_ttl)
async def clear_session(self, session_id: str):
await self.redis.delete(self._key(session_id))
await self.redis.delete(f"session_summary:{session_id}")
`
Long-Term Memory: Cross-Session Knowledge Accumulation
Three Long-Term Memory Modes
| Mode | Storage | Retrieval Method | Use Case |
|---|---|---|---|
| Vector Retrieval Memory | Milvus | Semantic similarity | Open-ended knowledge recall |
| Knowledge Graph Memory | Neo4j | Relationship traversal | Structured relationship reasoning |
| Structured Key-Value Memory | PostgreSQL | Exact match | User preferences/configuration |
Vector Retrieval Memory Implementation
`python from pymilvus import MilvusClient, DataType from openai import OpenAI import hashlib
class VectorLongTermMemory: def init(self, milvus_uri: str, embedding_model: str = "text-embedding-3-small"): self.client = MilvusClient(uri=milvus_uri) self.embedding_client = OpenAI() self.embedding_model = embedding_model self._ensure_collection()
def _ensure_collection(self):
if self.client.has_collection("agent_memory"):
return
schema = self.client.create_schema(auto_id=True, enable_dynamic_field=True)
schema.add_field(field_name="id", datatype=DataType.INT64, is_primary=True)
schema.add_field(field_name="user_id", datatype=DataType.VARCHAR, max_length=128)
schema.add_field(field_name="content", datatype=DataType.VARCHAR, max_length=65535)
schema.add_field(field_name="embedding", datatype=DataType.FLOAT_VECTOR, dim=1536)
schema.add_field(field_name="category", datatype=DataType.VARCHAR, max_length=64)
schema.add_field(field_name="timestamp", datatype=DataType.INT64)
index_params = self.client.prepare_index_params()
index_params.add_index(field_name="embedding", index_type="HNSW", metric_type="COSINE", params={"M": 16, "efConstruction": 200})
self.client.create_collection("agent_memory", schema=schema, index_params=index_params)
def _embed(self, text: str) -> list[float]:
response = self.embedding_client.embeddings.create(input=text, model=self.embedding_model)
return response.data[0].embedding
async def store(self, user_id: str, content: str, category: str = "general"):
embedding = self._embed(content)
self.client.insert("agent_memory", [{
"user_id": user_id,
"content": content,
"embedding": embedding,
"category": category,
"timestamp": int(time.time()),
}])
async def recall(self, user_id: str, query: str, top_k: int = 5) -> list[str]:
query_embedding = self._embed(query)
results = self.client.search(
"agent_memory",
data=[query_embedding],
filter=f'user_id == "{user_id}"',
limit=top_k,
output_fields=["content", "category"],
search_params={"metric_type": "COSINE", "params": {"ef": 64}},
)
return [r["entity"]["content"] for r in results[0]]
`
Structured Key-Value Memory Implementation
`python import asyncpg
class StructuredLongTermMemory: def init(self, db_url: str): self.db_url = db_url self.pool = None
async def init(self):
self.pool = await asyncpg.create_pool(self.db_url, min_size=2, max_size=10)
async with self.pool.acquire() as conn:
await conn.execute("""
CREATE TABLE IF NOT EXISTS user_memory (
id SERIAL PRIMARY KEY,
user_id VARCHAR(128) NOT NULL,
memory_key VARCHAR(256) NOT NULL,
memory_value TEXT NOT NULL,
category VARCHAR(64) DEFAULT 'general',
updated_at TIMESTAMP DEFAULT NOW(),
UNIQUE(user_id, memory_key)
)
""")
async def set_memory(self, user_id: str, key: str, value: str, category: str = "general"):
async with self.pool.acquire() as conn:
await conn.execute("""
INSERT INTO user_memory (user_id, memory_key, memory_value, category)
VALUES (, , , )
ON CONFLICT (user_id, memory_key)
DO UPDATE SET memory_value = , category = , updated_at = NOW()
""", user_id, key, value, category)
async def get_memory(self, user_id: str, key: str) -> Optional[str]:
async with self.pool.acquire() as conn:
row = await conn.fetchrow(
"SELECT memory_value FROM user_memory WHERE user_id = AND memory_key = ",
user_id, key
)
return row["memory_value"] if row else None
async def get_all_memories(self, user_id: str, category: str = None) -> list[dict]:
async with self.pool.acquire() as conn:
if category:
rows = await conn.fetch(
"SELECT memory_key, memory_value, category FROM user_memory WHERE user_id = AND category = ",
user_id, category
)
else:
rows = await conn.fetch(
"SELECT memory_key, memory_value, category FROM user_memory WHERE user_id = ",
user_id
)
return [{"key": r["memory_key"], "value": r["memory_value"], "category": r["category"]} for r in rows]
`
Conversation Compression: Essential for Long Conversation Scenarios
Three Compression Strategies
| Strategy | Compression Ratio | Information Loss | Latency | Use Case |
|---|---|---|---|---|
| Sliding window truncation | High | Large | 0ms | Simple conversations |
| LLM summary compression | Medium | Small | 200-500ms | Long conversations |
| Structured extraction | Low | Minimal | 100-300ms | Task-oriented conversations |
LLM Summary Compression
`python class ConversationCompressor: def init(self, llm_client, max_summary_tokens: int = 512): self.llm = llm_client self.max_summary_tokens = max_summary_tokens
async def compress(self, messages: list[dict], keep_recent: int = 4) -> list[dict]:
if len(messages) <= keep_recent + 2:
return messages
to_compress = messages[:-keep_recent]
recent = messages[-keep_recent:]
conversation_text = "\n".join(
f"{m['role']}: {m['content'][:200]}" for m in to_compress
)
response = self.llm.chat.completions.create(
model="Qwen/Qwen2.5-7B-Instruct",
messages=[{
"role": "system",
"content": "Compress the following conversation history into a concise summary, preserving key facts, decisions, and user preferences. Output only the summary without explanation."
}, {
"role": "user",
"content": conversation_text
}],
max_tokens=self.max_summary_tokens,
temperature=0.0,
)
summary = response.choices[0].message.content
return [
{"role": "system", "content": f"Previous conversation summary: {summary}"},
*recent,
]
`
Structured Information Extraction
`python class StructuredMemoryExtractor: def init(self, llm_client): self.llm = llm_client
async def extract(self, user_message: str, assistant_message: str) -> list[dict]:
response = self.llm.chat.completions.create(
model="Qwen/Qwen2.5-7B-Instruct",
messages=[{
"role": "system",
"content": """Extract structured information from the conversation that should be remembered long-term.
Output a JSON array where each element contains: key (memory key name), value (memory value), category (classification: preference/fact/decision/task)
Example output: [{"key":"preferred_language","value":"Python","category":"preference"}]
If there is no information worth remembering, output an empty array []""" }, { "role": "user", "content": f"User: {user_message}\nAssistant: {assistant_message}" }], max_tokens=256, temperature=0.0, response_format={"type": "json_object"}, )
try:
data = json.loads(response.choices[0].message.content)
return data.get("memories", data.get("items", []))
except json.JSONDecodeError:
return []
`
Production Deployment: Redis+Milvus Memory Service
Unified Memory Service Interface
`python class AgentMemoryService: def init(self, redis_url: str, milvus_uri: str, pg_url: str, llm_client): self.session_memory = SessionMemory(redis_url) self.vector_memory = VectorLongTermMemory(milvus_uri) self.structured_memory = StructuredLongTermMemory(pg_url) self.compressor = ConversationCompressor(llm_client) self.extractor = StructuredMemoryExtractor(llm_client)
async def on_user_message(self, session_id: str, user_id: str, message: str) -> list[str]:
vector_facts = await self.vector_memory.recall(user_id, message, top_k=3)
structured = await self.structured_memory.get_all_memories(user_id)
preference_facts = [f"{m['key']}: {m['value']}" for m in structured if m["category"] == "preference"]
return vector_facts + preference_facts
async def on_assistant_message(self, session_id: str, user_id: str, user_msg: str, assistant_msg: str):
await self.session_memory.save_message(session_id, "user", user_msg)
await self.session_memory.save_message(session_id, "assistant", assistant_msg)
memories = await self.extractor.extract(user_msg, assistant_msg)
for mem in memories:
await self.structured_memory.set_memory(user_id, mem["key"], mem["value"], mem["category"])
await self.vector_memory.store(user_id, f"User said: {user_msg}", category="conversation")
async def build_prompt(self, session_id: str, user_id: str, current_message: str) -> list[dict]:
memory_facts = await self.on_user_message(session_id, user_id, current_message)
history = await self.session_memory.get_history(session_id, limit=50)
messages = self.session_memory.build_context(
system_prompt="You are an AI assistant with memory that can remember user preferences and conversation history.",
memory_facts=memory_facts,
conversation_history=history,
)
messages.append({"role": "user", "content": current_message})
return messages
`
K8s Deployment
yaml apiVersion: apps/v1 kind: Deployment metadata: name: agent-memory-service namespace: ai-agent spec: replicas: 2 selector: matchLabels: app: agent-memory-service template: spec: containers: - name: memory-service image: myregistry/agent-memory-service:v1.0 ports: - containerPort: 8080 resources: requests: cpu: "1" memory: 512Mi limits: cpu: "2" memory: 1Gi env: - name: REDIS_URL value: "redis://redis:6379" - name: MILVUS_URI value: "http://milvus-svc:19530" - name: PG_URL value: "postgresql://postgres:password@postgres:5432/agent_memory"
Memory System Performance Benchmarks
| Operation | Latency (P50) | Latency (P99) | QPS |
|---|---|---|---|
| Save message (Redis) | 1.2ms | 3ms | 8000 |
| Get history (Redis) | 2ms | 5ms | 6000 |
| Vector store (Milvus) | 15ms | 30ms | 500 |
| Vector recall (Milvus) | 8ms | 15ms | 1000 |
| Structured store (PG) | 3ms | 8ms | 3000 |
| Structured query (PG) | 2ms | 5ms | 5000 |
| Conversation compression (LLM) | 350ms | 800ms | 50 |
Summary and Further Reading
The AI Agent memory system is the core infrastructure of Agent intelligence. The 3-layer architecture (working memory -> short-term memory -> long-term memory) solves three major problems: limited context windows, lost session state, and forgotten cross-session knowledge. Conversation compression and structured extraction are key technologies for long conversation scenarios.
Key Development Takeaways:
- 3-layer memory architecture: Working memory (context window) -> Short-term memory (Redis) -> Long-term memory (Milvus+PG)
- Token budget allocation: System 6% + Memory recall 12% + Conversation history 62% + Tools 12% + Output 6%
- Prefer LLM summary for conversation compression; use structured extraction for task-oriented conversations
- Three long-term memory modes: Vector retrieval (open-ended), Knowledge graph (relational), Structured key-value (exact match)
- Production deployment with the Redis+Milvus+PostgreSQL trio
Related Reading:
- MCP Protocol in Practice: Building AI Agent Tool Chains with Model Context Protocol — Agent tool invocation and memory coordination
- AI Agent Workflow Engine in Practice — Memory management in Agent frameworks
- Vector Database Production Tuning in Practice — Vector database tuning for memory retrieval
Authoritative References:
Try these browser-local tools — no sign-up required →