ARTICLE DETAIL

资讯详情

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

从零搭建带回溯记忆的LLM Agent系统:MCP协议与Docker部署实战

从零搭建带回溯记忆的LLM Agent系统:MCP协议与Docker部署实战 1. 从“hindsight”说起为什么我们需要给Agent装上“后视镜”第一次看到“hindsight”这个词我脑子里蹦出来的不是词典释义而是自己踩过的一个坑。去年我搭了一个基于LLM的客服Agent上线第一周表现惊艳第二周开始答非所问第三周直接“失忆”——用户明明三分钟前说过订单号它转头就问“请问您的订单号是多少”。排查了半天模型没换、Prompt没改、接口没挂问题出在记忆上Agent的working memory被新会话冲掉了历史上下文没有沉淀每次对话都像第一次见面。这就是“hindsight”要解决的核心问题。它不是某个具体的开源库而是一种设计思路——让Agent具备回溯性记忆能力能够把过去的交互、决策、结果存下来在需要的时候调出来用。你可以把它理解成给Agent装了一面“后视镜”开车时你盯着前方但变道、倒车、判断后车距离靠的全是后视镜里的信息。结合热搜词里的agent memory、LLM、MCP、Docker以及a-memguard这类主动防御框架这篇文章我想聊的不是“hindsight”这个词本身而是如何从零搭建一套带回溯记忆的Agent系统。适合谁看如果你正在用LLM做Agent、被上下文窗口限制折磨过、或者想搞清楚MCP协议到底怎么跟记忆存储结合那这篇内容应该能帮你省下不少试错时间。我会从架构设计讲到Docker部署从MCP协议讲到记忆分层尽量把每个“为什么”都说透。2. 核心架构拆解Agent记忆到底该怎么分层2.1 为什么单一上下文窗口撑不起真正的记忆很多人做Agent的第一反应是把所有历史对话塞进Prompt里。短会话没问题一旦超过几千token成本和延迟就失控了。更致命的是LLM对长上下文的注意力是衰减的——中间部分的信息容易被忽略这就是所谓的“lost in the middle”现象。我实测过一个案例把20轮对话历史全部塞进GPT-4的上下文问它第3轮提到的收货地址准确率只有六成左右。但如果把地址单独抽出来存成结构化字段需要时再注入准确率直接拉到接近100%。这说明记忆不是“存得多”就好而是要“存得对、取得准”。hindsight思路下的记忆分层我一般会分成四层层级名称存储内容生命周期典型实现L1工作记忆当前会话的即时上下文单次会话内存/RedisL2短期记忆最近N轮对话摘要数小时到数天Redis/PostgreSQLL3长期记忆用户偏好、事实性知识持久向量数据库L4回溯记忆历史决策链路、结果反馈持久可追溯图数据库/关系库L1和L2解决“记得住”L3解决“找得到”L4解决“说得清”。hindsight的重点在L4——不仅要记住结果还要记住当时为什么这么决策。比如Agent推荐了一个商品后来用户退货了这个“推荐-退货”的因果链要存下来下次推荐时才能规避。2.2 MCP协议在记忆系统中的角色定位热搜词里MCP出现频率极高很多人问“MCP是什么”。简单说MCPModel Context Protocol是一套让LLM与外部工具、数据源标准化交互的协议。你可以把它类比成USB-C——以前每个设备一个接口现在统一了插上就能用。在hindsight架构里MCP的价值在于把记忆存储抽象成标准化的工具调用。Agent不需要知道底层是Redis还是PostgreSQL只需要通过MCP Server暴露的接口去store_memory、query_memory、forget_memory。这样做的好处是换存储后端不用改Agent代码多个Agent可以共享同一套记忆服务记忆操作可以被审计和拦截这就跟a-memguard的防御思路接上了我自己的做法是写一个MCP Server暴露三个核心工具write_memory负责写入read_memory负责检索trace_memory负责回溯决策链。Agent通过MCP Client调用底层存储可以随时替换。2.3 Docker化部署为什么不用裸机跑热搜词里docker、docker desktop、docker安装教程扎堆出现说明很多人卡在环境这一步。我的建议很明确Agent记忆系统一定要Docker化。原因有三第一记忆系统依赖的组件多——向量库、关系库、缓存、MCP Server裸机装一遍环境能折腾一整天Docker Compose一个文件搞定。第二版本隔离向量库的版本升级经常有breaking change容器化后回滚就是换个tag的事。第三可移植本地跑通的配置直接搬到服务器不用担心“在我机器上是好的”。后面第4节我会给出完整的Docker Compose配置包括MySQL、Redis、向量库和MCP Server的编排。3. 核心细节解析记忆写入、检索与回溯的实操要点3.1 记忆写入什么时候该记什么时候不该记这是最容易被忽略的环节。很多人的Agent把每句话都往记忆库里塞结果检索时全是噪音。我的经验是写入要过三道筛子第一道信息密度筛。像“好的”“嗯嗯”“谢谢”这种对话直接丢弃。判断标准可以用一个简单的规则如果这句话去掉后不影响后续对话的理解就不记。第二道事实性筛。只记事实和决策不记情绪表达。比如“我住在杭州”要记“今天天气真差”不用记。这里可以借助LLM做一次轻量抽取把非结构化对话转成结构化字段。第三道时效性筛。有些信息有保质期比如“我下周出差”过期就该失效。写入时带上TTLTime To Live检索时自动过滤过期数据。代码层面我用一个简单的Python函数做写入前的预处理def should_write_memory(content: str, metadata: dict) - bool: # 过滤短句和寒暄 if len(content.strip()) 10: return False # 过滤无事实内容的对话 filler_patterns [好的, 嗯, 谢谢, 收到, 明白] if any(content.strip().startswith(p) for p in filler_patterns): return False # 检查是否包含可抽取的事实 if not metadata.get(has_fact, False): return False return True注意这个筛子不要做得太严否则会漏掉关键信息。我一般会保留一个“疑似重要”的缓冲区写入时标记为低置信度检索时降权处理而不是直接丢弃。3.2 记忆检索token的三个关键维度热搜词里有一条特别有意思llm的token三个点key我是谁、query我在找什么、value我能提供什么。这其实点出了检索的核心——不是所有记忆都平等检索时要按相关性排序。我用的检索策略是混合检索结合三个维度语义相似度用向量检索找语义相近的记忆权重占50%时间衰减越近的记忆权重越高用指数衰减函数计算权重占30%重要性评分写入时给每条记忆打一个重要性分权重占20%最终得分公式score 0.5 * cosine_similarity 0.3 * exp(-λ * age_hours) 0.2 * importance其中λ取0.01意味着大约70小时后时间权重衰减到一半。这个参数可以根据业务调整客服场景可以衰减快一点个人助理场景可以慢一点。检索时还有一个坑向量检索的top-k不能设太大。我试过top-k20结果注入Prompt后反而干扰了LLM判断。实测top-k5到8比较合适再配合一个重排序模型比如bge-reranker做二次筛选效果最稳。3.3 回溯记忆让Agent能解释“我为什么这么做”这是hindsight区别于普通记忆系统的关键。普通记忆只存“发生了什么”回溯记忆还要存“为什么发生”和“结果如何”。我的实现方式是在每次Agent做决策时写入一条决策记录包含四个字段{ decision_id: uuid, context: 用户询问退款政策, action: 调用退款查询工具, reasoning: 用户提到订单号且语气急切判断为退款诉求, outcome: 成功返回退款状态, timestamp: 2024-01-15T10:30:00Z }当后续出现类似场景时Agent可以先检索历史决策链看看当时是怎么处理的、结果好不好。如果历史决策导致过负面结果比如用户投诉这次就换一种策略。这就形成了一个闭环学习的机制。实操心得决策记录的reasoning字段不要写太长控制在50字以内。太长了检索时噪音大太短了又说不清。我一般让LLM用一句话概括决策依据效果最好。3.4 a-memguard思路的借鉴主动防御而非被动修补热搜词里的a-memguard: a proactive defense framework for llm-based agent memory给了我很大启发。传统做法是记忆被污染了再去清理a-memguard的思路是在写入和检索环节就做防御。我在自己的系统里借鉴了三点第一写入时做一致性校验。如果新记忆和已有记忆冲突比如用户先说住杭州后说住上海不直接覆盖而是标记为冲突让Agent在检索时看到两个版本并主动询问用户。第二检索时做来源追溯。每条记忆都记录来源哪次会话、哪个用户、什么时间检索结果里带上来源信息方便判断可信度。第三定期做记忆审计。每周跑一次脚本检查有没有孤立记忆没有关联决策链的、矛盾记忆、过期未清理的记忆。4. 完整实操用Docker搭建一套带hindsight能力的Agent记忆系统4.1 环境准备与Docker Compose编排先说环境。Windows用户装Docker Desktop时经常遇到virtualization support not detected这个报错九成是因为BIOS里没开虚拟化。进BIOS找到Intel VT-x或AMD-V开启后重启即可。如果还不行检查Hyper-V和WSL2是否冲突关掉Hyper-V改用WSL2后端。Linux用户直接装docker和docker-compose就行注意把当前用户加入docker组否则每次都要sudo。下面是完整的docker-compose.yml包含MySQL、Redis、Qdrant向量库和MCP Serverversion: 3.8 services: mysql: image: mysql:8.0 container_name: agent-memory-mysql environment: MYSQL_ROOT_PASSWORD: memory_root_2024 MYSQL_DATABASE: agent_memory MYSQL_USER: agent MYSQL_PASSWORD: agent_pass_2024 ports: - 3306:3306 volumes: - mysql_data:/var/lib/mysql - ./init.sql:/docker-entrypoint-initdb.d/init.sql command: --character-set-serverutf8mb4 --collation-serverutf8mb4_unicode_ci networks: - memory-net redis: image: redis:7-alpine container_name: agent-memory-redis ports: - 6379:6379 volumes: - redis_data:/data command: redis-server --appendonly yes --maxmemory 512mb --maxmemory-policy allkeys-lru networks: - memory-net qdrant: image: qdrant/qdrant:latest container_name: agent-memory-qdrant ports: - 6333:6333 - 6334:6334 volumes: - qdrant_data:/qdrant/storage networks: - memory-net mcp-server: build: ./mcp-server container_name: agent-memory-mcp ports: - 8080:8080 environment: MYSQL_HOST: mysql REDIS_HOST: redis QDRANT_HOST: qdrant EMBEDDING_MODEL: BAAI/bge-small-zh-v1.5 depends_on: - mysql - redis - qdrant networks: - memory-net volumes: mysql_data: redis_data: qdrant_data: networks: memory-net: driver: bridge几个关键点说明MySQL用8.0而不是5.7因为8.0的JSON字段支持更好存决策记录方便Redis设了maxmemory-policy allkeys-lru工作记忆满了自动淘汰最久未用的Qdrant用最新版向量检索性能比Chroma好不少尤其是数据量上到十万级以后MCP Server单独构建方便后续更新代码不用重建整个环境4.2 数据库表结构设计init.sql里建三张核心表CREATE TABLE memories ( id BIGINT AUTO_INCREMENT PRIMARY KEY, user_id VARCHAR(64) NOT NULL, content TEXT NOT NULL, memory_type ENUM(working, short_term, long_term, trace) NOT NULL, importance FLOAT DEFAULT 0.5, embedding_id VARCHAR(64), metadata JSON, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, expires_at TIMESTAMP NULL, INDEX idx_user_type (user_id, memory_type), INDEX idx_created (created_at) ); CREATE TABLE decisions ( id BIGINT AUTO_INCREMENT PRIMARY KEY, decision_id VARCHAR(64) UNIQUE NOT NULL, user_id VARCHAR(64) NOT NULL, context TEXT, action TEXT, reasoning TEXT, outcome TEXT, score FLOAT DEFAULT 0, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, INDEX idx_user (user_id), INDEX idx_decision (decision_id) ); CREATE TABLE memory_conflicts ( id BIGINT AUTO_INCREMENT PRIMARY KEY, user_id VARCHAR(64) NOT NULL, memory_a_id BIGINT, memory_b_id BIGINT, conflict_type VARCHAR(32), resolved BOOLEAN DEFAULT FALSE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP );memories表存所有记忆用memory_type区分层级。decisions表存决策链memory_conflicts表存冲突记录。三张表通过user_id关联检索时可以join出完整的记忆图谱。4.3 MCP Server核心实现MCP Server用Python写基于mcp官方SDK。核心暴露三个工具from mcp.server import Server from mcp.types import Tool, TextContent import json app Server(agent-memory) app.list_tools() async def list_tools(): return [ Tool( namewrite_memory, description写入一条记忆, inputSchema{ type: object, properties: { user_id: {type: string}, content: {type: string}, memory_type: {type: string, enum: [working, short_term, long_term, trace]}, importance: {type: number, default: 0.5}, metadata: {type: object} }, required: [user_id, content, memory_type] } ), Tool( nameread_memory, description检索相关记忆, inputSchema{ type: object, properties: { user_id: {type: string}, query: {type: string}, top_k: {type: integer, default: 5}, memory_types: {type: array, items: {type: string}} }, required: [user_id, query] } ), Tool( nametrace_memory, description回溯决策链, inputSchema{ type: object, properties: { user_id: {type: string}, context: {type: string}, limit: {type: integer, default: 3} }, required: [user_id, context] } ) ] app.call_tool() async def call_tool(name: str, arguments: dict): if name write_memory: return await handle_write(arguments) elif name read_memory: return await handle_read(arguments) elif name trace_memory: return await handle_trace(arguments)handle_write里做三件事调embedding模型生成向量、写入MySQL、写入Qdrant。handle_read做混合检索先向量召回再重排序。handle_trace查decisions表按context相似度返回历史决策。注意embedding模型建议用bge-small-zh-v1.5中文效果好且体积小CPU上跑单条推理只要几十毫秒。如果追求更高精度可以换bge-large-zh但显存占用会上去。4.4 与Agent的对接方式Agent侧通过MCP Client连接。以Python为例from mcp import ClientSession, StdioServerParameters from mcp.client.stdio import stdio_client async def get_memory_context(user_id: str, query: str): async with stdio_client(server_params) as (read, write): async with ClientSession(read, write) as session: await session.initialize() result await session.call_tool( read_memory, {user_id: user_id, query: query, top_k: 5} ) memories json.loads(result.content[0].text) # 拼接成Prompt上下文 context \n.join([m[content] for m in memories]) return context然后在构造Prompt时把检索到的记忆注入system message你是一个有记忆的助手。以下是关于当前用户的历史记忆 {memory_context} 请基于以上记忆回答用户问题。如果记忆中有冲突信息请主动向用户确认。这样Agent在回答时就能“想起”之前的交互而不是每次从零开始。5. 常见问题与排查技巧实录5.1 Docker相关高频问题速查问题现象根本原因解决方案virtualization support not detectedBIOS虚拟化未开启进BIOS开VT-x/AMD-V关Hyper-V容器间网络不通未加入同一network检查compose里networks配置MySQL容器启动后立即退出数据卷权限问题chown -R 999:999 ./mysql_dataQdrant连接超时端口未映射或防火墙检查6333端口映射关防火墙Redis内存溢出未设maxmemory加--maxmemory 512mb参数5.2 记忆检索不准的排查思路检索不准通常有三个原因按排查顺序来第一embedding模型不匹配。如果你用英文模型处理中文效果肯定差。检查模型是否支持中文可以用bge-small-zh做baseline对比。第二top_k设置不合理。太大噪音多太小漏信息。建议从5开始调每次加2观察效果变化。第三记忆写入时没做清洗。如果库里全是“好的”“谢谢”这种噪音检索再准也没用。回头检查写入筛子是否生效。我踩过最坑的一次是向量库里的向量维度和查询向量维度不一致导致检索结果全是随机的。排查了半天才发现是换了embedding模型但没重建索引。换模型必须重建向量索引这个坑一定要记住。5.3 记忆冲突的处理策略用户说“我住在杭州”过两天又说“我搬到上海了”。这时候系统里两条记忆冲突怎么处理我的策略是不自动覆盖而是标记冲突并让Agent主动确认。具体做法写入新记忆时先检索是否有同类型的旧记忆如果有且内容矛盾写入memory_conflicts表Agent下次对话时检索到冲突记录主动问用户“您之前提到住在杭州现在更新为上海吗”用户确认后旧记忆标记为superseded新记忆生效这样做的好处是避免误覆盖。有时候用户只是临时出差不是真的搬家自动覆盖就错了。5.4 性能优化的几个实操技巧记忆系统跑起来后性能瓶颈通常在向量检索和embedding生成上。分享几个我实测有效的优化embedding缓存相同内容不重复生成向量用Redis做一层缓存命中率能到40%以上批量写入不要一条一条写Qdrant攒够100条批量写吞吐量提升5倍索引预热Qdrant启动后先跑一批查询预热索引首次检索延迟从500ms降到50ms异步写入记忆写入不阻塞主流程丢到消息队列里异步处理Agent响应速度不受影响实操心得异步写入虽然快但要注意顺序问题。同一个用户的记忆如果乱序写入可能导致时间线错乱。我的做法是按user_id做分区同一用户的记忆走同一个队列保证顺序。6. 记忆系统的扩展方向与个人体会这套系统跑了大半年从最初的单机Redis到现在Docker Compose编排的四组件架构中间迭代了七八个版本。有几个扩展方向我觉得值得尝试一是记忆的可视化。现在记忆都在数据库里肉眼看不见。我后来加了一个简单的Web界面用图数据库的方式展示记忆之间的关联调试时直观很多。二是跨Agent记忆共享。多个Agent共用一套记忆服务时要注意权限隔离。我的做法是在MCP Server层加一个namespace参数不同Agent只能访问自己的namespace需要共享时显式授权。三是记忆的自动摘要。短期记忆积累多了之后定期用LLM做一次摘要压缩把10条相关记忆合并成1条高层摘要既省空间又提检索效率。最后分享一个我踩过的坑不要过早优化记忆系统。我一开始就想着做完美的分层、复杂的检索算法结果两周没跑通。后来简化成“先能存能取”跑起来之后再逐步加分层、加回溯、加防御。记忆系统是长出来的不是设计出来的。先把最简单的版本跑通让Agent能用上记忆然后再根据实际遇到的问题去迭代这样每一步都有明确的优化目标不会陷入过度设计的泥潭。
返回列表