ARTICLE DETAIL

资讯详情

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

Agent Memory实战:用Docker+MCP构建hindsight事后复盘记忆系统

Agent Memory实战:用Docker+MCP构建hindsight事后复盘记忆系统 1. 为什么“事后复盘”这件事值得单独造一个轮子做Agent开发的人都有一个共同的痛点对话跑完了一整轮模型表现不错但下次遇到类似任务它又像个新手一样从头摸索。你明明在上一轮里纠正过它的错误、补充过关键背景、甚至告诉过它“这个API的字段名是user_id不是uid”结果下一轮开新会话全部归零。这就是Agent Memory这个方向被反复讨论的根本原因。而hindsight这个词本身就很有意思——它指的是“事后的领悟”也就是事情发生之后才明白当初应该怎么做。把这个概念放到Agent身上它要解决的问题非常具体让Agent在任务结束后能够回过头去审视自己刚才做了什么、哪些地方踩了坑、哪些信息值得记住然后把这些沉淀成下一次可以直接调用的记忆。我最初接触这个思路是在做一个多轮工具调用的项目时。当时Agent每次调用MCP工具都要重新理解一遍工具描述遇到参数格式不对就反复试错一个简单的数据库查询能来回折腾七八轮。后来我意识到问题不在于模型不够聪明而在于它没有“记住上一次是怎么成功的”。hindsight要做的就是把这个“上一次”变成可检索、可复用、可演进的记忆资产。这篇文章适合三类人看一是正在做Agent应用、被上下文窗口和记忆管理折磨的开发者二是对MCP协议感兴趣、想知道记忆层怎么和工具调用层配合的技术人三是想用Docker快速搭一套可跑的记忆系统、不想从零造轮子的实践派。我会把设计思路、核心机制、Docker部署、MCP对接、踩坑记录全部摊开讲代码和配置都能直接抄。2. 核心思路拆解hindsight到底在记什么2.1 记忆不是日志是经过压缩的“经验切片”很多人第一反应是记忆嘛把对话历史存下来不就行了我试过存了三天数据库就爆了而且检索出来的全是废话。原始对话里80%的内容是寒暄、重复确认、格式调整真正有价值的可能就一两句话。hindsight的核心设计理念是事后压缩。它不在对话进行中实时记录而是在一个任务单元比如一次完整的工具调用链、一轮问答、一个代码生成任务结束后触发一次“复盘”流程。这个流程做三件事提取从原始交互中抽取出关键决策点、成功路径、失败原因、用户偏好。压缩把提取出来的内容用LLM重新组织成结构化条目而不是保留原文。索引给每条记忆打上语义标签和场景标签方便后续按需检索。打个比方原始对话就像监控录像hindsight做的是看完录像后写一份案件摘要——什么时间、什么地点、发生了什么、关键证据是什么。下次遇到类似案件直接翻摘要就行不用再看一遍录像。2.2 为什么选择MCP作为记忆的暴露层这里要解释一个关键选型为什么hindsight要把记忆能力通过MCP协议暴露出来而不是直接做一个SDK或者REST API。MCPModel Context Protocol本质上是一个让模型和外部能力对接的标准化协议。你可以把它理解成“AI世界的USB接口”——不管后面接的是数据库、文件系统还是记忆服务只要符合MCP规范模型就能用统一的方式去调用。我选择MCP的理由有三个第一解耦。记忆服务独立进程运行Agent框架换了一茬又一茬记忆层不用动。今天用LangChain明天换AutoGen后天自己手写循环只要MCP客户端还在记忆就能接上。第二工具化。MCP把记忆操作变成了模型可以主动调用的工具。模型可以自己决定“我现在需要查一下之前有没有处理过类似任务”而不是被动地等系统塞上下文。这种主动性对复杂任务特别重要。第三生态兼容。现在支持MCP的客户端越来越多从桌面应用到IDE插件都在接入。记忆服务做成MCP Server等于一次性对接了整个生态。注意MCP是软件协议层面的概念和硬件接口协议不是一回事。它的核心是定义模型与外部工具之间的通信格式包括工具描述、调用请求、返回结果的结构。2.3 存储层选型为什么是Docker 向量库 结构化存储hindsight的存储需求有两类一类是语义检索需要向量数据库另一类是精确查询和元数据过滤需要结构化存储。我的方案是Docker Compose编排三个服务服务作用选型建议向量存储存记忆的语义向量支持相似度检索Qdrant或Milvus轻量场景Qdrant更省资源结构化存储存记忆元数据、标签、时间戳、调用记录PostgreSQL或MySQL我用的MySQL 8.0缓存层存热点记忆和会话状态Redis主从模式保证可用性全部用Docker跑的好处是环境隔离、一键启动、迁移方便。我在Windows 11上用Docker DesktopLinux服务器上用Docker Compose配置基本一致。3. 核心机制深度解析记忆是怎么被写进去又读出来的3.1 记忆写入的触发时机与压缩策略写入时机很关键。如果每轮对话都写噪音太大如果只在会话结束写可能丢失中间的关键转折。hindsight采用的是任务边界触发一次完整的工具调用链结束用户明确表示“这个任务完成了”检测到话题切换或长时间无交互手动触发通过MCP工具调用触发后系统会把这段时间窗口内的交互内容送给一个专门的“复盘模型”。这个模型的任务不是继续对话而是做信息抽取。我用的提示词结构大致是这样的你是一个任务复盘助手。请分析以下交互记录提取 1. 任务目标是什么 2. 最终是否成功关键成功因素是什么 3. 遇到了哪些错误是如何修正的 4. 有哪些可复用的经验API格式、参数约定、用户偏好 5. 用一句话概括这条记忆的适用场景 输出格式为JSON字段包括goal, outcome, key_steps, pitfalls, reusable_knowledge, scenario_summary。这个步骤用的是一个较小的模型就够了比如7B级别的本地模型或者便宜的API调用。因为它的任务很聚焦不需要太强的推理能力。压缩后的记忆条目大概长这样{ goal: 查询用户表中最近7天注册且未验证邮箱的用户, outcome: success, key_steps: [使用user_id作为主键, 时间字段是created_at, 验证状态字段是email_verified], pitfalls: [最初用了uid导致字段不存在, 时间范围要用UTC], reusable_knowledge: 该数据库用户表主键为user_id时间字段created_at为UTC时间戳, scenario_summary: MySQL用户表查询涉及时间范围和状态过滤 }3.2 向量化与索引构建的细节每条压缩后的记忆需要被向量化才能支持语义检索。这里有个容易踩的坑不要只向量化scenario_summary。我一开始只把摘要向量化结果检索时经常漏掉关键细节。后来改成多字段拼接向量化text_for_embedding f 场景{scenario_summary} 目标{goal} 可复用知识{ .join(reusable_knowledge)} 关键步骤{ .join(key_steps)} 这样检索时即使用户的查询和摘要不完全匹配但只要命中了可复用知识或关键步骤也能被召回。向量维度取决于你用的embedding模型。我用的BGE-M31024维在Qdrant里建collection时指定curl -X PUT http://localhost:6333/collections/hindsight_memory \ -H Content-Type: application/json \ -d { vectors: { size: 1024, distance: Cosine } }同时结构化存储里要保留原始JSON和元数据方便做精确过滤。比如“只检索最近30天的记忆”或者“只检索与数据库相关的记忆”这些用向量库的payload过滤也能做但复杂条件还是走SQL更灵活。3.3 记忆检索的三种模式hindsight通过MCP暴露了三个检索工具对应三种使用场景第一种语义相似检索。模型传入一段自然语言描述返回最相似的N条记忆。适合“我之前有没有处理过类似任务”这种模糊查询。第二种标签精确检索。模型传入场景标签或工具名称返回该类别下的所有记忆。适合“把所有关于MySQL的记忆都调出来”这种明确需求。第三种混合检索。先按标签粗筛再在结果集内做语义排序。这是我实际用得最多的模式兼顾了准确率和召回率。MCP工具的定义大概是这样{ name: search_memory, description: 检索历史任务记忆支持语义相似、标签过滤和混合模式, inputSchema: { type: object, properties: { query: {type: string, description: 自然语言查询描述}, tags: {type: array, items: {type: string}, description: 场景标签过滤}, mode: {type: string, enum: [semantic, tag, hybrid], default: hybrid}, limit: {type: integer, default: 5} } } }3.4 记忆的生命周期管理记忆不是只增不减的。hindsight设计了一套简单的生命周期策略新鲜期7天内的记忆检索权重最高。稳定期7到90天的记忆正常权重但如果被多次命中会提升权重。归档期超过90天且从未被命中的记忆降权处理但不删除。淘汰超过180天且命中次数为0的记忆标记为可删除由人工确认后清理。这套策略通过结构化存储里的字段来控制created_at、last_hit_at、hit_count、status。检索时在SQL层做权重计算向量检索结果再按权重重新排序。4. Docker环境搭建与完整部署实操4.1 Docker Desktop安装与常见启动问题Windows 11上装Docker Desktop最容易卡在虚拟化支持这一步。如果你看到“Virtualization support not detected”或者“Docker Desktop failed to start because virtualization support is not enabled”按这个顺序排查进BIOS确认Intel VT-x或AMD-V已开启。不同主板位置不一样通常在Advanced或CPU Configuration里。Windows功能里确认“虚拟机平台”和“Windows Subsystem for Linux”都已勾选。如果用的是WSL2后端确认WSL版本是2用wsl --set-default-version 2设置。某些安全软件会拦截Hyper-V临时关闭试试。安装完成后建议把Docker Desktop的资源配置调一下内存至少给4GBCPU给2核以上。向量库和数据库同时跑资源不够会频繁OOM。4.2 Docker Compose编排文件详解下面是我实际在用的docker-compose.yml包含MySQL、Redis、Qdrant和hindsight服务本身version: 3.8 services: mysql: image: mysql:8.0 container_name: hindsight-mysql environment: MYSQL_ROOT_PASSWORD: hindsight_root_2024 MYSQL_DATABASE: hindsight MYSQL_USER: hindsight_user MYSQL_PASSWORD: hindsight_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: - hindsight-net redis: image: redis:7-alpine container_name: hindsight-redis ports: - 6379:6379 volumes: - redis_data:/data command: redis-server --appendonly yes networks: - hindsight-net qdrant: image: qdrant/qdrant:latest container_name: hindsight-qdrant ports: - 6333:6333 - 6334:6334 volumes: - qdrant_data:/qdrant/storage networks: - hindsight-net hindsight: build: . container_name: hindsight-server ports: - 8080:8080 environment: MYSQL_HOST: mysql MYSQL_PORT: 3306 MYSQL_DB: hindsight MYSQL_USER: hindsight_user MYSQL_PASSWORD: hindsight_pass_2024 REDIS_HOST: redis REDIS_PORT: 6379 QDRANT_HOST: qdrant QDRANT_PORT: 6333 EMBEDDING_MODEL: BAAI/bge-m3 depends_on: - mysql - redis - qdrant networks: - hindsight-net volumes: mysql_data: redis_data: qdrant_data: networks: hindsight-net: driver: bridge几个关键点说明MySQL的init.sql用来建表我放在项目根目录容器首次启动时自动执行。Redis开了AOF持久化防止重启丢缓存。Qdrant暴露6333HTTP和6334gRPC两个端口。hindsight服务自己构建镜像Dockerfile里装Python依赖和模型。4.3 数据库初始化脚本init.sql的内容CREATE TABLE IF NOT EXISTS memories ( id BIGINT AUTO_INCREMENT PRIMARY KEY, goal TEXT NOT NULL, outcome VARCHAR(32) NOT NULL, key_steps JSON, pitfalls JSON, reusable_knowledge JSON, scenario_summary VARCHAR(512), tags JSON, embedding_id VARCHAR(128), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, last_hit_at TIMESTAMP NULL, hit_count INT DEFAULT 0, status VARCHAR(16) DEFAULT active, INDEX idx_status (status), INDEX idx_created (created_at), INDEX idx_scenario (scenario_summary(255)) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE IF NOT EXISTS memory_hits ( id BIGINT AUTO_INCREMENT PRIMARY KEY, memory_id BIGINT NOT NULL, query_text TEXT, hit_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (memory_id) REFERENCES memories(id), INDEX idx_memory (memory_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;memory_hits表用来记录每次命中方便后续做权重调整和效果分析。4.4 启动与验证启动命令很简单docker compose up -d然后检查各服务状态docker compose ps验证MySQL连通docker exec -it hindsight-mysql mysql -uhindsight_user -phindsight_pass_2024 -e USE hindsight; SHOW TABLES;验证Qdrantcurl http://localhost:6333/collections验证Redisdocker exec -it hindsight-redis redis-cli ping全部返回正常后hindsight服务应该已经在8080端口监听。可以用curl测一下健康检查接口curl http://localhost:8080/health实操心得第一次启动时MySQL初始化需要时间hindsight服务如果启动太快会连不上数据库。我在代码里加了重试逻辑最多重试10次每次间隔3秒。你也可以在compose里加healthcheck但简单重试更省事。5. MCP对接与Agent集成实战5.1 MCP Server的实现要点hindsight作为MCP Server需要实现几个核心方法list_tools、call_tool。用Python的话官方有mcp库可以用。核心代码结构from mcp.server import Server from mcp.types import Tool, TextContent import json app Server(hindsight-memory) app.list_tools() async def list_tools(): return [ Tool( namesearch_memory, description检索历史任务记忆, inputSchema{ type: object, properties: { query: {type: string}, tags: {type: array, items: {type: string}}, mode: {type: string, enum: [semantic, tag, hybrid]}, limit: {type: integer, default: 5} }, required: [query] } ), Tool( namewrite_memory, description写入一条任务记忆, inputSchema{ type: object, properties: { goal: {type: string}, outcome: {type: string}, key_steps: {type: array, items: {type: string}}, pitfalls: {type: array, items: {type: string}}, reusable_knowledge: {type: array, items: {type: string}}, scenario_summary: {type: string}, tags: {type: array, items: {type: string}} }, required: [goal, outcome, scenario_summary] } ) ] app.call_tool() async def call_tool(name: str, arguments: dict): if name search_memory: results await search_memory(**arguments) return [TextContent(typetext, textjson.dumps(results, ensure_asciiFalse))] elif name write_memory: memory_id await write_memory(**arguments) return [TextContent(typetext, textfMemory written with id {memory_id})]5.2 在Agent框架中接入MCP不同框架接入MCP的方式略有差异但核心都是配置MCP Server的地址和启动方式。以常见的配置为例{ mcpServers: { hindsight: { command: docker, args: [exec, -i, hindsight-server, python, -m, hindsight.mcp_server], env: {} } } }如果hindsight暴露的是HTTP接口也可以用SSE方式连接{ mcpServers: { hindsight: { url: http://localhost:8080/mcp/sse } } }接入后Agent在需要时会自动调用search_memory。但实际用下来模型不会总是主动去查记忆。我的做法是在系统提示词里加一句在执行任何数据库查询或API调用之前先使用search_memory工具检查是否有相关的历史经验。这一句话让记忆命中率提升了至少三倍。5.3 记忆写入的自动化触发写入记忆有两种方式模型主动调用write_memory或者由外部编排层在任务结束时自动触发。我推荐混合模式编排层检测到任务完成信号后自动调用复盘流程生成记忆草稿然后通过write_memory写入。这样不依赖模型自觉保证记忆的连续性。复盘流程的触发条件可以配置def should_trigger_hindsight(session): if session.tool_call_count 3: return True if session.has_error and session.error_resolved: return True if session.user_said_done: return True if time_since_last_interaction(session) 300: return True return False5.4 与Browser Use MCP、Playwright MCP的配合有朋友问过browser use MCP和playwright MCP的区别这里顺带说一下browser use更偏向让模型自主操作浏览器完成开放任务playwright MCP更偏向精确的页面自动化和测试。两者都可以和hindsight配合——hindsight记住的是“这个网站的表单字段是什么”“登录流程是怎样的”下次操作同类网站时直接调用记忆不用重新探索。比如你第一次用browser use MCP登录某个后台系统踩了三个坑才找到正确的登录按钮。hindsight把这条经验记下来。第二次遇到同类系统模型先查记忆直接定位到正确的选择器省掉大量试错。6. 常见问题与排查技巧实录6.1 Docker网络不通的排查顺序Docker Compose默认创建一个bridge网络服务之间用服务名互相访问。如果hindsight连不上MySQL按这个顺序查docker compose ps确认所有容器都在运行。docker exec -it hindsight-server ping mysql测试网络连通。检查环境变量里的host是不是用的服务名mysql而不是localhost。检查MySQL是否真的初始化完成看日志docker logs hindsight-mysql。如果还是不通检查防火墙是否拦截了Docker的内部网络。我遇到过一次是MySQL启动时初始化脚本报错导致容器反复重启日志里能看到具体的SQL错误。把init.sql里的语法问题修掉就好了。6.2 向量检索结果不相关的调优如果检索出来的记忆和查询意图不匹配通常是这几个原因现象可能原因解决方向返回结果完全不相关embedding模型不适合中文换BGE-M3或类似多语言模型相关结果排在后排向量化文本太短拼接多字段一起向量化漏掉关键记忆标签过滤太严格改用混合模式放宽标签条件重复返回同一条去重逻辑缺失按memory_id去重后再排序新记忆检索不到索引更新延迟写入后强制刷新Qdrant索引我踩过最坑的一次是embedding模型用了英文为主的中文记忆检索效果极差。换成BGE-M3之后召回率肉眼可见地提升。6.3 记忆膨胀导致性能下降跑了一个月后记忆表到了几万条检索开始变慢。解决办法给Qdrant的collection建HNSW索引参数调优m设16ef_construct设100。MySQL的scenario_summary字段建前缀索引。定期归档低权重记忆把status改为archived检索时默认过滤掉。热点记忆放Redis缓存减少向量库查询次数。6.4 MCP工具调用失败的常见原因模型调用MCP工具失败报“provider rejected the request schema or tool payload”这类错误通常是工具定义的inputSchema和实际传入参数不匹配比如该传数组的传了字符串。必填字段缺失。参数类型不对比如limit传了字符串5而不是数字5。排查方法在MCP Server端打日志把收到的原始参数打印出来。我一般会在call_tool入口加一行logger.info(fTool called: {name}, args: {json.dumps(arguments)})这样一眼就能看出是模型传错了还是服务端解析错了。6.5 记忆写入的时机误判有时候任务还没真正结束复盘流程就被触发了导致记忆不完整。我的经验是宁可晚一点写也不要写半截。把触发条件设得保守一些比如工具调用次数阈值从3提到5或者要求至少有一次成功的结果返回。另外写入前可以让复盘模型判断一下“这条交互是否包含值得记忆的信息”。如果只是简单的问候或确认直接跳过不浪费存储和检索资源。7. 记忆系统的演进方向与个人实践体会hindsight这套东西我跑了大概三个月从最初的一个简单脚本演进到现在Docker Compose编排的完整服务。中间经历了几次大的调整从只存对话原文到结构化压缩从单一向量检索到混合检索从手动写入到自动触发。有一个体会特别深记忆的价值不在于多而在于准。我一开始追求记忆条数觉得存得越多越聪明。后来发现检索出十条不相关的记忆还不如只返回一条精准的。现在我的策略是严格控制写入质量宁可漏记不可错记。另一个体会是标签体系要提前设计。我最初没规划标签后来想按场景过滤时发现历史记忆全都没有标签只能重新跑一遍复盘流程补标签。建议一开始就定义好标签维度比如按工具类型database、browser、file、按任务类型query、write、debug、按领域user、order、payment。后续我打算把记忆的权重机制做得更细一些比如引入时间衰减因子和命中反馈闭环——模型用了某条记忆后如果任务成功就给这条记忆加分如果失败就减分。这样记忆库会自己进化越用越准。如果你也在做Agent记忆相关的东西建议先从最小可用版本跑起来一个MySQL存结构化记忆一个Qdrant存向量一个MCP Server暴露检索和写入接口。跑通之后再逐步加缓存、加权重、加生命周期管理。别一上来就追求大而全记忆这东西用起来比设计更重要。
返回列表