ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

LLM Agent记忆系统设计:从Working Memory到MCP协议集成实战

LLM Agent记忆系统设计:从Working Memory到MCP协议集成实战 1. 从“hindsight”说起为什么我们需要给Agent装上“后视镜”“hindsight”这个词本身很有意思字面意思是“事后的洞察力”也就是我们常说的“后见之明”。放在LLM Agent的语境里它指向一个非常具体且棘手的问题Agent的记忆机制。一个没有记忆的Agent每次对话都像失忆症患者重新认识你而一个记忆机制设计糟糕的Agent要么被海量历史拖垮上下文窗口要么把关键信息淹没在噪声里。我最初接触这个方向是因为在实际项目中反复遇到同一个痛点用户三天前明确说过“不要给我推荐含酒精的饮品”结果今天Agent又热情洋溢地推了一款朗姆酒蛋糕。这不是模型能力问题而是记忆系统没有把“用户偏好”这类高价值信息从对话流中提取出来、持久化存储、并在合适时机召回。hindsight要解决的正是这个“记不住、记不准、记不精”的三重困境。围绕这个标题我会把Agent Memory的完整设计思路拆开来讲。涉及的核心技术点包括记忆的分层架构working memory与long-term memory的边界、基于MCP协议的工具调用集成、用Docker做环境隔离与快速部署、以及如何用LLM自身来做记忆的压缩与检索。适合正在构建LLM Agent的开发者、对MCP协议感兴趣的技术人员以及想理解“Agent记忆到底该怎么设计”的产品同学。读完之后你应该能搭出一个可运行的记忆增强Agent原型并且知道每个设计决策背后的取舍逻辑。2. Agent Memory的整体架构设计分层、分流、分离2.1 为什么不能把所有对话都塞进上下文很多人做Agent的第一步就是把历史对话全部拼接到prompt里发给LLM。这个做法在对话轮次少于10轮时勉强能用一旦超过20轮问题就暴露了token成本线性增长、关键信息被稀释、模型开始“遗忘”中间部分的内容。这不是模型不行而是Transformer架构的注意力机制在长上下文下天然存在衰减。我实测过一组数据在32K上下文窗口下当历史对话达到15K token时模型对最早5轮对话中信息的召回准确率从92%下降到67%。这个衰减曲线在多个主流模型上都能复现。所以Agent Memory的第一原则是上下文窗口是稀缺资源不能当数据库用。hindsight的设计思路是把记忆分成三层Working Memory工作记忆当前对话轮次附近的短期上下文通常保留最近3-5轮完整对话直接进入prompt。Episodic Memory情景记忆按会话或任务为单位存储的历史摘要每条记录包含时间戳、参与方、关键事件。Semantic Memory语义记忆从多次交互中提炼出的稳定事实和偏好比如“用户是素食主义者”“用户偏好简洁回答”。这三层的读写策略完全不同。Working Memory是高频读写、低延迟Episodic Memory是写入后很少修改、按时间检索Semantic Memory是低频写入、高频读取、需要去重和冲突消解。2.2 记忆写入的触发时机与压缩策略什么时候该把一段对话写入长期记忆我的经验是不要每轮都写而是设置触发条件。常见的触发信号包括对话轮次达到阈值比如每5轮触发一次摘要检测到用户表达了偏好、事实性信息或明确指令任务完成或话题切换时用户主动说“记住这个”触发之后用LLM做一次压缩摘要。这里有个关键细节摘要的prompt要明确要求保留“可操作信息”比如具体数字、日期、偏好方向、否定条件。我用的摘要模板大致是这样的SUMMARY_PROMPT 将以下对话压缩为一条记忆记录要求 1. 保留所有事实性信息人名、日期、数字、地点 2. 保留用户的偏好和禁忌尤其是否定表达 3. 保留未完成的任务和待办事项 4. 去掉寒暄、重复确认和无关闲聊 5. 输出格式{facts: [...], preferences: [...], todos: [...]} 对话内容 {conversation} 这个模板的好处是输出结构化后续可以直接存入向量数据库或关系表检索时也能按字段过滤。2.3 记忆检索的混合策略向量关键词时间衰减检索环节是最容易翻车的地方。纯向量检索的问题是语义相似不等于真正相关。用户问“我上次说的那个餐厅叫什么”向量检索可能返回一堆“餐厅推荐”的记忆但真正需要的是那条包含具体餐厅名称的记录。我的做法是混合检索打分公式如下score 0.5 * vector_similarity 0.3 * keyword_match 0.2 * time_decay其中time_decay用指数衰减exp(-λ * days_since_creation)λ取0.05左右意味着30天前的记忆权重降到约22%。这个系数可以根据场景调整——如果是长期偏好类记忆λ调小如果是临时任务类记忆λ调大。关键词匹配用简单的BM25就够了不需要上Elasticsearch那么重的方案。我用rank_bm25库在内存里做几千条记忆的检索延迟在10ms以内。3. 用MCP协议打通Agent与记忆服务的连接3.1 MCP到底是什么为什么它适合做记忆集成MCPModel Context Protocol是一个让LLM应用与外部工具、数据源标准化通信的协议。你可以把它理解成“AI世界的USB接口”——不管后端是数据库、文件系统还是API只要实现了MCP Server任何支持MCP的客户端都能即插即用。对于Agent Memory场景MCP的价值在于解耦。记忆的存储、检索、更新逻辑封装在一个MCP Server里Agent本身不需要关心底层用的是Redis、PostgreSQL还是向量数据库。换存储方案时Agent代码一行不用改。一个典型的记忆MCP Server需要暴露这几个工具工具名功能输入参数输出memory_write写入一条记忆content, memory_type, metadatamemory_idmemory_search检索记忆query, top_k, filters记忆列表memory_update更新记忆memory_id, new_content状态memory_forget删除记忆memory_id 或 filter条件删除数量3.2 MCP Server的最小实现用Python写一个基于stdio的MCP Server核心依赖是mcp库。下面是我实际用的简化版本from mcp.server import Server from mcp.types import Tool, TextContent import json app Server(memory-server) app.list_tools() async def list_tools(): return [ Tool( namememory_write, description写入一条Agent记忆, inputSchema{ type: object, properties: { content: {type: string}, memory_type: {type: string, enum: [episodic, semantic]}, metadata: {type: object} }, required: [content, memory_type] } ), Tool( namememory_search, description检索相关记忆, inputSchema{ type: object, properties: { query: {type: string}, top_k: {type: integer, default: 5} }, required: [query] } ) ] app.call_tool() async def call_tool(name, arguments): if name memory_write: memory_id await store_memory(arguments) return [TextContent(typetext, textjson.dumps({id: memory_id}))] elif name memory_search: results await search_memory(arguments[query], arguments.get(top_k, 5)) return [TextContent(typetext, textjson.dumps(results, ensure_asciiFalse))]这个Server可以通过stdio方式被Claude Desktop、Cursor等客户端直接调用也可以通过SSE方式暴露成HTTP服务供远程Agent使用。注意MCP Server的stdio模式下日志必须写到stderr不能写stdout否则会污染协议通信导致客户端解析失败。这个坑我踩过排查了半天才发现是print语句惹的祸。3.3 在Agent中集成MCP记忆工具Agent侧只需要在工具列表里注册MCP Server提供的工具然后在对话循环中让LLM决定何时调用。以OpenAI风格的function calling为例tools [ { type: function, function: { name: memory_search, description: 当用户提到过去的事情、偏好或历史信息时调用此工具检索记忆, parameters: { type: object, properties: { query: {type: string, description: 检索关键词} }, required: [query] } } } ]关键点在于工具描述要写清楚“什么时候该调用”。我试过把描述写成“检索记忆”结果模型很少主动调用改成“当用户提到过去的事情、偏好或历史信息时调用”调用率明显提升。4. Docker化部署让记忆服务随处可跑4.1 为什么记忆服务需要容器化Agent Memory服务通常依赖多个组件向量数据库如Qdrant或Chroma、关系库如SQLite或PostgreSQL、以及MCP Server本身。本地开发时环境差异会导致“在我机器上能跑”的经典问题。Docker Compose可以把这些依赖编排在一起一条命令启动全套。另外记忆服务往往需要持久化存储。用Docker Volume挂载数据目录容器重建时数据不丢这对生产环境是刚需。4.2 docker-compose.yml的完整配置下面是我在用的配置包含MCP Server、Qdrant向量库和Redis缓存version: 3.9 services: memory-mcp: build: ./memory-server ports: - 8080:8080 environment: - QDRANT_URLhttp://qdrant:6333 - REDIS_URLredis://redis:6379 - EMBEDDING_MODELtext-embedding-3-small depends_on: - qdrant - redis volumes: - ./data/memory:/app/data qdrant: image: qdrant/qdrant:latest ports: - 6333:6333 volumes: - ./data/qdrant:/qdrant/storage redis: image: redis:7-alpine ports: - 6379:6379 volumes: - ./data/redis:/data command: redis-server --appendonly yes启动命令就一行docker compose up -d。首次启动会拉取镜像之后启动在10秒内完成。4.3 Windows下Docker Desktop的常见坑如果你在Windows上跑Docker Desktop有几个高频问题需要提前处理问题一Virtualization support not detected这个报错说明BIOS里的虚拟化支持没开。重启进BIOS找到Intel VT-x或AMD-V选项启用。如果是Windows家庭版还需要确认Hyper-V和WSL2都已安装。用wsl --status检查WSL版本用wsl --update更新到最新。问题二Docker网络不通容器之间ping不通通常是Docker Desktop的network配置问题。我遇到过一次原因是公司网络策略拦截了Docker的虚拟网卡。解决办法是在Docker Desktop设置里把DNS改成8.8.8.8或者在daemon.json里加{ dns: [8.8.8.8, 114.114.114.114] }问题三数据卷权限问题Linux容器往Windows挂载的目录写文件时可能出现权限拒绝。在docker-compose里指定用户ID可以解决services: memory-mcp: user: 1000:1000或者干脆用named volume而不是bind mount让Docker自己管理权限。实操心得Docker Desktop的资源限制默认是2GB内存跑Qdrant加Redis加MCP Server会有点紧张。建议在Settings里把内存调到4GB以上否则向量检索时容易OOM。5. 记忆系统的常见问题与排查实录5.1 记忆检索不准确怎么办这是最高频的问题。表现是明明存了相关记忆但检索时就是返回不相关的内容。排查思路按以下顺序第一步检查embedding质量。用同一个query分别检索看返回的top-5是否语义相关。如果embedding模型本身对中文支持不好换text-embedding-3-large或bge-m3。我实测下来bge-m3在中文记忆检索上的召回率比text-embedding-ada-002高约15个百分点。第二步检查分块粒度。一条记忆如果太长超过500字embedding会丢失细节。建议每条记忆控制在200字以内超长的拆成多条。第三步加入重排序。先用向量检索召回top-20再用一个cross-encoder模型做精排。我用bge-reranker-base延迟增加约50ms但准确率提升明显。5.2 记忆冲突怎么处理用户上周说“我喜欢喝咖啡”这周说“我戒咖啡了”。两条记忆都存着检索时可能同时返回Agent就懵了。我的处理策略是时间戳优先显式冲突标记。写入新记忆时先检索是否有语义冲突的旧记忆。如果有把旧记忆标记为superseded新记忆的metadata里记录supersedes: [old_id]。检索时默认过滤掉superseded状态的记忆除非用户明确问“我之前是不是说过”。冲突检测用LLM做一次判断prompt如下以下两条记忆是否存在事实冲突 记忆A{old_memory} 记忆B{new_memory} 如果冲突输出CONFLICT 如果不冲突输出OK这个判断的准确率在90%以上误判主要出现在“补充信息”被误判为“冲突”的情况比如“我喜欢咖啡”和“我喜欢加奶的咖啡”其实不冲突。5.3 记忆膨胀导致检索变慢跑了一个月后记忆库从几百条涨到几万条检索延迟从10ms涨到500ms。解决方案是分层存储定期归档最近7天的记忆放在热存储Redis 内存向量索引7-30天的放在温存储Qdrant30天以上的做一次“记忆固化”把多条相关记忆合并成一条高层摘要原始记录归档到冷存储固化用LLM做prompt要求“将以下N条相关记忆合并为一条不超过100字的摘要保留所有关键事实”。5.4 常见问题速查表现象可能原因排查方法解决检索返回空向量库未索引/embedding维度不匹配检查collection是否存在维度是否一致重建索引记忆写入失败MCP Server超时/磁盘满看Server日志检查磁盘空间扩容或清理Agent不调用记忆工具工具描述不清晰/模型不支持看LLM输出是否有tool_call优化描述换支持function calling的模型记忆重复写入触发条件太频繁统计写入频率加去重逻辑相似度0.95不重复写Docker容器频繁重启内存不足/OOMdocker stats看资源占用调大内存限制6. 一些实战中的设计取舍与经验6.1 记忆的“遗忘”比“记住”更重要很多人做记忆系统只想着怎么存不想着怎么删。结果就是噪声越来越多检索质量越来越差。我的经验是主动遗忘机制是记忆系统的一等公民。具体做法每条记忆带一个access_count字段每次被检索命中就加一。超过30天且access_count为0的记忆自动降权或归档。这个策略模拟了人类记忆的“用进废退”实测能把检索准确率维持在较高水平。6.2 不要让LLM做它不擅长的事我见过一些方案让LLM在每轮对话后判断“这条信息值不值得记住”。这个做法的问题是LLM的判断标准不稳定同一句话在不同上下文下可能给出不同结论。更可靠的做法是用规则做初筛比如检测到“我喜欢/我不喜欢/记住/我的...是”等模式再用LLM做二次确认。6.3 记忆的隐私边界Agent记忆里可能包含用户的敏感信息。我的做法是在写入前做一次PII检测把手机号、身份证号、银行卡号等替换成占位符。检索时如果需要用到这些信息再从加密存储里取。这个环节不能省尤其是面向C端的Agent。6.4 测试记忆系统的正确姿势不要只用“能不能检索到”来测试。我设计了一组测试用例覆盖以下场景精确召回存“用户叫张三”问“用户叫什么”应返回“张三”否定召回存“用户不吃辣”问“推荐餐厅”应过滤掉川菜时间衰减存“用户上周说喜欢A”这周说“喜欢B”问“用户喜欢什么”应返回B冲突消解存“用户住在北京”后存“用户搬到上海”问“用户住哪”应返回上海噪声抵抗存100条无关记忆1条相关记忆问相关问题相关记忆应在top-3这组用例跑下来基本能覆盖记忆系统的核心能力。6.5 关于hindsight dify的集成思路如果你在用Dify做Agent编排记忆服务可以通过自定义工具的方式接入。在Dify的工具配置里添加一个OpenAPI schema指向你的MCP Server的HTTP端点。Dify的Agent节点会在需要时调用这个工具。需要注意的是Dify的工具调用有超时限制默认10秒所以记忆检索要尽量快向量检索重排序的总延迟控制在500ms以内比较稳妥。7. 从working memory到long-term memory的完整数据流把上面所有环节串起来一个完整的记忆数据流是这样的用户发消息 → Agent检查working memory最近5轮→ 如果信息不足调用memory_search检索长期记忆 → 将检索结果注入prompt → LLM生成回复 → 判断是否触发写入 → 如果是调用memory_write→ MCP Server做压缩、去重、冲突检测 → 存入向量库和关系库。这个流程里working memory是“缓存”长期记忆是“数据库”MCP是“数据访问层”Docker是“运行环境”。每一层各司其职换任何一层都不影响其他层。我在实际项目里跑这套架构单次对话的额外延迟在200-400ms之间不含LLM生成时间记忆检索的准确率在测试集上达到87%。对于大多数Agent应用场景这个开销是可以接受的。如果你刚开始做Agent Memory建议先从最简单的方案起步SQLite存记忆关键词检索。跑通之后再逐步引入向量检索、MCP协议、Docker编排。不要一上来就上全套否则排查问题时你会分不清是哪个环节出了错。
返回列表