首页 / 资讯中心 / 文章详情

基于MCP与Docker的LLM Agent记忆系统:hindsight架构设计与实现

基于MCP与Docker的LLM Agent记忆系统:hindsight架构设计与实现 ★ FEATURED ARTICLE
1. 从“hindsight”说起为什么我们需要给Agent装上“后视镜”第一次看到“hindsight”这个词我脑子里蹦出来的不是词典释义而是自己踩过的一个坑。去年我搭了一个基于LLM的客服Agent上线头一周表现还行第二周开始用户投诉“它怎么又忘了昨天说过的话”。排查下来发现Agent的working memory在会话结束后就被清空了第二天来的用户虽然ID一样但Agent完全不记得之前聊过什么。这就是典型的“没有后视镜”——它只能看见眼前的路看不见走过的路。hindsight这个词字面意思是“事后诸葛亮”但在Agent memory这个语境下它指的是一套让LLM Agent能够回溯、检索、利用历史交互记忆的机制。你可以把它理解成给Agent装了一面后视镜让它不仅能处理当前query还能从过去的对话、操作、反馈中提取有价值的信息用来指导当下的决策。这件事为什么重要因为LLM本身是无状态的每次调用都是一次全新的推理它没有“记忆”这个硬件。你要让它表现得像一个有连续性的助手就必须在外部给它搭一套记忆系统。这套系统要解决的核心问题有三个第一存什么。不是所有对话都值得记token是有限的存储也是要花钱的。第二怎么存。是存原始文本还是存摘要还是存结构化的事实第三怎么取。用户问了一个问题你怎么从海量历史里找到最相关的那几条记忆这三个问题就是hindsight这类Agent memory方案要回答的。适合谁来读这篇如果你正在用LLM搭Agent不管是客服、编程助手、还是个人知识管理工具只要你遇到了“它记不住”的问题这篇内容就是写给你的。如果你还没开始搭但想了解Agent memory的设计思路也能从这里拿到一套可落地的参考方案。我会从整体设计讲到具体实现包括Docker环境搭建、MCP协议对接、存储选型、检索策略以及我在实际项目中踩过的坑。2. 整体设计思路hindsight的架构选型与核心考量2.1 为什么不是简单的“把对话存进数据库”很多人第一反应是记忆嘛不就是把对话记录存进MySQL或者Redis下次用的时候查出来拼进prompt我一开始也是这么干的结果发现两个致命问题。第一token爆炸。你把过去十轮对话原封不动拼进去prompt长度直接翻倍成本上去了推理速度下来了而且模型对超长上下文的注意力会稀释关键信息反而被淹没。第二检索不准。用户问“上次那个方案改好了吗”你用关键词匹配去查“方案”可能查出二十条不相关的记录真正相关的那条因为用词不同被漏掉了。hindsight的设计思路不是“存储检索”这么简单它更像是一个分层记忆系统。我参考了认知科学里人类记忆的模型把Agent memory分成三层working memory、episodic memory、semantic memory。Working memory就是当前会话的上下文存在内存里会话结束就释放。Episodic memory是具体的事件记录比如“用户在某次对话中提到了他的项目截止日期是周五”。Semantic memory是从多次交互中提炼出来的稳定知识比如“这个用户偏好简洁的回答风格”。三层记忆的存储介质、生命周期、检索方式都不一样混在一起存就会乱。2.2 存储选型为什么我最终选了向量数据库关系型数据库的组合存储这块我试过三种方案。第一种是纯关系型数据库用MySQL存对话记录查询用LIKE。优点是简单缺点是语义检索基本不可用。第二种是纯向量数据库比如Milvus或者Chroma把所有对话embedding后存进去。优点是语义检索强缺点是无法做结构化过滤比如“只查最近三天的记录”这种条件很难高效执行。第三种是混合方案关系型数据库存元数据和结构化字段向量数据库存embedding检索时先用结构化条件缩小范围再做向量相似度排序。我最终选了第三种。具体来说MySQL存每条记忆的元信息时间戳、会话ID、用户ID、记忆类型、重要性评分。向量数据库存对应的embedding向量。检索的时候先用SQL过滤出候选集比如“用户ID123且时间在最近七天”然后把这个候选集的向量拿出来做相似度计算。这样既保证了检索的语义相关性又保证了结构化条件的精确性。实测下来在十万条记忆的规模下检索延迟能控制在200毫秒以内完全够用。2.3 MCP协议的角色让Agent memory成为可插拔的模块MCPModel Context Protocol在这套架构里扮演的是“接口层”的角色。你可以把它理解成Agent和记忆系统之间的USB接口。没有MCP的时候你的Agent代码里要硬编码调用记忆系统的逻辑换一个记忆方案就要改代码。有了MCP记忆系统作为一个独立的服务运行Agent通过标准协议去调用它存记忆、取记忆、更新记忆都是标准化的操作。我为什么强调这个因为Agent memory这个领域变化太快了。今天你用这套方案明天可能出一个更好的embedding模型或者检索算法你不想每次都重写Agent的核心逻辑。MCP让你可以把记忆系统当成一个可替换的零件Agent只管调用接口底层怎么实现是记忆系统的事。而且MCP还支持工具调用你可以把“存记忆”和“取记忆”定义成两个toolAgent在推理过程中自主决定什么时候调用它们。这比在prompt里硬拼历史记录要灵活得多。2.4 Docker的角色环境隔离与一键部署Docker在这套方案里解决的是“在我机器上能跑”的问题。Agent memory系统涉及多个组件MySQL、向量数据库、MCP服务、可能还有一个embedding服务。如果每个都手动装光是版本兼容就能折腾一天。用Docker Compose编排一条命令拉起所有服务环境变量统一管理数据卷持久化换台机器也能快速复现。我试过在Windows上直接装MySQL和Milvus光是Milvus的依赖就搞了半天最后还因为WSL的问题跑不起来。后来全部改用Docker半小时搞定。而且Docker的隔离性让不同项目的依赖不会互相污染这点在同时维护多个Agent项目的时候特别重要。3. 核心细节解析记忆的写入、存储与检索3.1 记忆写入不是所有对话都值得记住写入策略是hindsight的第一个关键决策点。我的做法是给每条潜在记忆打一个“重要性评分”只有超过阈值的才写入长期存储。评分维度包括信息密度这条对话里有多少实体和事实、情感强度用户是否表达了强烈的情绪、重复频率类似内容是否多次出现、时效性是否涉及未来时间点。具体实现上我用了一个轻量级的LLM来做评分。每次会话结束后把对话摘要发给一个小的LLM让它输出0到1的重要性分数。这个LLM不需要很强7B参数的模型就够用成本很低。阈值我设的是0.6低于这个分数的直接丢弃。实测下来大约只有30%的对话会被写入长期记忆大大降低了存储和检索的压力。注意重要性评分不要用同一个LLM来做因为同一个模型可能有盲区。我试过用GPT-4做评分结果它把很多我觉得重要的东西评了低分。后来换了一个不同架构的小模型效果反而更好。这个经验不一定通用但值得试试。3.2 记忆存储结构化字段与向量字段的配合每条记忆在MySQL里的表结构大概是这样的CREATE TABLE agent_memory ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id VARCHAR(64) NOT NULL, session_id VARCHAR(64) NOT NULL, memory_type ENUM(episodic, semantic, working) NOT NULL, content TEXT NOT NULL, summary VARCHAR(512), importance FLOAT DEFAULT 0.5, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, last_accessed_at TIMESTAMP, access_count INT DEFAULT 0, embedding_id VARCHAR(128), INDEX idx_user_time (user_id, created_at), INDEX idx_type_importance (memory_type, importance) );content字段存原始文本summary存LLM生成的摘要embedding_id关联向量数据库里的向量。last_accessed_at和access_count用来做记忆的“热度”管理长时间不被访问的记忆会被降权甚至归档。向量数据库那边我用的是Milvus的轻量级替代方案Qdrant因为它单机部署更简单Docker镜像也小。每个向量是768维用bge-base-zh模型生成这个模型对中文语义的捕捉比OpenAI的text-embedding-ada-002要好而且可以本地跑不依赖外部API。3.3 记忆检索三路召回加重排序检索是hindsight最核心的部分。我的方案是三路召回向量相似度召回、关键词召回、时间衰减召回。向量召回负责语义匹配关键词召回负责精确匹配比如用户问“上次说的那个Python脚本”关键词“Python脚本”能直接命中时间衰减召回负责把最近的记忆优先带出来。三路召回各自取Top 20合并去重后得到候选集然后用一个交叉编码器做重排序。重排序模型我用的是bge-reranker-base它会把query和每条候选记忆拼在一起打分比单纯的向量相似度准确得多。最后取Top 5注入到Agent的prompt里。这里有个细节注入prompt的时候不要直接拼原始文本而是拼摘要加关键实体。原始文本太长摘要加实体能在保留核心信息的同时控制token消耗。我实测过用摘要加实体的方式prompt长度能减少60%而Agent的回答质量几乎没有下降。3.4 记忆更新与遗忘让记忆系统保持“新鲜”记忆不是写完就不管了。hindsight有一套更新机制当新的记忆和旧记忆冲突时比如用户之前说“我住在北京”后来又说“我搬到上海了”系统会把旧记忆标记为“已过期”而不是直接删除。这样Agent在回答“你记得我住哪吗”的时候能给出“你之前提到过北京但最近更新为上海”这样的回答比单纯说“上海”更有连续性。遗忘机制用的是LRU加重要性加权的策略。每条记忆有一个“保留分数”等于重要性乘以时间衰减因子再乘以访问频率。保留分数低于阈值的记忆会被移到冷存储检索时不参与召回但保留在数据库里以备审计。这个阈值我设的是0.2大概每三个月会清理掉15%的记忆。4. 实操过程从零搭建一套hindsight记忆系统4.1 环境准备Docker与Docker Compose的安装Windows用户直接去Docker官网下载Docker Desktop安装的时候注意勾选“Use WSL 2 instead of Hyper-V”。如果安装后启动报“Virtualization support not detected”去BIOS里把虚拟化打开Intel平台叫VT-xAMD平台叫SVM。Linux用户用apt或者yum装docker-ce和docker-compose-plugin就行。装完之后验证一下docker --version docker compose version两个命令都能输出版本号就说明环境OK了。如果docker命令要sudo才能跑把当前用户加到docker组里sudo usermod -aG docker $USER然后重新登录。4.2 用Docker Compose编排记忆系统我的docker-compose.yml大概长这样version: 3.8 services: mysql: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: memory_root MYSQL_DATABASE: agent_memory ports: - 3306:3306 volumes: - mysql_data:/var/lib/mysql command: --default-authentication-pluginmysql_native_password qdrant: image: qdrant/qdrant:latest ports: - 6333:6333 - 6334:6334 volumes: - qdrant_data:/qdrant/storage embedding: image: ghcr.io/huggingface/text-embeddings-inference:latest command: --model-id BAAI/bge-base-zh-v1.5 --port 8080 ports: - 8080:8080 volumes: - embedding_cache:/data mcp-server: build: ./mcp-server ports: - 9000:9000 environment: MYSQL_HOST: mysql QDRANT_HOST: qdrant EMBEDDING_HOST: embedding depends_on: - mysql - qdrant - embedding volumes: mysql_data: qdrant_data: embedding_cache:这个编排文件定义了四个服务MySQL存元数据Qdrant存向量embedding服务提供向量化能力mcp-server是Agent调用的接口层。所有服务在同一个Docker网络里互相用服务名访问不用管IP。启动命令就一句docker compose up -d第一次启动会拉镜像大概需要五到十分钟取决于网速。之后启动就是秒级。4.3 MCP服务的实现定义存记忆和取记忆两个工具MCP服务的核心是暴露两个toolstore_memory和retrieve_memory。我用Python的mcp库来实现核心代码大概是这样from mcp.server import Server, NotificationOptions from mcp.server.models import InitializationOptions import mcp.server.stdio import mcp.types as types server Server(hindsight-memory) server.list_tools() async def handle_list_tools(): return [ types.Tool( namestore_memory, description存储一条Agent记忆, inputSchema{ type: object, properties: { user_id: {type: string}, content: {type: string}, memory_type: {type: string, enum: [episodic, semantic]} }, required: [user_id, content] } ), types.Tool( nameretrieve_memory, description检索相关记忆, inputSchema{ type: object, properties: { user_id: {type: string}, query: {type: string}, top_k: {type: integer, default: 5} }, required: [user_id, query] } ) ]store_memory的实现逻辑是先调embedding服务生成向量然后写入MySQL和Qdrant。retrieve_memory的实现逻辑是三路召回加重排序返回Top K条记忆的摘要和实体。这里有个坑要注意MCP服务默认用stdio通信但如果你要远程调用得改成SSE或者WebSocket。我一开始用stdio本地测试没问题部署到服务器上就发现Agent连不上。后来改成SSE在MCP配置里指定wss://或者https://的地址才行。4.4 Agent侧的对接让LLM自主决定何时存取记忆Agent侧不需要写太多代码只需要在系统prompt里告诉LLM它有两个工具可用然后在推理循环里处理tool call就行。我用的是LangChain的AgentExecutor配置大概是这样from langchain.agents import AgentExecutor, create_openai_tools_agent from langchain_mcp import MCPToolkit toolkit MCPToolkit(server_urlhttp://localhost:9000/sse) tools toolkit.get_tools() agent create_openai_tools_agent(llm, tools, prompt) executor AgentExecutor(agentagent, toolstools, verboseTrue)系统prompt里我会加一段说明“你有一个记忆系统可以在需要时调用store_memory存储重要信息调用retrieve_memory检索历史记忆。当用户提到过去的事情时先检索记忆再回答。”实测下来LLM对工具调用的判断还挺准的。用户说“记住我喜欢喝美式”它会调store_memory。用户问“我上次说我喜欢喝什么”它会先调retrieve_memory再回答。偶尔会有误判比如用户只是随口说了一句“今天天气不错”它也去存记忆这时候重要性评分就起作用了低分的记忆会被过滤掉。4.5 参数调优召回数量、重排序阈值、注入格式这套系统有几个关键参数需要根据实际场景调。召回数量我设的是每路20条总共最多60条候选。重排序后取Top 5注入prompt。这个5不是拍脑袋定的我试过3、5、8、10发现5在回答质量和token消耗之间平衡最好。3的时候偶尔会漏掉关键记忆8以上token消耗增加明显但质量提升有限。重排序的分数阈值我设的是0.3低于这个分数的候选直接丢弃。这样能避免把不相关的记忆硬塞进prompt。注入格式我用的是[相关记忆] - (2024-01-15) 用户提到他的项目截止日期是周五需要在此之前完成API对接。 - (2024-01-10) 用户偏好简洁的回答风格不喜欢长篇大论。每条记忆带时间戳按时间倒序排列。这样LLM能感知到记忆的新旧程度回答时会更准确。5. 常见问题与排查技巧实录5.1 Docker相关启动失败与网络不通问题一Docker Desktop启动报“Virtualization support not detected”。这个我在Windows上遇到过两次。第一次是BIOS里虚拟化没开进BIOS找到Intel VT-x或者AMD SVM设为Enabled。第二次是Hyper-V和WSL2冲突在“启用或关闭Windows功能”里把Hyper-V关掉只留“适用于Linux的Windows子系统”和“虚拟机平台”。问题二容器之间网络不通。我在docker-compose里用服务名互相访问比如mcp-server连mysql用mysql:3306。有一次怎么都连不上排查发现是MySQL容器启动比mcp-server慢mcp-server启动时MySQL还没ready。解决办法是在depends_on里加healthcheck或者直接在应用层做重试。问题三MySQL 8.0认证插件问题。MySQL 8.0默认用caching_sha2_password有些老客户端连不上。我在command里加了--default-authentication-pluginmysql_native_password问题解决。5.2 MCP相关连接失败与工具调用异常问题一MCP服务连不上。最常见的原因是协议不匹配。MCP支持stdio、SSE、WebSocket三种传输方式Agent侧和Server侧必须一致。我一开始Server用stdioAgent用SSE怎么都连不上。后来统一改成SSE就好了。问题二工具调用返回schema错误。报错信息类似“provider rejected the request schema or tool payload”。这个通常是inputSchema定义有问题比如required字段没写全或者类型不匹配。我的经验是inputSchema里的每个字段都要有明确的typerequired数组要准确不要用anyOf这种复杂结构有些LLM对复杂schema的支持不好。问题三工具调用太频繁。LLM有时候会过度调用工具每轮对话都去检索记忆。解决办法是在系统prompt里加限制“只在用户明确提到过去的事情时才调用retrieve_memory不要每轮都调用。”另外可以在MCP服务侧加一个调用频率限制比如同一个user_id一分钟内最多调用5次。5.3 记忆质量相关检索不准与记忆冲突问题一检索出来的记忆不相关。排查思路是分步验证先看向量召回的结果如果向量召回就不准说明embedding模型不适合你的场景换一个模型试试。如果向量召回准但重排序后不准说明重排序模型有问题或者阈值设得太低。如果都准但LLM回答还是不对说明注入格式有问题试试调整摘要的详细程度。问题二新旧记忆冲突。比如用户先说“我在北京”后说“我在上海”。我的处理方式是给每条记忆加一个“有效期”字段新记忆写入时把同类型的旧记忆标记为expired。检索时默认只召回未过期的记忆但保留一个“显示历史”的选项让Agent能回答“你之前提到过北京但最近更新为上海”。问题三记忆太多导致检索变慢。当记忆数量超过十万条时向量检索的延迟会明显上升。解决办法是分片按user_id哈希分到不同的collection里。另外可以定期归档冷记忆把超过三个月未访问且重要性低于0.3的记忆移到冷存储不参与实时检索。5.4 常见问题速查表问题现象可能原因排查步骤解决方案Docker启动失败虚拟化未开启检查BIOS设置开启VT-x/SVM容器间网络不通启动顺序问题查看容器日志加healthcheck或重试MCP连接失败协议不匹配检查两端配置统一传输协议工具调用报schema错误inputSchema定义问题检查required和type简化schema结构检索结果不相关embedding模型不适配分步验证召回和重排序换模型或调阈值记忆冲突缺少有效期管理检查记忆更新逻辑加expired标记检索变慢记忆数量过大查看collection大小分片或归档冷记忆5.5 几个我踩过的坑和独家技巧第一个坑是embedding模型的维度选择。我一开始用1536维的模型检索精度确实高但存储成本和检索延迟也上去了。后来换成768维的bge-base-zh精度只降了不到2个百分点但存储省了一半检索速度快了40%。对于大多数Agent memory场景768维完全够用没必要追求高维。第二个坑是摘要的生成时机。我一开始是在写入记忆时生成摘要后来发现有些记忆写入时看起来不重要但后来变得重要了这时候摘要已经固定了没法调整。后来改成检索时动态生成摘要虽然增加了一点延迟但灵活性好很多。具体做法是检索出Top 5后用一个小的LLM对这5条记忆做一次摘要压缩再注入prompt。第三个技巧是关于记忆的“温度”。我在每条记忆上加了一个access_count字段每次被检索到就加一。检索时给access_count高的记忆加一个小的权重这样常用的记忆会更容易被再次召回形成正向循环。这个技巧是从缓存系统里借鉴过来的实测对提升检索相关性有帮助。第四个技巧是关于MCP服务的日志。MCP的tool call默认不打日志出了问题很难排查。我在mcp-server里加了一个中间件把每次tool call的输入输出都写到日志文件里。这样当Agent行为异常时可以回溯它到底存了什么、取了什么。这个日志我建议保留至少七天对于调试Agent的记忆行为非常有用。6. 记忆系统的扩展方向与个人体会这套hindsight方案跑了大半年支撑了一个日活几千的Agent应用整体稳定性还不错。如果让我给正在搭Agent memory的朋友提建议我会说先从简单的方案开始不要一上来就搞三层记忆加三路召回。我一开始就是过度设计结果调试了两周才跑通。后来简化成“MySQL存摘要向量库存embedding单路向量召回”半天就上线了效果也能接受。等业务量上来了再逐步加关键词召回、重排序、分层记忆这些优化。扩展方向上我最近在试的是把semantic memory做成一个知识图谱。现在的semantic memory还是文本形式检索靠向量相似度。如果做成图谱实体和关系都是结构化的检索可以更精确而且能做推理。比如用户问“我上次提到的那个项目现在什么状态”图谱里如果有“项目-状态”这个关系直接查就行不用靠向量匹配。这个方向我还在探索等有成熟经验了再分享。另一个方向是记忆的跨Agent共享。现在每个Agent有自己的记忆系统如果多个Agent协作记忆是隔离的。如果能通过MCP协议把记忆系统做成一个共享服务多个Agent可以读写同一套记忆协作效率会高很多。这个在技术上不难难的是权限管理和冲突解决还在琢磨。最后分享一个我在实际使用中的体会Agent memory的效果七分靠检索三分靠存储。很多人把精力花在怎么存上但其实检索才是决定用户体验的关键。你存了一万条记忆但用户问问题的时候你取不出最相关的那五条那跟没存一样。所以我的建议是先把检索链路调通、调准再回头优化存储策略。检索准了哪怕只存了少量高质量记忆效果也比存一堆但取不准要好得多。
阅读完成 · 觉得有帮助?
咨询建站