
1. 从“hindsight”说起为什么记忆是 Agent 落地的最后一公里第一次看到 “hindsight” 这个词是在给一个客服 Agent 做复盘的时候。用户投诉说同一个问题上周已经反馈过这周换了个客服 Agent 接待结果对方像完全没听过一样从头再问一遍。技术团队排查了半天发现不是模型不行也不是提示词写得烂而是这个 Agent 压根没有“记忆”——它每次对话都是白纸一张上一秒聊完下一秒就忘。这就是 hindsight 这个词戳中我的地方。它的字面意思是“后见之明”也就是事后回头看才明白的那件事。放到 Agent 语境里它其实指向一个非常具体的能力Agent 能不能记住过去发生过什么并且在需要的时候把这段记忆调出来用。没有这个能力Agent 永远只能做“一次性问答”做不了真正的长期助手。围绕 hindsight 这个核心最近一年冒出来一堆相关概念agent memory、LLM、MCP、Docker。这几个词不是随便凑在一起的它们其实构成了一个完整的落地链条。LLM 是大脑负责理解和生成agent memory 是海马体负责存储和检索MCP 是神经接口负责让大脑和外部工具、数据源对话Docker 是培养皿负责把这一整套东西稳定地跑起来。少了任何一环Agent 都只能停留在 demo 阶段。我写这篇东西不是要讲什么高深理论而是想把过去大半年在 Agent 记忆系统上踩过的坑、试过的方案、最后跑通的架构原原本本讲一遍。适合谁看如果你正在做 Agent 产品或者打算给自己的 LLM 应用加上“记住用户”的能力又或者你只是好奇 MCP 和 Docker 到底怎么配合起来用那这篇应该能帮你省下不少试错时间。我会从设计思路讲到具体实现从参数选择讲到故障排查尽量做到你照着抄就能跑起来。2. Agent Memory 的整体设计思路为什么不能只靠上下文窗口2.1 上下文窗口不是记忆它只是草稿纸很多人第一次做 Agent 记忆第一反应是“我把历史对话全塞进 prompt 不就行了”。我一开始也这么干过结果很快就撞墙了。上下文窗口再大它也是有限的而且成本随长度线性上涨。更关键的是上下文窗口里的内容是“平铺”的模型要在几千上万 token 里自己找相关信息效果非常不稳定。你问它“我上次说的那个地址”它可能翻到三段无关的闲聊就是找不到那句关键信息。所以上下文窗口的本质是工作记忆working memory相当于你桌面上摊开的草稿纸写满了当前任务相关的东西。而真正的**长期记忆long-term memory**必须外置存到数据库或者文件系统里需要的时候再按需检索回来。这个区分非常重要它决定了你整个架构是“堆 prompt”还是“建系统”。我后来总结了一个简单的判断标准如果一段信息在下一轮对话里大概率用不到但它未来某天可能有用那它就不该留在上下文里而应该写进外部记忆。比如用户的偏好、历史订单、上次投诉的原因这些都属于长期记忆。而当前这轮对话的具体问题、刚查到的数据属于工作记忆留在上下文里就行。2.2 记忆的三个核心动作写入、检索、遗忘一个能用的 Agent 记忆系统必须处理好三个动作。第一个是写入也就是什么时候把什么信息存下来。这里最容易犯的错是“什么都存”结果记忆库变成垃圾场检索的时候全是噪音。我的做法是让 LLM 自己判断在每轮对话结束时让它输出一个结构化结果标明这轮有没有值得长期记住的信息有的话是什么类型、什么内容。第二个是检索也就是在需要的时候把相关记忆找回来。这里的关键是“相关”不是“全部”。我试过最简单的做法是把所有记忆按时间倒序塞回去效果很差因为最近的未必是最相关的。后来改成基于向量相似度的检索效果好很多但也带来了新问题向量检索对“精确匹配”不敏感比如用户问“我的订单号 12345”向量检索可能返回一堆语义相近但订单号不对的记忆。所以我现在用的是混合检索向量相似度加关键词匹配两路结果合并排序。第三个是遗忘这个最容易被忽略但恰恰是让记忆系统长期可用的关键。记忆不是越多越好过期的、矛盾的、低价值的信息必须清理掉。我见过一个 Agent 因为记住了用户三个月前随口说的一句“我最近在减肥”结果三个月后还在推荐低卡食谱用户早就忘了这回事体验非常割裂。遗忘策略可以是基于时间的超过 N 天降权也可以是基于冲突的新记忆覆盖旧记忆还可以是基于价值的LLM 打分低的直接丢弃。2.3 为什么选 MCP 作为记忆的接入层记忆系统建好了接下来要解决的是“怎么让 Agent 用上它”。早期我是在代码里硬编码调用Agent 要查记忆就调一个内部函数。这样做能用但扩展性极差每加一种记忆类型就要改一次代码而且不同 Agent 之间没法复用。MCPModel Context Protocol的出现解决了这个问题。它本质上是一套标准协议把“能力”封装成一个个 serverAgent 通过统一的接口去调用。记忆系统可以做成一个 MCP server对外暴露几个工具write_memory、search_memory、forget_memory。Agent 不需要知道记忆存在哪、用什么数据库、检索算法是什么它只需要知道“我有一个叫 memory 的工具可以用”。这样做的好处非常明显。第一解耦记忆系统的实现可以随便换Agent 侧完全无感。第二复用同一个记忆 server 可以同时给多个 Agent 用甚至给不同的应用用。第三可观测所有记忆操作都走 MCP 协议日志和监控天然统一。我现在的做法是凡是 Agent 需要的外部能力一律做成 MCP server记忆只是其中一个。2.4 Docker 在整套架构里的角色最后说 Docker。很多人觉得 Docker 只是部署工具跟 Agent 记忆没关系。但我的实际体验是Docker 是让这套架构从“能跑”变成“稳定跑”的关键。原因有三个。第一记忆系统依赖的组件很多向量数据库、关系数据库、缓存、MCP server 本身。这些组件版本兼容性很敏感直接在宿主机上装很容易出现“我本地能跑服务器上跑不起来”的情况。用 Docker 把每个组件容器化环境就固定了换机器也能一键拉起。第二Agent 的调试经常需要“重置状态”。比如我想测试记忆写入逻辑就得把记忆库清空重来。如果记忆库跑在 Docker 里我直接删容器重建就行几秒钟的事。如果装在宿主机上清理起来很麻烦还容易误删其他数据。第三MCP server 本身也适合容器化。一个记忆 MCP server 跑在容器里通过标准端口对外提供服务Agent 侧只需要配置一个地址就能连上。这样本地开发、测试环境、生产环境可以用同一套镜像行为一致减少“环境差异”导致的 bug。3. 核心细节解析记忆系统的数据结构与检索策略3.1 记忆的存储结构不只是文本加向量很多人做记忆就是存一条文本加一个向量简单粗暴。我一开始也这样后来发现不够用。一条记忆至少需要这几个字段内容content、类型type、时间戳timestamp、来源source、重要性importance、向量embedding。类型用来区分是事实、偏好、事件还是任务来源用来追溯这条记忆是从哪轮对话来的重要性用来做检索排序和遗忘决策。我现在的表结构大概是这样id、content、type、source、importance、created_at、last_accessed_at、access_count、embedding。其中last_accessed_at和access_count很关键它们让记忆有了“热度”概念。经常被检索到的记忆权重更高长期没人访问的记忆逐渐降权这比单纯按时间遗忘更符合真实使用场景。还有一个细节是记忆的粒度。太粗了检索不准太细了检索太碎。我的经验是一条记忆最好对应一个“原子事实”比如“用户偏好顺丰快递”是一条“用户上次投诉原因是配送慢”是另一条。不要把一整段对话直接存成一条记忆那样检索出来是一大坨模型还得自己从中提取效率低。3.2 向量检索的坑相似不等于相关向量检索是记忆系统的核心但它有几个坑必须提前知道。第一个坑是相似度阈值。余弦相似度 0.8 以上算相关这个经验值在通用场景下还行但在垂直领域经常失灵。比如医疗领域“高血压”和“低血压”向量距离很近但语义完全相反。所以阈值不能一刀切得根据你的领域数据调。第二个坑是嵌入模型的选型。不同嵌入模型对同一段文本的向量表示差异很大。我试过用通用模型和领域模型做对比在客服场景下领域微调过的模型检索准确率能高出 20% 以上。选型的时候不要只看榜单一定要拿自己的真实数据测。测试方法很简单准备 50 条查询和对应的正确记忆看不同模型的前 5 命中率。第三个坑是多语言混合。如果你的用户中英文混着说嵌入模型必须支持多语言否则中文查询检索不到英文记忆。我踩过这个坑后来换成多语言模型才解决。另外记忆写入的时候最好统一语言或者至少记录语言标签检索时按语言过滤。3.3 混合检索的实现向量加关键词加规则纯向量检索不够纯关键词检索也不够所以我现在用的是混合检索。具体做法是查询进来后同时走三路。第一路是向量检索取 top 20第二路是关键词检索用倒排索引或者数据库的 LIKE取 top 20第三路是规则检索比如“如果查询里包含订单号直接精确匹配订单号字段”。三路结果合并后用一个加权的分数排序向量分占 0.5关键词分占 0.3规则命中占 0.2最后取 top 5 返回给 Agent。这个权重不是拍脑袋定的是我拿真实查询日志调出来的。调参方法也简单准备一批查询人工标注哪些记忆是真正相关的然后网格搜索权重组合看哪个组合的 NDCG 最高。我实测下来向量权重在 0.4 到 0.6 之间效果最好太低会漏掉语义相关但字面不同的记忆太高会引入语义相近但实际无关的噪音。还有一个技巧是查询改写。用户的问题往往很口语直接拿去检索效果一般。我会先用 LLM 把查询改写成更适合检索的形式比如把“我上次那个快递咋样了”改写成“用户历史订单 配送状态 查询”。这一步能明显提升召回率代价是多一次 LLM 调用延迟增加几百毫秒。如果对延迟敏感可以只在复杂查询上做改写。3.4 记忆写入的时机与去重写入时机很关键。我的做法是在每轮对话结束后触发一次写入判断而不是每句话都写。判断逻辑是让 LLM 看这轮对话输出一个 JSON包含should_write、memory_type、memory_content、importance。should_write为 false 就跳过为 true 才写入。去重是另一个必须处理的点。用户可能反复说同一件事比如“我喜欢喝美式”如果每次都写一条记忆库很快就冗余了。我的去重策略是写入前先做一次检索如果找到相似度超过 0.9 的已有记忆就不新增而是更新那条记忆的last_accessed_at和access_count。如果相似度在 0.7 到 0.9 之间就让 LLM 判断是“补充”还是“冲突”补充就合并内容冲突就用新的覆盖旧的。这里有个经验去重阈值不要设太高。我一开始设 0.95结果很多语义相同但表述不同的记忆没被识别出来库里一堆重复。后来降到 0.85效果好很多。但也不能太低低于 0.8 会误合并不同记忆。0.85 左右是我实测比较稳的值。4. 实操过程从零搭一套带记忆的 Agent4.1 环境准备Docker 安装与常见故障先说环境。我假设你用的是 Windows 或者 LinuxMac 也类似。第一步是装 Docker Desktop。Windows 上装的时候最容易遇到两个问题。一个是WSL2 没装Docker Desktop 启动会报错提示需要 WSL2 后端。解决办法是管理员权限打开 PowerShell跑wsl --install然后重启。另一个是虚拟化没开报错信息里会有 “virtualization support not detected” 之类的字样。这个得进 BIOS 开 VT-x 或者 AMD-V不同主板位置不一样一般在 CPU 配置里。装好之后验证一下命令行跑docker --version和docker compose version都能输出版本号就 OK。如果docker ps报错说连不上 daemon多半是 Docker Desktop 没启动或者当前用户不在 docker 用户组里。Linux 上把用户加进 docker 组就行sudo usermod -aG docker $USER然后重新登录。提示Windows 上如果 Docker Desktop 一直卡在启动界面先检查 Hyper-V 和 WSL2 是否都启用。两个都开有时候会冲突我的经验是只用 WSL2 后端把 Hyper-V 关掉稳定性更好。4.2 用 Docker Compose 拉起记忆系统依赖记忆系统需要几个组件PostgreSQL存结构化记忆、pgvector向量检索扩展、Redis缓存热点记忆、以及 MCP server 本身。我用 Docker Compose 把它们编排在一起一个docker-compose.yml搞定。version: 3.8 services: postgres: image: pgvector/pgvector:pg16 environment: POSTGRES_USER: agent POSTGRES_PASSWORD: agent_pass POSTGRES_DB: memory ports: - 5432:5432 volumes: - pgdata:/var/lib/postgresql/data healthcheck: test: [CMD-SHELL, pg_isready -U agent] interval: 5s retries: 5 redis: image: redis:7-alpine ports: - 6379:6379 command: redis-server --appendonly yes memory-mcp: build: ./memory-mcp ports: - 8080:8080 environment: DATABASE_URL: postgresql://agent:agent_passpostgres:5432/memory REDIS_URL: redis://redis:6379 depends_on: postgres: condition: service_healthy redis: condition: service_started volumes: pgdata:这里有几个细节值得说。pgvector/pgvector:pg16这个镜像已经内置了 vector 扩展不用自己编译。healthcheck很重要它保证 postgres 真正 ready 之后 memory-mcp 才启动否则会连不上数据库。depends_on配合condition用比单纯写depends_on靠谱得多。启动命令就一句docker compose up -d。第一次会拉镜像可能要几分钟。起来之后docker compose ps看状态都是 running 就对了。如果 memory-mcp 一直重启看日志docker compose logs memory-mcp多半是数据库连接串写错了或者 postgres 还没 ready。4.3 记忆 MCP Server 的核心实现MCP server 我用 Python 写因为生态最成熟。核心是暴露三个工具write_memory、search_memory、forget_memory。下面是最关键的search_memory实现逻辑。async def search_memory(query: str, top_k: int 5, memory_type: str None): # 1. 查询改写 rewritten await rewrite_query(query) # 2. 生成查询向量 query_embedding embed(rewritten) # 3. 向量检索 vector_results await db.fetch( SELECT id, content, type, importance, 1 - (embedding $1) AS similarity FROM memories WHERE ($2::text IS NULL OR type $2) ORDER BY embedding $1 LIMIT 20 , query_embedding, memory_type) # 4. 关键词检索 keyword_results await db.fetch( SELECT id, content, type, importance, ts_rank(to_tsvector(simple, content), plainto_tsquery(simple, $1)) AS rank FROM memories WHERE content ILIKE % || $1 || % OR ($2::text IS NULL OR type $2) ORDER BY rank DESC LIMIT 20 , rewritten, memory_type) # 5. 合并排序 merged merge_and_rank(vector_results, keyword_results) # 6. 更新访问记录 for mem in merged[:top_k]: await db.execute( UPDATE memories SET last_accessed_at NOW(), access_count access_count 1 WHERE id $1 , mem[id]) return merged[:top_k]这段代码里有几个关键点。是 pgvector 的余弦距离操作符1 - 距离就是相似度。ts_rank是 PostgreSQL 全文检索的排序函数用simple配置是因为中文分词需要额外扩展先用 simple 做基础匹配。合并排序的时候我给向量分乘 0.5关键词分乘 0.3再加一个基于importance和access_count的加权分乘 0.2。rewrite_query那一步是可选的如果延迟敏感可以去掉。我的实测是加上改写后检索准确率提升大概 15%但延迟增加 300 到 500 毫秒。如果你的场景对延迟要求高可以只在查询长度超过一定阈值时才改写。4.4 接入 AgentMCP 客户端配置MCP server 跑起来之后Agent 侧要配置连接。不同框架配置方式不一样但核心都是填一个 server 地址。以常见的配置为例大概长这样{ mcpServers: { memory: { url: http://localhost:8080/sse, transport: sse } } }这里transport用sse是因为 MCP 支持多种传输方式SSE 是最通用的一种。如果你的 Agent 框架支持 stdio 传输也可以把 server 做成命令行工具通过 stdio 通信省去网络开销。我两种都用过SSE 的好处是 server 可以独立部署多个 Agent 共享stdio 的好处是简单不用管端口和网络。配置好之后Agent 启动时会自动发现 memory server 提供的工具。你可以在 Agent 的 prompt 里告诉它“你有 write_memory、search_memory、forget_memory 三个工具当用户提到需要长期记住的信息时调用 write_memory当需要回忆历史信息时调用 search_memory。” 这样 Agent 就会在合适的时机自动调用。注意不要让 Agent 每轮都调 search_memory那样会拖慢响应。我的做法是在 prompt 里加判断条件比如“只有当用户的问题涉及历史信息、个人偏好、之前发生过的事情时才调用 search_memory”。这样能过滤掉大部分不必要的检索。5. 常见问题与排查技巧实录5.1 记忆检索不准的排查思路检索不准是最常见的问题表现是 Agent 明明有相关记忆但就是没检索出来或者检索出来一堆无关的。排查我一般按这个顺序走。先看查询本身。把用户原始查询和改写后的查询都打出来看看改写有没有跑偏。我遇到过改写把“帮我查下上次那个事”改成“查询历史事件”结果检索出一堆无关记忆。这种情况要么调改写 prompt要么干脆关掉改写。再看向量质量。拿几条典型查询手动算一下它们和正确记忆的相似度。如果相似度低于 0.7说明嵌入模型不适合你的领域考虑换模型或者做微调。如果相似度正常但没检索出来检查是不是被memory_type过滤掉了或者top_k设太小。最后看排序逻辑。把三路检索的原始结果都打出来看看正确记忆在哪一路、排第几。如果正确记忆在向量路排第 15但合并后掉出 top 5说明权重需要调。我一般会把向量权重调高一点或者给importance高的记忆额外加权。5.2 记忆写入过多或过少的平衡写入过多会让记忆库膨胀检索噪音大写入过少会让 Agent 记不住东西。这个平衡点得靠调。我的经验是先宽松后收紧。初期把should_write的判断标准放宽让 LLM 多写一些然后观察一周看哪些记忆从来没被检索过哪些被检索了但用户反馈不好。根据这些数据反过来收紧写入标准。具体调法在写入 prompt 里加几个反例告诉 LLM“以下情况不要写入寒暄、重复信息、临时状态、用户明确说不用记的”。同时给importance打分加约束比如“只有 importance 大于 0.6 的才写入”。这样能过滤掉大部分低价值记忆。还有一个技巧是定期清理。我写了一个定时任务每周跑一次把access_count为 0 且创建超过 30 天的记忆标记为待删除让 LLM 最后判断一次是否真的没用。这样能保持记忆库的精简。5.3 Docker 网络与连接问题速查Docker 相关的坑主要集中在网络上。下面这张表是我遇到过的典型问题和解决办法。问题现象可能原因解决办法容器间连不上不在同一网络用 docker compose 默认网络或手动创建 network宿主机连不上容器端口没映射检查 ports 配置确认端口没被占用容器连不上外网DNS 配置问题在 compose 里指定 dns: 8.8.8.8数据库连接超时健康检查没配加 healthcheck用 depends_on condition数据丢失没挂 volume关键数据目录必须挂 volume还有一个常见问题是端口冲突。比如你宿主机上已经装了 PostgreSQL 占了 5432容器再映射 5432 就会失败。解决办法是把宿主机端口改成 5433容器内还是 5432连接的时候用 5433。这个在 compose 里写5433:5432就行。5.4 性能优化让检索快起来记忆检索的延迟主要花在三个地方查询改写、向量生成、数据库检索。查询改写和向量生成都是模型调用延迟大头在这。优化手段有几个。第一缓存。高频查询的向量可以缓存到 Redis下次同样查询直接取。我实测缓存命中率能到 40% 左右平均延迟降了三分之一。第二批量。如果一轮对话需要检索多次合并成一次批量检索减少模型调用次数。第三降级。延迟敏感的场景可以跳过查询改写直接用原始查询做向量检索。准确率降一点但延迟能降一半。第四索引。pgvector 的向量索引一定要建不然数据量大了检索会慢得离谱。建索引的语句是CREATE INDEX ON memories USING ivfflat (embedding vector_cosine_ops) WITH (lists 100);。lists的值根据数据量调一般数据量的平方根左右比较合适。6. 记忆系统的扩展方向与个人体会6.1 从单一记忆到分层记忆现在这套系统是单层记忆所有记忆平铺在一起。但真实场景里记忆其实是有层次的。比如用户的长期偏好是一层最近几轮对话的上下文是一层当前任务的临时状态又是一层。分层之后检索可以按层过滤效率更高也更符合认知科学里的记忆模型。我最近在试的做法是加一个layer字段分long_term、session、task三层。long_term存偏好和事实session存本次会话的上下文task存当前任务的中间状态。检索的时候根据查询类型决定查哪层或者多层合并。这个还在调但初步效果不错尤其是 session 层能明显减少长期记忆的噪音干扰。6.2 记忆的主动遗忘与冲突消解遗忘这件事我越来越觉得它是记忆系统的核心竞争力。一个不会遗忘的系统最终一定会被自己的记忆淹没。我现在用的遗忘策略是组合式的时间衰减加访问频率加重要性。具体公式大概是score importance * 0.5 log(access_count 1) * 0.3 - days_since_access * 0.01score 低于阈值的进入待删除队列。冲突消解是另一个难点。用户改主意了比如之前说喜欢咖啡后来说改喝茶了。系统得能识别这是冲突用新的覆盖旧的。我的做法是在写入时做冲突检测如果新记忆和旧记忆相似度高但内容矛盾就让 LLM 判断哪个更新然后标记旧记忆为superseded检索时默认过滤掉。6.3 我踩过的几个印象深刻的坑第一个坑是记忆污染。有一次测试的时候我往记忆库里灌了一批假数据忘了清理结果上线后 Agent 一直推荐一些莫名其妙的东西。排查了半天才发现是测试数据混进去了。教训是测试环境和生产环境的记忆库必须物理隔离用不同的数据库实例绝对不能共用。第二个坑是向量维度不匹配。我中途换过一次嵌入模型从 768 维换到 1024 维但忘了重建索引结果检索一直报错。换嵌入模型一定要重建所有向量和索引这个操作最好写成一个迁移脚本别手动搞。第三个坑是并发写入冲突。多个 Agent 同时写记忆偶尔会出现重复写入。后来加了数据库层面的唯一约束配合写入前的去重检查才解决。如果你的系统是多 Agent 共享记忆并发控制一定要提前考虑。6.4 后续可以怎么扩展这套架构跑通之后扩展方向其实很多。一个方向是多模态记忆把图片、语音也纳入记忆体系用对应的嵌入模型生成向量存到同一个库里。另一个方向是记忆共享让多个 Agent 之间共享部分记忆比如一个团队里的多个助手共享用户偏好。还有一个方向是记忆可视化做一个界面让用户能看到 Agent 记住了什么并且能手动编辑和删除这对建立用户信任很有帮助。我个人最看好的方向是记忆的主动整理。现在的记忆写入是被动的用户说什么就记什么。未来可以让 Agent 定期回顾记忆库主动发现关联、合并冗余、提炼更高层的抽象。比如从“用户喜欢美式”“用户早上喝咖啡”“用户不喜欢加糖”提炼出“用户是黑咖啡爱好者”。这种主动整理能让记忆系统从“存储”进化到“理解”价值会大很多。最后分享一个小技巧调试记忆系统的时候一定要把每次检索的原始结果和最终返回结果都打日志。我一开始只看最终结果排查问题很费劲。后来把三路检索的中间结果都记下来一眼就能看出是哪一路出了问题。这个日志习惯帮我省了大量时间强烈建议你也加上。