尧图精选

Agent Memory 分层架构与 MCP 实战:从记忆写入到 Docker 部署

🕒 发布时间:2026/10/1 18:15:30 📁 来源:尧图网络
1. 从“hindsight”说起为什么记忆是 Agent 落地的最后一公里“hindsight”这个词本身很有意思字面意思是“事后的洞察力”也就是我们常说的“后见之明”。把这个词放到 Agent Memory 这个语境里它其实精准地戳中了一个痛点一个 LLM Agent 在跑完一轮任务之后能不能回过头来把刚才发生的事、做过的决策、踩过的坑变成下一次可以直接调用的经验。这件事听起来简单做起来极难因为它牵扯到记忆的写入、存储、检索、衰减、冲突消解这一整套链路。我接触过不少做 Agent 的团队模型选的是顶配工具链也搭得很全MCP 协议接了一堆Docker 环境跑得飞起但一到多轮任务就露馅——Agent 像个失忆症患者上一轮告诉它“用户偏好用中文回复”下一轮它又开始飙英文上一轮已经确认过某个 API 的鉴权方式下一轮它又重新试错一遍。这不是模型能力问题是记忆架构没设计好。hindsight 这个项目标题本质上就是在问一个问题Agent 的记忆到底该怎么存、怎么取、怎么用才能让它在时间维度上真正“长记性”。这篇文章适合三类人看。第一类是正在做 Agent 应用开发、被多轮上下文折磨过的工程师第二类是对 MCP 协议、Docker 部署、LLM 记忆机制感兴趣、想动手搭一套可复现方案的技术爱好者第三类是产品侧的同学想搞清楚 Agent Memory 到底能带来什么体验差异。我会从架构设计讲到实操部署从记忆分层讲到检索策略尽量把每个“为什么这么设计”讲透而不是只丢一堆配置让你抄。需要提前说明的是Agent Memory 这个领域目前没有银弹hindsight 也不是某个现成的开源项目名它更像是一类设计思路的代称——让 Agent 具备“回看过去、提炼经验”的能力。我下面讲的内容是基于当前主流实践向量检索、结构化记忆、MCP 工具调用、Docker 容器化部署做的一套可落地组合方案你可以把它当成一个参考架构按自己的业务裁剪。2. 记忆分层设计别把所有东西都塞进向量库2.1 为什么“一个向量库走天下”是错的很多人一提 Agent Memory第一反应就是“上向量数据库把对话历史 embedding 一下存进去检索的时候做相似度匹配”。这个方案在 Demo 阶段能跑通但一上生产就崩。原因有三个。第一语义相似不等于任务相关。用户上一轮说“帮我查一下北京明天的天气”这一轮说“那上海呢”两句话 embedding 相似度很高但如果你只靠向量检索很可能把三天前另一段关于“北京天气”的旧对话也捞出来而那段对话的上下文比如当时用户问的是穿衣建议和当前任务对比城市天气完全不搭。向量检索擅长找“像”的不擅长找“对”的。第二记忆有生命周期。有些记忆是永久的用户身份、偏好设置有些是会话级的当前任务的中间状态有些是临时的刚刚调用工具返回的结果。全塞一个库里检索时没有优先级噪音会淹没信号。第三结构化信息会被 embedding 稀释。比如“用户 ID 是 10086VIP 等级 3上次投诉时间是 3 月 12 日”这种信息embedding 之后变成一串浮点数检索出来还得靠 LLM 再解析一遍既浪费 token 又容易出错。这类信息应该用结构化存储直接 key-value 查。所以 hindsight 思路下的记忆架构核心是分层。我一般把它分成四层工作记忆、情景记忆、语义记忆、程序记忆。下面逐层拆。2.2 四层记忆的职责划分与存储选型工作记忆Working Memory是当前会话的“草稿纸”存的是这一轮任务正在用的上下文用户刚说的话、Agent 刚调用的工具返回、中间推理步骤。它的特点是读写极频繁、生命周期短会话结束或任务完成就清。存储上我推荐直接用内存 Redis不要碰向量库。Redis 的 List 或 Hash 结构足够读写延迟在毫秒级而且支持 TTL 自动过期省得你手动清理。情景记忆Episodic Memory是“发生过什么”的记录比如“3 月 12 日用户投诉了物流延迟Agent 给出了补偿方案用户接受了”。这类记忆需要按时间线检索也需要按语义检索。我的做法是双写一份结构化记录进 PostgreSQL带时间戳、事件类型、参与方一份摘要 embedding 进向量库用于语义召回。检索时先用结构化条件过滤时间范围再用向量做二次排序。语义记忆Semantic Memory是“我知道什么”的事实性知识比如“用户偏好中文回复”“这个 API 的 rate limit 是 100 QPS”。这类记忆更新频率低、读取频率高适合用结构化存储 缓存。我通常用 PostgreSQL 存主数据Redis 做热点缓存向量库只存那些需要模糊匹配的比如“用户提到过喜欢某个品牌”这种非精确事实。程序记忆Procedural Memory是“怎么做”的技能比如“调用退款接口前必须先查订单状态”。这类记忆最容易被忽略但对 Agent 稳定性影响最大。它适合用规则引擎或工作流引擎来存比如用 JSON 描述前置条件、执行步骤、后置校验Agent 在执行任务前先查一遍程序记忆避免重复踩坑。记忆类型存储介质生命周期检索方式典型内容工作记忆Redis / 内存会话级直接读取当前对话、工具返回情景记忆PostgreSQL 向量库长期时间过滤 语义召回历史事件、任务记录语义记忆PostgreSQL Redis长期精确查询 缓存用户偏好、事实知识程序记忆规则引擎 / JSON长期条件匹配操作规范、避坑规则这个分层不是拍脑袋定的它对应的是认知科学里对人类记忆的经典划分。你不需要严格照搬但“按用途分存储”这个原则一定要守住。我见过太多项目把用户偏好和临时工具返回混在一个向量库里结果检索时要么召回一堆无关的要么把重要偏好淹没了。2.3 记忆写入的时机与去重策略分层定好了下一个问题是什么时候写我的经验是三个触发点。第一个触发点是任务完成时。一轮任务跑完把关键决策、结果、用户反馈提炼成一条情景记忆。注意是“提炼”不是“全存”原始对话太长了直接存进去既占空间又降检索质量。提炼可以用 LLM 做摘要prompt 大概是“请用一句话总结这次任务的目标、采取的行动、最终结果以及任何值得下次注意的点”。第二个触发点是用户显式纠正时。用户说“不对应该是这样”这属于高价值信号必须立刻写入语义记忆或程序记忆。这类记忆的权重应该设高检索时优先返回。第三个触发点是工具调用失败时。失败经验比成功经验更值得记因为它能防止 Agent 重复踩坑。我一般会把失败原因、错误码、当时的参数一起存进程序记忆下次遇到类似场景先查一遍。去重是个容易被忽视的坑。同一个事实被反复写入向量库里会出现大量近似重复的记录检索时浪费 token 还降低精度。我的做法是写入前先做一次相似度检查如果已有记录的相似度超过阈值比如 0.92就更新旧记录的权重和时间戳而不是新增一条。这个逻辑可以用向量库的 upsert 配合 metadata 过滤来实现。3. MCP 协议在记忆系统里的角色别把它只当工具调用3.1 MCP 的本质是“能力协商层”MCPModel Context Protocol这两年被讨论得很多但很多人对它的理解停留在“让 LLM 调用外部工具”这个层面。这个理解不算错但太窄了。MCP 真正的价值在于它定义了一套标准化的能力协商机制Agent 不需要提前知道有哪些工具可用它可以通过 MCP 动态发现、动态调用、动态获取上下文。放到记忆系统里MCP 可以承担三个角色。第一个角色是记忆读写接口。你可以把记忆库封装成一个 MCP Server暴露memory_write、memory_search、memory_update这几个工具Agent 通过 MCP 协议来操作记忆而不是硬编码在业务逻辑里。这样做的好处是记忆层和 Agent 层解耦换存储后端不用改 Agent 代码。第二个角色是上下文注入。MCP 支持 resource 概念你可以把用户画像、历史摘要这些内容注册成 resourceAgent 在启动时自动拉取作为 system prompt 的一部分。这比每次手动拼 prompt 优雅得多。第三个角色是跨 Agent 记忆共享。如果你有多个 Agent 协作比如一个负责客服、一个负责售后它们可以通过同一个 MCP Server 共享记忆避免各自为政。这个场景在复杂业务里很常见MCP 的标准化协议让这件事变得可行。3.2 用 MCP 封装记忆服务的实操结构我拿一个具体例子来说明怎么搭。假设你要做一个客服 Agent 的记忆服务用 MCP 封装目录结构大概是这样memory-mcp-server/ ├── src/ │ ├── index.ts # MCP Server 入口 │ ├── tools/ │ │ ├── write.ts # memory_write 工具 │ │ ├── search.ts # memory_search 工具 │ │ └── update.ts # memory_update 工具 │ ├── resources/ │ │ └── profile.ts # 用户画像 resource │ └── storage/ │ ├── redis.ts # 工作记忆 │ ├── postgres.ts # 结构化记忆 │ └── vector.ts # 向量记忆 ├── Dockerfile └── docker-compose.ymlmemory_write工具的入参设计很关键我一般要求传这几个字段content记忆内容、typeepisodic / semantic / procedural、importance1-5 权重、ttl可选过期时间、metadata结构化标签。出参返回写入后的记忆 ID 和去重结果。memory_search的入参包括query查询文本、type可选限定记忆类型、top_k返回条数、time_range可选时间过滤。内部实现是先按 type 和 time_range 做结构化过滤再对候选集做向量召回最后用 importance 和时间衰减做重排序。这里有个细节值得说时间衰减函数。一条三个月前的记忆和一条昨天的记忆即使语义相似度一样权重也应该不同。我用的公式是score similarity * importance * exp(-λ * days_ago)λ 取 0.01 左右意味着大约 70 天后权重衰减到一半。这个参数可以根据业务调客服场景可以衰减快一点知识库场景可以慢一点。3.3 MCP 工具调用的错误处理与重试MCP 调用不是百分百可靠的网络抖动、服务重启、参数错误都会导致失败。我在实操里总结了几个必须处理的场景。第一个场景是工具不存在。Agent 可能调用了一个没注册的工具名这时候 MCP Server 应该返回明确的错误码而不是静默失败。Agent 侧要能捕获这个错误并决定是换工具还是放弃。第二个场景是参数校验失败。比如memory_write的type传了一个不存在的值Server 应该返回 400 级别的错误并附带合法值列表。Agent 拿到错误后可以自动修正重试。第三个场景是超时。记忆检索如果超过 2 秒还没返回Agent 不应该干等应该走降级逻辑——比如只返回工作记忆跳过长期记忆检索。这个降级策略要提前设计好不能等线上出问题再补。重试策略我一般设成最多 2 次间隔用指数退避200ms、500ms。超过 2 次就放弃记录日志让 Agent 走无记忆路径。记住记忆是增强不是依赖记忆服务挂了 Agent 也得能跑。4. Docker 化部署把记忆服务跑成可复现的环境4.1 为什么记忆服务一定要容器化我强烈建议把记忆服务容器化原因有三个。第一依赖复杂。一个完整的记忆服务要跑 Redis、PostgreSQL、向量库比如 Qdrant 或 Milvus、MCP Server手动装环境能折腾一整天还容易版本冲突。第二环境一致性。开发机跑通了测试环境跑不通这种事太常见了容器化能把这个变量消掉。第三弹性伸缩。记忆检索是 IO 密集型流量上来的时候需要快速扩容容器编排比手动部署快得多。Docker Desktop 在 Windows 和 macOS 上都能用Linux 上直接装 Docker Engine 就行。这里有个坑要提醒Windows 上装 Docker Desktop 需要开启 WSL2 或者 Hyper-V如果 BIOS 里虚拟化没开会报 “Virtualization support not detected” 的错误。解决办法是进 BIOS 把 Intel VT-x 或 AMD-V 打开然后在 Windows 功能里启用“虚拟机平台”和“适用于 Linux 的 Windows 子系统”。4.2 docker-compose 编排记忆服务全栈下面是我常用的一套 docker-compose 配置把记忆服务需要的组件都编排进去。你可以直接拿去改。version: 3.9 services: redis: image: redis:7-alpine ports: - 6379:6379 volumes: - redis_data:/data command: redis-server --appendonly yes healthcheck: test: [CMD, redis-cli, ping] interval: 10s timeout: 3s retries: 3 postgres: image: postgres:16-alpine ports: - 5432:5432 environment: POSTGRES_USER: memory POSTGRES_PASSWORD: memory_pass POSTGRES_DB: agent_memory volumes: - pg_data:/var/lib/postgresql/data healthcheck: test: [CMD-SHELL, pg_isready -U memory] interval: 10s timeout: 3s retries: 3 qdrant: image: qdrant/qdrant:latest ports: - 6333:6333 - 6334:6334 volumes: - qdrant_data:/qdrant/storage healthcheck: test: [CMD, curl, -f, http://localhost:6333/healthz] interval: 10s timeout: 3s retries: 3 memory-mcp: build: ./memory-mcp-server ports: - 8080:8080 environment: REDIS_URL: redis://redis:6379 POSTGRES_URL: postgresql://memory:memory_passpostgres:5432/agent_memory QDRANT_URL: http://qdrant:6333 DECAY_LAMBDA: 0.01 TOP_K: 5 depends_on: redis: condition: service_healthy postgres: condition: service_healthy qdrant: condition: service_healthy volumes: redis_data: pg_data: qdrant_data:这份配置里有几个设计点值得解释。healthcheck是必须的因为depends_on默认只等容器启动不等服务就绪没有 healthcheck 的话 memory-mcp 可能在数据库还没初始化完就启动直接报连接错误。volumes保证数据持久化容器重启不丢记忆。DECAY_LAMBDA和TOP_K做成环境变量方便不同环境调参。启动命令就一行docker compose up -d第一次跑会拉镜像、建表、初始化向量库 collection大概需要两三分钟。跑起来之后用docker compose ps检查所有服务是不是 healthy 状态。4.3 向量库 collection 的初始化与索引参数Qdrant 的 collection 需要手动创建并且要指定向量维度和距离度量。维度取决于你用的 embedding 模型比如 OpenAI 的 text-embedding-3-small 是 1536 维BGE-M3 是 1024 维。距离度量一般用 Cosine因为文本 embedding 通常做了归一化。from qdrant_client import QdrantClient from qdrant_client.models import Distance, VectorParams client QdrantClient(urlhttp://localhost:6333) client.create_collection( collection_nameagent_memory, vectors_configVectorParams( size1024, distanceDistance.COSINE ), hnsw_config{ m: 16, ef_construct: 100 } )m和ef_construct是 HNSW 索引的参数。m控制每个节点的连接数越大召回率越高但内存占用越大16 是个平衡点。ef_construct控制建索引时的搜索深度100 是常用值。这两个参数在数据量小于 100 万时基本不用调超过之后再考虑优化。还有一个容易踩的坑payload 索引。如果你经常按type或user_id过滤一定要给这些字段建 payload 索引否则 Qdrant 会做全量扫描性能差一个数量级。client.create_payload_index( collection_nameagent_memory, field_nametype, field_schemakeyword ) client.create_payload_index( collection_nameagent_memory, field_nameuser_id, field_schemakeyword )5. 记忆检索的实战调优从“能查到”到“查得准”5.1 混合检索向量 关键词 结构化过滤纯向量检索的召回率在真实业务里往往不够看尤其是当查询里包含专有名词、订单号、产品型号这类信息时embedding 会把这些细节抹平。我的做法是混合检索先用结构化条件做粗筛再用关键词做精确匹配最后用向量做语义补充三路结果合并后重排序。具体流程是这样用户查询进来先解析出结构化条件比如 user_id、时间范围、记忆类型用这些条件从 PostgreSQL 里捞一批候选同时对查询做关键词提取用 BM25 或 PostgreSQL 的全文检索捞一批再把查询 embedding 一下从 Qdrant 里召回一批。三批结果用 RRFReciprocal Rank Fusion算法合并公式是score Σ 1/(k rank_i)k 一般取 60。RRF 的好处是不需要归一化不同来源的分数直接按排名融合简单又稳。我实测下来混合检索比纯向量检索的召回率能提升 20% 到 30%尤其是在长尾查询上。5.2 重排序让最相关的记忆排在最前面召回之后是重排序。这一步可以用 Cross-Encoder 模型比如 BGE-Reranker它对 query 和 document 做联合编码精度比双塔模型高不少。代价是慢所以只对 Top 20 的候选做重排选出 Top 5 返回给 Agent。重排序的输入是(query, memory_content)对输出是相关性分数。我一般会把 importance 和时间衰减也乘进去最终分数是rerank_score * importance * decay。这样既考虑了语义相关性又考虑了记忆本身的价值和新鲜度。有个细节要注意重排序模型和 embedding 模型最好用同一家的比如都用 BGE 系列这样语义空间一致效果更稳。混用不同家的模型有时候会出现分数分布不匹配的问题。5.3 检索结果注入 Prompt 的格式设计检索出来的记忆怎么塞进 prompt这件事直接影响 Agent 的使用效果。我见过有人直接把记忆列表拼成一段文本丢进去结果 Agent 要么忽略要么误用。我的做法是结构化注入给每条记忆标注类型、时间、置信度让 Agent 知道该怎么用。[记忆上下文] 以下是与当前任务相关的历史记忆按相关性排序 1. [语义记忆 | 置信度 0.95 | 2024-03-10] 用户偏好中文回复不喜欢冗长的解释。 2. [程序记忆 | 置信度 0.88 | 2024-03-08] 调用退款接口前必须先查询订单状态否则会报 400 错误。 3. [情景记忆 | 置信度 0.72 | 2024-03-05] 用户曾投诉物流延迟当时给出的补偿方案是优惠券用户接受。 请结合以上记忆处理当前任务如记忆与当前情况冲突以当前情况为准。最后那句“如记忆与当前情况冲突以当前情况为准”很重要它给 Agent 一个优先级判断依据避免被过期记忆带偏。置信度字段也让 Agent 知道哪些记忆更可信。6. 常见问题与排查技巧实录6.1 记忆检索返回空结果怎么办这是最常见的问题排查思路按顺序来。先确认向量库 collection 里到底有没有数据用count接口查一下。如果数据量为 0说明写入环节有问题检查 MCP Server 的日志看memory_write有没有报错。如果数据量正常但检索为空检查 embedding 模型是不是和写入时用的同一个维度不匹配会直接报错但有些客户端会静默返回空。还有一个隐蔽的原因过滤条件太严。比如你同时限定了typesemantic和time_range最近 7 天但语义记忆可能三个月才更新一次自然查不到。这时候应该放宽条件或者做多路检索某一路为空不影响其他路。6.2 记忆写入重复导致检索噪音大前面提过去重这里补充一个实操技巧用内容哈希做精确去重用向量相似度做模糊去重。内容哈希很简单对记忆文本做 MD5写入前查一下哈希是否存在。模糊去重就是算余弦相似度超过阈值就更新而不是新增。阈值怎么定我的经验是 0.92 到 0.95 之间。太低会误合并不同记忆太高去重效果不明显。这个值可以用一批标注数据调出来没有标注数据的话就先用 0.93观察一段时间再调。6.3 Docker 容器间网络不通的排查容器间网络不通九成是服务名写错了或者端口没对上。docker-compose 里服务之间用服务名做 hostname比如 memory-mcp 连 Redis 应该用redis://redis:6379而不是localhost:6379。localhost 在容器里指向容器自己不是宿主机。如果服务名没错还是不通用docker compose exec memory-mcp ping redis测一下连通性。ping 不通说明网络层有问题检查是不是在不同的 network 里。docker-compose 默认会创建一个共享 network所有服务都在里面一般不会有问题除非你手动指定了 network。还有一个坑是启动顺序。虽然配了depends_on和 healthcheck但如果你的应用启动时只连一次数据库连不上就退出那还是会有问题。解决办法是在应用侧加重试逻辑启动时连不上就等几秒重试最多重试 10 次。6.4 记忆膨胀导致成本失控跑一段时间后向量库越来越大检索越来越慢token 消耗越来越高。这是记忆系统必须面对的熵增问题。我的应对策略是分级衰减 定期归档。分级衰减是指不同重要性的记忆有不同的保留策略。importance 为 5 的记忆永久保留4 的保留一年3 的保留半年2 的保留一个月1 的保留一周。这个策略用定时任务实现每天扫一遍过期的删掉或归档到冷存储。定期归档是指把超过一定时间的记忆从向量库移到对象存储需要的时候再捞回来。这样向量库始终保持在一个可控的规模检索性能不会随时间退化。问题现象可能原因排查方法解决方案检索返回空数据未写入 / 维度不匹配 / 过滤过严查 count、查日志、放宽条件修复写入链路、统一模型、多路检索检索噪音大重复写入 / 权重未衰减查重复率、查时间分布去重、加时间衰减、调阈值容器网络不通服务名错误 / 网络隔离ping 测试、查 network用服务名、检查 network 配置成本失控记忆膨胀 / 检索过多查数据量、查 token 消耗分级衰减、定期归档、限制 top_k7. 我在实操中踩过的几个坑第一个坑是embedding 模型换版本。有一次我把 embedding 模型从 v1 升到 v2忘了重新 embedding 历史数据结果新旧向量混在一个 collection 里检索结果乱七八糟。教训是换模型必须重建 collection或者用双 collection 做灰度迁移。第二个坑是MCP Server 无状态设计。我一开始把会话状态存在 MCP Server 的内存里结果一重启就丢。后来改成所有状态都落 RedisServer 本身无状态随便重启扩容都不影响。第三个坑是过度依赖记忆。有段时间 Agent 什么都查记忆连“今天星期几”都要检索一遍延迟高得离谱。后来我加了个规则只有涉及用户偏好、历史事件、操作规范的问题才查记忆通用知识直接走模型本身。这个边界要划清楚不然记忆系统会变成性能瓶颈。第四个坑是忽略记忆的隐私属性。有些记忆包含用户敏感信息不能明文存。我的做法是敏感字段加密存储检索时只返回脱敏后的摘要需要详情时再走鉴权解密。这个在合规要求高的场景里是必须的。8. 后续可以怎么扩展如果你已经把基础版跑通了可以考虑几个扩展方向。一是记忆的主动遗忘让 Agent 自己判断哪些记忆不再需要主动删除而不是等 TTL 过期。二是跨用户记忆迁移比如同一个团队的用户可以共享某些程序记忆提升整体效率。三是记忆的可解释性当 Agent 做出某个决策时能回溯到是哪条记忆影响了它这对调试和审计很有价值。我个人在实际操作中的体会是Agent Memory 这件事架构设计占七成调优占三成。分层分对了后面就是参数问题分层分错了怎么调都是打补丁。hindsight 这个词提醒我们记忆的价值不在于存了多少而在于能不能在正确的时刻把正确的经验送到正确的地方。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →