Agent Memory 三层架构与 MCP 协议:用 Docker 部署 LLM 智能体记忆服务
发布时间:2026/9/30 4:39:13来源:尧图网络
1. 从 hindsight 说起为什么 Agent Memory 是 LLM 落地的下一个关键战场第一次看到 hindsight 这个词我脑子里蹦出来的不是词典里的事后诸葛亮而是一个很具体的工程问题当一个 LLM Agent 跑完一整轮任务之后它到底记住了什么是只记住了最后一句对话还是把中间踩过的坑、试错的路径、被否决的方案都留了下来这个问题听起来很虚但只要你在生产环境里部署过基于 LLM 的 Agent就会知道它有多要命。hindsight 这个项目标题本身就是一个隐喻——后见之明。它指向的核心领域是Agent Memory智能体记忆也就是让 LLM 驱动的 Agent 具备跨会话、跨任务的记忆能力。围绕它的关键词非常密集agent memory、LLM、MCP、Docker再加上热搜里那一长串 a-memguard、working memory、LLM wiki、MCP 协议、Playwright MCP、BurpSuite MCP 等等基本可以勾勒出当前这个方向的全貌——记忆层正在从上下文窗口的附属品变成独立的基础设施。这篇文章适合谁看三类人一是正在做 LLM Agent 应用、被聊三句就失忆折磨的开发者二是想搞清楚 MCP 协议到底在解决什么问题的架构师三是准备用 Docker 把一整套 Agent Memory 服务跑起来、但不知道从哪下手的运维或全栈工程师。我会把 hindsight 背后的记忆模型、MCP 的接入方式、Docker 的部署细节、以及实际踩过的坑全部摊开讲一遍。不讲空话只讲能复现的东西。先说结论性的判断Agent Memory 不是把历史对话塞进向量库这么简单。它至少包含三层——working memory工作记忆当前任务内的短期状态、episodic memory情景记忆跨会话的事件流、semantic memory语义记忆沉淀下来的知识。hindsight 这类项目的价值就在于把这三层用统一的接口管起来并且通过 MCP 暴露给任意 LLM 客户端。下面逐层拆。2. Agent Memory 的三层结构与 hindsight 的设计取舍2.1 为什么不能只靠 Context Window很多人第一反应是现在模型上下文都 128K、200K 了直接把历史全塞进去不就行了我实测过这条路在 demo 阶段能跑一上生产就崩。原因有三个而且都是硬伤。第一是成本。上下文越长每次请求的 token 消耗是线性甚至超线性增长的。一个每天跑几千次调用的 Agent如果每次都带 100K token 的历史账单会难看到你想关掉服务。第二是注意力稀释。模型对长上下文的中间部分存在明显的lost in the middle现象你把关键信息埋在 80K 位置它很可能视而不见。第三是状态污染。上一轮任务里被否决的方案、过期的配置、错误的中间结论如果无差别地留在上下文里会持续干扰当前推理。所以 hindsight 这类项目的第一个设计取舍就是记忆必须被主动管理而不是被动堆积。它需要决定什么该记、什么该忘、什么该压缩、什么该提升为长期知识。这就是为什么热搜里会出现 agent 存储 working memory 这种词——working memory 是显式建模的对象不是上下文窗口的别名。2.2 Working / Episodic / Semantic 三层怎么分工我把这三层用一个具体场景讲清楚。假设你有一个帮用户排查服务器问题的 Agent。Working memory是当前这次排查的临时状态用户说网站打不开Agent 记下待验证DNS、端口、进程、磁盘然后逐个排查每查一项就更新这个清单。任务结束这层基本可以丢弃或归档。它的特点是生命周期短、结构强、读写频繁。Episodic memory是这台服务器上周也出过类似问题当时是磁盘满了。它按时间线记录事件带上下文谁、什么时候、做了什么、结果如何。这层的价值在于让 Agent 能说上次这么干解决了而不是每次都从零开始。Semantic memory是从多次 episodic 里提炼出的稳定知识这台机器的 /var/log 分区容易满阈值是 85%。它不绑定具体时间是抽象后的规则或事实。hindsight 的设计思路我理解下来是用统一的存储抽象把三层收口但对每层用不同的检索策略。working memory 走精确 key 查询快episodic 走时间 语义混合检索semantic 走向量 图谱检索。这个分层不是拍脑袋而是因为三层的访问模式差异太大用一套索引硬扛会导致要么慢要么不准。2.3 与 LLM Wiki、GraphRAG 的关系热搜里反复出现 LLM wiki 知识库、LLM ontology、RAG GraphRAG LLM wiki 本体 RAG这些其实和 Agent Memory 是同一件事的不同切面。LLM wiki 可以理解为 semantic memory 的一种组织形态——把知识以 wiki 式的条目 链接结构存起来而不是散落的 chunk。GraphRAG 则是用图结构表达实体和关系让检索能沿着关系走而不是只做向量相似度。hindsight 如果要做得扎实semantic 层大概率会借鉴 GraphRAG 的思路实体 关系 本体ontology。为什么需要 ontology因为纯向量的检索在多跳推理上很弱。你问哪个服务依赖这个数据库向量检索可能给你一堆提到数据库的文档但答不出依赖链。有了本体Agent 才能沿着service - depends_on - database这样的边去查。这里有个实操经验不要一上来就上重型图谱。我见过太多项目初期就搭 Neo4j 本体建模结果数据量没上来维护成本先把自己拖死。合理的路径是先用结构化表 向量库跑通等 episodic 数据积累到一定量、确实出现多跳查询需求了再引入图。hindsight 这种项目如果提供可插拔的存储后端就是给了你这条渐进路径。3. MCP 协议Agent Memory 的标准插座3.1 MCP 到底解决什么问题热搜里有人问 mcp 是什么、mcp 是软件协议 硬件协议那个概念叫什么来着。我用一句话解释MCPModel Context Protocol是让 LLM 客户端和外部能力工具、数据源、记忆服务之间用统一协议对话的规范。类比一下它就像 USB-C——以前每个外设一个专用接口现在统一了插上就能用。在 hindsight 的语境里MCP 的意义在于记忆服务不需要为每个 LLM 客户端单独写适配。你的记忆后端跑成一个 MCP Server那么 Claude Desktop、各种 IDE 里的 AI 助手、自研的 Agent 框架只要支持 MCP就能直接连上来读写记忆。这就是为什么热搜里会出现 agent mcp、playwright mcp、burpsuite mcp、blender mcp、unity mcp 这一大串——每个垂直领域都在把自己的能力包装成 MCP Server。MCP 的核心概念有三个Resources资源可读的数据、Tools工具可调用的动作、Prompts提示模板。记忆服务通常三者都用Resources 暴露当前有哪些记忆Tools 提供remember、recall、forget这些动作Prompts 提供如何把记忆注入对话的模板。3.2 把 hindsight 接成 MCP Server 的关键设计如果你要把一个记忆系统做成 MCP Server我建议的工具集是这样设计的工具名作用关键参数注意事项memory_write写入一条记忆content, layer, tags, ttllayer 决定存哪层ttl 控制过期memory_search检索记忆query, layer, top_k, time_range混合检索别只走向量memory_forget删除/失效记忆id 或 filter软删除优先保留审计memory_promote把 episodic 提升为 semanticsource_ids, summary需要去重和冲突检测memory_stats查看记忆规模layer用于监控和容量规划这里有个容易被忽略的坑MCP 工具的返回内容会直接进入 LLM 的上下文所以memory_search的返回必须做截断和排序。我见过有人一次返回 50 条记忆每条 500 字直接把上下文撑爆还稀释了真正相关的信息。正确做法是返回 top_k一般 3-5 条 每条摘要 一个展开的引用 id让模型按需再取。另一个坑是写入的幂等性。Agent 很容易重复写同一条记忆比如每轮都记住用户叫张三。MCP Server 侧必须做去重——可以用内容哈希也可以用语义相似度阈值。我一般设 0.92 的余弦相似度作为去重线超过就合并而不是新增。3.3 MCP 连接配置的实操细节热搜里出现 谷歌浏览器扩展设置中启用「mcp 连接」、wss://api.xiaozhi.me/mcp/?token... 这类内容说明 MCP 的接入方式已经不止本地 stdio 一种。常见的传输方式有两类stdio本地进程通信和HTTP/SSE 或 WebSocket远程通信。本地 stdio 的配置长这样以通用 MCP 客户端为例{ mcpServers: { hindsight-memory: { command: docker, args: [ run, -i, --rm, -e, MEMORY_BACKENDsqlite, -v, hindsight-data:/data, hindsight/memory-server:latest ] } } }远程方式则配置 URL 和 token{ mcpServers: { hindsight-memory: { url: https://your-host/mcp, headers: { Authorization: Bearer YOUR_TOKEN } } } }注意远程 MCP 一定要走鉴权token 不要硬编码在客户端配置文件里明文长期保存能走环境变量就走环境变量。stdio 方式虽然简单但每次调用都起一个容器冷启动开销不小高频场景建议用常驻服务 远程连接。4. Docker 部署实战从零把记忆服务跑起来4.1 环境准备与常见启动失败排查热搜里 docker安装、windows安装docker、linux安装docker、virtualization support not detected docker desktop failed to start 这些词说明很多人卡在第一步。我按平台说清楚。Windows上装 Docker Desktop最常见的报错就是 Virtualization support not detected。这不是 Docker 的锅是 BIOS 里的虚拟化开关没开。进 BIOS 找Intel VT-x或AMD-V打开。如果开了还报错检查是不是和 Hyper-V、WSL2 冲突——Docker Desktop 现在默认用 WSL2 后端需要确保 WSL2 已安装并设为默认版本wsl --install wsl --set-default-version 2Linux上装 Docker别用发行版自带的旧版本用官方脚本或官方源。装完记得把当前用户加进 docker 组否则每条命令都要 sudosudo usermod -aG docker $USER newgrp dockermacOS相对省心装 Docker Desktop 即可但注意 Apple Silicon 和 Intel 的镜像架构差异拉镜像时确认有 arm64 版本否则会走 Rosetta 模拟性能打折。4.2 用 Docker Compose 编排记忆服务单跑一个容器不够记忆服务通常需要应用 数据库 向量库三件套。我用 Docker Compose 编排这样一条命令起全套。下面是一个可参考的骨架version: 3.9 services: memory-api: image: hindsight/memory-server:latest ports: - 8080:8080 environment: - DB_URLpostgres://mem:mempostgres:5432/memory - VECTOR_URLhttp://qdrant:6333 - EMBEDDING_MODELbge-m3 depends_on: postgres: condition: service_healthy qdrant: condition: service_started volumes: - ./config:/app/config postgres: image: postgres:16 environment: - POSTGRES_USERmem - POSTGRES_PASSWORDmem - POSTGRES_DBmemory volumes: - pg-data:/var/lib/postgresql/data healthcheck: test: [CMD-SHELL, pg_isready -U mem] interval: 5s retries: 5 qdrant: image: qdrant/qdrant:latest volumes: - qdrant-data:/qdrant/storage volumes: pg-data: qdrant-data:为什么用 Postgres Qdrant 而不是全塞一个库因为结构化查询和向量检索的负载特征完全不同。Postgres 扛元数据、时间线、关系查询Qdrant 扛向量近邻搜索。混在一起要么向量检索慢要么事务能力弱。分开部署各自调优是更稳的选择。4.3 网络不通与容器间通信的坑热搜里 docker网络不通 是个高频问题。Compose 默认会创建一个 bridge 网络服务之间用服务名互相访问上面配置里postgres、qdrant就是主机名。但有几个坑第一别用 localhost。容器里的 localhost 是容器自己不是宿主机也不是别的容器。连数据库要用服务名。第二端口映射和容器内端口是两回事。ports: 8080:8080是宿主机 8080 映射到容器 8080容器内部服务监听的是后者。第三健康检查没配好会导致启动顺序错乱。上面depends_on里用了condition: service_healthy就是等 Postgres 真正能接受连接了再起 API否则 API 启动时连不上库直接崩。排查网络问题我常用的三板斧docker compose ps # 看容器状态 docker compose logs memory-api # 看应用日志 docker compose exec memory-api sh # 进容器内部测连通性 # 容器内 nc -zv postgres 5432 curl http://qdrant:6333/healthz提示如果容器内nc或curl不存在用getent hosts postgres先确认 DNS 解析再判断是网络层还是应用层问题。很多网络不通其实是 DNS 没解析对。5. 记忆写入与检索的核心实现细节5.1 写入Token 三元组与记忆的我是谁热搜里有一句很有意思的话llm的token三个点key我是谁、query我在找什么、value我能提供什么。这其实是在讲记忆的键值建模。一条记忆不是一段裸文本它至少要有主体谁记的、意图为什么记、内容记了什么。我设计写入接口时会强制要求这几个字段def memory_write(content, layer, actor, intent, tagsNone, ttlNone): content: 记忆正文 layer: working / episodic / semantic actor: 记忆归属哪个 agent / 哪个用户 intent: 写入意图为什么记这条 tags: 分类标签 ttl: 过期时间working memory 通常必填 embedding embed(content) record { content: content, layer: layer, actor: actor, intent: intent, tags: tags or [], embedding: embedding, created_at: now(), expires_at: now() ttl if ttl else None, } # 去重先查相似度 similar vector_search(embedding, top_k1) if similar and similar[0].score 0.92: return merge_memory(similar[0].id, record) return insert(record)为什么要actor和intent因为多 Agent 场景下记忆必须隔离。A Agent 的记忆不能污染 B Agent 的推理。而intent是检索时的重要过滤维度——用户问上次怎么解决的你只想召回解决方案类意图的记忆而不是所有相关记忆。5.2 检索混合检索与重排序纯向量检索的问题在于它对精确匹配不敏感。用户问错误码 E5021 怎么处理向量检索可能返回一堆讲错误处理的泛泛内容却漏掉真正提到 E5021 的那条。所以生产级记忆检索必须是混合检索向量召回 关键词召回BM25 元数据过滤然后重排序。我的实现顺序是元数据过滤先按 actor、layer、time_range 缩小范围。双路召回向量 top 20 BM25 top 20。融合用 RRFReciprocal Rank Fusion合并两路结果。重排序用一个小的 cross-encoder 模型对 top 10 精排。截断返回 top 3-5每条带摘要。def memory_search(query, actor, layerNone, top_k5): filters {actor: actor} if layer: filters[layer] layer vec_hits vector_search(embed(query), filtersfilters, top_k20) kw_hits bm25_search(query, filtersfilters, top_k20) fused rrf_merge(vec_hits, kw_hits) reranked cross_encoder_rerank(query, fused[:10]) return [summarize(h) for h in reranked[:top_k]]这套流程实测下来比纯向量检索的命中率提升非常明显尤其是在有专有名词、错误码、人名的场景。5.3 遗忘与提升记忆的生命周期管理记忆系统最容易被忽视的是遗忘机制。不遗忘的系统会越来越慢、越来越吵。我的策略是Working memory任务结束即归档或删除TTL 默认 24 小时。Episodic memory保留原始记录但定期做摘要压缩——把一周的同类事件合并成一条。Semantic memory从 episodic 中提升而来需要去重、冲突检测、人工或模型审核。提升promote这一步是 hindsight 这类系统的精髓。它不是自动把所有 episodic 都变成 semantic而是识别出反复出现的模式。比如同一个问题被解决了 5 次第 5 次之后就可以提炼成一条 semantic 规则。这里可以用一个简单的频次阈值 聚类def promote_candidates(actor, min_occurrence3): episodes fetch_episodic(actor, last_n_days30) clusters cluster_by_semantic_similarity(episodes, threshold0.85) candidates [] for c in clusters: if len(c) min_occurrence: summary llm_summarize([e.content for e in c]) candidates.append({summary: summary, sources: [e.id for e in c]}) return candidates注意提升这一步不要全自动。我踩过的坑是模型把一次性的偶发事件误判为规律结果 semantic memory 里塞了一堆错误规则反而误导后续推理。稳妥做法是提升候选先进入待审队列由人工或一个独立的校验 Agent 确认后再入库。6. 常见问题与排查技巧实录6.1 记忆检索答非所问的排查路径这是最高频的问题。用户明明问过Agent 却说不知道。排查顺序我总结成一张表现象可能原因排查方法解决完全召回不到写入失败或 actor 不匹配查 DB 里有没有这条记录检查写入链路和 actor 隔离召回但排序靠后向量模型不适配领域看 top 20 里有没有目标换领域 embedding 或加 BM25召回但内容被截断摘要过度压缩对比原文和摘要调整摘要长度或返回原文引用召回旧版本没有失效机制查 created_at 和 ttl加时间衰减权重多 Agent 串味actor 过滤缺失查检索 filter强制 actor 隔离我特别想强调时间衰减。记忆检索的排序分数里应该加入时间因子让新记忆有更高权重。公式可以简单到final_score relevance * exp(-lambda * age_days)lambda 取 0.01 到 0.05 之间具体看业务对新鲜度的敏感程度。6.2 LLM 请求被拒与 Schema 问题热搜里 llm request failed: provider rejected the request schema or tool payload 是个典型报错。这通常发生在 MCP 工具调用时工具的参数 schema 和模型实际传的不一致。常见原因参数类型不匹配模型传字符串schema 要整数、必填字段缺失、枚举值超出范围。我的处理经验是schema 要宽松校验要严格。schema 里尽量用string而不是integer然后在服务端做类型转换和校验。因为不同模型对 schema 的理解能力差异很大过于严格的 schema 会导致大量调用失败。同时工具描述description要写得极其清楚把每个参数的用途、格式、示例都写进去——模型是靠描述来决定怎么填参数的。6.3 性能与容量什么时候该分库记忆量上来之后单库会扛不住。我的经验阈值是单表超过 500 万条或者 P99 检索延迟超过 500ms就该考虑分片了。分片维度优先选actor按用户/Agent 分因为记忆查询天然带 actor 过滤分片后查询能精准路由。另一个优化点是冷热分离。最近 7 天的记忆放热存储内存或 SSD更早的放冷存储。检索时先查热miss 了再查冷。这个策略能把大部分查询的延迟压到很低因为实际使用中绝大多数检索都集中在近期记忆上。7. 安全与防御a-memguard 带来的启示热搜里 a-memguard: a proactive defense framework for llm-based agent memory 这个词值得单独说。记忆系统一旦成为基础设施它就成了攻击面。攻击者可以通过污染记忆来操纵 Agent 的行为——比如往记忆里写入这个用户是管理员或这个操作是安全的后续 Agent 就会照着执行。防御思路有几层。写入侧要做来源校验和内容审核不是所有输入都能直接进记忆。存储侧要做完整性校验防止记忆被篡改。读取侧要做可信度标注让模型知道哪些记忆是高置信的、哪些是待验证的。a-memguard 提的proactive主动防御我理解就是在写入和提升环节就介入而不是等出事了再查。实操上我建议至少做两件事一是记忆写入审计日志每条记忆记录来源、时间、写入者可追溯二是敏感记忆隔离涉及权限、凭证、关键配置的记忆单独存储检索时走更严格的校验。这两条不复杂但能挡掉大部分低级攻击。8. 我在这套东西上踩过的几个真实坑最后分享几个只有真跑过才会知道的细节。第一个坑embedding 模型换了历史记忆全废。我一开始用某个通用 embedding后来换成领域模型发现新旧向量不在同一空间检索直接乱套。教训是embedding 模型版本必须和记忆一起版本化换模型时要全量重算或者双写过渡。别想着混用。第二个坑working memory 忘了设 TTL。结果跑了一个月working memory 表比 episodic 还大检索慢得离谱。working memory 的本质是临时草稿纸用完就该扔一定要设过期。第三个坑MCP 工具返回太长把上下文撑爆。前面提过但值得再强调。记忆检索的返回一定要摘要 引用而不是全文。让模型按需展开而不是一次喂饱。第四个坑多 Agent 共享记忆没做隔离。两个 Agent 用同一个 actor结果 A 的临时状态被 B 当成事实。actor 隔离是硬要求不是可选项。第五个坑以为向量库能扛一切。实际上元数据过滤、时间范围查询、关系查询向量库都很弱。老老实实上 Postgres 做结构化层向量库只做它擅长的事。这套 hindsight 式的记忆架构我现在的做法是Postgres 管元数据和关系Qdrant 管向量MCP 做统一出口Docker Compose 做编排。四件套跑起来之后Agent 的失忆症基本治好了。至于 semantic 层的图谱化等 episodic 数据攒够了再上不急。
网站建设高端定制企业官网