尧图精选

LLM Agent 记忆系统实战:Working Memory、MCP 协议与 Docker 部署

🕒 发布时间:2026/10/1 3:57:09 📁 来源:尧图网络
1. 从“hindsight”这个词说起为什么记忆是 Agent 最被低估的能力“hindsight”这个词本身很有意思字面意思是“事后的洞察力”也就是我们常说的“后见之明”。放在 LLM Agent 的语境里它指向一个非常具体且关键的问题Agent 能不能记住过去发生过的事情并且在后续决策中真正用上这些记忆大多数人搭 Agent 的时候第一反应是接工具、调 API、写 prompt。这些当然重要但真正决定一个 Agent 能不能从“一次性问答机器”进化成“持续工作的助手”的是它的记忆系统。我见过太多项目工具链搭得很漂亮MCP 接了一堆但 Agent 每次对话都像失忆一样从头开始用户体验直接崩掉。结合热搜词里出现的agent memory、working memory、MCP、Docker这些关键词可以判断这个方向的核心诉求是如何给 LLM Agent 构建一套可靠的记忆存储与检索机制并且通过 MCP 协议和容器化部署让它真正跑起来。这篇文章适合谁看如果你正在做 Agent 相关的开发或者你已经在用 MCP 接各种工具但发现 Agent “记不住事”那这篇内容就是写给你的。我会从记忆的本质讲起拆解 working memory 和 long-term memory 的区别然后落到 MCP 协议怎么承载记忆读写最后给出 Docker 环境下的完整部署思路和我自己踩过的坑。注意本文讨论的“记忆”指的是 Agent 在运行过程中对上下文、历史交互、任务状态的存储与调用不涉及任何用户隐私数据的采集或跨境传输问题。2. Agent 记忆的本质不是存下来就完事了2.1 Working Memory 和 Long-term Memory 的分界线在哪很多人一上来就说“我要给 Agent 加记忆”但没想清楚加的是哪种记忆。这里必须先把这个概念理清楚否则后面架构设计一定跑偏。Working memory工作记忆类比人的短期记忆它承载的是当前任务执行过程中需要临时保持的信息。比如 Agent 正在帮你订机票它需要记住你说了出发城市、目的地、时间、舱位偏好这些信息在任务完成之前必须一直可用。Working memory 的典型特征是生命周期短、容量有限、读写频率极高。Long-term memory长期记忆类比人的长期记忆它跨越会话存在。比如 Agent 上次帮你订机票时你说了“我偏好靠窗座位”这次你再让它订票它应该能主动想起这个偏好。长期记忆的特征是持久化存储、容量大、检索需要索引和排序。我自己的经验是大部分 Agent 项目失败不是因为长期记忆没做好而是工作记忆根本没管好。你想想一个 Agent 连当前对话里三轮之前说过的约束都记不住你给它加再多长期记忆也没用因为它在单次任务里就已经开始胡言乱语了。所以正确的顺序是先把 working memory 做扎实再考虑 long-term memory 的持久化和检索。2.2 为什么简单的 context window 堆叠不够用有人会说现在 LLM 的 context window 都到 128K 甚至 1M token 了我把所有历史都塞进去不就行了这个想法在 demo 阶段没问题但一到生产环境就会撞墙。原因有三个第一成本。每次请求都把全部历史塞进去token 消耗是线性增长的。一个跑了 50 轮对话的 Agent每轮都带上全部历史你的 API 账单会教你做人。第二注意力稀释。这是更隐蔽的问题。当 context 里塞了大量无关历史时LLM 对关键信息的注意力会被稀释。你会发现 Agent 开始忽略你在第 3 轮说的关键约束因为它在第 47 轮的时候已经“看不过来”了。第三一致性问题。历史信息之间可能矛盾。你第 5 轮说“预算 5000”第 20 轮说“预算可以放宽到 8000”如果两句话都在 context 里LLM 可能随机选一个。你需要一个机制来标记哪条信息是最新的、有效的。所以记忆系统的核心不是“存”而是选择性地存、有策略地取。这就引出了后面要讲的存储结构和检索策略。2.3 记忆的三个核心维度谁、找什么、能提供什么热搜词里有一条特别有意思“llm的token三个点key我是谁、query我在找什么、value我能提供什么”。这其实是在用 key-value 的思维来理解记忆。我把它展开一下Agent 记忆系统里每一条记忆记录本质上都包含三个维度的信息维度含义示例身份标识Who这条记忆属于谁、在什么场景下产生用户A的偏好、任务B的中间状态检索意图What当前需要什么类型的信息用户偏好、历史决策、工具调用结果内容载荷Value实际存储的信息本体“偏好靠窗座位”、“上次用的是MySQL 8.0”这个三元组看起来简单但实际设计的时候最难的是“检索意图”这一层。因为 Agent 在运行时它自己往往不知道“我现在需要什么记忆”。你不能指望 LLM 每次都精准地说出“我需要检索用户偏好类记忆”。我的做法是在 Agent 的决策循环里显式地插入一个记忆检索步骤。在每次调用 LLM 之前先用当前 query 去记忆库里做一次相似度检索把 top-k 相关记忆注入到 context 里。这样就不依赖 LLM 自己“想起来”要检索记忆。3. MCP 协议在记忆系统里扮演什么角色3.1 MCP 不是硬件协议它是 Agent 和外部能力之间的标准接口热搜词里有人问“mcp 是软件协议 硬件协议那个概念叫什么来着”这里先澄清一下MCPModel Context Protocol是一个软件层的通信协议它定义的是 LLM 应用和外部工具/数据源之间的交互标准。跟硬件协议比如 I2C、SPI 那种完全是两个层面的东西。MCP 的核心价值在于解耦。在没有 MCP 之前你每接一个工具就要写一套适配代码工具换了接口你就得改 Agent 代码。有了 MCP 之后工具方按照协议暴露能力Agent 方按照协议调用能力双方不需要知道对方内部怎么实现的。放到记忆系统里MCP 的意义是记忆存储可以作为一个独立的 MCP Server 存在Agent 通过标准的 MCP 接口来读写记忆而不是把记忆逻辑硬编码在 Agent 里面。3.2 把记忆做成 MCP Server 的架构思路我实际用过的一种架构是这样的Agent (MCP Client) | |-- MCP Protocol (stdio / SSE) | Memory MCP Server | |-- Working Memory (in-memory / Redis) |-- Long-term Memory (vector DB / SQLite) |-- Retrieval Engine (embedding rerank)Agent 在需要存取记忆的时候通过 MCP 协议发送请求。Memory Server 负责实际的存储、索引、检索逻辑。这样做的好处是记忆层可以独立升级你今天用 SQLite 存明天想换向量数据库Agent 侧代码不用动。多个 Agent 可以共享记忆层如果你有多个 Agent 协作它们可以连同一个 Memory Server。调试方便记忆的读写有独立的日志和监控出问题好定位。具体到 MCP 的工具定义Memory Server 通常会暴露这几个 tool{ tools: [ { name: store_memory, description: 存储一条记忆, inputSchema: { type: object, properties: { content: {type: string}, memory_type: {type: string, enum: [working, long_term]}, metadata: {type: object} } } }, { name: retrieve_memory, description: 根据查询检索相关记忆, inputSchema: { type: object, properties: { query: {type: string}, top_k: {type: integer, default: 5}, memory_type: {type: string} } } } ] }这个设计的关键点是memory_type 参数。Agent 在存的时候要明确这是工作记忆还是长期记忆因为两者的存储策略和过期策略完全不同。3.3 MCP 连接方式的选择stdio 还是 SSEMCP 支持多种传输方式最常见的是 stdio 和 SSE。在记忆系统这个场景下我的选择建议是stdio适合本地开发、单机部署。Memory Server 和 Agent 跑在同一台机器上通过标准输入输出通信。优点是简单、延迟低、不需要网络配置。缺点是没法跨机器共享。SSEServer-Sent Events适合分布式部署。Memory Server 作为独立的 HTTP 服务运行多个 Agent 可以通过网络连接。优点是灵活、可扩展。缺点是需要处理网络异常、认证、并发等问题。我自己的项目里开发阶段用 stdio上线之后切到 SSE。切换的时候只需要改 MCP Client 的连接配置Memory Server 本身的逻辑不用动这就是协议标准化的好处。提示如果你用 SSE 方式部署 Memory Server一定要注意并发写入的问题。多个 Agent 同时写同一条记忆的元数据时需要加锁或者用乐观并发控制否则会出现数据覆盖。4. Docker 环境下的记忆服务部署实操4.1 为什么记忆服务值得容器化热搜词里 Docker 相关的词出现频率极高docker安装、docker desktop、docker网络不通、windows安装docker、ubuntu安装docker并运行python环境……这说明大量开发者在这个环节卡过。记忆服务容器化的理由很直接环境一致性向量数据库、embedding 模型、Python 依赖版本这些东西在不同机器上装出来的结果可能不一样。容器化之后开发机和服务器跑的是同一个镜像。资源隔离embedding 计算是吃内存的如果你把它和 Agent 跑在同一个进程里Agent 的响应延迟会被拖累。分开容器部署可以独立限制资源。持久化清晰记忆数据是需要持久化的用 Docker volume 挂载出来容器重建数据不丢。4.2 一个可用的 docker-compose 配置拆解下面是我实际在用的一个配置做了简化但核心结构保留version: 3.8 services: memory-server: build: ./memory-server ports: - 8080:8080 environment: - MEMORY_BACKENDsqlite - EMBEDDING_MODELall-MiniLM-L6-v2 - WORKING_MEMORY_TTL3600 - LONG_TERM_DB_PATH/data/memory.db volumes: - memory-data:/data healthcheck: test: [CMD, curl, -f, http://localhost:8080/health] interval: 30s timeout: 10s retries: 3 redis: image: redis:7-alpine ports: - 6379:6379 volumes: - redis-data:/data command: redis-server --appendonly yes volumes: memory-data: redis-data:几个关键点解释一下MEMORY_BACKENDsqlite是给小型项目用的零配置、单文件、够用。如果你记忆量上到百万级换成 PostgreSQL pgvector 或者专门的向量数据库。WORKING_MEMORY_TTL3600是工作记忆的过期时间单位秒。一小时没有访问的工作记忆自动清理。这个值要根据你的任务时长来调任务平均跑 10 分钟的话TTL 设 1800 就够了。healthcheck这个别省。记忆服务挂了但 Agent 不知道会导致 Agent 一直重试或者静默失败。有了 healthcheck编排层可以自动重启不健康的容器。Redis 单独拆出来是因为工作记忆的读写频率太高用 Redis 做缓存层比直接读写 SQLite 快一个数量级。长期记忆落 SQLite工作记忆走 Redis这个组合在小规模场景下性价比最高。4.3 Docker 网络不通这个坑我踩过三次热搜词里“docker网络不通”这个词让我很有共鸣。记忆服务部署最常见的翻车场景就是Agent 容器连不上 Memory Server 容器。排查链路我总结成这样第一步确认两个容器在同一个 network 里。docker-compose 默认会创建一个 bridge network所有 service 都在里面。但如果你是手动docker run的默认用的是 bridge 网络容器之间只能用 IP 通信不能用服务名。第二步确认端口映射对不对。容器内部的 8080 端口映射到宿主机的 8080这个没问题。但如果你在 Agent 容器里用localhost:8080去连那肯定连不上因为 localhost 指的是 Agent 容器自己。要用服务名memory-server:8080。第三步确认防火墙和 SELinux。在 Linux 上跑的时候宿主机的防火墙规则可能拦截了容器间的流量。SELinux 开启的情况下还需要给 volume 挂载加:z或:Z标签。第四步看 DNS 解析。docker exec -it agent-container nslookup memory-server看看能不能解析出 IP。解析不了就是 network 配置问题。这四步走完99% 的网络问题都能定位。4.4 Windows 上跑 Docker Desktop 的额外注意事项热搜词里有“windows安装docker”和“virtualization support not detected docker desktop failed to start”这个错误太经典了。Windows 上跑 Docker Desktop底层依赖 WSL2 或者 Hyper-V。出现 “virtualization support not detected” 通常是两个原因BIOS 里没开虚拟化Intel VT-x 或 AMD-V 需要在 BIOS 里手动启用。这个跟操作系统无关是硬件层面的开关。Hyper-V 和 WSL2 冲突如果你之前装过 VirtualBox 或者 VMware它们可能占用了 Hyper-V 层。需要在“启用或关闭 Windows 功能”里确认 Hyper-V 和“虚拟机平台”都勾选了。另外Windows 上跑记忆服务有个性能坑文件系统 IO 慢。WSL2 访问 Windows 文件系统的速度远低于访问 Linux 原生文件系统。所以 SQLite 数据库文件一定要放在 WSL2 的文件系统里比如/home/user/data不要放在/mnt/c/...下面。我实测过同样的写入操作放在/mnt/c下比放在 WSL2 原生路径下慢 5 到 10 倍。5. 记忆检索策略存进去容易取出来对才是难点5.1 纯向量检索的三个失效场景很多人做记忆检索第一反应就是 embedding 向量相似度。这个方案在 demo 阶段效果不错但生产环境会遇到几个典型失效场景场景一时间敏感型查询。用户问“我上次说的预算是多少”向量检索可能召回三条不同时间说的预算但它不知道哪条是最新的。这时候需要时间衰减因子越新的记忆权重越高。场景二精确匹配需求。用户问“我的订单号是多少”这是一个精确查找不是语义相似度问题。向量检索可能召回一堆语义相近但订单号不对的记忆。这时候需要关键词索引做兜底。场景三多跳推理。用户问“我上次订的那个航班用的哪张信用卡”这需要先找到航班记录再从航班记录关联到支付记录。纯向量检索做不了这种关联查询需要图结构或者关系型索引。我的做法是混合检索向量检索负责语义召回关键词索引负责精确匹配时间衰减负责排序三者加权融合。具体权重根据你的场景调我一般用 0.6 向量 0.3 关键词 0.1 时间衰减作为起点。5.2 记忆的写入策略什么时候该存什么时候不该存检索策略重要但写入策略更基础。垃圾进垃圾出如果存了一堆无用记忆检索再厉害也白搭。我的写入原则是三条第一显式约束必须存。用户在对话中明确表达的偏好、限制、要求这些必须存。比如“我不要红色”、“预算不超过 5000”、“用 Python 不用 Java”。第二工具调用结果选择性存。不是所有工具返回都要存。查询类工具的结果比如“今天天气怎么样”存了没意义下次问的时候重新查就行。但配置类、状态类的结果要存比如“数据库连接串是 xxx”、“任务 ID 是 xxx”。第三中间推理过程不存。Agent 的思考链chain of thought不需要存。这些是过程性信息对后续决策没有直接价值存了反而污染检索结果。具体实现上我会在 Memory Server 里加一个write filter层用规则 小模型判断一条信息是否值得存储。规则负责硬性过滤比如长度小于 10 个字符的不存小模型负责语义判断比如判断这是偏好还是闲聊。5.3 记忆冲突的处理当新旧信息矛盾时怎么办这是记忆系统里最棘手的问题之一。用户先说“预算 5000”后说“预算改成 8000”两条记忆都在库里检索的时候召回哪条我的处理方案是版本化 失效标记每条记忆有一个valid_from和valid_until字段。新记忆写入时如果检测到与旧记忆冲突同一主体、同一属性、不同值就把旧记忆的valid_until设为当前时间新记忆的valid_from设为当前时间。检索的时候只召回valid_until为空或者大于当前时间的记忆。冲突检测怎么做简单场景用规则同一用户 同一属性 key 不同 value 冲突。复杂场景需要 LLM 判断比如“预算 5000”和“别超过 8000”是不是冲突。这个判断可以异步做不阻塞主流程。注意冲突检测不要做得太激进。有些信息看起来矛盾但实际上不矛盾比如“我喜欢红色”和“我讨厌红色衣服”这是两个不同维度的偏好。误判冲突会导致记忆被错误失效。6. 实测中遇到的几个非典型问题6.1 Embedding 模型的冷启动延迟第一次调用 embedding 模型的时候模型需要加载到内存这个延迟可能达到几秒甚至十几秒。如果你的 Memory Server 是在请求到来时才懒加载模型第一个请求会超时。解决方案是在容器启动时预热模型。在 Dockerfile 的 entrypoint 里加一步启动后先跑一次 dummy embedding把模型加载到内存然后再开始接受请求。这样第一个真实请求的延迟就正常了。6.2 工作记忆的并发读写Agent 可能是多线程的或者你有多个 Agent 实例共享同一个 Memory Server。工作记忆的并发读写如果不加控制会出现脏读和丢失更新。我的做法是工作记忆的写操作走 Redis 的原子操作用HSET而不是先HGET再HSET。读操作直接用HGETALL。如果需要更复杂的原子性用 Redis 的 Lua 脚本或者事务。长期记忆的写入频率低用数据库的事务就够了。但要注意 SQLite 的写锁是库级别的并发写入会串行化。如果写入量大换 PostgreSQL。6.3 记忆检索的延迟预算Agent 的响应时间预算是有限的。如果记忆检索占了 500msLLM 推理占了 2s用户感知到的延迟就是 2.5s。记忆检索的延迟必须控制在总预算的 10% 到 20% 以内。优化手段有几个缓存高频检索同样的 query 在短时间内重复出现直接返回缓存结果。异步预取在 Agent 开始思考的时候就并行发起记忆检索不要等 LLM 说“我需要记忆”才去查。限制 top_k召回 5 条和召回 50 条延迟差好几倍但效果可能差不多。从 5 开始调不够再加。我实测下来在 SQLite 本地 embedding 模型的配置下单次检索延迟可以压到 50ms 以内。如果换成远程 embedding API延迟会到 200-500ms这时候缓存就很重要了。6.4 记忆数据的备份和迁移记忆是 Agent 的核心资产丢了就没了。Docker volume 虽然比容器内存储可靠但也不是绝对安全。我的备份策略是每天定时把 SQLite 文件导出成 JSON 快照存到对象存储。恢复的时候从快照导入。这个方案简单粗暴但有效比搞复杂的数据库复制靠谱。迁移的时候注意 embedding 维度要一致。如果你换了 embedding 模型维度从 384 变成 768旧的向量索引就废了需要全量重新计算。所以选 embedding 模型的时候要慎重尽量选一个稳定的、长期维护的。7. 关于这套方案的一些个人体会我从去年开始在自己的项目里迭代这套记忆系统最大的感受是记忆系统的复杂度不在于技术而在于产品决策。什么叫产品决策就是你要决定 Agent 记住什么、忘记什么、什么时候主动回忆、什么时候假装不知道。这些决策没有标准答案取决于你的用户预期和使用场景。比如一个客服 Agent它应该记住用户的历史问题但不应该在用户没提的时候主动说“你上次也问过这个问题”。而一个个人助理 Agent主动回忆反而是加分项。技术层面MCP 协议确实让记忆服务的集成变得干净了很多。以前我要在每个 Agent 里写一套记忆读写逻辑现在抽成一个 MCP Server所有 Agent 共用。Docker 化之后部署和迁移也省心了。如果你现在正在做 Agent 项目我的建议是先把 working memory 用 Redis 管起来把当前任务的上下文一致性做扎实。这一步的投入产出比最高。长期记忆可以后面再加不急。最后分享一个我调试记忆系统时常用的小技巧在 Memory Server 里加一个/debug/dump接口把当前所有记忆以人类可读的格式输出出来。每次 Agent 行为异常的时候先 dump 一下记忆库看看它到底记住了什么、没记住什么。十次有八次问题就出在记忆的写入或检索环节而不是 LLM 本身。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →