尧图精选

LLM Agent记忆管理实战:基于MCP与Docker构建分层记忆系统

🕒 发布时间:2026/10/1 18:03:39 📁 来源:尧图网络
1. 从“hindsight”说起为什么我们需要给Agent装上“后视镜”“hindsight”这个词本身很有意思字面意思是“事后的洞察力”也就是我们常说的“后见之明”。放在LLM Agent的语境里它指向一个非常具体且关键的问题Agent如何记住过去发生过的事情并在后续决策中有效地利用这些记忆如果你正在做Agent相关的开发大概率遇到过这样的场景用户第一轮说“帮我查一下北京明天的天气”Agent调用天气API返回了结果第二轮用户说“那后天呢”Agent却一脸茫然完全不知道“那”指的是什么。这不是模型不够聪明而是它没有“记忆”——或者说它的记忆机制没有把上一轮的上下文有效地传递下来。这就是hindsight要解决的核心问题。它不是一个具体的开源项目名称而是一类Agent记忆管理方案的代称。围绕这个主题涉及的技术栈非常广从最基础的对话历史缓存到基于向量数据库的长期记忆存储再到通过MCP协议实现跨会话的记忆共享以及用Docker容器化部署整套记忆服务。热搜词里出现的agent memory、LLM、MCP、Docker恰好构成了这个领域的四根支柱。这篇文章适合谁看如果你正在开发基于LLM的Agent应用或者对Agent的记忆机制感到困惑不知道working memory和long-term memory该怎么设计又或者你想了解MCP协议在Agent记忆场景下怎么落地那这篇内容应该能给你一些可以直接参考的思路和代码级别的实操方案。我会从架构设计讲到具体实现包括Docker环境的搭建、MCP服务的配置、记忆存储的选型对比以及我在实际项目中踩过的坑。2. Agent记忆体系的核心架构与选型逻辑2.1 为什么Agent的记忆不能只靠上下文窗口很多人刚开始做Agent的时候会有一个朴素的想法既然GPT-4或者Claude的上下文窗口已经到128K甚至200K token了那我直接把所有对话历史都塞进去不就行了这个思路在小规模场景下确实能跑通但一旦进入生产环境问题就会集中爆发。首先是成本问题。每次请求都把完整的对话历史带上token消耗是线性增长的。假设一轮对话平均500 token聊了100轮就是50000 token按GPT-4的定价单次请求的成本就相当可观了。其次是延迟问题上下文越长模型的推理时间越长用户体验会明显下降。更关键的是注意力稀释——当上下文里塞了大量无关的历史信息时模型对当前问题的关注度反而会下降出现“答非所问”的情况。所以一个成熟的Agent记忆体系必须做到分层管理。我在实际项目中通常把它分为三层工作记忆Working Memory当前会话的短期上下文通常保留最近N轮对话直接放在prompt里。N的取值一般在5到10之间具体取决于任务的复杂度和模型的上下文窗口大小。情景记忆Episodic Memory跨会话的历史交互记录以结构化形式存储需要时通过检索召回。比如用户上次提到的偏好、之前解决过的问题等。语义记忆Semantic Memory从历史交互中提炼出的通用知识或规则比如“这个用户总是偏好简洁的回答”“这类问题的标准处理流程是什么”。hindsight的核心价值就在于它提供了一套机制让这三层记忆能够协同工作而不是各自为政。2.2 记忆存储的选型向量数据库 vs 关系型数据库 vs 混合方案说到记忆存储很多人第一反应就是上向量数据库比如Pinecone、Weaviate、Qdrant或者Milvus。向量检索确实适合语义相似度匹配的场景但它并不是万能的。我做过一个对比测试在一个客服Agent的场景下用户问“我上次买的那个东西什么时候到”如果用纯向量检索系统需要把“上次买的那个东西”映射到具体的订单信息这个映射过程依赖的是实体识别和关系推理而不是简单的语义相似度。这种情况下关系型数据库比如PostgreSQL反而更合适因为订单号、用户ID、时间戳这些结构化信息用SQL查询的准确率和效率都远高于向量检索。所以我的建议是采用混合方案存储类型适用场景推荐技术检索方式向量存储语义相似的历史对话、知识片段Qdrant / Milvus余弦相似度关系存储结构化实体、订单、用户偏好PostgreSQLSQL查询键值存储会话状态、临时缓存RedisKey-Value图存储实体关系推理、知识图谱Neo4jCypher查询在实际部署中我通常用Docker Compose把这几类存储编排在一起通过统一的记忆管理服务对外提供接口。这样Agent端只需要调用一个recall(query)方法底层具体走哪种检索路径由服务内部决定。2.3 MCP协议在记忆管理中的角色定位MCPModel Context Protocol是Anthropic推出的一个开放协议它的核心思想是把工具调用标准化。在Agent记忆场景下MCP的价值在于它让记忆服务可以作为一个独立的Server存在任何支持MCP协议的Agent都可以通过标准接口访问记忆功能而不需要每个Agent都自己实现一套记忆逻辑。举个例子你可以用Python写一个MCP Server暴露以下几个工具store_memory(content, metadata)存储一条记忆recall_memory(query, top_k)检索相关记忆forget_memory(memory_id)删除指定记忆summarize_session(session_id)对会话进行摘要压缩然后任何Agent——不管它是用LangChain、AutoGPT还是自己手写的——只要支持MCP协议就可以直接调用这些工具。这种解耦设计的好处是记忆服务的升级和维护完全独立于Agent本身团队协作时前后端可以并行开发。热搜词里提到的“ruoyi-vue-pro合并mcp功能”和“trae ide搭载burp suite mcp server”其实都是这个思路的延伸——把MCP作为能力扩展的标准接口让不同的系统能够无缝对接。3. 用Docker搭建Agent记忆服务的完整实操3.1 环境准备与Docker安装要点在开始之前你需要确保本地环境满足以下条件操作系统Windows 10/11WSL2、macOS 12、或Ubuntu 20.04内存至少8GB推荐16GB以上因为要同时跑多个容器磁盘至少20GB可用空间Docker Desktop最新稳定版Windows用户特别注意安装Docker Desktop时如果遇到“Virtualization support not detected”的错误需要进BIOS开启虚拟化支持Intel VT-x或AMD-V。这个坑我踩过好几次尤其是联想和惠普的笔记本默认出厂设置里虚拟化是关闭的。安装完成后用以下命令验证环境docker --version docker compose version docker run hello-world如果hello-world能正常输出说明Docker环境没问题。3.2 用Docker Compose编排记忆服务栈下面是我在实际项目中常用的一套Docker Compose配置包含了Qdrant向量存储、PostgreSQL关系存储、Redis缓存和记忆管理服务本身version: 3.8 services: qdrant: image: qdrant/qdrant:latest ports: - 6333:6333 - 6334:6334 volumes: - qdrant_data:/qdrant/storage environment: - QDRANT__SERVICE__GRPC_PORT6334 postgres: image: postgres:16-alpine ports: - 5432:5432 volumes: - pg_data:/var/lib/postgresql/data environment: POSTGRES_USER: agent POSTGRES_PASSWORD: agent_secret POSTGRES_DB: agent_memory redis: image: redis:7-alpine ports: - 6379:6379 volumes: - redis_data:/data command: redis-server --appendonly yes memory-service: build: ./memory-service ports: - 8000:8000 depends_on: - qdrant - postgres - redis environment: - QDRANT_HOSTqdrant - POSTGRES_HOSTpostgres - REDIS_HOSTredis - EMBEDDING_MODELtext-embedding-3-small volumes: qdrant_data: pg_data: redis_data:这里有几个设计决策值得说明为什么选Qdrant而不是MilvusQdrant的部署更轻量单节点模式下资源占用小而且它的过滤检索功能很强——你可以在向量检索的同时加上metadata过滤条件比如“只检索最近7天的记忆”或“只检索某个用户的数据”。Milvus功能更全但部署复杂度高适合大规模集群场景。为什么PostgreSQL和Redis都要PostgreSQL存结构化数据比如用户表、会话表、记忆的元数据。Redis做热数据的缓存比如当前活跃会话的上下文。两者分工明确不要试图用一个数据库解决所有问题。memory-service的Dockerfile怎么写这是一个Python FastAPI服务核心依赖包括qdrant-client、psycopg2、redis、openai用于生成embedding和mcpMCP协议SDK。FROM python:3.11-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . CMD [uvicorn, main:app, --host, 0.0.0.0, --port, 8000]3.3 记忆服务的核心接口实现记忆服务的核心逻辑围绕三个操作展开写入、检索、遗忘。下面是一个简化版的实现重点展示关键设计思路。from fastapi import FastAPI from qdrant_client import QdrantClient from qdrant_client.models import PointStruct, Filter, FieldCondition, MatchValue import psycopg2 import redis import openai import uuid from datetime import datetime app FastAPI() qdrant QdrantClient(hostqdrant, port6333) pg_conn psycopg2.connect(hostpostgres, dbnameagent_memory, useragent, passwordagent_secret) redis_client redis.Redis(hostredis, port6379, decode_responsesTrue) COLLECTION_NAME agent_memories def get_embedding(text: str) - list: response openai.Embedding.create( modeltext-embedding-3-small, inputtext ) return response[data][0][embedding] app.post(/memory/store) async def store_memory(content: str, session_id: str, memory_type: str episodic, metadata: dict None): memory_id str(uuid.uuid4()) embedding get_embedding(content) # 写入向量库 qdrant.upsert( collection_nameCOLLECTION_NAME, points[ PointStruct( idmemory_id, vectorembedding, payload{ content: content, session_id: session_id, memory_type: memory_type, timestamp: datetime.utcnow().isoformat(), **(metadata or {}) } ) ] ) # 写入关系库 with pg_conn.cursor() as cur: cur.execute( INSERT INTO memories (id, session_id, content, memory_type, created_at) VALUES (%s, %s, %s, %s, %s), (memory_id, session_id, content, memory_type, datetime.utcnow()) ) pg_conn.commit() return {memory_id: memory_id, status: stored} app.post(/memory/recall) async def recall_memory(query: str, session_id: str None, top_k: int 5): query_embedding get_embedding(query) # 构建过滤条件 filter_conditions None if session_id: filter_conditions Filter( must[FieldCondition(keysession_id, matchMatchValue(valuesession_id))] ) results qdrant.search( collection_nameCOLLECTION_NAME, query_vectorquery_embedding, limittop_k, query_filterfilter_conditions ) memories [] for hit in results: memories.append({ content: hit.payload[content], score: hit.score, timestamp: hit.payload.get(timestamp), memory_type: hit.payload.get(memory_type) }) return {memories: memories, count: len(memories)}这段代码有几个关键点需要注意Embedding模型的选择text-embedding-3-small的维度是1536性价比高。如果对精度要求更高可以用text-embedding-3-large3072维但存储成本和检索延迟都会增加。实测下来对于Agent记忆这种场景small版本已经够用了。过滤条件的构建Qdrant的Filter支持must、should、must_not三种逻辑可以组合出很复杂的过滤规则。比如“检索某个用户最近3天的情景记忆”就是must里面加两个条件。写入时的双写策略向量库和关系库同时写入保证数据一致性。如果对性能要求极高可以改成异步写入但要注意处理失败重试。3.4 工作记忆的滑动窗口与摘要压缩工作记忆的管理策略直接影响Agent的响应质量。我试过几种方案最终稳定下来的是一种滑动窗口摘要压缩的混合策略。具体做法是保留最近K轮完整对话K通常取5当对话轮数超过K时把最早的几轮对话交给LLM做摘要摘要结果作为一条“情景记忆”存入长期存储同时从工作记忆中移除原始对话。def manage_working_memory(session_id: str, new_message: dict, max_turns: int 5): key fworking_memory:{session_id} # 获取当前工作记忆 history redis_client.lrange(key, 0, -1) history [json.loads(h) for h in history] # 追加新消息 history.append(new_message) # 如果超出窗口压缩最早的对话 if len(history) max_turns * 2: # 每轮包含user和assistant两条 to_compress history[:2] # 取最早的一轮 summary summarize_conversation(to_compress) # 存入长期记忆 store_memory( contentsummary, session_idsession_id, memory_typeepisodic, metadata{compressed_from: working_memory} ) # 从工作记忆中移除 history history[2:] # 更新Redis redis_client.delete(key) for msg in history: redis_client.rpush(key, json.dumps(msg)) redis_client.expire(key, 3600) # 1小时过期 return history这个策略的好处是工作记忆始终保持在可控的token范围内同时重要的历史信息不会丢失而是被压缩后存入长期记忆。摘要的质量取决于prompt的设计我的经验是让LLM重点关注“用户提到了哪些实体”“达成了什么结论”“还有什么未解决的问题”这三个维度。4. 记忆检索的进阶技巧与常见问题排查4.1 混合检索策略向量关键词时间衰减纯向量检索有一个容易被忽视的问题它擅长语义匹配但不擅长精确匹配。比如用户问“订单号12345的状态”向量检索可能会返回一堆语义相似但订单号不同的记忆。这时候就需要引入关键词检索作为补充。我的做法是在检索层做一个加权融合def hybrid_recall(query: str, session_id: str, top_k: int 5): # 向量检索 vector_results vector_search(query, session_id, top_k * 2) # 关键词检索基于PostgreSQL的全文检索 keyword_results keyword_search(query, session_id, top_k * 2) # 时间衰减因子 now datetime.utcnow() def time_decay(timestamp_str): ts datetime.fromisoformat(timestamp_str) hours_ago (now - ts).total_seconds() / 3600 return 1.0 / (1.0 0.01 * hours_ago) # 每小时衰减1% # 融合打分 scores {} for r in vector_results: scores[r[id]] scores.get(r[id], 0) 0.7 * r[score] for r in keyword_results: scores[r[id]] scores.get(r[id], 0) 0.3 * r[score] # 应用时间衰减 for mid in scores: memory get_memory_by_id(mid) scores[mid] * time_decay(memory[timestamp]) # 排序返回 sorted_ids sorted(scores, keyscores.get, reverseTrue)[:top_k] return [get_memory_by_id(mid) for mid in sorted_ids]权重分配0.7向量0.3关键词是我在几个项目中调出来的经验值。如果你的场景对精确匹配要求更高可以把关键词权重调到0.4甚至0.5。时间衰减系数0.01也是可调的对于时效性强的场景比如新闻资讯类Agent可以调到0.05。4.2 常见问题速查表在实际部署和运行过程中我遇到过不少问题这里整理成一张速查表方便你快速定位问题现象可能原因排查方法解决方案记忆检索返回空结果向量库collection未创建检查Qdrant日志初始化时自动创建collection检索结果不相关Embedding模型不匹配确认写入和检索用同一模型统一模型版本重建索引Docker容器间网络不通未在同一networkdocker network inspect在compose中显式定义network记忆写入延迟高同步调用Embedding API查看服务日志耗时改为批量异步写入Redis内存溢出未设置过期时间redis-cli info memory所有key设置TTLPostgreSQL连接数耗尽连接未释放pg_stat_activity使用连接池摘要质量差Prompt设计不合理人工检查摘要结果优化prompt增加示例4.3 记忆安全与隐私保护Agent记忆里可能包含用户的敏感信息比如地址、电话、偏好等。在设计记忆服务时有几个安全原则必须遵守最小化存储只存Agent决策必需的信息不要什么都往记忆里塞。比如用户的身份证号如果Agent不需要用它来做任何决策就不要存。加密存储敏感字段在写入数据库前做加密处理。PostgreSQL支持pgcrypto扩展可以在数据库层面做加密。访问控制记忆服务的API需要鉴权不同用户的记忆严格隔离。我在实现中通常用session_iduser_id双重过滤确保A用户永远检索不到B用户的记忆。定期清理设置记忆的TTL过期的记忆自动删除。对于情景记忆我通常设置30天过期对于语义记忆可以设置更长但也要定期review。热搜词里提到的“a-memguard: a proactive defense framework for llm-based agent memory”其实就是在做这方面的工作——主动防御Agent记忆被恶意注入或泄露。虽然这个框架本身我还没有深度使用但它的思路值得借鉴在记忆写入前做内容审核在记忆检索时做权限校验在记忆输出时做脱敏处理。5. 从hindsight到forward-looking记忆系统的演进方向5.1 记忆的主动遗忘与重要性评估一个健康的记忆系统不仅要会“记”还要会“忘”。人类大脑会自动遗忘不重要的信息Agent的记忆系统也应该如此。我目前采用的策略是基于重要性的淘汰机制每条记忆在写入时让LLM打一个重要性分数1-10分。检索时重要性分数作为加权因子之一。当存储空间接近上限时优先淘汰低分且长时间未被检索到的记忆。def evaluate_importance(content: str) - int: prompt f请评估以下信息对Agent未来决策的重要性打分1-10分。 1分表示完全不重要如寒暄、确认收到 10分表示极其重要如用户的核心偏好、关键业务规则。 信息内容{content} 只输出一个数字。 response llm.invoke(prompt) return int(response.strip())这个打分过程会增加写入延迟所以我的做法是异步执行——先写入后台再补打分。对于实时性要求极高的场景可以先用规则引擎做粗筛比如包含“记住”“重要”“以后”等关键词的记忆直接给高分。5.2 多Agent记忆共享与隔离当系统中有多个Agent协作时记忆的共享和隔离就变成一个复杂问题。比如一个客服系统里售前Agent和售后Agent需要共享用户的基本信息但各自的对话历史应该隔离。我的方案是在记忆的metadata里加一个access_scope字段private仅创建者可见shared同一用户的所有Agent可见global所有Agent可见如通用知识检索时根据当前Agent的权限过滤。这种设计在MCP协议下实现起来很自然因为MCP Server可以在工具层面做权限控制Agent端不需要关心具体的过滤逻辑。5.3 记忆系统的可观测性建设最后聊一个容易被忽视但非常重要的点可观测性。记忆系统上线后你需要知道它到底有没有在工作、工作得好不好。我通常会在以下几个维度埋点写入量每天新增多少条记忆按类型分布检索命中率检索请求中返回非空结果的比例检索相关性人工抽样评估检索结果的相关性记忆利用率被检索到的记忆占总记忆的比例延迟分布写入和检索的P50、P95、P99延迟这些指标可以用Prometheus采集Grafana做可视化。当检索命中率突然下降或者延迟P99飙升时能第一时间发现并排查。我在实际项目中的体会是记忆系统的调优是一个持续迭代的过程。刚开始不要追求完美先把基础链路跑通然后根据实际使用中的反馈逐步优化。比如最开始可以用固定的top_k5后面根据命中率数据动态调整最开始可以用单一的向量检索后面再引入混合策略。关键是先让系统跑起来有了数据之后再谈优化。最后分享一个小技巧在开发阶段我习惯在记忆服务的返回结果里带上debug_info字段包含检索耗时、过滤条件、原始分数等信息。这个字段在生产环境可以关掉但在调试时非常有用能帮你快速定位是检索逻辑的问题还是数据本身的问题。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →