news 2026/10/4 11:25:39

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于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_PASSWORD=your_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。核心是暴露几个 tool:

from mcp.server import Server from mcp.types import Tool, TextContent server = Server("memory-mcp") @server.list_tools() async def list_tools(): return [ Tool( name="store_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( name="retrieve_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 不是一个纯技术问题,它更像是一个产品问题。什么样的记忆值得存、存多久、怎么用,这些决策需要结合具体业务场景来定。技术方案只是工具,真正决定系统好不好用的,是对业务的理解深度。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/4 11:23:47

第一章:先唠明白,TaoToken 与 Spring AI 到底是个啥?

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/4 11:17:39

ROS2机器人开发实战路线图:从DDS通信到真实部署

1. 这不是“教程搬运”,而是一份ROS2机器人开发的实战路线图你点开这个标题,大概率是刚接触机器人开发,手头可能连一台能跑Linux的旧笔记本都没有,或者刚装完Ubuntu却卡在sudo apt update报错那一步;也可能是学过ROS1&…

作者头像 李华
网站建设 2026/10/4 11:16:33

PIC18LF45K80 + MR25H40CDF嵌入式存储方案:工业数据记录与掉电保护实战

去年做一台工业现场仪表,主控选了 Microchip 的 PIC18LF45K80,产品要求一边在 CAN 网络里跑通信协议,一边把运行数据实时记录到外部存储里,掉电瞬间还得把关键参数抢救下来。一开始我直接用了单片机内部 EEPROM,实测写…

作者头像 李华
网站建设 2026/10/4 11:13:51

M4 MacBook Pro 卡启动选项怎么办?DFU 固件修复完整指南

先说一句大实话:M4 MacBook Pro 卡在启动选项界面,十有八九不是屏幕坏了,也不是硬盘彻底报销,而是底层固件或系统引导文件出了问题。这种时候很多人第一反应是重装系统,但如果你连启动选项都进不去,装系统也…

作者头像 李华