基于MCP与Docker构建LLM Agent分层记忆系统:从hindsight到长期记忆实践

发布时间:2026/10/4 11:25:44
基于MCP与Docker构建LLM Agent分层记忆系统:从hindsight到长期记忆实践 1. 从“hindsight”说起为什么我们需要给 Agent 装上一双“后视之眼”第一次看到 “hindsight” 这个词是在给一个基于 LLM 的客服 Agent 做故障复盘的时候。当时用户反馈“上周明明告诉过它我的订单号这周再问它又忘了”我翻遍了对话日志发现 Agent 每一轮都在重新理解上下文历史信息像沙子一样从指缝里漏掉。那一刻我意识到问题不在模型本身而在于我们从来没给 Agent 设计一套像样的记忆机制。hindsight 这个词字面意思是“事后的洞察力”放到 Agent 领域它指向的正是那层被大多数项目忽略的能力——让 Agent 能够回看、检索、复用过去发生过的事情而不是每次都从零开始。这篇文章想聊的就是围绕 “hindsight” 这个核心概念怎么给 LLM Agent 搭一套真正能用的记忆系统。涉及到的关键词包括 agent memory、LLM、MCP、Docker这些不是孤立的技术点而是一条完整的链路LLM 是大脑agent memory 是记忆MCP 是连接外部世界的协议Docker 是让这一切跑起来的容器底座。如果你正在做 Agent 相关的项目或者被“Agent 记不住东西”这个问题折磨过那接下来的内容应该能帮你少走一些弯路。先说清楚这套东西适合谁看。如果你是完全没接触过 LLM 应用开发的新手建议先补一下 token、prompt、embedding 这些基础概念不然读起来会有点吃力。如果你已经用 LangChain、Dify 或者自己手写调用过大模型接口那这篇内容可以直接拿来当参考方案。如果你正在用 RuoYi-Vue-Pro 这类框架做企业级应用想合并 MCP 功能那第三、四部分的内容会对你有直接帮助。我个人的判断是2024 年之后做 Agent记忆系统不再是“锦上添花”而是“生死线”。一个没有记忆的 Agent本质上就是一个高级一点的问答机器人它无法积累经验无法形成用户画像无法在多次交互中保持一致性。hindsight 要解决的就是这个问题。2. Agent Memory 的核心设计从 working memory 到长期记忆的分层思路2.1 为什么 Agent 的记忆不能只有“上下文窗口”很多人对 Agent 记忆的理解停留在“把历史对话塞进 prompt”这个层面。这种做法在对话轮次少的时候没问题但一旦超过模型的上下文窗口限制就开始出问题。我实测过一个场景用 128K 上下文窗口的模型做多轮客服当对话轮次超过 40 轮之后模型对早期信息的召回率明显下降而且 token 成本飙升得厉害。更麻烦的是即使窗口够大模型也会出现“中间遗忘”现象——开头和结尾的信息记得住中间的内容容易被忽略。所以 agent memory 必须分层。我参考了认知科学里人类记忆的分类方式把 Agent 的记忆拆成三层working memory、episodic memory、semantic memory。working memory 就是当前对话的上下文容量有限随用随弃episodic memory 是具体发生过的事件记录比如“用户张三在 3 月 5 日咨询过退款”semantic memory 是从多次事件中抽象出来的知识比如“张三是一个对价格敏感、偏好邮件沟通的用户”。这个分层思路的好处是每一层可以用不同的存储方案。working memory 放在内存里就行episodic memory 用关系型数据库或者文档数据库semantic memory 用向量数据库做语义检索。三者之间通过检索策略串联起来而不是把所有东西都塞进 prompt。2.2 记忆的写入、检索与遗忘三个容易被忽略的环节大部分教程只讲“怎么存记忆”但实际做下来写入、检索、遗忘这三个环节才是真正决定系统好不好用的地方。写入环节的核心问题是“什么值得记”。我的做法是给每条消息打一个重要性分数分数由几个维度加权得出是否包含用户偏好、是否包含事实性信息、是否是任务关键节点。分数低于阈值的直接丢弃不进入长期记忆。这个阈值需要根据业务场景调客服场景可以低一点代码助手场景可以高一点。检索环节的核心问题是“怎么找得准”。纯向量检索有个坑就是语义相似不等于真正相关。我试过用纯向量检索找“用户上次提到的订单号”结果返回了一堆语义相近但实际无关的对话。后来改成混合检索先用关键词过滤出包含订单号格式的候选集再用向量相似度排序。这样准确率提升很明显。遗忘环节最容易被忽略但恰恰是 hindsight 这个概念的精髓。人类记忆会随时间衰减Agent 记忆也应该有衰减机制。我的做法是给每条记忆加一个时间衰减因子越久远的记忆权重越低检索时自然排在后面。同时设置一个硬性过期时间超过一定天数的低重要性记忆直接清理。这样既控制了存储成本也避免了过时信息干扰当前决策。2.3 用 MCP 把记忆能力标准化输出MCP 是软件协议这个概念最近在 Agent 圈子里讨论得很多。简单说MCP 定义了一套标准接口让 Agent 能够以统一的方式调用外部工具和数据源。把 agent memory 封装成 MCP Server好处是记忆能力可以跨项目复用不用每个 Agent 都重新实现一遍。我目前的做法是写一个 memory-mcp-server暴露几个核心接口store_memory、retrieve_memory、forget_memory、summarize_memory。Agent 通过 MCP 协议调用这些接口底层存储用什么数据库对 Agent 透明。这样换存储方案的时候Agent 侧代码完全不用动。这里有个实操细节要注意MCP 的 tool payload 有 schema 限制记忆的元数据结构设计得太复杂会导致调用失败。我踩过一次坑把记忆的 metadata 设计成了嵌套五层的 JSON结果 MCP 客户端直接报 schema 校验错误。后来改成扁平结构只保留最必要的字段问题就解决了。3. 用 Docker 搭一套可复现的 Agent Memory 环境3.1 容器化选型为什么不用裸机部署Agent memory 系统涉及多个组件向量数据库、关系型数据库、缓存、MCP Server、Agent 运行时。如果每个都裸机部署光是环境依赖就能折腾一整天。我试过在一台新机器上手动装 Milvus 加 PostgreSQL 加 Redis中间遇到 glibc 版本不兼容、端口冲突、配置文件路径错误等一堆问题最后花了六个小时才跑通。用 Docker 之后同样的环境用 docker compose 一条命令就能起来。更重要的是容器化保证了开发环境和生产环境的一致性。我见过太多“本地跑得好好的上线就挂”的案例根源都是环境差异。Docker 把这种差异消灭掉了。Windows 用户需要注意Docker Desktop 依赖 WSL2 或者 Hyper-V。如果启动时报 “virtualization support not detected”要去 BIOS 里开启虚拟化支持。这个问题我遇到过好几次每次都是帮同事排查最后发现都是 BIOS 设置的问题。3.2 docker compose 编排文件详解下面是我在用的 docker-compose.yml 核心结构做了简化但保留了关键配置version: 3.8 services: postgres: image: postgres:16-alpine environment: POSTGRES_DB: agent_memory POSTGRES_USER: agent POSTGRES_PASSWORD: ${DB_PASSWORD} volumes: - pg_data:/var/lib/postgresql/data ports: - 5432:5432 healthcheck: test: [CMD-SHELL, pg_isready -U agent] interval: 10s retries: 5 redis: image: redis:7-alpine command: redis-server --appendonly yes volumes: - redis_data:/data ports: - 6379:6379 qdrant: image: qdrant/qdrant:latest volumes: - qdrant_data:/qdrant/storage ports: - 6333:6333 memory-mcp: build: ./memory-mcp depends_on: postgres: condition: service_healthy redis: condition: service_started qdrant: condition: service_started environment: DB_URL: postgresql://agent:${DB_PASSWORD}postgres:5432/agent_memory REDIS_URL: redis://redis:6379 QDRANT_URL: http://qdrant:6333 ports: - 8080:8080 volumes: pg_data: redis_data: qdrant_data:这个编排文件里有几个设计决策值得说明。PostgreSQL 用 alpine 版本是为了减小镜像体积但要注意 alpine 的 locale 设置可能导致中文排序异常如果业务涉及中文检索建议换成标准版本。Redis 开启 appendonly 是为了持久化Agent memory 的缓存丢了虽然不致命但重建成本不低。Qdrant 作为向量数据库选它是因为部署简单、API 友好而且对 Docker 支持很好。healthcheck 那段很关键。memory-mcp 服务依赖 postgres 就绪才能启动如果不加 condition: service_healthy容器启动顺序不保证memory-mcp 可能在 postgres 还没准备好连接的时候就启动然后崩溃重启。这个问题我排查了很久才定位到。3.3 环境变量与密钥管理数据库密码这类敏感信息不要硬编码在 compose 文件里。我用的是 .env 文件加环境变量引用的方式# .env DB_PASSWORDyour_strong_password_here然后在 compose 文件里用${DB_PASSWORD}引用。.env 文件要加入 .gitignore避免误提交。生产环境建议用 Docker secrets 或者外部密钥管理服务比 .env 更安全。还有一个容易忽略的点容器内的服务发现。memory-mcp 连接 postgres 时host 要写服务名postgres而不是localhost。因为每个容器有自己的网络命名空间localhost 指向的是容器自身。这个坑我见过太多人踩包括我自己早期也犯过。4. 记忆系统的核心实现从 token 三元组到语义检索4.1 理解 LLM 的 token 三元组key、query、value热词里有一条“llm的token三个点key我是谁、query我在找什么、value我能提供什么”这个说法很形象地概括了注意力机制的核心。在 Transformer 里每个 token 会生成三个向量query、key、value。Query 代表“我在找什么”Key 代表“我是谁”Value 代表“我能提供什么”。注意力计算就是拿当前 token 的 query 去和所有 token 的 key 做点积得到权重然后对 value 加权求和。理解这个机制对设计记忆系统很有帮助。因为记忆检索本质上也是同样的逻辑用户的当前 query 是“我在找什么”存储的记忆条目有各自的 key标签、元数据和 value具体内容。检索过程就是匹配 query 和 key然后返回对应的 value。我在设计记忆 schema 的时候会刻意让每条记忆的 key 部分更丰富。比如一条关于用户偏好的记忆key 不只是“用户偏好”这个标签还包括用户 ID、时间戳、来源渠道、置信度等。这样检索时匹配的维度更多召回更精准。4.2 向量化与索引选对 embedding 模型很关键记忆检索的准确性很大程度上取决于 embedding 模型的质量。我对比过几个常用的中文 embedding 模型在 Agent memory 这个场景下差异还挺明显的。模型维度中文效果推理速度适用场景text-embedding-3-small1536良好快通用场景成本敏感text-embedding-3-large3072优秀中等精度要求高的场景bge-large-zh1024优秀中等纯中文场景m3e-base768良好快资源受限场景我的选择是 bge-large-zh 做主力因为它对中文语义的捕捉更细腻而且可以本地部署不依赖外部 API。如果业务涉及多语言text-embedding-3-large 更合适。索引方面Qdrant 支持 HNSW 和 IVF 两种索引。HNSW 查询快但内存占用高IVF 内存友好但需要训练。Agent memory 的数据量通常不会特别大百万级以内我一般直接用 HNSW省心。4.3 记忆的存储结构设计一条完整的记忆记录我通常设计成这样的结构{ id: mem_20240315_001, user_id: user_123, session_id: sess_456, content: 用户表示偏好邮件沟通不喜欢电话, memory_type: preference, importance: 0.85, created_at: 2024-03-15T10:30:00Z, last_accessed: 2024-03-20T14:22:00Z, access_count: 3, decay_factor: 0.95, tags: [communication, preference], embedding: [0.123, -0.456, ...], source: chat, confidence: 0.9 }这个结构里importance 和 decay_factor 是控制记忆生命周期的关键字段。importance 在写入时确定decay_factor 随时间动态调整。检索时最终得分是similarity * importance * decay_factor这样既考虑了语义相关性也考虑了记忆的重要性和新鲜度。access_count 和 last_accessed 用于实现“用进废退”机制。被频繁访问的记忆decay_factor 衰减得更慢相当于强化了记忆。这个灵感来自人类记忆的巩固机制。4.4 MCP Server 的实现要点memory-mcp-server 我用 Python 写基于官方 MCP SDK。核心是暴露几个 toolfrom mcp.server import Server from mcp.types import Tool, TextContent server Server(memory-mcp) server.list_tools() async def list_tools(): return [ Tool( namestore_memory, description存储一条新的记忆, inputSchema{ type: object, properties: { content: {type: string}, memory_type: {type: string}, importance: {type: number}, tags: {type: array, items: {type: string}} }, required: [content, memory_type] } ), Tool( nameretrieve_memory, description检索相关记忆, inputSchema{ type: object, properties: { query: {type: string}, top_k: {type: integer, default: 5}, memory_type: {type: string} }, required: [query] } ) ]这里有个设计取舍inputSchema 要尽量简单。我一开始想把所有字段都做成必填结果 Agent 调用时经常因为缺字段失败。后来改成只有核心字段必填其他都有默认值调用成功率明显提升。另一个要点是错误处理。MCP 调用失败时返回的错误信息要足够清晰方便 Agent 决定是重试还是降级。我见过一些实现直接抛异常Agent 拿到一个模糊的错误信息完全不知道该怎么办。5. 常见问题与排查技巧实录5.1 记忆检索不准的排查思路检索不准是最常见的问题表现是 Agent 答非所问或者明明存过的信息检索不出来。排查我一般按这个顺序来第一步确认记忆是否真的写入了。直接查数据库看记录是否存在。有时候是写入环节的 bug比如 embedding 生成失败导致整条记录没存进去。第二步检查 embedding 是否正常。把 query 和记忆内容的 embedding 拿出来手动算一下余弦相似度。如果相似度很低但语义上明显相关说明 embedding 模型不适合这个场景需要换模型。第三步看检索的过滤条件是不是太严。比如 memory_type 过滤、时间范围过滤任何一个条件设置不当都会导致召回为空。我习惯先把过滤条件全部去掉看纯向量检索的结果再逐步加回过滤条件。第四步检查 decay_factor 的影响。如果记忆太旧decay_factor 很低即使语义相关也会被排到后面。这时候要调整衰减曲线或者对某些类型的记忆关闭衰减。5.2 Docker 环境下的典型故障Docker 相关的故障我整理了一个速查表现象可能原因解决方法容器启动后立即退出启动命令错误或依赖未就绪查看 docker logs检查 depends_on 配置容器间网络不通不在同一 network用 docker compose 自动创建的网络或手动指定端口冲突宿主机端口被占用改映射端口或停掉占用进程数据丢失未挂载 volume检查 volumes 配置数据目录必须挂载镜像拉取慢网络问题配置镜像加速器内存不足容器内存限制调整 deploy.resources.limits其中“容器间网络不通”这个我踩过最多次。docker compose 默认会创建一个 network所有服务都在里面用服务名就能互相访问。但如果手动用 docker run 启动不指定 network容器之间就隔离了。所以我现在一律用 compose 管理不用裸 run。5.3 MCP 接入的常见坑MCP 接入过程中我遇到过几个典型问题。一个是 schema 校验失败报 “provider rejected the request schema or tool payload”这个通常是 tool 的 inputSchema 定义和实际传参不匹配。解决方法是把 schema 打印出来和实际 payload 逐字段对比。另一个是 MCP Server 找不到报 “codex无法找到mcp” 类似的错误。这通常是配置文件路径不对或者 Server 没启动。MCP 的配置一般在一个 JSON 文件里要确保路径是绝对路径相对路径容易出问题。还有一个是授权问题比如接入 Figma MCP 或者蓝湖 MCP 时的授权流程。这类第三方 MCP 通常需要 OAuth 或者 API Key授权失败时要检查 token 是否过期、权限范围是否足够。5.4 记忆系统的性能优化经验当记忆数据量增长到十万级以上时检索性能会明显下降。我做过几轮优化效果比较明显的几个措施第一给向量索引加量化。Qdrant 支持 scalar quantization把 float32 的向量压缩成 int8内存占用降到四分之一检索速度提升明显精度损失在可接受范围内。第二冷热分离。最近 30 天的记忆放在热存储内存或 SSD更早的放在冷存储。检索时先查热存储不够再查冷存储。这个策略对客服场景特别有效因为大部分查询都集中在近期记忆。第三批量写入。单条写入的开销主要在 embedding 生成和索引更新批量写入可以摊薄这些开销。我一般攒够 50 条或者每隔 5 秒批量写一次。第四缓存高频查询。用 Redis 缓存最近检索过的 query 和结果命中率能到 30% 左右对降低延迟有帮助。6. 记忆系统的扩展方向与个人实践体会6.1 从 hindsight 到 foresight记忆的预测性使用hindsight 是回看但记忆系统的价值不止于回看。当记忆积累到一定量级可以做预测性使用。比如根据用户的历史行为模式预测他下一步可能的需求提前准备好相关信息。这个思路在推荐系统里很常见搬到 Agent 记忆系统里同样适用。我目前在做的一个实验是用历史记忆训练一个轻量的预测模型在用户发起对话之前就预判他可能问什么提前把相关记忆加载到 working memory 里。这样首轮响应的质量会高很多。当然这个还在探索阶段效果还不稳定但方向我觉得是对的。6.2 多 Agent 场景下的记忆共享单个 Agent 的记忆系统相对简单多 Agent 协作时记忆共享就复杂了。几个 Agent 之间怎么共享记忆、怎么避免冲突、怎么保证一致性都是问题。我目前的方案是引入一个共享记忆层所有 Agent 通过 MCP 访问同一个 memory server。每个 Agent 有自己的私有记忆空间同时可以读写共享空间。共享空间的写入需要经过冲突检测避免两个 Agent 同时写入矛盾的信息。这个方案还在迭代中主要问题是共享记忆的检索效率以及怎么判断一条记忆应该放在私有空间还是共享空间。我的经验法则是与特定用户相关的放私有与任务或知识相关的放共享。6.3 我踩过的几个印象深刻的坑第一个坑是 embedding 维度不一致。有一次换 embedding 模型忘了重建索引结果新写入的记忆是 1024 维旧记忆是 768 维检索时直接报错。教训是换模型必须重建全部索引没有捷径。第二个坑是时间戳时区问题。服务器用 UTC业务侧用本地时间导致记忆的时间衰减计算错乱。后来统一用 UTC 存储展示时再转本地时间问题解决。第三个坑是记忆内容过长。有一条记忆存了一整篇文档embedding 之后语义被稀释检索时怎么都匹配不上。后来限制单条记忆的长度超过 500 字的先做摘要再存效果好很多。第四个坑是并发写入冲突。多个请求同时写入同一条记忆的更新导致数据覆盖。加了乐观锁之后解决用版本号控制冲突时重试。6.4 给正在做 Agent Memory 的朋友几点建议如果你刚开始做 Agent memory我的建议是从简单开始。不要一上来就搞复杂的多层记忆架构先用 PostgreSQL 加 pgvector 把基本流程跑通验证了价值再逐步优化。很多团队死在过度设计上系统还没跑起来就搭了一堆组件最后维护成本高到无法承受。另外记忆系统的评估很重要。我建议建一个测试集包含典型的检索场景每次改动后跑一遍看召回率和准确率的变化。没有评估指标优化就是盲人摸象。最后别忘了隐私和合规。记忆系统存储的是用户数据该加密的加密该脱敏的脱敏该设置过期时间的设置过期时间。这个不是技术问题是底线问题。我在实际项目里最大的体会是Agent memory 不是一个纯技术问题它更像是一个产品问题。什么样的记忆值得存、存多久、怎么用这些决策需要结合具体业务场景来定。技术方案只是工具真正决定系统好不好用的是对业务的理解深度。