ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Agent Memory落地实践:基于MCP与Docker的分层记忆架构设计

Agent Memory落地实践:基于MCP与Docker的分层记忆架构设计 1. 从“hindsight”说起为什么记忆是Agent落地的最后一公里“hindsight”这个词本身很有意思字面意思是“事后的洞察力”也就是我们常说的“后见之明”。放在Agent Memory这个语境里它指向一个非常具体的技术痛点一个LLM驱动的Agent在完成一轮任务之后能不能把这次任务里踩过的坑、验证过的路径、用户偏好、环境约束这些东西沉淀下来下次遇到类似场景时直接调用而不是从零开始重新推理。我接触过不少做Agent落地的团队大家一开始都把精力砸在工具调用、Prompt工程、工作流编排上等到Demo跑通、准备上生产的时候才发现真正卡脖子的地方是记忆。一个没有记忆的Agent本质上是一个每次对话都失忆的客服用户每次都要重复交代背景Agent每次都要重新试错。这种体验在单轮问答里不明显一旦进入多轮、跨会话、长周期的任务场景问题就会被无限放大。“hindsight”这个项目标题我理解它要解决的核心问题就是让Agent具备对历史交互的结构化记忆能力并且这种记忆是可检索、可更新、可遗忘的。它不是一个简单的对话历史堆砌而是一套围绕Agent工作记忆working memory设计的存储与检索机制。热搜词里出现的“agent 存储 working memory”、“agent memory”、“LLM”、“MCP”、“Docker”这几个关键词基本勾勒出了这个项目的技术轮廓用Docker做环境隔离与部署用MCP协议做工具与资源的标准化接入用LLM做记忆的抽取、压缩与检索决策最终服务于Agent的长期记忆管理。这篇文章适合谁看如果你正在做Agent应用开发尤其是涉及多轮对话、任务型Agent、个人助理类产品的这篇文章会帮你把记忆层的设计思路理清楚。如果你只是刚接触LLM应用开发对MCP、Docker这些词还比较陌生也没关系我会从最基础的概念讲起把每个技术选型背后的“为什么”说透。整篇内容基于我对Agent Memory这个方向的工程实践理解来展开结合热搜词里暴露出来的真实需求给出一套可参考、可复现的方案。2. 核心思路拆解Agent Memory到底该怎么设计2.1 为什么不能直接把对话历史塞进Context很多人第一反应是记忆不就是把历史对话存下来下次拼到Prompt里吗这个做法在早期确实能用但很快会撞到三堵墙。第一堵墙是Token成本。一个Agent如果每天和用户交互几十轮每轮对话平均500 Token一周下来就是几万Token的历史。每次请求都把全量历史塞进去Token消耗会线性增长成本扛不住而且大部分历史信息对当前任务是无关的。第二堵墙是注意力稀释。LLM的Context Window虽然越来越大但“能放进去”和“能有效利用”是两回事。当Context里塞了大量无关历史模型对关键信息的注意力会被稀释表现反而下降。这就是为什么很多Agent在长对话后期会“变傻”。第三堵墙是信息结构化程度低。原始对话历史是非结构化的里面混杂了寒暄、试错、重复确认、无效信息。真正有价值的记忆是结构化的用户偏好是什么、上次任务卡在哪、哪个工具调用失败了、环境有什么约束。这些信息需要被抽取和重组而不是原样存储。所以“hindsight”这类项目的核心设计思路一定是分层记忆 按需检索而不是全量历史拼接。2.2 分层记忆模型Working Memory与Long-term Memory参考认知科学里对人类记忆的分类Agent Memory通常也分两层Working Memory工作记忆当前会话内的短期记忆生命周期就是这一轮对话。它存储的是当前任务的上下文、最近几轮的工具调用结果、用户的即时指令。Working Memory的特点是读写频繁、容量有限、会话结束即释放。热搜词里“agent 存储 working memory”说的就是这个东西。Long-term Memory长期记忆跨会话持久化的记忆存储在外部数据库或向量库里。它存储的是用户偏好、历史任务摘要、已验证的知识、失败教训。Long-term Memory的特点是写入频率低、检索频率高、需要定期压缩和清理。这两层之间的桥梁是记忆抽取与写入在会话过程中或会话结束时由LLM对Working Memory里的内容做摘要和结构化抽取把值得长期保留的部分写入Long-term Memory。下次会话开始时根据当前任务Query从Long-term Memory里检索相关记忆注入到Working Memory里。这个设计的好处是Context里永远只放和当前任务相关的记忆Token可控长期记忆经过结构化处理检索效率高两层分离各自的生命周期管理清晰。2.3 为什么选MCP做记忆的接入层MCPModel Context Protocol这两年在Agent生态里出现频率极高。热搜词里“mcp协议”、“mcp是软件协议 硬件协议那个概念叫什么来着”、“playwright mcp”、“chrome devtools mcp”这些词说明大家对MCP的关注度很高但很多人对它的定位还比较模糊。我的理解是MCP是一套标准化的上下文接入协议它解决的是“Agent如何统一地访问外部资源和工具”的问题。在没有MCP之前每接一个工具就要写一套适配代码工具多了之后维护成本爆炸。MCP把工具、资源、Prompt模板都抽象成标准接口Agent侧只需要实现一次MCP Client就能接入所有符合MCP Server规范的能力。把MCP用在Agent Memory上好处很直接记忆的读写、检索、更新都可以封装成MCP Server暴露的能力。Agent不需要关心底层用的是Redis、Postgres还是向量数据库只需要通过MCP协议调用“写入记忆”、“检索记忆”、“更新记忆”这几个标准操作。这样记忆层的实现可以独立演进Agent侧代码不用动。热搜词里“ruoyi-vue-pro合并mcp功能”、“trae ide 搭载 burp suite mcp server”这些案例说明MCP的落地场景正在快速扩展。对于“hindsight”这个项目用MCP做记忆接入层是一个符合趋势的选择。2.4 Docker在其中的角色环境一致性与快速部署热搜词里Docker相关的词非常多“Docker”、“Docker Desktop”、“docker安装”、“docker安装教程”、“windows安装docker”、“docker网络不通”、“docker安装redis主从”、“docker安装mysql8.0并使用”。这说明很多人在实际部署Agent Memory时卡在了环境环节。Docker在这个项目里的价值主要有三个第一依赖隔离。Agent Memory通常需要向量数据库、关系数据库、缓存、消息队列等多个组件。如果直接装在宿主机上版本冲突、端口占用、配置污染的问题会让人崩溃。用Docker Compose把这些组件编排起来每个组件独立容器互不干扰。第二环境一致性。开发环境用Docker测试环境和生产环境用同一套镜像避免“在我机器上能跑”的经典问题。对于Agent Memory这种涉及多个外部依赖的系统环境一致性尤其重要。第三快速复现。一篇技术文章如果只讲思路不给可运行的部署方案读者很难复现。用Docker Compose把整套环境打包读者一条命令就能拉起来学习成本大幅降低。3. 核心细节解析与实操要点3.1 记忆的数据结构设计Key-Query-Value三元组热搜词里有一条很关键“llm的token三个点key我是谁、query我在找什么、value我能提供什么”。这个描述其实点出了记忆检索的核心逻辑。在Agent Memory里一条记忆通常可以抽象成三个维度Key我是谁这条记忆属于哪个主体。可能是用户ID、Agent实例ID、任务ID。Key决定了记忆的归属和隔离边界。Query我在找什么检索时的查询意图。通常是一个向量化的语义表示也可能是关键词组合。Query决定了记忆的召回相关性。Value我能提供什么记忆的实际内容。可以是文本摘要、结构化JSON、工具调用记录、用户偏好标签。在实际存储时我建议用一张主表加一张向量索引表的结构字段类型说明memory_idUUID记忆唯一标识owner_keyString归属主体如user_idmemory_typeEnum记忆类型preference/fact/experience/tool_resultcontentText记忆正文embeddingVector语义向量用于相似度检索metadataJSONB扩展字段时间戳、来源会话、置信度created_atTimestamp创建时间last_accessed_atTimestamp最后访问时间用于LRU淘汰access_countInteger访问次数用于热度排序这个结构的好处是owner_key做隔离memory_type做分类过滤embedding做语义检索metadata做灵活扩展。查询时可以组合使用先按owner_key过滤再按memory_type筛选最后用embedding做相似度排序。注意embedding的维度要和所选模型对齐。如果用OpenAI的text-embedding-3-small维度是1536如果用BGE系列常见的是768或1024。维度不匹配会导致检索报错。3.2 记忆写入策略什么时候写、写什么、写多少记忆写入是整套系统里最容易出问题的地方。写得太频繁存储膨胀、检索噪声大写得太少关键信息丢失。我的经验是采用事件驱动 阈值触发的混合策略。事件驱动在几个关键节点触发记忆抽取。比如任务完成时、用户明确表达偏好时、工具调用失败时、用户纠正Agent时。这些节点产生的信息价值密度高值得写入。阈值触发Working Memory累积到一定轮数比如10轮或一定Token量比如4000 Token时触发一次摘要压缩把压缩后的结果写入Long-term Memory。写入内容需要经过LLM做结构化抽取Prompt大概长这样你是一个记忆抽取器。请从以下对话片段中提取值得长期保留的记忆。 输出JSON格式每条记忆包含 - type: preference | fact | experience | tool_result - content: 一句话描述不超过50字 - confidence: 0.0-1.0表示这条记忆的可靠程度 对话片段 {conversation_chunk} 只输出JSON数组不要输出其他内容。这个Prompt的关键点是限定输出格式、限定内容长度、要求置信度。置信度这个字段很有用后续检索时可以按置信度过滤低置信度的记忆不参与召回。实操心得记忆抽取的Prompt里一定要加“如果对话中没有值得长期保留的信息输出空数组”。否则LLM会强行编造记忆导致存储里全是噪声。3.3 记忆检索策略向量检索 规则过滤 重排序检索是记忆系统的另一个核心环节。单纯用向量相似度检索有几个问题可能召回语义相似但主体不对的记忆可能召回过期或低置信度的记忆可能召回太多Context塞不下。我的做法是三步走第一步规则过滤。按owner_key、memory_type、时间范围、置信度阈值做硬过滤。这一步把候选集从全量缩小到几十条。第二步向量检索。对过滤后的候选集做embedding相似度计算取Top-K通常K10到20。第三步重排序。用一个轻量级的LLM或Cross-Encoder对Top-K结果做精排按与当前Query的相关性重新排序取Top-N通常N3到5注入Context。这个流程的好处是规则过滤保证主体和类型正确向量检索保证语义相关重排序保证最终质量。三步下来注入Context的记忆数量可控质量有保障。热搜词里“rag和llm wiki”、“rag graphrag llm wiki 本体rag”这些词说明大家对RAG的进阶形态很关注。Agent Memory的检索本质上就是一种特殊的RAG检索的不是文档而是历史交互中沉淀的记忆。GraphRAG的思路也可以借鉴把记忆之间的关联关系建成图检索时可以做多跳推理。3.4 MCP Server的接口设计把记忆能力封装成MCP Server需要暴露哪些接口我建议至少包含以下几个接口名输入输出说明memory_writeowner_key, type, content, metadatamemory_id写入一条记忆memory_searchowner_key, query, type_filter, top_kmemory_list检索记忆memory_updatememory_id, content, metadatasuccess更新记忆memory_deletememory_idsuccess删除记忆memory_summarizeowner_key, session_idsummary对会话做摘要并写入这几个接口覆盖了记忆的增删改查和摘要。Agent侧只需要实现MCP Client就能调用这些能力。MCP Server内部可以用任何技术栈实现Python、Node.js、Go都行只要符合MCP协议规范。热搜词里“mcp是软件协议 硬件协议那个概念叫什么来着”这个问题我的理解是MCP是软件层面的协议类比的话有点像USB协议在硬件层面的角色——定义了一套标准接口让不同设备工具可以即插即用。MCP让不同的工具和资源可以标准化地接入Agent。4. 实操过程与核心环节实现4.1 环境准备Docker Compose一键拉起全套依赖先说环境。Agent Memory需要的基础组件包括Postgres存结构化记忆、Redis存Working Memory和缓存、Qdrant或Milvus存向量、一个MCP Server进程。用Docker Compose编排是最省事的方案。version: 3.9 services: postgres: image: postgres:16-alpine environment: POSTGRES_USER: agent POSTGRES_PASSWORD: agent_pass POSTGRES_DB: agent_memory ports: - 5432:5432 volumes: - pg_data:/var/lib/postgresql/data redis: image: redis:7-alpine ports: - 6379:6379 volumes: - redis_data:/data qdrant: image: qdrant/qdrant:latest ports: - 6333:6333 - 6334:6334 volumes: - qdrant_data:/qdrant/storage mcp-memory-server: build: ./mcp-memory-server ports: - 8080:8080 environment: POSTGRES_URL: postgresql://agent:agent_passpostgres:5432/agent_memory REDIS_URL: redis://redis:6379/0 QDRANT_URL: http://qdrant:6333 depends_on: - postgres - redis - qdrant volumes: pg_data: redis_data: qdrant_data:这个Compose文件定义了四个服务。Postgres存结构化记忆Redis存Working MemoryQdrant存向量mcp-memory-server是自定义的MCP Server镜像。注意Windows环境下用Docker Desktop需要确保WSL2后端已启用。热搜词里“virtualization support not detected docker desktop failed to start”这个问题通常是因为BIOS里虚拟化没开或者Hyper-V和WSL2冲突。解决办法是进BIOS开启VT-x/AMD-V然后在Windows功能里确保“虚拟机平台”和“适用于Linux的Windows子系统”都勾选。启动命令docker compose up -d启动后检查各服务状态docker compose ps如果某个服务起不来用docker compose logs service_name看日志。热搜词里“docker网络不通”的问题常见原因是容器间用了localhost而不是服务名。在Compose网络里容器之间应该用服务名做主机名比如postgres:5432而不是localhost:5432。4.2 MCP Server核心实现记忆写入与检索MCP Server用Python实现核心依赖是mcp库和qdrant-client。先看记忆写入的逻辑import json from mcp.server import Server from mcp.types import Tool, TextContent from qdrant_client import QdrantClient from qdrant_client.models import PointStruct, VectorParams, Distance import psycopg2 import redis app Server(memory-server) qdrant QdrantClient(urlhttp://qdrant:6333) pg_conn psycopg2.connect(postgresql://agent:agent_passpostgres:5432/agent_memory) r redis.from_url(redis://redis:6379/0) COLLECTION agent_memory def ensure_collection(): collections qdrant.get_collections().collections if COLLECTION not in [c.name for c in collections]: qdrant.create_collection( collection_nameCOLLECTION, vectors_configVectorParams(size1536, distanceDistance.COSINE) ) app.tool() async def memory_write(owner_key: str, memory_type: str, content: str, metadata: dict None): embedding get_embedding(content) memory_id str(uuid.uuid4()) with pg_conn.cursor() as cur: cur.execute( INSERT INTO memories (memory_id, owner_key, memory_type, content, metadata) VALUES (%s, %s, %s, %s, %s), (memory_id, owner_key, memory_type, content, json.dumps(metadata or {})) ) pg_conn.commit() qdrant.upsert( collection_nameCOLLECTION, points[PointStruct( idmemory_id, vectorembedding, payload{owner_key: owner_key, memory_type: memory_type, content: content} )] ) return TextContent(typetext, textjson.dumps({memory_id: memory_id}))这段代码做了两件事把记忆写入Postgres做持久化把向量写入Qdrant做检索索引。两边用同一个memory_id关联。检索逻辑app.tool() async def memory_search(owner_key: str, query: str, type_filter: str None, top_k: int 5): query_vector get_embedding(query) filter_conditions {owner_key: owner_key} if type_filter: filter_conditions[memory_type] type_filter results qdrant.search( collection_nameCOLLECTION, query_vectorquery_vector, query_filterFilter(must[ FieldCondition(keyk, matchMatchValue(valuev)) for k, v in filter_conditions.items() ]), limittop_k ) memories [{memory_id: r.id, content: r.payload[content], score: r.score} for r in results] return TextContent(typetext, textjson.dumps(memories, ensure_asciiFalse))检索时先按owner_key和type做过滤再做向量相似度搜索。返回结果按相似度排序。实操心得embedding的生成建议单独封装成一个函数方便替换模型。如果用的是本地部署的embedding模型注意首次加载会有冷启动延迟可以在服务启动时预热。4.3 Working Memory的Redis实现Working Memory用Redis的List或Stream结构存储每个会话一个Keydef append_working_memory(session_id: str, role: str, content: str): key fwm:{session_id} entry json.dumps({role: role, content: content, ts: time.time()}) r.rpush(key, entry) r.expire(key, 3600) # 1小时过期 def get_working_memory(session_id: str, last_n: int 10): key fwm:{session_id} entries r.lrange(key, -last_n, -1) return [json.loads(e) for e in entries]Working Memory设置过期时间是关键。会话结束后Working Memory应该被清理有价值的部分已经通过摘要写入Long-term Memory了。4.4 记忆摘要与压缩的触发逻辑摘要触发放在会话结束或Working Memory达到阈值时async def maybe_summarize(session_id: str, owner_key: str): key fwm:{session_id} length r.llen(key) if length 10: return entries get_working_memory(session_id, last_nlength) conversation \n.join([f{e[role]}: {e[content]} for e in entries]) prompt f从以下对话中提取值得长期保留的记忆。 输出JSON数组每条包含type(preference/fact/experience/tool_result)、content(不超过50字)、confidence(0-1)。 如果没有值得保留的信息输出[]。 对话 {conversation} response await llm_call(prompt) memories json.loads(response) for mem in memories: if mem[confidence] 0.6: await memory_write(owner_key, mem[type], mem[content], {source_session: session_id}) r.delete(key)这个逻辑在每次会话轮次结束后调用一次达到阈值就触发摘要摘要完成后清空Working Memory。5. 常见问题与排查技巧实录5.1 Docker环境类问题速查问题现象可能原因排查方法解决方案Docker Desktop启动失败提示virtualization support not detectedBIOS虚拟化未开启任务管理器→性能→CPU看“虚拟化”是否已启用进BIOS开启VT-x/AMD-V容器间网络不通用了localhost而非服务名docker compose exec svc ping target改用Compose服务名做主机名端口被占用宿主机已有服务占用端口netstat -ano | findstr 5432改Compose端口映射或停掉占用进程数据卷权限错误容器内用户与宿主机用户UID不匹配docker compose logs svc在Compose里指定user或调整卷权限镜像拉取慢默认镜像源速度慢docker pull观察速度配置镜像加速器热搜词里“docker安装mysql8.0并使用”、“docker安装redis主从”这些需求思路是一样的用Compose定义服务配置环境变量挂载数据卷。MySQL 8.0要注意的是默认认证插件是caching_sha2_password老客户端可能不兼容可以在Compose里加command: --default-authentication-pluginmysql_native_password。5.2 记忆检索质量问题排查问题一检索结果不相关。先检查embedding模型是否一致——写入和检索必须用同一个模型。再检查owner_key过滤是否正确如果owner_key传错会检索到别人的记忆。最后看Top-K是否太小适当增大K值再重排序。问题二记忆重复写入。同一个信息被多次抽取写入。解决办法是在写入前做一次相似度检查如果已有相似度超过0.95的记忆就跳过或更新而不是新增。问题三记忆过期不清理。Long-term Memory只增不减存储膨胀。建议加一个定时任务按last_accessed_at和access_count做LRU淘汰超过90天未访问且访问次数低于3次的记忆归档或删除。问题四摘要丢失关键信息。LLM摘要有随机性可能漏掉重要细节。缓解办法是在Prompt里明确要求保留“用户明确表达的偏好”和“工具调用失败的原因”这两类信息并在摘要后做一次校验如果摘要长度异常短触发重新摘要。5.3 MCP接入常见坑热搜词里“browser use mcp 跟 playwright mcp 有什么区别”、“chrome devtools mcp playwright mcp”这些问题反映的是MCP生态里工具选择的困惑。我的经验是MCP Server的选择要看具体场景。Playwright MCP适合做浏览器自动化Chrome DevTools MCP适合做页面调试和性能分析Browser Use MCP更偏向于让Agent自主操作浏览器。三者有重叠但侧重点不同。对于Agent Memory场景MCP Server的接入要注意Token传递热搜词里出现了wss://api.xiaozhi.me/mcp/?tokeneyjhbgcioijfuzi1niisinr5cci6ikpxvcj9.eyj这样的URL说明MCP Server可能需要鉴权。Token要安全存储不要硬编码在代码里用环境变量注入。超时设置MCP调用是网络请求要设置合理的超时时间。记忆检索建议超时3秒写入建议超时5秒。错误处理MCP Server不可用时Agent应该有降级策略比如跳过记忆检索直接回答而不是整个流程卡死。5.4 性能优化经验批量写入如果一次摘要产生多条记忆用批量接口写入减少网络往返。Qdrant的upsert支持批量Postgres用executemany。缓存热点记忆高频访问的记忆可以缓存在Redis里减少向量库查询。缓存Key用mem_cache:{owner_key}:{query_hash}过期时间设短一点比如5分钟。异步化记忆写入和摘要不应该阻塞主对话流程。用消息队列或后台任务异步处理Agent侧只负责把事件发出去。向量索引调优Qdrant的HNSW索引参数m和ef_construct影响检索速度和精度。数据量小的时候默认值就行数据量大了之后需要调优。一般m16、ef_construct100是个不错的起点。6. 记忆安全与演进方向6.1 记忆投毒与防御热搜词里出现了“a-memguard: a proactive defense framework for llm-based agent memory”这说明Agent Memory的安全问题已经开始被关注。记忆投毒的攻击路径是攻击者通过对话诱导Agent写入错误记忆后续检索时这些错误记忆被召回导致Agent行为异常。防御思路有几层写入校验对写入的记忆做置信度过滤低置信度的记忆不写入。对来源做标记来自外部不可信输入的记忆降低权重。检索隔离不同安全级别的记忆分开存储检索时按安全级别过滤。用户偏好类记忆和工具调用结果类记忆不应该混在一起。定期审计定期对Long-term Memory做抽样审计检查是否有异常记忆。异常检测可以用LLM做让LLM判断某条记忆是否合理。遗忘机制支持按条件批量删除记忆比如删除某个时间段内来自某个会话的所有记忆。这在发现投毒后做清理时很有用。6.2 记忆的可解释性Agent为什么做出某个决策如果决策依据是某条记忆这条记忆应该能被追溯。实现方式是在记忆的metadata里记录来源会话ID和抽取时间Agent在回答时可以引用记忆ID方便调试和审计。6.3 多Agent共享记忆单个Agent的记忆是私有的但多个Agent协作时可能需要共享部分记忆。设计上可以用owner_key做区分agent:{agent_id}是私有记忆shared:{team_id}是团队共享记忆。检索时同时查私有和共享按权重合并。热搜词里“llm wiki”、“llm wiki知识库”、“llm wiki项目”这些词指向的是另一个方向把记忆和知识库打通。Agent Memory沉淀的是交互经验LLM Wiki沉淀的是领域知识两者结合可以让Agent既有“记性”又有“学问”。实现上可以把Wiki的检索也封装成MCP Server和Memory Server并列Agent根据需要选择调用。6.4 从Hindsight到Foresight“hindsight”是事后记忆但Agent Memory的终极形态应该包含“foresight”——基于历史记忆做前瞻性推理。比如根据用户过去的行为模式预测用户下一步可能需要什么提前准备。这需要在记忆检索之上加一层推理层用LLM对检索到的记忆做归纳和预测。这个方向目前还在探索阶段但思路是清晰的记忆不只是存储和检索还要能参与推理和决策。我在实际项目里的体会是先把基础的分层记忆和检索做扎实再考虑上层推理不要一上来就追求智能基础不牢后面全是坑。最后分享一个我在部署时踩过的坑Docker Compose里服务的启动顺序用depends_on只能保证容器启动顺序不能保证服务就绪。Postgres容器起来了但数据库还没初始化完MCP Server就去连接会失败。解决办法是在MCP Server的启动脚本里加一个等待逻辑用pg_isready轮询Postgres就绪后再启动。这个细节在文档里通常不会写但实际部署时必踩。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进