恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
LLM Agent 长期记忆架构实战:基于 MCP 与 Docker 的分层记忆系统
首页
资讯中心
/
LLM Agent 长期记忆架构实战:基于 MCP 与 Docker 的分层记忆系统
LLM Agent 长期记忆架构实战:基于 MCP 与 Docker 的分层记忆系统
发布时间:2026/9/30 15:21:36
1. 从“hindsight”说起为什么我们需要给 Agent 装上“后视之明”“hindsight”这个词本身很有意思字面意思是“事后的洞察力”也就是我们常说的“后见之明”。放在 LLM Agent 的语境里它指向一个非常具体且棘手的问题Agent 的记忆系统到底该怎么设计才能让它在后续任务中真正“记得住、找得回、用得上”之前发生过的事情。我接触过不少做 Agent 的团队大家一开始都很兴奋地把 LLM 接上工具、接上 MCP跑几个 demo 觉得效果惊艳但一旦进入多轮、长周期、跨会话的真实场景问题就暴露了Agent 会忘记三天前用户说过的偏好会重复问已经回答过的问题会在长对话里把早期关键约束丢掉。这不是模型能力不够而是记忆架构没搭对。“hindsight”这个项目标题我理解它要解决的核心就是 Agent 的长期记忆与回溯能力。它不是一个单纯的向量数据库封装而是围绕agent memory这一层结合LLM的推理能力、MCP的标准化工具调用协议以及Docker带来的可复现部署环境去构建一套“可回溯、可检索、可推理”的记忆系统。适合谁来参考我认为三类人最需要一是正在做多轮对话产品的工程师二是研究 Agent 长期记忆机制的研究者三是想把 MCP 生态用起来的应用开发者。哪怕你只是刚听说 MCP 是什么这篇文章也会从最基础的概念讲起让你能跟着把一套记忆系统跑起来。我下面会从整体设计思路、核心细节、实操落地、问题排查四个大块展开中间穿插我自己踩过的坑和实测有效的参数配置。内容会比较长但都是能直接抄作业的东西。2. 整体设计与思路拆解记忆不是数据库是分层结构2.1 为什么“把对话存进向量库”远远不够很多人对 Agent 记忆的第一反应是把历史对话 embedding 一下塞进向量数据库需要的时候检索 top-k 不就行了我早期也这么干过实测下来问题一大堆。最典型的是检索出来的片段缺乏上下文关联Agent 拿到一段孤立的对话根本不知道这段话是在什么前提下说的。比如用户说过“预算控制在五千以内”单独检索出来Agent 不知道这是买相机还是买电脑的预算用起来就会出错。“hindsight”这类项目要解决的就是这个断层。它的核心思路我拆成三层工作记忆working memory、情景记忆episodic memory、语义记忆semantic memory。工作记忆就是当前会话的上下文窗口这个 LLM 本身就有情景记忆是“什么时候发生了什么”带时间戳和事件边界语义记忆是从大量情景中抽象出来的稳定知识比如“这个用户偏好简洁回复”。三层各司其职检索时按需调用而不是一股脑全塞给模型。这个分层不是拍脑袋想的它对应认知科学里人类记忆的经典模型。放到工程上好处非常实在工作记忆保证当前对话流畅情景记忆保证跨会话能回溯具体事件语义记忆保证 Agent 对用户和领域有稳定认知。三者用不同的存储和检索策略成本和效果都能兼顾。2.2 MCP 在这里扮演什么角色记忆的“标准接口层”MCP 全称 Model Context Protocol你可以把它理解成一套让 LLM 和外部工具、数据源对话的“通用插座标准”。在“hindsight”里MCP 的价值在于把记忆的读写操作标准化成工具调用。Agent 不需要在 prompt 里硬编码“去查数据库”而是通过 MCP 暴露的memory_search、memory_write、memory_summarize这类工具由模型自己决定什么时候该记、什么时候该查。我为什么强调这一点因为记忆系统的难点从来不是存储而是决策——什么时候该写入、写入什么粒度、什么时候该检索、检索多少条。把这些决策交给 LLM 通过 MCP 工具来驱动比写死规则灵活得多。比如用户说“我下周要去杭州出差”Agent 可以调用memory_write把这条存成情景记忆同时触发一次memory_summarize更新语义记忆里的“用户近期行程”。整个过程模型自主判断不需要你写 if-else。MCP 还有一个隐性好处可替换性。今天你用本地 SQLite 做记忆后端明天想换成 Postgres 或者云服务只要 MCP 工具接口不变Agent 侧代码一行不用改。这对快速迭代的项目太重要了。2.3 Docker 化部署让记忆系统“开箱即跑”记忆系统涉及多个组件LLM 推理服务、向量数据库、关系型数据库、MCP Server、可能还有 embedding 模型。如果每个都手动装光是环境依赖就能劝退一半人。Docker 在这里的作用是把整套环境打包成可复现的镜像一条docker compose up就能拉起全部服务。我实测下来用 Docker Compose 编排是最省心的方案。一个compose.yaml里定义好mcp-server、vector-db、postgres、redis四个服务网络内部互通数据卷持久化。这样无论你是在 Mac、Windows 还是 Linux 上只要 Docker 装好跑起来的行为是一致的。后面我会给出具体的 compose 配置和参数说明。3. 核心细节解析与实操要点记忆的写入、检索与衰减3.1 记忆写入Token 三元组“我是谁、我在找什么、我能提供什么”热词里有一条很有意思“llm的token三个点key我是谁、query我在找什么、value我能提供什么”。这其实是在用类比讲注意力机制里的 QKV但放到记忆系统里同样适用。我在设计记忆写入时就是围绕这三个问题来决定存什么Key我是谁这条记忆的主体是谁是用户、是 Agent 自己、还是某个外部实体。写入时必须打标签否则后续检索会混淆。Query我在找什么这条记忆未来可能在什么场景下被检索到这决定了你要给记忆打哪些检索维度比如时间、主题、情感倾向。Value我能提供什么这条记忆的实际内容以及它的置信度和时效性。具体到代码层面我用的写入结构大概是这样memory_item { id: uuid4().hex, agent_id: assistant-01, user_id: user-42, content: 用户偏好用中文回复且不喜欢冗长解释, memory_type: semantic, tags: [preference, language, style], confidence: 0.92, created_at: timestamp, last_accessed: timestamp, access_count: 0, decay_score: 1.0 }这里有几个参数是我反复调过的。confidence表示这条记忆的可信度从对话里直接抽取的可以给高一点从模型推断出来的要给低一点避免错误记忆污染。decay_score是衰减分数初始为 1.0随着时间推移和未被访问而下降检索时优先返回高分记忆。这个机制模拟了人类记忆的遗忘曲线实测能有效控制记忆库膨胀。注意写入粒度一定要控制。我见过有人把每一轮对话原封不动存进去结果记忆库几周就爆了检索质量还差。正确做法是先做一轮摘要或抽取把“用户说了什么”压缩成“用户表达了什么偏好/事实/意图”再写入。3.2 记忆检索混合检索比纯向量检索稳得多纯向量检索在记忆场景下有个致命问题它擅长语义相似不擅长精确匹配和时间过滤。用户问“我上次说的那个截止日期是哪天”向量检索可能返回一堆关于日期的泛泛对话但真正需要的是那条带具体日期的情景记忆。我的方案是混合检索向量相似度 关键词匹配 时间衰减加权 标签过滤四路结果融合排序。具体权重我调了很久最终稳定在一组参数上向量相似度占 0.5关键词 BM25 占 0.2时间新鲜度占 0.2访问频率占 0.1。这个配比不是理论最优但在我的测试集上召回率和准确率的平衡最好。你可以根据自己场景调整比如客服场景可以加大时间权重知识问答场景可以加大向量权重。检索的伪代码逻辑def retrieve_memories(query, user_id, top_k8): vector_hits vector_db.search(embed(query), top_ktop_k*2) keyword_hits bm25_index.search(query, top_ktop_k*2) merged merge_and_dedupe(vector_hits, keyword_hits) scored [] for m in merged: score (0.5 * m.vector_score 0.2 * m.keyword_score 0.2 * freshness(m.last_accessed) 0.1 * min(m.access_count / 10, 1.0)) scored.append((score, m)) scored.sort(reverseTrue) return [m for _, m in scored[:top_k]]检索出来之后不要直接把原始记忆塞进 prompt。我习惯再做一步记忆重排和压缩把 top-k 记忆按主题聚类每组用 LLM 压缩成一句话再拼进上下文。这样既保留了信息又控制了 token 消耗。实测下来8 条原始记忆压缩后大约占 200 token比直接塞 800 token 效果好很多。3.3 记忆衰减与遗忘不是bug是feature很多人舍不得删记忆觉得存得越多越好。我一开始也这样结果 Agent 检索时被大量过时、低价值记忆干扰回答质量反而下降。后来我引入了衰减机制让记忆像人类一样自然遗忘。衰减公式我用的是指数衰减加访问增强decay_score base_score * exp(-λ * days_since_last_access) α * log(1 access_count)其中 λ 控制衰减速度我设成 0.05意味着大约 14 天不访问分数降到初始的 50% 左右。α 是访问增强系数设成 0.1让频繁访问的记忆保持高权重。当decay_score低于 0.2 时记忆进入“冷存储”不再参与常规检索但保留在库里以备归档查询。这个机制带来的好处很直接记忆库规模可控检索信噪比高Agent 回答更聚焦。我建议你在上线前先跑一周观察衰减分布再微调 λ 和 α不同业务节奏差别很大。4. 实操过程与核心环节实现从零把 hindsight 跑起来4.1 环境准备Docker 安装与常见坑第一步是把 Docker 装好。Windows 用户最容易遇到的就是 “Virtualization support not detected” 这个报错Docker Desktop 起不来。原因通常是 BIOS 里没开虚拟化或者 Hyper-V/WSL2 没启用。解决顺序是先进 BIOS 开 Intel VT-x 或 AMD-V然后在 Windows 功能里勾选“虚拟机平台”和“适用于 Linux 的 Windows 子系统”重启后再装 Docker Desktop。Linux 用户相对简单用官方脚本装就行curl -fsSL https://get.docker.com -o get-docker.sh sudo sh get-docker.sh sudo usermod -aG docker $USER装完记得重新登录一次让用户组生效。验证用docker run hello-world能跑通就说明基础环境没问题。提示国内网络环境下拉镜像可能慢建议配置镜像加速器。具体在 Docker Desktop 的 Settings 里找 Docker Engine加一行 registry-mirrors 配置即可。这里不展开具体地址你按所在环境选择合规的加速服务。4.2 用 Docker Compose 编排记忆系统我把整套 hindsight 拆成四个服务写在一个compose.yaml里version: 3.9 services: mcp-server: build: ./mcp-server ports: - 8080:8080 environment: - VECTOR_DB_URLhttp://vector-db:6333 - POSTGRES_URLpostgresql://mem:mempostgres:5432/memory - REDIS_URLredis://redis:6379 depends_on: - vector-db - postgres - redis vector-db: image: qdrant/qdrant:latest volumes: - ./data/qdrant:/qdrant/storage ports: - 6333:6333 postgres: image: postgres:16 environment: - POSTGRES_USERmem - POSTGRES_PASSWORDmem - POSTGRES_DBmemory volumes: - ./data/pg:/var/lib/postgresql/data redis: image: redis:7-alpine volumes: - ./data/redis:/data这里选 Qdrant 做向量库是因为它支持过滤和 payload 索引做混合检索很方便。Postgres 存结构化记忆元数据Redis 做热点记忆缓存和会话状态。三个存储各司其职不要试图用一个数据库全包我试过性能和灵活性都吃亏。启动就一条命令docker compose up -d第一次会拉镜像、编译 mcp-server大概几分钟。起来之后用docker compose ps确认四个服务都是 running 状态。4.3 MCP Server 的核心工具实现MCP Server 是记忆系统的对外接口我实现了五个核心工具工具名作用关键参数memory_write写入一条记忆content, memory_type, tags, confidencememory_search检索记忆query, top_k, memory_type_filtermemory_summarize压缩记忆为摘要memory_ids, max_tokensmemory_forget主动遗忘memory_id 或 filter 条件memory_stats查看记忆库状态user_id以memory_search为例核心逻辑就是前面讲的混合检索。我用 Python 的mcpSDK 来写暴露成标准 MCP 工具这样任何支持 MCP 的客户端都能直接调用。实测下来Claude Desktop、Trae IDE 这些都能无缝接入。from mcp.server import Server from mcp.types import Tool, TextContent app Server(hindsight-memory) app.list_tools() async def list_tools(): return [ Tool( namememory_search, description检索 Agent 长期记忆返回最相关的记忆片段, inputSchema{ type: object, properties: { query: {type: string}, top_k: {type: integer, default: 8}, memory_type: {type: string, enum: [episodic, semantic, all]} }, required: [query] } ) ] app.call_tool() async def call_tool(name, arguments): if name memory_search: results retrieve_memories( arguments[query], top_karguments.get(top_k, 8), memory_typearguments.get(memory_type, all) ) return [TextContent(typetext, textformat_results(results))]这段代码的关键在于inputSchema要写清楚模型才能正确构造调用参数。我踩过的坑是 schema 里类型写错导致模型传参格式不对工具调用一直失败。后来养成习惯每个工具上线前先用 MCP Inspector 手动测一遍。4.4 接入 LLM 与 Agent 循环MCP Server 跑起来后下一步是让 LLM 用上它。以常见的 Agent 框架为例你需要在系统提示里告诉模型你有记忆工具可用什么时候该用。我的提示词模板大概是这样你是一个带长期记忆的助手。在回答前先判断是否需要检索记忆 - 如果用户提到过去的事情、偏好、约定调用 memory_search - 如果用户提供了新的稳定信息偏好、事实、计划调用 memory_write - 如果对话很长定期调用 memory_summarize 压缩上下文 记忆检索结果会以 [MEMORY] 开头注入请结合这些信息回答。实测这个提示词能让模型在 80% 以上的场景正确触发记忆工具。剩下的 20% 主要是边界情况比如用户说“随便”这种模糊表达模型不确定要不要记。我的处理是加一条规则不确定时优先检索写入则要求置信度高于 0.7 才执行。Agent 循环里记忆检索和工具调用是并行的。我一般把memory_search放在第一轮拿到结果后再决定是否调用其他工具。这样能避免记忆检索被其他工具调用挤掉。5. 常见问题与排查技巧实录5.1 记忆检索不准先查 embedding 模型再查分块策略检索不准是最常见的问题。我的排查顺序是第一看 embedding 模型是否适合中文场景很多开源模型英文强中文弱换一个多语言模型效果立竿见影。第二看记忆分块粒度太细会丢上下文太粗会引入噪声我一般控制在 100-300 字一条。第三看混合检索权重是否合理纯向量不行就加关键词纯关键词不行就加向量。有个具体案例用户问“我上次说的那个项目截止日期”检索总是返回无关内容。后来发现是记忆写入时没打时间标签检索时无法按时间过滤。加上event_time字段并在检索时做时间范围过滤后准确率从 40% 提到 85%。5.2 Docker 网络不通九成是服务名和端口写错Docker Compose 里服务之间通信用服务名不是 localhost。我见过太多人把VECTOR_DB_URL写成http://localhost:6333结果容器内根本连不上。正确写法是http://vector-db:6333其中vector-db是 compose 里定义的服务名。如果确认服务名没错还是不通用docker compose exec mcp-server ping vector-db测连通性。ping 不通就检查两个服务是否在同一网络compose 默认会创建共享网络但如果你手动指定了 network 就要确认配置一致。5.3 记忆库膨胀定期归档 冷热分离跑一段时间后记忆库会变大检索变慢。我的做法是冷热分离decay_score 高于 0.5 的热记忆留在 Qdrant 主集合低于 0.5 的移到冷存储集合检索时默认只查热集合。归档任务用定时脚本每天跑一次把冷记忆批量迁移。另外语义记忆要定期合并。比如用户多次表达“喜欢简洁回复”不要存成十条而是合并成一条并提高 confidence。我写了个简单的合并逻辑同 user_id、同 tags、内容相似度高于 0.9 的记忆用 LLM 合并成一条。5.4 常见问题速查表现象可能原因排查动作解决方向工具调用失败inputSchema 类型错误用 MCP Inspector 手动测修正 schema 类型定义检索结果无关embedding 模型不匹配换多语言模型对比更换或微调 embedding记忆写入过多未做摘要直接存原文查看写入日志加摘要抽取步骤容器间不通URL 用了 localhostping 服务名测试改用 compose 服务名响应变慢记忆库过大查集合大小和索引冷热分离 归档记忆冲突新旧信息矛盾查同主题记忆加时间戳新覆盖旧提示每次改动记忆策略后一定要用固定测试集回归。我维护了一个 50 条的问答对覆盖偏好回忆、事实回溯、时间查询等场景改完跑一遍看准确率变化避免拍脑袋调参。6. 我在实际项目里的一些体会这套 hindsight 记忆架构我在两个项目里落地过一个是客服 Agent一个是个人助理 Agent。客服场景下情景记忆的时效性要求极高我把时间衰减系数调到 0.1让一周前的记忆快速降权效果比默认值好很多。个人助理场景则相反用户偏好这类语义记忆要长期稳定我把语义记忆的衰减系数设成 0.01基本不衰减。还有一个体会是记忆系统的价值不在于存了多少而在于检索时能不能把对的那条找出来。我早期追求记忆库规模后来发现精简后的记忆库检索准确率反而更高。现在我的原则是宁可少存不可乱存宁可多压缩不可直接塞。最后分享一个小技巧给记忆加一个source字段记录这条记忆是从哪轮对话、哪个工具调用产生的。排查问题时能快速定位来源也能在记忆冲突时判断哪条更可信。这个字段我一开始没加后来补数据补得很痛苦建议你一开始就设计进去。