AI Agent多轮记忆实战:构建长期记忆与上下文压缩系统
AI与大数据
摘要
- AI Agent的记忆系统分为3层:工作记忆(上下文窗口)、短期记忆(会话级)、长期记忆(跨会话持久化)
- 对话上下文压缩是长对话场景的刚需,LLM摘要压缩可将100轮对话压缩到2K tokens内
- 长期记忆的3种存储模式:向量检索记忆、知识图谱记忆、结构化键值记忆
- 记忆检索的RAG方案:将历史对话嵌入向量数据库,按相关性召回关键片段
- 本文提供从记忆架构设计到生产部署的完整方案,含Redis+Milvus持久化实现
目录
AI Agent记忆系统的3层架构
┌──────────────────────────────────────────────────────────────┐
│ AI Agent 3层记忆架构 │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Layer 1: 工作记忆 (Working Memory) │ │
│ │ 上下文窗口内的对话历史 │ │
│ │ 容量: 4K-128K tokens │ │
│ │ 延迟: 0ms (已在内存) │ │
│ │ 生命周期: 单次请求 │ │
│ └──────────────────────────────────────────────────────┘ │
│ ↓ 溢出压缩 │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Layer 2: 短期记忆 (Short-Term Memory) │ │
│ │ 当前会话的完整对话状态 │ │
│ │ 存储: Redis / 内存缓存 │ │
│ │ 容量: 无限制 │ │
│ │ 生命周期: 单次会话 (关闭浏览器后过期) │ │
│ └──────────────────────────────────────────────────────┘ │
│ ↓ 关键信息提取 │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Layer 3: 长期记忆 (Long-Term Memory) │ │
│ │ 跨会话的用户偏好、知识、历史 │ │
│ │ 存储: Milvus向量库 + PostgreSQL结构化 │ │
│ │ 容量: 无限制 │ │
│ │ 生命周期: 永久 │ │
│ └──────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
3层记忆对比
| 维度 | 工作记忆 | 短期记忆 | 长期记忆 |
|---|---|---|---|
| 存储 | LLM上下文窗口 | Redis/Memory | Milvus+PostgreSQL |
| 容量 | 4K-128K tokens | 无限制 | 无限制 |
| 延迟 | 0ms | 1-5ms | 10-30ms |
| 生命周期 | 单次请求 | 单次会话 | 永久 |
| 检索方式 | 直接访问 | Key-Value | 向量+结构化 |
| 典型内容 | 当前对话 | 会话历史 | 用户偏好/知识 |
工作记忆:上下文窗口管理
Token预算分配
┌──────────────────────────────────────────────────────────┐
│ 8K上下文窗口的Token预算分配 │
│ │
│ System Prompt: 500 tokens (6%) │
│ 长期记忆召回: 1000 tokens (12%) │
│ 对话历史: 5000 tokens (62%) │
│ 工具调用结果: 1000 tokens (12%) │
│ 预留输出空间: 500 tokens (6%) │
│ ───────────────────────────────── │
│ 合计: 8000 tokens │
└──────────────────────────────────────────────────────────┘
上下文窗口管理器
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 = "用户相关记忆:\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
短期记忆:会话级状态持久化
Redis会话存储
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}")
长期记忆:跨会话知识积累
3种长期记忆模式
| 模式 | 存储 | 检索方式 | 适用场景 |
|---|---|---|---|
| 向量检索记忆 | Milvus | 语义相似度 | 开放式知识回忆 |
| 知识图谱记忆 | Neo4j | 关系遍历 | 结构化关系推理 |
| 结构化键值记忆 | PostgreSQL | 精确匹配 | 用户偏好/配置 |
向量检索记忆实现
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]]
结构化键值记忆实现
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 ($1, $2, $3, $4)
ON CONFLICT (user_id, memory_key)
DO UPDATE SET memory_value = $3, category = $4, 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 = $1 AND memory_key = $2",
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 = $1 AND category = $2",
user_id, category
)
else:
rows = await conn.fetch(
"SELECT memory_key, memory_value, category FROM user_memory WHERE user_id = $1",
user_id
)
return [{"key": r["memory_key"], "value": r["memory_value"], "category": r["category"]} for r in rows]
对话压缩:长对话场景的刚需
3种压缩策略
| 策略 | 压缩比 | 信息损失 | 延迟 | 适用场景 |
|---|---|---|---|---|
| 滑动窗口截断 | 高 | 大 | 0ms | 简单对话 |
| LLM摘要压缩 | 中 | 小 | 200-500ms | 长对话 |
| 结构化提取 | 低 | 极小 | 100-300ms | 任务型对话 |
LLM摘要压缩
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": "将以下对话历史压缩为简洁摘要,保留关键事实、决策和用户偏好。只输出摘要内容,不要解释。"
}, {
"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"之前的对话摘要:{summary}"},
*recent,
]
结构化信息提取
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": """从对话中提取需要长期记住的结构化信息。
输出JSON数组,每个元素包含:key(记忆键名)、value(记忆值)、category(分类:preference/fact/decision/task)
示例输出:
[{"key":"preferred_language","value":"Python","category":"preference"}]
如果没有需要记住的信息,输出空数组 []"""
}, {
"role": "user",
"content": f"用户: {user_message}\n助手: {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 []
生产部署:Redis+Milvus记忆服务
记忆服务统一接口
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_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="你是一个有记忆的AI助手,能记住用户的偏好和历史对话。",
memory_facts=memory_facts,
conversation_history=history,
)
messages.append({"role": "user", "content": current_message})
return messages
K8s部署
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"
记忆系统性能基准
| 操作 | 延迟(P50) | 延迟(P99) | QPS |
|---|---|---|---|
| 保存消息(Redis) | 1.2ms | 3ms | 8000 |
| 获取历史(Redis) | 2ms | 5ms | 6000 |
| 向量存储(Milvus) | 15ms | 30ms | 500 |
| 向量召回(Milvus) | 8ms | 15ms | 1000 |
| 结构化存储(PG) | 3ms | 8ms | 3000 |
| 结构化查询(PG) | 2ms | 5ms | 5000 |
| 对话压缩(LLM) | 350ms | 800ms | 50 |
总结与引流
AI Agent记忆系统是Agent智能的核心基础设施。3层架构(工作记忆→短期记忆→长期记忆)解决了上下文窗口有限、会话状态丢失、跨会话知识遗忘三大问题。对话压缩和结构化提取是长对话场景的关键技术。
开发要点回顾:
- 3层记忆架构:工作记忆(上下文窗口)→短期记忆(Redis)→长期记忆(Milvus+PG)
- Token预算分配:System 6% + 记忆召回 12% + 对话历史 62% + 工具 12% + 输出 6%
- 对话压缩优先用LLM摘要,任务型对话用结构化提取
- 长期记忆3种模式:向量检索(开放式)、知识图谱(关系型)、结构化键值(精确匹配)
- 生产部署Redis+Milvus+PostgreSQL三件套
相关阅读:
- MCP协议实战:用Model Context Protocol构建AI Agent工具链 — Agent工具调用与记忆协同
- AI Agent工作流引擎实战 — Agent框架中的记忆管理
- 向量数据库生产调优实战 — 记忆检索的向量库调优
权威参考:
本站提供浏览器本地工具,免注册即可试用 →
#AI Agent记忆管理#多轮对话记忆#长期记忆架构#Agent状态持久化#对话上下文压缩#2026