ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

LLM Agent 记忆机制实战:基于 MCP 与 Docker 的 hindsight 落地指南

LLM Agent 记忆机制实战:基于 MCP 与 Docker 的 hindsight 落地指南 1. 从“hindsight”这个词说起为什么它值得单独拿出来聊“hindsight”这个词本身的意思是“事后之明”——事情发生之后回头看才明白当时应该怎么做。把这个词放到 LLM Agent 的技术语境里它指向的东西就非常具体了Agent 在完成任务之后如何把这次经历沉淀下来让下一次遇到类似情况时不再从零开始。这件事听起来像是“给 Agent 加个记忆库”这么简单但真正动过手的人都知道Agent 记忆这块的坑远比想象中多。你给它存了一堆对话历史它检索的时候把无关的旧内容也捞出来反而干扰了当前推理你给它做了摘要压缩结果关键的工具调用参数被压没了你换了向量库、调了 top-k发现召回质量忽好忽坏根本找不到稳定的调参规律。热搜词里同时出现了 hindsight、agent memory、LLM、MCP、Docker 这几个词说明关注这个方向的人大概率正在做或者准备做这么一件事给基于 LLM 的 Agent 搭建一套可落地的记忆机制并且用 MCP 协议和 Docker 把整套东西跑起来。这篇文章就围绕这条线展开把 hindsight 这个方向下 Agent 记忆的设计思路、MCP 的接入方式、Docker 环境的搭建细节以及实际跑起来之后会遇到的问题尽量讲透。适合谁看如果你已经在用 LLM 做 Agent 类项目或者正在研究 Agent 的长期记忆方案又或者你只是听说过 MCP 但还没真正接过一个 MCP Server那这篇内容应该能帮你省掉不少自己摸索的时间。我会尽量把“为什么这么设计”讲清楚而不是只丢一堆配置让你抄。2. Agent 记忆到底难在哪不是存不下是取不准2.1 记忆的三个层次与 hindsight 的定位在动手之前先把 Agent 记忆这件事拆开看。业界比较通用的分法是把 Agent 记忆分成三层工作记忆working memory当前这一轮任务里的上下文包括用户输入、中间推理、工具调用结果。它活在当前会话的 context window 里会话结束就没了。短期记忆short-term memory跨几轮对话的历史通常用对话摘要或者滑动窗口来维护目的是让 Agent 记得“刚才聊到哪了”。长期记忆long-term memory跨会话、跨任务的沉淀包括用户偏好、领域知识、成功/失败的经验。这一层才是 hindsight 真正要解决的问题。hindsight 的核心价值在于第三层。它要回答的问题是一次任务执行完之后哪些信息值得留下来留下来之后以什么形式存储下次遇到相似场景时怎么准确地把它捞出来这三个问题里第一个和第三个是最难的。存什么决定了记忆库的质量怎么取决定了记忆库能不能真正被用上。很多项目失败不是因为存储方案不行而是因为存了一堆噪音取的时候又取不准。2.2 为什么“全存下来”是最糟糕的策略我见过不少人的第一版实现是这样的把每一轮对话的完整历史都塞进向量库检索的时候按相似度取 top-5 拼进 prompt。跑 demo 的时候看起来没问题一旦对话轮次多了问题立刻暴露第一检索噪音。用户三个月前问过一句“今天天气怎么样”和当前“帮我分析这份财报”的任务在向量空间里可能因为某些通用词而相似度不低结果被捞出来塞进 prompt白白占用 token 还干扰推理。第二信息过时。用户上个月说“我住在北京”这个月搬到了上海如果两条记忆都被检索出来Agent 到底信哪条没有时间衰减和冲突消解机制记忆越多反而越乱。第三成本失控。全量存储意味着 embedding 调用量、向量库存储量、检索计算量都随对话量线性增长长期跑下来成本很难看。所以 hindsight 这个方向真正要做的不是“存”而是筛选和结构化。一次任务结束后Agent 应该做一次“事后复盘”判断这次经历里哪些是通用经验、哪些是任务特定细节、哪些是一次性信息可以直接丢弃。这个筛选过程本身就可以用 LLM 来做——让模型自己总结“这次任务我学到了什么”比机械地存原始对话质量高得多。2.3 记忆写入的时机选择任务结束 vs 实时写入这里有个设计决策值得单独说记忆是在任务执行过程中实时写入还是等任务结束后统一写入实时写入的好处是信息不丢失坏处是任务还没结束Agent 自己都不知道这次会不会成功写进去的可能是错误的中间状态。比如 Agent 尝试了一个方案失败了实时写入的话这条失败经验会被存下来但它其实只是探索过程的一部分不是最终结论。任务结束后统一写入的好处是Agent 已经知道最终结果了可以带着“ hindsight ”的视角去总结哪些步骤是关键的、哪些是弯路、最终成功的方案是什么。这样写进去的记忆质量明显更高。我的建议是混合策略关键的工具调用结果和用户明确表达的偏好可以实时写入这类信息不会因为任务成败而改变而任务级的经验总结放到任务结束后统一做。这样既不会丢失关键信息又保证了经验记忆的质量。3. 用 MCP 把记忆能力做成一个独立服务3.1 MCP 解决的到底是什么问题MCPModel Context Protocol这两年被讨论得很多但很多人第一次接触的时候会困惑它和普通的 API 调用有什么区别用一句话概括MCP 是一套让 LLM 应用以标准化方式接入外部能力的协议。在 MCP 出现之前你要给 Agent 加一个“查数据库”的能力得自己写工具定义、自己处理参数解析、自己管理调用生命周期。每换一个 LLM 框架这套东西可能就要重写一遍。MCP 把这些抽象成了标准协议工具怎么描述、参数怎么传、结果怎么返回都有统一的格式。把 Agent 记忆做成一个 MCP Server好处非常直接解耦记忆逻辑独立于 Agent 主程序换 Agent 框架不用重写记忆模块。复用同一个记忆 Server 可以被多个 Agent 客户端连接。可测试记忆的写入、检索、更新可以单独测试不用把整个 Agent 跑起来。热搜词里出现了mcp server、mcp教程、playwright mcp、blender mcp这些词说明 MCP 的生态正在快速铺开。记忆服务作为 Agent 的基础设施做成 MCP Server 是很自然的选择。3.2 记忆 MCP Server 的工具设计一个记忆 MCP Server 应该暴露哪些工具我的实践经验是至少这四个工具名作用关键参数memory_write写入一条记忆content、memory_type、tags、importancememory_search语义检索记忆query、top_k、memory_type_filtermemory_update更新已有记忆memory_id、new_content、reasonmemory_forget删除或标记失效记忆memory_id、reason这里有几个设计细节值得展开。memory_type 字段很重要。它把记忆分成几类比如user_preference用户偏好、task_experience任务经验、domain_knowledge领域知识、ephemeral临时信息。检索的时候可以按类型过滤避免用户偏好和任务经验混在一起被检索出来。importance 字段用于排序和淘汰。不是所有记忆都同等重要。用户说“我以后都用中文回复我”这种偏好importance 应该给高分而“这次任务用了 pandas 读取 CSV”这种细节importance 可以低一些。当记忆库容量接近上限时优先淘汰低 importance 且长期未被检索的记忆。memory_update 而不是直接覆盖。记忆是会变的用户偏好会变领域知识会更新。直接覆盖会丢失历史而保留更新记录可以让 Agent 在需要时回溯“为什么这条记忆变成了现在这样”。3.3 检索策略向量检索不够要加规则过滤纯向量检索在记忆场景下是不够用的。原因前面说过语义相似不等于当前有用。我的做法是在向量检索外面套一层规则过滤时间衰减每条记忆带一个时间戳检索时按相似度 × 时间衰减因子排序。衰减因子可以用指数衰减半衰期根据记忆类型调整——用户偏好的半衰期长一些比如 90 天任务经验的半衰期短一些比如 14 天。类型过滤当前任务如果是“写代码”就优先检索task_experience和domain_knowledge类型的记忆user_preference类型只在需要个性化输出时才检索。冲突消解如果检索出两条内容矛盾的同类型记忆取时间更新的那条同时把旧的那条标记为superseded。这套组合策略实测下来比纯向量检索的召回质量稳定很多。调参的时候重点调时间衰减的半衰期和类型过滤的优先级这两个参数对结果影响最大。4. Docker 环境搭建把记忆服务跑起来4.1 为什么用 Docker 而不是直接跑记忆服务涉及向量数据库、embedding 服务、MCP Server 三个组件直接在本机跑的话依赖冲突、端口占用、版本不一致这些问题会消耗大量时间。用 Docker 编排的好处是环境隔离和可复现——今天跑通的配置明天换台机器照样能跑。热搜词里有docker安装、docker desktop、docker安装教程、windows安装docker、linux安装docker这些说明不少人在环境搭建这一步就卡住了。我把自己踩过的坑整理一下。4.2 组件编排与端口规划一套典型的记忆服务 Docker 编排包含这些组件services: vector-db: image: qdrant/qdrant:latest ports: - 6333:6333 volumes: - ./data/qdrant:/qdrant/storage embedding: image: ghcr.io/huggingface/text-embeddings-inference:latest ports: - 8080:80 command: --model-id BAAI/bge-small-zh-v1.5 memory-mcp: build: ./memory-mcp ports: - 3000:3000 environment: - VECTOR_DB_URLhttp://vector-db:6333 - EMBEDDING_URLhttp://embedding:80 depends_on: - vector-db - embedding端口规划上向量库用 6333embedding 服务用 8080MCP Server 用 3000互不冲突。注意memory-mcp里访问其他服务用的是 Docker 内部网络的主机名vector-db、embedding不是localhost。这是新手最容易搞错的地方——在容器里写localhost:6333是访问容器自己不是访问向量库容器。4.3 Windows 下 Docker Desktop 的常见启动问题Windows 用户装 Docker Desktop 最常遇到两个问题。第一个是虚拟化没开。报错信息通常是virtualization support not detected或者Docker Desktop failed to start because virtualization is not enabled。解决办法是进 BIOS 打开虚拟化支持Intel 平台叫 VT-xAMD 平台叫 SVM然后在 Windows 功能里确认“虚拟机平台”和“适用于 Linux 的 Windows 子系统”都勾上了。改完要重启不重启不生效。第二个是 WSL2 后端的内存占用。Docker Desktop 默认用 WSL2 后端跑向量库和 embedding 服务的时候内存吃得比较凶。可以在用户目录下建一个.wslconfig文件限制内存[wsl2] memory8GB processors4 swap2GB这个配置对跑记忆服务来说够用了。如果你的机器内存小于 16GB建议把 embedding 服务换成更小的模型或者直接用外部 API 做 embedding省掉本地这个容器。4.4 容器间网络不通的排查链路docker网络不通是热搜里出现的问题我把自己遇到过的排查顺序列一下先确认容器都在同一网络里。docker network inspect network_name看容器列表如果不在同一个网络用docker network connect接进去。确认服务监听的是 0.0.0.0 而不是 127.0.0.1。有些服务默认只监听本地回环容器间就访问不到。检查服务启动配置里的 host 参数。确认端口映射和容器内端口一致。ports: 6333:6333左边是宿主机端口右边是容器内端口写反了宿主机就连不上。用docker exec进容器手动 curl 一下。比如docker exec -it memory-mcp curl http://vector-db:6333/healthz能通说明网络没问题问题在应用层。这个排查顺序从网络层到应用层逐层缩小范围比盲目重启容器高效得多。5. 记忆写入与检索的实操细节5.1 一次任务结束后到底该总结什么任务结束后让 LLM 做总结prompt 的设计直接决定记忆质量。我试过很多版本最后稳定下来的模板大概是这样的你是一个 Agent 记忆整理助手。请根据以下任务执行记录提取值得长期保留的记忆。 任务目标{goal} 执行过程{trace} 最终结果{result} 请按以下格式输出 1. 任务经验这次任务中可复用的方法或教训 2. 用户偏好用户在这次交互中表现出的偏好 3. 领域知识这次任务涉及的、下次可能用到的知识 4. 可丢弃信息不需要保留的临时细节 每条记忆请标注重要程度高/中/低。这个模板的关键在于强制分类。如果不分类模型倾向于把所有东西混在一起总结成一段话存进去之后检索时粒度太粗用不上。分类之后每类记忆单独存储、单独检索精度高很多。5.2 检索结果怎么拼进 prompt检索出记忆之后怎么把它拼进当前任务的 prompt也有讲究。我的做法是分区块拼接[用户偏好] - 用户偏好用中文回复重要度高 - 用户习惯用 pandas 处理表格数据重要度中 [相关任务经验] - 上次处理类似财报分析时先用 pandas 做数据清洗再分析效果较好重要度中 [领域知识] - 该公司上一季度营收同比增长 12%重要度低可能过时分区块的好处是模型能清楚知道每条记忆的性质用户偏好用来调整输出风格任务经验用来指导执行策略领域知识用来补充事实。混在一起的话模型可能会把用户偏好当成事实来用那就出问题了。另外记忆区块要放在 system prompt 里不要放在 user message 里。放 system prompt 里模型会把它当成背景知识放 user message 里模型可能误以为这是当前任务的一部分。5.3 记忆冲突的处理实例举个实际遇到的例子。用户第一次交互时说“我不用 TypeScript用 JavaScript”存了一条user_preference记忆。两周后用户说“这个项目用 TypeScript 写吧”又存了一条。如果不处理冲突下次检索时两条都会被捞出来Agent 就不知道该用哪个。我的处理逻辑是写入新记忆时先检索同类型、同主题的旧记忆如果发现冲突把旧记忆标记为superseded新记忆的supersedes字段指向旧记忆 ID。检索时默认只返回未被 superseded 的记忆需要历史回溯时才把整条链拉出来。这个机制实现起来不复杂但对记忆库的长期可用性影响很大。没有冲突消解的记忆库用不了多久就会变成一团乱麻。6. 实测中遇到的几个坑与应对6.1 embedding 模型选型对检索质量的影响我一开始用的是某个通用英文 embedding 模型结果中文记忆的检索质量很差。换成中文优化的模型比如 BGE 系列的中文版之后召回准确率明显提升。这个坑的教训是embedding 模型要和记忆内容的语言匹配如果你的 Agent 主要处理中文就别用英文为主的模型。另一个细节是 embedding 维度。维度高的模型表达能力强但存储和检索成本也高。中小规模记忆库几万条以内用 512 维或 768 维就够了没必要上 1536 维。6.2 记忆库膨胀的速度远超预期实测下来一个活跃的 Agent 每天产生的记忆条数在几十到几百条之间取决于任务复杂度。如果不做淘汰一个月就是几千条一年就是几万条。虽然向量库能扛住这个量级但检索质量会随着噪音增加而下降。我的做法是加一个定期清理任务每周跑一次把 importance 为低、且过去 30 天未被检索过的记忆归档不是删除移到冷存储。这样热记忆库保持精简检索质量稳定。6.3 MCP 连接超时与重试MCP Server 和 Agent 客户端之间的连接偶尔会超时尤其是在记忆检索涉及向量库查询的时候。如果向量库响应慢MCP 调用就会卡住。我的处理是在 MCP Server 内部加超时和降级检索超过 2 秒就返回空结果让 Agent 先继续执行不要因为记忆检索卡住整个任务。这个降级策略很重要。记忆是增强能力不是核心链路不能让它成为单点故障。7. 关于 hindsight 这个方向的一些个人判断做了几个月的 Agent 记忆之后我越来越觉得这个方向的核心难点不在技术实现而在记忆的价值判断。存什么、不存什么、什么时候该忘掉这些决策目前主要靠 LLM 的总结能力和人工设计的规则还没有特别成熟的自动化方案。热搜里出现的a-memguard: a proactive defense framework for llm-based agent memory这个词挺有意思说明已经有人在做记忆的安全防护了。记忆库如果被污染Agent 的行为会被持续影响这个风险确实值得重视。我自己的做法是在写入记忆前加一层校验过滤掉明显异常的内容比如超长文本、包含可疑指令的文本虽然简单但能挡掉大部分低级污染。如果你正准备给自己的 Agent 加记忆能力我的建议是先从最小可用版本开始一个 MCP Server、一个向量库、一套简单的写入和检索逻辑先跑起来看效果再逐步加时间衰减、冲突消解、类型过滤这些机制。一上来就设计一套复杂的记忆架构大概率会在调试上耗掉所有精力最后连基本功能都没跑通。记忆这件事本质上是在给 Agent 积累“经验”。经验的价值不在于多而在于准。一条准确的记忆胜过一百条模糊的记录。这个判断我在实际项目里验证过很多次希望对你也有参考价值。
RELATED READING

延伸阅读

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