ARTICLE DETAIL

资讯详情

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

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

基于MCP与Docker的LLM Agent记忆系统:hindsight设计实战 1. 从“hindsight”说起为什么我们需要给Agent装上“后视镜”“hindsight”这个词直译过来就是“后见之明”或者更通俗一点——“事后诸葛亮”。但在LLM Agent的开发语境里它指向的是一个非常具体且棘手的问题Agent的记忆机制。你肯定遇到过这种情况跟一个基于LLM的Agent聊了十几轮它突然忘了你三分钟前说过的关键约束或者你让它处理一个多步骤任务它执行到第三步就忘了第一步的输出结果。这不是模型不够聪明而是它的“工作记忆”和“长期记忆”没有设计好。我最初接触“hindsight”这个概念是在折腾一个基于MCP协议的多工具Agent项目时。当时Agent需要调用Docker容器里的MySQL做数据查询再通过Playwright MCP去抓取网页信息最后汇总成报告。结果发现每次工具调用返回后Agent对上下文的把握就弱一分到了第五轮它甚至开始编造之前根本没执行过的步骤。这就是典型的“记忆断层”——Agent没有能力回看自己走过的路没有“hindsight”。所以这篇博文我想聊的就是如何围绕“hindsight”这个核心理念给LLM Agent搭建一套靠谱的记忆系统。这套系统要解决三个问题记什么working memory的粒度、怎么记存储结构与检索策略、怎么用在MCP工具调用链中如何注入历史信息。适合正在用Docker部署Agent服务、用MCP协议连接工具、被Agent“失忆”问题折磨过的开发者。我会从设计思路讲到实操配置把踩过的坑和验证过的方案都摊开来说。2. Agent记忆系统的整体设计与核心思路拆解2.1 为什么“hindsight”不是简单的聊天历史堆叠很多人第一反应是记忆嘛不就是把对话历史全塞进prompt里我一开始也这么干结果token消耗爆炸不说模型还会被无关信息干扰。比如你让Agent查“上个月华东区的销售数据”它把前面二十轮关于“如何配置Docker网络”的讨论也带进去了注意力完全被稀释。“hindsight”的核心在于结构化回看。它要求Agent在每一步决策前能主动检索“过去发生了什么与当前任务相关的事”而不是被动地接收全部历史。这就像你开车时看后视镜——你不需要盯着后窗玻璃看全部风景你只需要看到后方车道有没有车。所以我们的设计目标很明确建立一个分层、可检索、带时效标记的记忆库让Agent在需要时能精准“回看”。具体来说我把记忆分成三层工作记忆Working Memory当前任务链中最近3-5步的工具调用结果和关键决策点。这部分直接注入prompt保证Agent不“断片”。情景记忆Episodic Memory按任务会话为单位存储的完整交互记录带时间戳和任务标签。用于跨会话检索“上次类似任务是怎么处理的”。语义记忆Semantic Memory从历史交互中提炼出的稳定知识比如“用户偏好用表格输出”“某个API的调用频率限制是每分钟10次”。这部分用向量库存储支持语义检索。注意不要试图用一套存储解决所有问题。工作记忆用内存或Redis情景记忆用关系型数据库如MySQL语义记忆用向量数据库如Chroma或Qdrant。混在一起只会让检索逻辑变得一团糟。2.2 MCP协议在记忆系统中的角色定位MCPModel Context Protocol在这里扮演的是“记忆总线”的角色。Agent通过MCP Server暴露记忆读写接口其他工具比如Playwright MCP、BurpSuite MCP在执行前后都可以通过标准协议向记忆总线发送事件。这样做的好处是解耦记忆系统不关心是哪个工具产生的数据它只负责按统一格式存储和检索。我实测下来用MCP做记忆总线比直接在Agent代码里硬编码记忆逻辑要灵活得多。比如你新增了一个Blender MCP工具它只需要在调用完成后向记忆Server发一个memory.write请求带上{task_id, step, tool_name, input_summary, output_summary, timestamp}记忆系统就能自动归档。Agent在下一步决策前通过memory.query拉取相关历史整个链路非常清晰。2.3 Docker化部署的考量为什么不用裸机跑Agent记忆系统涉及多个组件Redis做工作记忆缓存、MySQL做情景记忆持久化、向量库做语义检索、再加上MCP Server本身。裸机部署的话依赖冲突和端口管理能把你逼疯。用Docker Compose编排每个组件独立容器网络通过自定义bridge连接数据卷挂载到宿主机既隔离又可控。而且Docker Desktop在Windows上虽然偶尔抽风比如那个经典的“Virtualization support not detected”报错但一旦跑起来开发体验还是很顺的。我建议用docker-compose.yml把整个记忆栈定义清楚一键docker compose up -d就能拉起全套服务。下面我会给出具体的配置。3. 核心细节解析与实操要点3.1 工作记忆的粒度控制记什么、记多少工作记忆是最容易出问题的地方。记太少Agent会重复问已经回答过的问题记太多token爆炸且干扰决策。我的经验是只记录“状态变更”和“关键决策”不记录寒暄和中间过程。具体来说每一步工具调用后向工作记忆写入以下字段字段说明示例step_id步骤序号3tool_name调用的工具mysql_queryintent这一步要达成什么查询华东区销售总额result_digest结果摘要不超过50字总额为1,234,567元环比增长12%status成功/失败/部分成功successtimestamp时间戳2025-01-15T10:23:45Z工作记忆的容量控制在最近5步。超过5步的自动降级到情景记忆。为什么是5步因为大多数Agent任务链在5步内能完成核心逻辑超过5步的往往是异常分支或重试这些信息对当前决策的边际价值递减很快。实操心得result_digest字段一定要强制截断。我试过让模型自己总结结果它有时候会输出一大段废话反而把工作记忆撑爆。后来改成用规则截取前50个字符加省略号效果反而更稳。3.2 情景记忆的存储结构MySQL表设计情景记忆用MySQL存表结构如下CREATE TABLE episodic_memory ( id BIGINT AUTO_INCREMENT PRIMARY KEY, session_id VARCHAR(64) NOT NULL, task_id VARCHAR(64) NOT NULL, step_id INT NOT NULL, tool_name VARCHAR(64), intent TEXT, input_payload JSON, output_payload JSON, status VARCHAR(16), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, INDEX idx_session (session_id), INDEX idx_task (task_id), INDEX idx_created (created_at) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里的关键是session_id和task_id的区分。session_id标识一次完整的用户会话task_id标识会话中的一个子任务。比如用户说“帮我分析一下上个月的销售数据然后生成报告”这算一个session但里面包含“查询数据”和“生成报告”两个task。检索时Agent可以先按task_id找同类任务的历史再按session_id找当前会话的上下文。Docker部署MySQL 8.0的命令docker run -d \ --name agent-memory-mysql \ -e MYSQL_ROOT_PASSWORDyour_strong_password \ -e MYSQL_DATABASEagent_memory \ -p 3306:3306 \ -v /path/to/mysql-data:/var/lib/mysql \ mysql:8.0 \ --character-set-serverutf8mb4 \ --collation-serverutf8mb4_unicode_ci注意-v挂载数据卷这一步千万别省。我踩过一次坑容器重启后数据全没了因为没挂载卷MySQL的数据存在容器内部容器一删就全丢。3.3 语义记忆的向量化策略用LLM做“记忆提炼”语义记忆不是简单地把历史对话扔进向量库。那样做的话检索出来的都是原始对话片段噪声太大。我的做法是每完成一个task用LLM对情景记忆做一次“提炼”生成3-5条结构化知识再存入向量库。提炼的prompt模板大致如下你是一个记忆提炼助手。请从以下任务执行记录中提取出对未来类似任务有参考价值的稳定知识。 输出格式为JSON数组每条知识包含 - content: 知识内容一句话 - category: 分类用户偏好/工具限制/业务规则/其他 - confidence: 置信度0-1 任务记录 {episodic_memory_json}比如从“查询华东区销售数据”这个任务中可能提炼出“用户偏好以万元为单位展示金额”“MySQL查询超时阈值为30秒”“华东区数据表名为sales_east”。这些知识存入向量库后下次遇到类似任务Agent可以先检索语义记忆直接拿到这些约束不用重新试错。向量库我用的是ChromaDocker部署很简单docker run -d \ --name agent-memory-chroma \ -p 8000:8000 \ -v /path/to/chroma-data:/chroma/chroma \ chromadb/chroma:latest3.4 MCP Server的记忆接口设计MCP Server需要暴露两个核心工具memory_write和memory_query。用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(agent-memory) server.list_tools() async def handle_list_tools(): return [ types.Tool( namememory_write, description写入一条记忆记录, inputSchema{ type: object, properties: { layer: {type: string, enum: [working, episodic, semantic]}, payload: {type: object} }, required: [layer, payload] } ), types.Tool( namememory_query, description检索记忆, inputSchema{ type: object, properties: { layer: {type: string}, query: {type: string}, top_k: {type: integer, default: 5} }, required: [layer, query] } ) ]Agent在每次工具调用前后通过MCP协议调用这两个接口。比如Playwright MCP抓取完网页后Agent先调memory_write把结果摘要写入工作记忆再调memory_query拉取相关历史最后把历史当前结果一起送给LLM做决策。4. 实操过程与核心环节实现4.1 环境准备Docker Compose一键拉起记忆栈先把整个记忆系统的Docker Compose文件写好。我习惯把配置放在docker-compose.yml里包括MySQL、Redis、Chroma和MCP Server四个服务version: 3.8 services: mysql: image: mysql:8.0 container_name: agent-memory-mysql environment: MYSQL_ROOT_PASSWORD: your_strong_password MYSQL_DATABASE: agent_memory ports: - 3306:3306 volumes: - ./data/mysql:/var/lib/mysql command: --character-set-serverutf8mb4 --collation-serverutf8mb4_unicode_ci networks: - agent-net redis: image: redis:7-alpine container_name: agent-memory-redis ports: - 6379:6379 volumes: - ./data/redis:/data networks: - agent-net chroma: image: chromadb/chroma:latest container_name: agent-memory-chroma ports: - 8000:8000 volumes: - ./data/chroma:/chroma/chroma networks: - agent-net mcp-memory-server: build: ./mcp-memory-server container_name: mcp-memory-server ports: - 8080:8080 environment: MYSQL_HOST: mysql REDIS_HOST: redis CHROMA_HOST: chroma depends_on: - mysql - redis - chroma networks: - agent-net networks: agent-net: driver: bridgemcp-memory-server的Dockerfile也很简单FROM python:3.11-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . CMD [python, server.py]requirements.txt里主要就是mcp、pymysql、redis、chromadb这几个包。实操心得Windows上跑Docker Desktop如果遇到“Virtualization support not detected”报错先去BIOS里确认Intel VT-x或AMD-V是开启的。然后在Windows功能里勾选“虚拟机平台”和“Windows Subsystem for Linux”。这两步做完重启基本就能解决。别去折腾Hyper-VWSL2后端更稳。4.2 工作记忆的读写实现Redis 滑动窗口工作记忆用Redis的List结构实现每个session一个key比如working_memory:{session_id}。每次写入用LPUSH读取用LRANGE 0 4取最近5条。超过5条的自动RPOP掉最旧的。import redis import json r redis.Redis(hostlocalhost, port6379, decode_responsesTrue) def write_working_memory(session_id, step_data): key fworking_memory:{session_id} r.lpush(key, json.dumps(step_data)) r.ltrim(key, 0, 4) # 只保留最近5条 r.expire(key, 3600) # 1小时过期 def read_working_memory(session_id): key fworking_memory:{session_id} items r.lrange(key, 0, -1) return [json.loads(item) for item in items]这里ltrim是关键它保证工作记忆不会无限增长。expire设置1小时过期是因为大多数Agent会话不会超过1小时过期后自动清理避免Redis内存泄漏。4.3 情景记忆的写入与检索MySQL 全文索引情景记忆的写入在每次工具调用完成后触发。我封装了一个write_episodic函数import pymysql import json from datetime import datetime def write_episodic(session_id, task_id, step_id, tool_name, intent, input_payload, output_payload, status): conn pymysql.connect(hostlocalhost, userroot, passwordyour_strong_password, databaseagent_memory) cursor conn.cursor() sql INSERT INTO episodic_memory (session_id, task_id, step_id, tool_name, intent, input_payload, output_payload, status, created_at) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s) cursor.execute(sql, ( session_id, task_id, step_id, tool_name, intent, json.dumps(input_payload), json.dumps(output_payload), status, datetime.utcnow() )) conn.commit() cursor.close() conn.close()检索的时候Agent通常需要“找当前task之前的相关步骤”。SQL查询如下SELECT step_id, tool_name, intent, output_payload, status FROM episodic_memory WHERE task_id %s AND step_id %s ORDER BY step_id DESC LIMIT 10;这个查询返回当前task中、当前步骤之前的所有步骤按时间倒序排列。Agent拿到后可以从中提取关键信息注入prompt。注意output_payload字段可能很大直接塞进prompt会爆token。我的做法是在检索后加一层“摘要压缩”用规则提取JSON中的关键字段比如total、count、error只把这些字段拼成简短文本。4.4 语义记忆的向量化与检索Chroma LLM提炼语义记忆的写入分两步先提炼再向量化。提炼用LLM向量化用Chroma自带的embedding函数。import chromadb from chromadb.utils import embedding_functions client chromadb.HttpClient(hostlocalhost, port8000) ef embedding_functions.DefaultEmbeddingFunction() collection client.get_or_create_collection(namesemantic_memory, embedding_functionef) def write_semantic_memory(task_id, knowledge_items): for item in knowledge_items: collection.add( documents[item[content]], metadatas[{ task_id: task_id, category: item[category], confidence: item[confidence] }], ids[f{task_id}_{hash(item[content])}] ) def query_semantic_memory(query_text, top_k5): results collection.query( query_texts[query_text], n_resultstop_k ) return results[documents][0]提炼的LLM调用可以用任何你手头的模型关键是prompt要设计好。我试过用GPT-4和Claude效果都不错但成本考虑的话用本地部署的7B模型做提炼也够用毕竟只是信息抽取任务。4.5 完整调用链演示从用户提问到记忆回看假设用户问“帮我查一下上个月华东区的销售数据然后跟华南区对比一下。”Step 1Agent解析意图生成task_id写入工作记忆。Step 2Agent调用memory_query检索语义记忆看有没有“华东区销售数据”相关的历史知识。假设检索到一条“华东区数据表名为sales_east金额单位为元”。Agent把这个约束注入prompt。Step 3Agent调用MySQL MCP工具执行查询。查询完成后调用memory_write写入工作记忆和情景记忆。Step 4Agent调用memory_query检索工作记忆拿到上一步的查询结果摘要。然后调用MySQL MCP查询华南区数据。Step 5Agent对比两个结果生成报告。任务完成后触发语义记忆提炼把“用户偏好对比分析”等知识存入向量库。整个链路中记忆系统通过MCP协议与Agent解耦Agent不需要知道记忆存在哪里、怎么检索它只需要调用标准接口。5. 常见问题与排查技巧实录5.1 Docker网络不通导致MCP Server连不上MySQL这是最常见的问题。现象是MCP Server日志报Connection refused但MySQL容器明明在跑。原因通常是两个容器不在同一个Docker网络里。排查步骤确认docker-compose.yml里所有服务都声明了同一个networks。进入MCP Server容器用ping mysql测试DNS解析。如果ping不通检查docker network ls和docker network inspect agent-net看容器是否真的加入了网络。实操心得Docker Compose默认会创建一个网络但如果你手动docker run启动某个容器它不会自动加入这个网络。所以要么全部用Compose管理要么手动--network agent-net指定。5.2 工作记忆写入后读不到Redis key过期太快我设置过expire为300秒结果一个长任务跑了6分钟中间的工作记忆全丢了。后来改成3600秒并且加了“每次写入时刷新过期时间”的逻辑。def write_working_memory(session_id, step_data): key fworking_memory:{session_id} r.lpush(key, json.dumps(step_data)) r.ltrim(key, 0, 4) r.expire(key, 3600) # 每次写入都刷新过期时间5.3 语义记忆检索结果不相关embedding模型选型问题Chroma默认的embedding模型是all-MiniLM-L6-v2对中文支持一般。如果你的Agent主要处理中文任务建议换成text2vec-base-chinese或者用OpenAI的text-embedding-3-small。ef embedding_functions.SentenceTransformerEmbeddingFunction( model_nameshibing624/text2vec-base-chinese )换模型后需要重建collection因为向量维度变了。5.4 MCP工具调用返回schema错误有时候Agent调用memory_write会报provider rejected the request schema or tool payload。这通常是inputSchema定义和实际传入的payload不匹配。比如schema里payload是object但Agent传了个string。排查方法在MCP Server里加日志打印收到的原始请求对比schema定义。我后来把payload的schema放宽成{type: object, additionalProperties: true}兼容性好了很多。5.5 常见问题速查表问题现象可能原因解决方法MCP Server连不上MySQL容器不在同一网络统一用Compose管理或手动指定network工作记忆丢失Redis key过期增大expire时间每次写入刷新语义检索不准embedding模型不匹配换中文模型重建collectionschema报错inputSchema与实际payload不符放宽schema加日志排查Docker Desktop启动失败虚拟化未开启BIOS开启VT-x/AMD-V启用WSL2MySQL数据丢失未挂载数据卷-v挂载宿主机目录6. 记忆系统的扩展方向与个人经验这套记忆系统跑通之后我陆续加了一些扩展。比如记忆衰减机制情景记忆里的记录如果超过30天没有被检索到就自动归档到冷存储减少主库压力。再比如跨Agent记忆共享多个Agent实例通过同一个MCP Memory Server共享语义记忆这样新启动的Agent能直接继承历史知识不用从零开始。还有一个我觉得很有价值的扩展是记忆冲突检测。当新提炼的知识与已有知识矛盾时比如“用户偏好表格输出”vs“用户偏好纯文本输出”系统会标记冲突并降低旧知识的置信度而不是直接覆盖。这样Agent在检索时能看到冲突自己决定采信哪一条。我个人在实际操作中的体会是记忆系统的核心不是存储而是检索。你存再多数据如果检索不出来或者检索不准等于没存。所以我在检索层花的时间比存储层多得多。工作记忆的滑动窗口大小、情景记忆的SQL查询条件、语义记忆的top_k和相似度阈值这些参数都需要根据你的具体任务反复调。没有一劳永逸的配置只有不断迭代的调优。最后分享一个小技巧在MCP Memory Server里加一个memory_stats工具返回各层记忆的条目数、平均检索耗时、命中率等指标。这样你能直观看到记忆系统的运行状态哪里是瓶颈一目了然。我靠这个工具发现过语义记忆的检索耗时占了整个Agent响应时间的40%后来加了缓存才降下来。
返回列表