做企业级 Agent 应用,最难的不是把模型接入业务,而是让 Agent 在跨会话、跨业务线、长时间运行后还记得上下文。很多团队把几百页文档、几千轮对话全部塞进 Prompt,Context 越拼越长,效果越来越差,延迟和成本反而一起涨。
这次我们把“企业级 Agent Memory 架构”完整拆开:从 Context 窗口聊到真正的 Long-term Memory,讲清楚分层记忆模型怎么做,短期记忆、长期记忆各用什么存储,再落到 RAG 检索增强和 MCP 工具协议两个方向,最后给出一套带代码的实现模板。文章不是 PPT 式的概念罗列,而是可以直接照着搭的工程方案:Redis 做会话级短期记忆,向量库做跨会话长期记忆,RAG 做记忆召回,MCP 把记忆能力暴露给 Agent 工具调用。
关注本地部署、接口服务、批量任务、知识库回填和多 Agent 协作的读者,这篇文章可以直接收藏。
1. 企业级 Agent Memory 架构核心能力速览
先把方案的全貌放出来,后面所有代码和步骤都是围绕这张表展开的。
| 能力项 | 说明 |
|---|---|
| 架构方案 | 分层记忆模型:短期记忆 + 工作记忆 + 长期记忆 |
| 短期记忆 | Redis 会话缓存,按 session_id 隔离,支持 TTL 过期 |
| 长期记忆 | 向量数据库持久化,存储对话摘要、用户偏好、事实性知识 |
| 记忆召回 | embedding 向量检索 + 关键词多路召回 + rerank |
| 服务接入 | MCP 协议工具 + REST API 双通道 |
| 批量任务 | 批量会话摘要、记忆归档、向量重建、记忆压缩 |
| 上下文工程 | Prompt 动态注入,按“当前会话 + 相关记忆 + 知识库”三段拼接 |
| 推理硬件 | 本地 embedding 模型可用 CPU 运行,接入云端模型 API 则无需 GPU |
| 适合场景 | 智能客服、企业 Copilot、流程助手、多轮数据分析、知识库问答 |
这套架构解决的关键问题只有一个:让 Agent 从“无状态调用”变成“有状态服务”。
2. 为什么 Agent 需要长效记忆:从 Context 到 Memory
先看一个真实的工程困境。企业里的 Agent 通常不是一次性问答,而是以周、月为周期的持续服务。用户可能三天后回来继续上次的需求,或者在钉钉/飞书群里让 Agent 处理跨部门的流程审批。如果每一次对话都从零开始,用户每次都要重复需求背景、身份信息、项目上下文,这是不可接受的产品体验。
Context 窗口是有限资源。以主流大模型为例,上下文窗口从 4K 到 200K 甚至更大,看起来很大,但企业真实场景里,一份流程文档可能 20K token,一套业务规则 30K token,再加上历史对话、用户画像、工具返回结果,很快超出窗口边界。更麻烦的是,上下文过长会导致模型“迷失在中间”,召回准确率下降,响应延迟和成本同步上升。
所以工程上必须把记忆从 Prompt 里解放出来,放到外部存储中。这就是 Agent Memory 架构的出发点:
- 短期记忆:当前会话内的对话轮次、临时状态,存放在 Redis 这类高速缓存里,TTL 过期后自动清理。
- 工作记忆:当前任务正在处理的中间结果、工具调用记录、待确认字段,属于“正在思考”的数据。
- 长期记忆:跨会话持久化的用户偏好、关键事实、历史决策、项目上下文,存放在向量库或结构化存储中,按需召回。
真正的 Long-term Memory 不是把历史对话原样存下来,而是对记忆做提取、压缩、索引、召回、遗忘的全生命周期管理。这决定了 Agent 是“听起来聪明”还是“真的了解用户”。
从上下文工程的视角看,Long-term Memory 的本质是让 Agent 在有限的上下文预算内,拿到“当前时刻最该知道的信息”。它和 RAG 有天然的结合点:把长期记忆当作一类特殊的文档库,用语义检索的方式按需召回。区别在于,RAG 的知识来源通常是企业文档、网页、数据库记录,而记忆库的来源是用户与 Agent 的交互历史。
3. 企业级 Agent Memory 架构设计
3.1 分层记忆模型
整个架构按记忆生命周期分成四层,每一层对应不同的存储介质和访问频率。
| 记忆层 | 存储介质 | 生命周期 | 访问频率 | 示例 |
|---|---|---|---|---|
| 短期记忆 | Redis | 分钟到小时 | 极高 | 当前会话最近 20 轮对话 |
| 工作记忆 | Redis / 内存 | 任务进行中 | 高 | 工具调用结果、待确认参数 |
| 长期记忆 | 向量库 + 关系库 | 天到月 | 中 | 用户偏好、历史决策、项目上下文 |
| 业务知识库 | 向量库 | 版本化管理 | 低 | 企业文档、规则、FAQ |
短期记忆和工作记忆可以共用 Redis 实例,但建议用不同的 key 前缀区分,避免数据互相污染。
3.2 记忆生命周期
记忆不是“写进去就不管了”,它和业务系统一样需要生命周期管理:
- 写入:每次对话结束后,把关键信息写入短期记忆。
- 提取:定期或按事件触发,从短期记忆中提取用户偏好、事实、决策记录。
- 摘要:对长对话进行摘要压缩,避免长期记忆库无限膨胀。
- 归档:超过一定时效的记忆降级为冷数据,减少检索噪音。
- 召回:新对话开始时,从长期记忆中检索与当前 Query 最相关的记忆片段。
- 遗忘:用户要求删除或数据过期时,彻底清除相关记忆。
这个流程是长效记忆系统最核心的部分,也是长期记忆和“历史记录文件”的区别所在。
3.3 关键组件职责
- Memory Service:统一管理记忆读写、提取、摘要、删除,对外提供 REST API 和 MCP 工具。
- Redis Store:短期记忆和工作记忆的存储,使用 Hash 或 String 结构存储 JSON 序列化的消息。
- Vector Store:长期记忆的索引存储,保存 embedding 向量和原始内容。
- Extractor:从对话中抽取记忆条目,可以用规则匹配、小模型或大模型函数调用实现。
- RAG Pipeline:负责将查询语句 embedding 化、检索、rerank、拼装上下文。
- MCP Server:把记忆能力封装为标准工具,Agent 可通过 MCP 协议直接调用。
4. 技术选型与本地部署环境准备
4.1 技术栈清单
下面这套技术栈是通用选择,具体组件可按团队已有基础替换:
| 组件 | 选型 | 用途 |
|---|---|---|
| 缓存 | Redis 7.x | 短期记忆、会话状态 |
| 向量库 | Milvus / pgvector / Chroma | 长期记忆向量索引 |
| Embedding | text-embedding 模型或 OpenAI/通义 Embedding API | 文本向量化 |
| Agent 框架 | LangGraph / LangChain 或自研 | 编排记忆读写逻辑 |
| MCP SDK | fastmcp 或官方 MCP Python SDK | 暴露记忆工具 |
| 应用框架 | FastAPI | REST API |
| 部署方式 | Docker Compose 或 K8s | 服务编排 |
对于个人开发者或小团队验证,最轻量的组合是:Redis + SQLite + Chroma + FastAPI,全部本地启动,不需要 GPU。只要 embedding 使用云端 API,CPU 就能跑完整套服务。
4.2 环境检查清单
部署前先做一轮环境检查:
- Docker 与 Docker Compose 已安装,版本不低于 20.10。
- Python 版本 3.10 至 3.12,不建议直接用 3.13,部分依赖还未完全兼容。
- Redis 端口 6379 未被占用。
- 向量库端口(如 Milvus 默认 19530)未被占用。
- 如需本地 embedding 模型,预留至少 8GB 内存;磁盘空间预留 20GB 以上。
- 如果要访问云端大模型 API,需要有合法的 API Key 和网络权限。
4.3 项目目录结构
建议按照下面结构组织代码,记忆相关的模块单独放置,方便后续拆成独立服务。
agent-memory-demo/ ├── app/ │ ├── main.py # FastAPI 入口 │ ├── memory/ │ │ ├── store.py # Redis 短期记忆 │ │ ├── vector.py # 向量库操作 │ │ ├── extractor.py # 记忆提取与摘要 │ │ └── service.py # 记忆服务统一入口 │ ├── rag/ │ │ ├── pipeline.py # RAG 召回流程 │ │ └── rerank.py # 重排序 │ ├── mcp_server.py # MCP Server │ └── schemas.py # Pydantic 模型 ├── docker-compose.yml ├── requirements.txt └── config.yaml5. 安装部署与启动方式
5.1 启动基础设施
使用 Docker Compose 启动 Redis 和向量库,这里以 Redis + Chroma 为例,资源占用最小,适合本地验证。
version: "3.9" services: redis: image: redis:7-alpine container_name: agent-redis ports: - "6379:6379" command: redis-server --appendonly yes volumes: - redis-data:/data chroma: image: chromadb/chroma:latest container_name: agent-chroma ports: - "8001:8000" volumes: - chroma-data:/data volumes: redis-data: chroma-data:启动命令:
docker compose up -d启动后确认两个服务状态:
docker ps docker exec -it agent-redis redis-cli ping正常时 Redis 返回 PONG。
5.2 Python 依赖安装
创建虚拟环境并安装依赖:
python -m venv .venv source .venv/bin/activate # Windows 使用 .venv\Scripts\activate pip install fastapi uvicorn redis chromadb openai pydantic-settings如果需要用 MCP,额外安装 MCP SDK:
pip install mcp fastmcp注意:Chromadb 版本更新较快,如果安装后启动报错,先检查 Python 版本和依赖版本兼容性,优先使用官方文档给出的组合。
5.3 启动记忆服务
先写一个最简单的 FastAPI 服务验证环境。创建app/main.py:
from fastapi import FastAPI from pydantic import BaseModel app = FastAPI(title="Agent Memory Service") class HealthResponse(BaseModel): status: str redis: str vector_store: str @app.get("/health", response_model=HealthResponse) async def health(): return HealthResponse( status="ok", redis="checking", vector_store="checking" )启动:
uvicorn app.main:app --host 127.0.0.1 --port 8000访问http://127.0.0.1:8000/health,能看到 JSON 响应说明服务正常。后续所有记忆功能都挂载在这个服务上。
6. 功能测试与效果验证
6.1 Redis 短期记忆读写测试
短期记忆的核心功能是:按 session_id 存储最近对话,支持过期清理。使用 Redis Hash 结构,field 是消息 id,value 是 JSON 序列化的消息对象。
实现app/memory/store.py:
import json import time from typing import Optional import redis redis_client = redis.Redis(host="127.0.0.1", port=6379, db=0) SESSION_TTL = 3600 # 短期记忆保留 1 小时 def append_message(session_id: str, role: str, content: str) -> str: """写入一条会话消息""" message_id = f"{session_id}:{int(time.time() * 1000)}" message = { "id": message_id, "role": role, "content": content, "ts": int(time.time()) } redis_client.hset( f"session:{session_id}", message_id, json.dumps(message, ensure_ascii=False) ) # 刷新过期时间 redis_client.expire(f"session:{session_id}", SESSION_TTL) return message_id def get_recent_messages(session_id: str, limit: int = 20) -> list: """取最近 N 条消息""" raw = redis_client.hgetall(f"session:{session_id}") messages = [] for msg_id, msg_json in raw.items(): messages.append(json.loads(msg_json)) messages.sort(key=lambda x: x["ts"]) return messages[-limit:]测试方法:
from app.memory.store import append_message, get_recent_messages session_id = "user_1001" append_message(session_id, "user", "我想查上个月华东区的销售数据") append_message(session_id, "assistant", "请提供具体的时间范围和指标维度") messages = get_recent_messages(session_id, limit=10) print(len(messages)) # 预期输出 2判断标准:消息写入后能按时间顺序读回,TTL 过期后自动消失。如果拿不到预期消息,优先检查 Redis 连接和 key 前缀。
6.2 长期记忆持久化测试
长期记忆存在向量库中,每条记忆是一个文本片段,例如“用户偏好按周查看数据报表”“用户所在部门是销售部”。写入时生成 embedding,检索时用 query embedding 做近邻搜索。
实现app/memory/vector.py:
from typing import Optional import chromadb # 假设 py 文件在 app/memory 目录下 PLUGIN = None client = chromadb.HttpClient(host="127.0.0.1", port=8001) collection = client.get_or_create_collection(name="agent_memories") def embed_text(text: str) -> list: """ 通用 embedding 占位接口。 实际使用时替换为具体 embedding 模型或云端 API。 """ # 假实现,生产环境必须替换 return [0.0] * 768 def save_memory(user_id: str, content: str, metadata: Optional[dict] = None): """写入一条长期记忆""" memory_id = f"{user_id}:{abs(hash(content))}" embedding = embed_text(content) collection.upsert( ids=[memory_id], embeddings=[embedding], documents=[content], metadatas=[{ "user_id": user_id, **metadata or {} }] ) return memory_id def search_memory(user_id: str, query: str, top_k: int = 3) -> list: """召回用户相关长期记忆""" q_embedding = embed_text(query) results = collection.query( query_embeddings=[q_embedding], n_results=top_k, where={"user_id": user_id} ) return results["documents"][0]测试方法:先写入三条测试记忆,再搜索语义相近的 query,确认能召回最相关的一条。
from app.memory.vector import save_memory, search_memory save_memory("user_1001", "用户偏好使用柱状图查看销售趋势") save_memory("user_1001", "用户所在的部门是华东销售部") save_memory("user_1001", "用户每天上午查看数据看板") result = search_memory("用户喜欢怎么看报表") print(result)预期结果应该包含“用户偏好使用柱状图查看销售趋势”。如果召回结果不相关,优先检查 embedding 效果和 top_k 设置。
6.3 RAG 多路召回与上下文拼接测试
长期记忆召回只是 RAG 的一种特例。在企业级场景中,还需要把文档知识、记忆、实时搜索结果统一做成“多路召回”,再重排去重后注入 Prompt。这样才能避免向量检索单路召回不准确的问题。
实现一个简化版 RAG Pipeline:
from typing import List class RAGPipeline: def __init__(self, retriever_funcs: List[callable]): # retriever_funcs: 每一路召回函数,输入 query,输出候选片段列表 self.retriever_funcs = retriever_funcs def retrieve(self, query: str, top_k: int = 5): candidates = [] for func in self.retriever_funcs: candidates.extend(func(query)) # 简化版:按分数倒序去重 seen = set() result = [] for item in sorted(candidates, key=lambda x: x["score"], reverse=True): # item: {text, score, source} if item["text"] not in seen: seen.add(item["text"]) result.append(item) return result[:top_k] def build_prompt(self, query: str, memory_items: List[str], documents: List[str]) -> str: memory_block = "\n".join(memory_items) or "(暂无相关历史记忆)" doc_block = "\n".join(documents) prompt = f"""你是企业知识助手。请结合历史记忆和知识库回答用户问题。 历史记忆: {memory_block} 知识库内容: {doc_block} 用户问题: {query} """ return prompt测试的重点是 Prompt 拼接是否正确:历史记忆在前、知识库内容居中、用户问题最贴近模型。很多 Agent 效果差不是模型不行,而是 Prompt 拼接顺序和信息冗余把模型带偏了。
6.4 记忆替换与删除测试
长效记忆系统必须支持用户主动删除记忆,合规场景下这是硬性要求。
def delete_memory(user_id: str, memory_id: str): """按 ID 删除一条长期记忆""" collection.delete(ids=[memory_id]) def clear_user_memory(user_id: str): """清空某用户的全部长期记忆""" all_memories = collection.get(where={"user_id": user_id}) ids = all_memories["ids"] if ids: collection.delete(ids=ids)验证逻辑:删除后再搜索,结果中不应再出现该条记忆。如果仍然出现,多半是向量库缓存或查询条件问题,检查 where 条件是否包含 user_id。
7. 接口 API 与批量任务
7.1 REST API 设计
记忆服务对外提供 REST API,方便业务系统接入。
| 路径 | 方法 | 功能 |
|---|---|---|
| /v1/memory/short | POST | 写入短期记忆 |
| /v1/memory/short/{session_id} | GET | 读取短期记忆 |
| /v1/memory/long | POST | 写入长期记忆 |
| /v1/memory/long/search | POST | 搜索长期记忆 |
| /v1/memory/long/{memory_id} | DELETE | 删除单条记忆 |
| /v1/rag/prompt | POST | 构建 RAG Prompt |
在 main.py 中补充路由:
from fastapi import FastAPI from app.memory import store, vector from app.rag.pipeline import RAGPipeline app = FastAPI(title="Agent Memory Service") class ShortMemoryBody(BaseModel): session_id: str role: str content: str class LongMemoryBody(BaseModel): user_id: str content: str class SearchMemoryBody(BaseModel): user_id: str query: str top_k: int = 3 @app.post("/v1/memory/short") async def write_short_memory(body: ShortMemoryBody): message_id = store.append_message(body.session_id, body.role, body.content) return {"message_id": message_id} @app.post("/v1/memory/long") async def write_long_memory(body: LongMemoryBody): memory_id = vector.save_memory(body.user_id, body.content) return {"memory_id": memory_id} @app.post("/v1/memory/long/search") async def search_long_memory(body: SearchMemoryBody): results = vector.search_memory(body.user_id, body.query, body.top_k) return {"items": results}7.2 curl 调用示例
写入短期记忆:
curl -X POST http://127.0.0.1:8000/v1/memory/short \ -H "Content-Type: application/json" \ -d '{"session_id": "user_1001", "role": "user", "content": "帮我查一下华东区销售数据"}'搜索长期记忆:
curl -X POST http://127.0.0.1:8000/v1/memory/long/search \ -H "Content-Type: application/json" \ -d '{"user_id": "user_1001", "query": "用户偏好怎么看报表"}'Python 调用示例:
import requests base_url = "http://127.0.0.1:8000" response = requests.post( f"{base_url}/v1/memory/long/search", json={"user_id": "user_1001", "query": "用户偏好怎么看报表", "top_k": 3}, timeout=10 ) print(response.json())7.3 批量记忆归档任务
生产环境常见需求:每天凌晨把当天所有短期记忆做摘要,写入长期记忆库。批量任务的实现思路是使用独立队列,逐批读取 session,调用摘要模型生成记忆条目,再写入向量库。
import asyncio async def batch_archive_sessions(): """批量归档短期记忆到长期记忆库,生产环境由定时任务触发""" # 伪代码:通过 Redis SCAN 找出当天活跃 session active_sessions = ["user_1001", "user_1002"] for session_id in active_sessions: messages = store.get_recent_messages(session_id, limit=100) summary = await summarize_messages(messages) # 调用大模型生成摘要 vector.save_memory( user_id=session_id, content=summary, metadata={"source": "archive", "type": "session_summary"} ) return {"archived": len(active_sessions)}批量任务必须做好幂等控制:同一个 session 不能重复写入同样的记忆。建议在 metadata 中加入 session_id 和 batch_date,写入前先检查是否已存在。
8. 资源占用与性能观察
8.1 Redis 内存观察
短期记忆在 Redis 中占用内存与消息量成正比。观察方法:
redis-cli info memory redis-cli --bigkeys如果内存增长过快,先看是不是 TTL 没有生效,再检查是否有过大的 value。单条消息不要超过 64KB,长响应可以截断或摘要后再存入。
8.2 向量库性能观察
向量查询的延迟受集合大小、向量维度、索引类型影响。本地 Chroma 在小数据量下查询毫秒级,百万级数据则建议切换 Milvus 或专用向量库。性能观察维度:
- 写入延迟:单条记忆写入耗时。
- 查询延迟:一次检索 top_k 的耗时。
- 召回率:标注一批测试 query,看正确记忆是否排进前 k。
如果查询变慢,先确认 embedding 维度是否统一,不同模型返回的向量维度不一致会导致全表扫描。
8.3 大模型调用延迟与上下文长度
记忆架构引入后,每个请求的成本结构发生变化:
- embedding 调用增加,但按条计费成本很低。
- 检索耗时在 RAG 中通常可以控制在 50ms 以内。
- Prompt 长度从“全量历史”变成“相关记忆 + 固定知识条目”,整体 token 数通常下降。
- Agent 因为拿到相关记忆,重试和澄清次数减少,反而节省总成本。
建议在日志中记录每个请求的 prompt token 数、检索耗时长、模型响应时间,建立基线后再做优化。
9. 常见问题与排查方法
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 短期记忆写入后读不到 | Redis key 过期或连接被断 | 查看 Redis logs、检查 TTL | 调整 SESSION_TTL,检查连接池配置 |
| 向量检索结果不相关 | Embedding 模型质量差或语义不在同一空间 | 对比不同 query 的召回结果 | 换更强的 embedding 模型,或多路召回 |
| 向量维度不一致 | 使用了不同 embedding 模型 | 检查 collection 元信息 | 固定模型版本,建立模型映射表 |
| MCP 工具调用失败 | MCP Server 未启动或工具名错误 | 查看 MCP Server 日志 | 检查工具注册名,重新加载 Agent 配置 |
| Redis 连接超时 | 受保护模式下无法远程连接 | 执行 redis-cli ping | 修改 redis.conf 绑定地址,注意安全组限制 |
| Prompt 拼接过长 | 记忆和知识条目去重失效 | 查看日志中的 prompt token 数 | 提高去重阈值,限制单路召回条数 |
| 批量归档重复写入 | 缺少幂等控制 | 检查长期记忆库相同 metadata 数量 | 在写入前做 session_id + batch_date 去重 |
| 服务启动后端口冲突 | 8001 或 8000 被其他进程占用 | 执行 netstat 查看端口 | 修改启动端口或关闭占用进程 |
10. 最佳实践与合规边界
10.1 架构实践建议
- 第一版不要做太重。先用 Redis + SQLite + Chroma 验证记忆链路,再迁移到生产级组件。
- 长期记忆写入前一定要做抽取和摘要,切忌原样保存所有对话,否则向量库会变成垃圾场。
- 记忆召回必须加 user_id 过滤,这是多租户隔离的最低要求。
- 多路召回 + rerank 比单路向量检索稳定得多,特别是业务术语多、近义词多的企业场景。
- 记忆系统要预留删除接口,不要只在数据库里硬删,最好有软删除和审计日志。
10.2 隐私与数据合规
记忆系统中存储了大量用户行为和企业业务数据,必须遵循最小化原则:
- 默认不采集与任务无关的个人敏感信息。
- 涉及用户身份、联系方式、健康信息等内容,必须获得明确授权。
- 用户发起删除请求时,应在规定时间内完成记忆清理,包括向量库中的 embedding。
- 企业内部使用时,记忆库的访问权限要与业务系统一致,不能比业务系统更高的开放度。
- 不要将真实姓名、手机号等敏感字段作为向量的直接文本内容,可先脱敏处理。
工程团队上线此类系统前,建议交给法务和隐私团队做一次数据流评估。技术能力可以快速搭建,但数据合规的坑一旦踩下去,修复成本极高。
11. 总结与下一步
这套 Agent Memory 架构最值得尝试的点是:把记忆从 Prompt 移到存储层之后,Agent 的多轮对话能力和跨会话连续性会有一个明显的提升,而且不会因为上下文无限增长拖垮推理性能。
建议最先验证三个功能:短期记忆的 Redis 读写是否稳定、长期记忆的语义召回是否准确、MCP 工具调用是否连通。这三个点通了,整个记忆链路就通了。
最容易踩的坑有两个:一是忘记做记忆抽取和摘要,直接灌原始对话导致检索效果差;二是长期记忆库没有做租户隔离,用户 A 的记忆被用户 B 召回,这在企业环境里是严重的合规事故。
后续可以继续扩展的方向包括:GraphRAG 做实体关系记忆、MCP 接入数据库查询工具、记忆压缩策略的自动调优、以及跨多 Agent 的共享记忆池。先把基础记忆链路跑通,再逐步往上加深,比一开始就堆一套复杂系统要可靠得多。