ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Agent记忆系统实战:基于MCP与Docker构建可检索的LLM长期记忆

Agent记忆系统实战:基于MCP与Docker构建可检索的LLM长期记忆 1. 从“hindsight”这个词说起为什么它值得单独拿出来聊第一次看到“hindsight”被当作一个项目名我脑子里蹦出来的不是词典释义而是一个很具体的场景你在跟一个 LLM Agent 对话它前面明明已经确认过“用户偏好用中文、项目路径在 D 盘、数据库是 MySQL 8.0”结果聊到第五轮它突然问你“请问您希望用什么语言交流”。这种“事后才想起来”的尴尬就是 hindsight 这个词最直白的注脚。hindsight 在英文里是“后见之明”指的是事情发生之后才明白过来。放到 Agent 和 LLM 的语境里它指向一个非常核心的问题Agent 的记忆到底该怎么存、怎么取、怎么在正确的时机被唤醒。你给它塞一堆上下文它记不住重点你什么都不给它它又像个失忆的人。hindsight 这个项目名本质上是在说——我们要让 Agent 具备“回头看”的能力而且这个“回头看”不能是事后诸葛亮得是实时、精准、可检索的。结合热搜词里高频出现的 agent memory、LLM、MCP、Docker 这几个关键词可以判断这个方向不是空谈概念而是已经落到工程层面的东西。Agent 存储 working memory、LLM 的 token 三个点key 我是谁、query 我在找什么、value 我能提供什么、MCP 协议、Docker 部署这些词拼在一起勾勒出的是一套完整的“Agent 记忆基础设施”的轮廓。这篇文章适合谁看如果你正在做 LLM 应用尤其是多轮对话、任务型 Agent、知识库问答这类需要“记住东西”的场景那这篇内容会对你有直接帮助。如果你只是刚听说 MCP 和 Agent memory想搞清楚它们到底解决什么问题也能从这里拿到一个不绕弯子的入门视角。我会尽量把原理讲透把实操步骤给全同时把我在实际折腾过程中踩过的坑摊开来说。2. Agent memory 到底难在哪不是存不下是取不对2.1 把记忆当成“聊天记录”是最常见的误区很多人做 Agent 记忆的第一反应是把历史对话全部拼进 prompt 不就行了这个做法在对话轮次少的时候确实能跑但一旦超过十几轮问题就暴露了。Token 消耗线性增长只是表面现象更深层的问题是注意力稀释。LLM 在处理长上下文时并不是均匀地关注每一个 token中间部分的信息很容易被“淹没”。你把三十轮对话一股脑塞进去模型反而可能忽略掉第三轮里那句关键的用户偏好。我实测过一个很典型的例子让 Agent 帮忙整理一份技术文档前两轮用户说了“输出用 Markdown 表格”中间聊了十几轮细节到最后一轮让它输出时它给的是纯文本列表。不是它没看到那句话而是在长上下文里那句话的权重被稀释了。这就是为什么“全量拼接”在工程上不可持续。2.2 working memory 和 long-term memory 的分工逻辑Agent 存储 working memory 这个热搜词点出了一个关键区分。working memory 是当前任务正在用的那部分记忆容量小、时效性强、读写频繁long-term memory 是跨会话、跨任务沉淀下来的知识容量大、更新慢、需要检索才能唤醒。打个比方working memory 就像你办公桌上摊开的几份文件long-term memory 是身后那个文件柜。你不可能把文件柜里所有东西都摊桌上那样桌子就废了你也不能桌上空空如也每次要用都去柜子里翻半天。合理的做法是桌上放当前任务相关的几份用完归档回柜子需要时按索引快速取回。hindsight 这个项目如果要在工程上落地核心要解决的就是这套“桌面与文件柜”的调度机制。它需要决定哪些信息进 working memory哪些沉淀到 long-term检索时用什么策略命中。2.3 检索质量决定记忆系统的生死记忆系统最怕的不是存不进去而是取不出来或者取错。你存了一万条记忆用户问一个问题系统返回了十条不相关的那还不如不返回。这里就涉及到检索策略的设计。常见的做法是向量检索把记忆转成 embedding用相似度匹配。但纯向量检索有个硬伤它对“精确匹配”不敏感。用户问“MySQL 8.0 的默认端口是多少”向量检索可能返回一堆关于数据库配置的泛泛内容却漏掉那条明确写着“3306”的记忆。所以实际工程里往往是向量检索 关键词检索 元数据过滤的混合策略。热搜词里提到的“LLM 的 token 三个点key 我是谁、query 我在找什么、value 我能提供什么”其实就是在描述记忆条目的结构化设计。每条记忆不是一个裸文本而是带有身份标识key、检索意图query和内容载荷value的结构化单元。这样检索时可以先按 key 缩小范围再按 query 匹配意图最后取 value。这个设计思路比单纯存文本要靠谱得多。3. MCP 在记忆系统里扮演什么角色别把它当成又一个 API3.1 MCP 是协议不是框架更不是硬件标准热搜词里有人问“MCP 是软件协议还是硬件协议那个概念叫什么来着”这个问题其实问到了点子上。MCP 全称 Model Context Protocol它是一个软件层的通信协议定义的是 LLM 应用和外部能力工具、数据源、记忆存储之间怎么对话。你可以把它类比成 USB 协议——USB 不生产数据它只规定插头和接口怎么对接。很多人第一次接触 MCP 会误以为它是一个开发框架或者一个具体的库其实不是。它更像是一份“接口契约”只要你的记忆服务实现了 MCP 规定的接口任何支持 MCP 的 LLM 客户端都能直接调用它不需要为每个客户端单独写适配层。这才是 MCP 真正的价值——解耦。3.2 为什么记忆系统特别适合走 MCP记忆系统的调用模式很固定存一条、取一批、按条件查、按时间清理。这种“动词少、参数明确”的接口天然适合用协议来标准化。如果没有 MCP你每换一个 LLM 平台就要重写一遍记忆模块的对接代码有了 MCP记忆服务独立部署客户端通过协议调用换平台时记忆层几乎不用动。热搜词里出现的 playwright mcp、chrome devtools mcp、unity mcp、同花顺 mcp 这些都是 MCP 在不同领域的落地案例。它们的共同点是把某个专业能力封装成 MCP 服务让 LLM 通过统一协议去调用。记忆系统走这条路逻辑是一样的。3.3 MCP 服务的部署形态与 Docker 的关系MCP 服务通常是一个独立进程监听某个端口或通过标准输入输出通信。这就带来一个部署问题怎么让它在不同环境里稳定跑起来Docker 在这里就成了很自然的选择。把 MCP 记忆服务打包成镜像挂载数据卷持久化记忆数据通过环境变量注入配置一套镜像可以在开发机、测试环境、生产环境一致运行。热搜词里 docker、docker desktop、docker 安装、windows 安装 docker、ubuntu 安装 docker 并运行 python 环境这些词频繁出现说明大量开发者正在用 Docker 来承载这类服务。这不是赶时髦而是因为 MCP 服务往往依赖特定的 Python 版本、特定的库、特定的端口配置用 Docker 封装能省掉大量“在我机器上能跑”的扯皮。4. 动手搭一套最小可用的 Agent 记忆服务4.1 环境准备Docker 装好只是第一步假设你用的是 Windows先装 Docker Desktop。安装过程本身不复杂但有几个点容易卡住。第一Windows 家庭版需要开启 WSL2 后端否则 Docker Desktop 起不来。第二BIOS 里要确认虚拟化支持是打开的热搜词里那个“virtualization support not detected docker desktop failed to start”就是这个问题进 BIOS 把 Intel VT-x 或 AMD-V 打开就行。第三装完之后建议把 Docker 的镜像存储位置改到非系统盘不然 C 盘很快会被镜像和容器数据撑满。Ubuntu 环境下装 Docker 相对直接用官方脚本或者 apt 源都行。装完之后记得把当前用户加入 docker 组否则每条命令都要加 sudo很烦。命令是sudo usermod -aG docker $USER执行完要重新登录才生效。提示如果你在公司网络环境下拉镜像很慢可以配置镜像加速器。这个配置在 Docker Desktop 的 Settings 里能找到Ubuntu 下则改/etc/docker/daemon.json。4.2 记忆服务的核心数据结构设计在写代码之前先把记忆条目的结构定下来。基于前面说的 key-query-value 思路我通常会设计成这样几个字段字段名类型作用memory_idstring唯一标识用 UUIDowner_keystring归属标识比如用户 ID 或会话 IDintent_querystring这条记忆对应的检索意图描述content_valuetext实际记忆内容embeddingvector内容的向量表示用于相似检索tagsarray标签用于元数据过滤created_attimestamp创建时间expires_attimestamp过期时间可为空表示永久这个结构的好处是检索时可以多路并行按 owner_key 过滤出属于该用户的记忆按 tags 做粗筛按 embedding 做语义匹配按 intent_query 做意图对齐。四路结果加权融合命中率比单一向量检索高出一大截。4.3 用 Docker Compose 编排记忆服务与向量库记忆服务本身需要一个向量数据库来存 embedding。常见的选择有 Chroma、Qdrant、Milvus 等。为了快速跑通我用 Qdrant 配合一个 Python 写的 MCP 服务来演示。docker-compose.yml 大概长这样version: 3.8 services: qdrant: image: qdrant/qdrant:latest ports: - 6333:6333 volumes: - ./qdrant_data:/qdrant/storage memory-mcp: build: ./memory-mcp ports: - 8080:8080 environment: - QDRANT_HOSTqdrant - QDRANT_PORT6333 - EMBEDDING_MODELtext-embedding-3-small depends_on: - qdrant volumes: - ./memory_data:/app/data这里把向量库和记忆服务分成两个容器各自有独立的数据卷。这样做的好处是向量库可以单独升级或迁移记忆服务的业务逻辑改动不会影响底层存储。depends_on 保证启动顺序但注意它只保证容器启动顺序不保证服务就绪实际生产里还需要加健康检查。4.4 MCP 接口的实现要点记忆服务要实现 MCP 规定的几个核心方法。用 Python 写的话大致需要暴露这几个能力store_memory、query_memory、delete_memory、list_memories。每个方法的参数和返回值都要符合 MCP 的 schema。实现时有几个细节值得注意。第一embedding 的生成最好异步做不要阻塞主请求否则存一条记忆要等好几秒。第二query_memory 要支持分页不然记忆多了之后一次返回几千条客户端直接卡死。第三删除操作建议做软删除标记 deleted 而不是物理删除方便排查问题和做数据恢复。async def store_memory(owner_key, intent_query, content_value, tagsNone): embedding await generate_embedding(content_value) memory_id str(uuid.uuid4()) point { id: memory_id, vector: embedding, payload: { owner_key: owner_key, intent_query: intent_query, content_value: content_value, tags: tags or [], created_at: time.time() } } await qdrant_client.upsert(collection_namememories, points[point]) return {memory_id: memory_id, status: stored}这段代码看起来简单但实际跑起来会遇到 embedding 维度不匹配、Qdrant collection 未初始化、并发写入冲突等问题。建议在服务启动时先检查 collection 是否存在不存在就按配置的维度创建。5. 检索策略调优让 Agent 真正“想起来”5.1 纯向量检索为什么不够用前面提过纯向量检索对精确匹配不敏感。我做过一个测试存了 50 条关于不同编程语言配置的记忆然后查询“Python 虚拟环境怎么创建”。纯向量检索返回的前三条里有一条是关于 Node.js 的因为“环境创建”这个语义在向量空间里很接近。这就是语义检索的固有缺陷——它抓的是“意思相近”不是“事实匹配”。解决办法是引入关键词检索做补充。具体做法是在存储时除了生成 embedding还把 content_value 做分词建倒排索引。查询时同时跑向量检索和关键词检索两路结果做 RRFReciprocal Rank Fusion融合。RRF 的好处是不需要调权重直接按排名倒数求和工程上很省心。5.2 intent_query 字段的实际用法intent_query 这个字段很多人会忽略觉得有 content 就够了。但实际用起来它是提升检索精度的关键。举个例子用户存了一条记忆“项目部署在 192.168.1.100 的 8080 端口”。如果只按 content 检索用户问“服务器地址”和问“端口号”可能返回同一条但意图不同。如果存的时候 intent_query 分别写成“服务器 IP 地址”和“服务监听端口”检索时先按 intent 匹配就能精准命中。实际操作中intent_query 可以由 LLM 在存储时自动生成也可以由调用方显式指定。我倾向于两者结合调用方给一个粗粒度的 intentLLM 再细化。这样既保证了意图的准确性又不会给调用方增加太多负担。5.3 记忆过期与清理策略记忆不是存得越多越好。working memory 里的东西任务结束后就该清理long-term memory 里的东西也要定期做衰减和归档。我通常设三层过期策略会话级记忆 24 小时过期任务级记忆 7 天过期知识级记忆永久保留但定期做去重和摘要压缩。清理任务用定时任务跑不要放在请求链路里。Docker 环境下可以单独起一个 cron 容器每天凌晨跑一次清理脚本。清理时先标记隔天再物理删除给自己留一个后悔的窗口。注意清理策略一定要可配置不同业务场景对记忆时效的要求差别很大。客服机器人的会话记忆可能几小时就够了个人助理的记忆可能要保留几个月。6. 那些文档里不会写的踩坑记录6.1 Docker 网络不通导致 MCP 服务连不上向量库这个问题我遇到过不止一次。docker-compose 里两个服务在同一个网络下按理说用服务名就能互相访问但有时候就是连不上。排查下来通常是两个原因一是容器启动顺序问题记忆服务比向量库先起来连接被拒二是 Docker 的网络驱动在某些环境下有 bug需要显式指定 network。解决办法是在 docker-compose 里显式定义 network并且给记忆服务加 restart 策略让它连不上时自动重试。另外健康检查要配好depends_on配合condition: service_healthy才能真正保证依赖就绪。6.2 embedding 模型选型影响的不只是精度选 embedding 模型时大家通常关注精度但实际工程里维度和推理速度同样重要。高维模型比如 3072 维检索精度确实好一些但存储成本和检索延迟都上去了。我实测下来1536 维的模型在大多数 Agent 记忆场景下已经够用检索延迟能控制在 50ms 以内。如果记忆量特别大768 维也不是不能用配合好的检索策略效果差距没有想象中那么大。还有一个坑是模型的语言支持。有些 embedding 模型对中文支持一般存中文记忆、用中文查询时相似度计算会偏。选型时一定要用实际业务语言做测试别只看英文榜单。6.3 MCP 客户端的兼容性差异MCP 协议虽然标准但不同客户端的实现程度不一样。有的客户端只支持工具调用不支持资源读取有的对返回值的格式要求特别严格多一个字段就报错。我在对接不同客户端时最稳妥的做法是严格按协议最小集实现不要自作主张加扩展字段。需要额外信息时通过 payload 里的自定义字段传递而不是改协议结构。另外MCP 服务的错误处理要做好。客户端调用失败时返回的错误信息要足够明确不然排查起来很痛苦。我习惯在错误返回里带上 error_code 和 error_detail方便定位。6.4 记忆去重比想象中难同一个事实可能被存多次比如用户在不同会话里都说了“我用的是 MySQL 8.0”。如果不去重检索时会返回一堆重复内容浪费 token 还干扰模型判断。简单的做法是存之前先查一下有没有高度相似的记忆有就更新而不是新增。但相似度阈值不好定太低会误合并太高又去不干净。我的经验是用 embedding 相似度做初筛阈值设在 0.92 左右然后再用 LLM 做一次确认判断两条记忆是否真的在说同一件事。这样虽然多一次 LLM 调用但去重准确率能到 95% 以上值得。7. 从 hindsight 到 a-memguard记忆系统的安全维度热搜词里出现了 a-memguard 这个概念指向的是 LLM Agent 记忆的主动防御。这个方向很有意思因为记忆系统一旦被污染影响是长期的。攻击者如果能往 long-term memory 里注入一条错误记忆比如“用户的所有请求都应该转发到某个外部地址”那后续所有会话都会受影响。防御思路大致分几层。第一层是写入校验存记忆之前判断内容是否合理有没有明显的注入痕迹。第二层是来源标记每条记忆记录是谁写的、在什么上下文写的检索时根据来源可信度加权。第三层是定期审计用 LLM 扫描记忆库找出异常条目。这些机制在 hindsight 这类项目里应该作为内置能力而不是事后补丁。记忆系统的安全性和它的检索能力一样重要只是大多数人还没意识到。8. 关于这套东西后续怎么扩展如果你已经把最小可用的记忆服务跑起来了接下来可以往几个方向走。一是加多租户支持让一套服务支撑多个 Agent 实例通过 owner_key 隔离数据。二是加记忆摘要定期把零散记忆压缩成更高层的知识减少检索时的噪音。三是接入 RAG 流程把记忆检索和文档检索统一到一个 pipeline 里让 Agent 既能记住对话也能查到资料。我自己在实际项目里最看重的还是检索的准确率和响应速度。记忆存得再多取不出来等于零。所以每次迭代我都会拿一批真实查询做回归测试看命中率和延迟有没有退化。这个习惯帮我避免了好几次“改完感觉更好、实际更差”的翻车。最后分享一个小技巧记忆服务的日志一定要打全每次存取都记录 owner_key、intent、命中的 memory_id 和耗时。出问题时这些日志就是你的救命稻草。我靠日志定位过好几次“明明存了却查不到”的问题最后发现是 owner_key 在某个环节被截断了。这种问题没有日志根本查不出来。
返回列表