ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Agent记忆系统实战:基于MCP与Docker构建可持久化、可检索、可演进的LLM记忆架构

Agent记忆系统实战:基于MCP与Docker构建可持久化、可检索、可演进的LLM记忆架构 1. 从“hindsight”说起为什么Agent的记忆问题值得单独拎出来做“hindsight”这个词本身很有意思字面意思是“事后的洞察力”也就是我们常说的“后见之明”。放在LLM Agent的语境里它指向一个非常具体且长期被低估的问题Agent在完成任务之后能不能回过头来从自己的历史交互中提炼出有价值的经验并在下一次遇到类似场景时真正用上。我接触过不少Agent项目从简单的工具调用到复杂的多步推理绝大多数团队在早期都会把精力砸在“当前这一步能不能做对”上——prompt怎么写、工具怎么调、MCP协议怎么接。但跑了一段时间之后一个共性问题就会浮出水面Agent没有记忆或者说它的记忆是死的。每次对话都是重新开始昨天踩过的坑今天照踩不误上周用户纠正过的偏好这周完全想不起来。这不是模型能力的问题是架构设计里压根没给“记忆”留位置。“hindsight”这个项目标题结合agent memory、LLM、MCP、Docker这几个关键词我判断它要解决的核心问题是给LLM-based Agent构建一套可持久化、可检索、可演进的记忆系统让Agent具备“回头看”的能力。具体来说它需要做到几件事把Agent与环境的交互历史存下来从历史中提取结构化的经验或知识在新任务到来时检索相关记忆并注入上下文同时通过MCP协议与外部工具链打通用Docker保证部署的一致性和可移植性。这套东西适合谁看如果你正在做Agent应用开发不管是客服机器人、代码助手还是自动化工作流只要你发现Agent“记不住事”“重复犯错”“每次都要重新教”那这套思路就值得你花时间研究。如果你只是刚接触LLM应用还没到需要记忆系统的阶段那可以先了解概念等遇到瓶颈了再回来深入。我下面会从设计思路、核心细节、实操落地、问题排查几个维度把“hindsight”这类Agent记忆系统的构建过程拆开来讲。所有内容基于我对Agent memory领域的实践认知和常见工程方案来展开不会停留在概念层面而是尽量给出可复现的路径。2. 整体设计思路Agent记忆系统到底该怎么搭2.1 为什么不能直接把对话历史塞进上下文很多人第一反应是记忆嘛把历史对话全部拼到prompt里不就行了这个方案在Demo阶段能用一旦上生产就会撞墙。原因有三层。第一层是token成本。假设你的Agent每天处理1000次交互每次交互平均2000 token一个月下来就是6000万token的历史数据。你不可能每次都把全部历史塞进去成本扛不住延迟也扛不住。第二层是信息密度。原始对话历史里大量内容是冗余的、重复的、无意义的。用户说“你好”“谢谢”“帮我查一下”这些内容对后续决策几乎没有价值。真正有价值的是那些纠正性反馈、偏好声明、任务成功/失败的关键节点。把原始历史直接塞进去等于让模型在噪音里找信号。第三层是检索效率。当记忆量大了之后你需要的是“根据当前任务找到最相关的几条记忆”而不是“把所有记忆都过一遍”。这本质上是一个检索问题不是存储问题。所以“hindsight”这类系统的核心设计思路一定是分层处理原始交互日志存一份用于审计和回溯结构化记忆存一份用于检索和注入经验/规则存一份用于长期演进。三层各司其职不能混在一起。2.2 记忆的分层模型working memory、episodic memory、semantic memory借鉴认知科学的分类Agent记忆通常分三层Working memory工作记忆是当前任务上下文里正在用的信息生命周期最短通常就是当前对话窗口内的内容。它的作用是保证Agent在单次任务中的连贯性。Episodic memory情景记忆是具体的事件记录比如“2024年3月15日用户要求查询订单状态Agent调用了订单查询工具返回了结果用户表示满意”。它记录的是“发生了什么”带有时间戳和上下文。Semantic memory语义记忆是从多个情景中抽象出来的规律或知识比如“这个用户偏好用中文回复”“查询订单时需要先验证用户身份”“每周五下午系统会维护查询会超时”。它记录的是“什么是对的/有效的”。“hindsight”的价值在于它不只是存而是从episodic向semantic的转化。这个转化过程就是“hindsight”的字面意思——事后从具体经历中提炼出可复用的洞察。2.3 为什么选MCP和Docker作为基础设施MCPModel Context Protocol在这里的角色是标准化Agent与外部资源的连接方式。记忆系统不是一个孤立的数据库它需要和工具调用、知识库、外部API打通。MCP提供了一套协议让Agent可以用统一的方式访问这些资源而不需要为每个工具写一套适配代码。Docker的角色是环境一致性。Agent记忆系统涉及多个组件向量数据库、关系型数据库、缓存、消息队列、MCP server。这些东西在开发机上跑通不难难的是在测试环境、生产环境、不同同事的机器上都能一致运行。Docker Compose可以把整套依赖打包一条命令启动避免“在我机器上是好的”这类问题。这两个选择背后的逻辑是一致的降低集成成本和运维成本。Agent记忆系统本身已经够复杂了基础设施层面能标准化就标准化不要把精力浪费在环境配置上。3. 核心细节解析记忆的写入、检索与演进3.1 记忆写入什么该记什么不该记写入策略直接决定了记忆系统的质量。我的经验是不要试图记录一切。全量记录的结果是检索时噪音太大反而降低效果。一个可操作的写入触发条件清单用户显式纠正用户说“不对”“不是这个意思”“你应该先做X再做Y”这类信号必须记录而且优先级最高。任务成功/失败节点一个多步任务完成或失败时记录关键步骤和结果用于后续类似任务的参考。偏好声明用户说“我喜欢用表格展示”“以后回复简短一点”这类偏好需要写入semantic memory。工具调用异常某个工具连续失败、超时、返回异常格式这类信息对后续决策很有价值。高频模式如果某个操作在短时间内重复出现可能意味着需要抽象成规则。写入的时候要注意结构化。不要存原始文本而是存成带字段的JSON{type, timestamp, context, action, outcome, lesson}。这样后续检索和聚合都方便。注意写入操作最好是异步的不要阻塞主流程。Agent回复用户之后后台再慢慢处理记忆写入。否则每次交互都要等记忆系统响应延迟会很难看。3.2 记忆检索怎么找到“对的那几条”检索的核心是相关性排序。常见方案是向量检索关键词检索的混合模式。向量检索负责语义相似度把当前任务描述embedding之后去记忆库里找余弦相似度最高的几条。关键词检索负责精确匹配比如用户提到了“订单号12345”那就直接找包含这个订单号的记忆。混合检索的权重需要调。我的经验是在Agent记忆场景下关键词匹配的权重要比通用搜索场景更高。因为Agent记忆里很多内容是具体的工具名、参数名、错误码这些词的语义embedding区分度不高但精确匹配非常有效。检索出来之后还需要一层重排序。可以用一个小模型或者规则来打分考虑的因素包括时间衰减越近的记忆越重要、类型权重纠正性反馈普通事件、使用频率被检索过多次的记忆可能更通用。最终注入上下文的时候不要把所有检索结果都塞进去。控制在3-5条每条压缩成一句话或一个短段落。太多会稀释注意力反而干扰模型判断。3.3 记忆演进从情景到语义的抽象这是“hindsight”最核心也最难的部分。系统需要定期比如每天凌晨跑一个批处理任务把episodic memory聚类、抽象、归纳成semantic memory。具体做法可以是把最近一段时间的情景记忆按任务类型分组每组用LLM做一次总结提取出“在这个场景下什么做法有效什么做法无效”。总结结果写入semantic memory同时保留原始情景记忆的引用。这个过程的难点在于避免过度泛化。比如从“用户A在查询订单时喜欢先看物流状态”抽象成“所有用户查询订单时都应该先看物流状态”这就错了。所以抽象的时候要保留条件限定semantic memory的条目应该是“在X条件下Y做法通常有效”的形式。另一个难点是冲突处理。新的semantic memory和旧的冲突怎么办我的做法是保留版本号和时间戳检索时优先用新的但旧的也不删用于追溯。如果冲突频繁出现说明这个场景本身就不稳定不应该抽象成规则。4. 实操落地用Docker和MCP把系统跑起来4.1 环境准备与Docker Compose编排先明确组件清单。一个最小可用的Agent记忆系统需要组件用途推荐选型向量数据库存储记忆embedding支持相似度检索Qdrant或Milvus关系型数据库存储结构化记忆、元数据、版本信息PostgreSQL缓存加速高频检索存储会话状态RedisMCP Server对外暴露记忆读写接口自研或基于开源框架应用服务记忆写入、检索、演进的业务逻辑Python FastAPIDocker Compose的编排思路是每个组件一个service通过内部网络通信数据卷持久化。下面是一个可参考的compose文件结构version: 3.9 services: qdrant: image: qdrant/qdrant:latest ports: - 6333:6333 volumes: - qdrant_data:/qdrant/storage postgres: image: postgres:16 environment: POSTGRES_DB: agent_memory POSTGRES_USER: memory_user POSTGRES_PASSWORD: memory_pass volumes: - pg_data:/var/lib/postgresql/data ports: - 5432:5432 redis: image: redis:7-alpine ports: - 6379:6379 memory-service: build: ./memory-service depends_on: - qdrant - postgres - redis environment: QDRANT_HOST: qdrant PG_HOST: postgres REDIS_HOST: redis ports: - 8000:8000 volumes: qdrant_data: pg_data:启动命令就一句docker compose up -d。等所有服务healthy之后记忆系统的底座就搭好了。注意Windows环境下如果Docker Desktop启动报“virtualization support not detected”需要进BIOS开启虚拟化支持Intel VT-x或AMD-V然后在Windows功能里确认WSL2或Hyper-V已启用。这个问题很常见但排查起来不复杂。4.2 MCP Server的实现要点MCP Server是Agent访问记忆系统的入口。它需要暴露几个核心工具write_memory写入一条记忆参数包括类型、内容、上下文、时间戳。search_memory根据查询文本检索相关记忆返回排序后的结果。get_preferences获取用户的偏好类记忆。summarize_episodes触发情景记忆到语义记忆的抽象。实现的时候参数schema要设计得足够明确。比如write_memory的type字段应该是枚举值correction/preference/event/rule而不是自由文本。这样后续检索和聚合的时候才能做精确过滤。MCP Server和记忆服务之间通过内部HTTP或gRPC通信。MCP Server本身不直接连数据库它只做协议转换和参数校验业务逻辑下沉到memory-service。这样分层的好处是MCP Server可以独立部署和扩缩容不会因为记忆服务的负载影响协议层的稳定性。4.3 记忆写入与检索的代码骨架写入逻辑的核心是先判断是否值得记再决定记成什么类型。下面是一个简化的Python示例def should_write(interaction: dict) - bool: if interaction.get(user_correction): return True if interaction.get(task_status) in (success, failure): return True if interaction.get(preference_declared): return True return False def classify_memory(interaction: dict) - str: if interaction.get(user_correction): return correction if interaction.get(preference_declared): return preference if interaction.get(task_status): return event return event def write_memory(interaction: dict): if not should_write(interaction): return mem_type classify_memory(interaction) record { type: mem_type, timestamp: interaction[timestamp], context: interaction.get(context, ), content: interaction.get(summary, ), metadata: interaction.get(metadata, {}), } # 写入PostgreSQL pg.insert(memories, record) # 生成embedding写入Qdrant vector embed(record[content]) qdrant.upsert(collectionmemories, vectorvector, payloadrecord)检索逻辑的核心是混合检索重排序def search_memory(query: str, top_k: int 5): # 向量检索 query_vec embed(query) vector_results qdrant.search( collectionmemories, query_vectorquery_vec, limittop_k * 3 ) # 关键词检索 keyword_results pg.search_fulltext(memories, query, limittop_k * 3) # 合并去重 merged merge_results(vector_results, keyword_results) # 重排序 ranked rerank(merged, query) return ranked[:top_k]重排序的规则可以很简单时间越近加分类型是correction或preference加分被检索次数多加分。不需要上模型规则引擎就够用。4.4 记忆注入的上下文组装检索到记忆之后怎么注入到Agent的prompt里也有讲究。我的做法是按类型分组用不同的模板偏好类记忆放在system prompt的末尾用“用户偏好”小节列出。纠正类记忆放在当前任务描述之前用“历史反馈”小节提醒。事件类记忆放在工具调用结果之后用“相关历史”小节参考。每条记忆压缩成一句话不要超过50字。如果检索到5条总共注入的token控制在300以内。这个量级对模型来说刚好能注意到又不会喧宾夺主。5. 常见问题与排查技巧实录5.1 记忆检索不相关怎么办这是最常见的问题。排查顺序如下先看embedding模型是否合适。如果你用的是通用文本embedding模型在Agent记忆这种短文本、专业术语多的场景下效果可能不好。可以试试针对检索任务优化的模型或者在embedding之前先做一次关键词提取把关键实体拼到文本前面。再看检索参数。top_k设太小会漏设太大引入噪音。我的经验是向量检索取top 15关键词检索取top 15合并后重排序取top 5。这个比例在多数场景下比较平衡。最后看记忆本身的质量。如果写入的记忆内容太笼统比如“用户查询了订单”那检索什么都匹配不上。写入的时候要尽量具体“用户查询订单12345的物流状态期望看到预计送达时间”。5.2 记忆写入过多导致检索变慢写入策略太宽松什么垃圾都往里塞检索时自然慢。解决办法是加一层写入过滤只有满足触发条件的才写。另外定期做记忆压缩把超过30天的、从未被检索过的、类型是event的记忆归档到冷存储不参与在线检索。Qdrant和PostgreSQL都支持按时间分区可以把冷数据放到单独的分区检索时只查热分区。这个优化能把检索延迟从几百毫秒降到几十毫秒。5.3 Docker网络不通导致服务间调用失败Docker Compose默认会创建一个内部网络所有service在同一个网络里可以用service名互相访问。但有几个坑端口映射和内部访问是两回事。你在compose里写了ports: 5432:5432这是把容器端口映射到宿主机。容器之间互相访问用的是service名容器端口不是宿主机端口。depends_on不保证服务就绪。depends_on只保证启动顺序不保证服务已经可以接受请求。需要在应用层做重试或者用healthcheckcondition。环境变量里的host要写service名。比如memory-service连PostgreSQLPG_HOST应该写postgres不是localhost。排查的时候进容器里ping一下目标service名或者用nc -zv postgres 5432测端口连通性。多数问题都是host写错了。5.4 记忆演进任务跑不动或结果质量差记忆演进是批处理任务跑不动通常是数据量太大或者LLM调用超时。解决办法是分批处理每次只处理一个时间窗口的数据比如按天分组。每组内部再按任务类型聚类减少LLM调用次数。结果质量差通常是抽象过度或抽象不足。抽象过度就是前面说的把特例当规律。抽象不足就是总结出来的东西和原始情景差不多没有提炼价值。调prompt的时候明确要求“保留条件限定”“用‘在X情况下Y做法有效’的句式”效果会好很多。5.5 常见问题速查表问题现象可能原因排查动作解决方向检索结果不相关embedding模型不适配人工检查top 10结果换模型或加关键词提取检索延迟高记忆量过大或索引未优化看Qdrant/PostgreSQL慢查询加分区、归档冷数据服务间调用失败Docker网络配置错误容器内ping/nc测试检查host和端口配置记忆演进质量差prompt过于笼统人工抽检总结结果加条件限定和句式约束写入阻塞主流程同步写入看接口响应时间改异步写入记忆冲突新旧规则矛盾查版本和时间戳保留版本检索优先新的6. 一些实操心得和后续扩展方向我在搭这类系统的时候最大的体会是不要一开始就追求完美。先跑通“写入-检索-注入”这个最小闭环哪怕检索效果一般先让Agent能用上记忆。然后根据实际bad case去调写入策略和检索参数。记忆系统的质量是迭代出来的不是设计出来的。另一个心得是日志要打全。每次检索返回了什么、注入了什么、Agent用了哪条记忆、结果如何这些都要记下来。没有这些日志你根本不知道记忆系统到底有没有起作用。我习惯在memory-service里加一个retrieval_log表记录每次检索的query、返回的记忆ID、最终注入的记忆ID。后面分析的时候一目了然。后续扩展的话有几个方向可以考虑。一是多Agent共享记忆让不同Agent之间能互相学习但这需要解决权限和冲突问题。二是记忆的可解释性让Agent能说出“我之所以这么做是因为之前遇到过类似情况”这对调试和用户信任都有帮助。三是记忆的主动遗忘有些记忆过时了、错误了需要能被标记和淘汰而不是永远留在库里。这些方向目前都还在探索阶段没有特别成熟的方案。但“hindsight”这个思路本身是对的Agent不能只活在当下它需要回头看才能往前走得更稳。
返回列表