ARTICLE DETAIL

资讯详情

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

基于MCP与Docker的LLM Agent记忆系统:hindsight事后复盘机制实践

基于MCP与Docker的LLM Agent记忆系统:hindsight事后复盘机制实践 1. 从“hindsight”说起为什么Agent的记忆需要“事后复盘”第一次看到“hindsight”这个词我脑子里蹦出来的不是词典释义而是自己踩过的一个坑。去年做客服Agent的时候用户第一轮说“帮我查下上周的订单”Agent查了第二轮用户说“那个订单的物流呢”Agent直接懵了——它不知道“那个订单”指的是什么。上下文窗口就那么大对话一长前面的信息被挤掉了Agent的表现就像金鱼七秒记忆。这就是Agent Memory要解决的核心问题。而“hindsight”这个词字面意思是“事后聪明”放在Agent记忆的语境里我理解它指的是一种事后回溯、复盘式记忆机制——Agent不只是记住当前对话还能在需要的时候“回头看”之前发生过什么从历史交互中提取有用信息。这跟人类的工作记忆和长期记忆有点像你不需要记住今天早上吃了什么但如果有人问你“上次那家川菜馆叫什么”你得能翻出来。这个项目标题“hindsight”结合热搜词里的agent memory、LLM、MCP、Docker基本可以判断这是一个围绕LLM Agent记忆系统的工程化实践项目。它要解决的问题很具体Agent在多轮对话、多任务场景下怎么存、怎么取、怎么用历史信息。适合谁看如果你正在做Agent应用被上下文长度折磨过或者想了解MCP协议怎么跟记忆系统结合这篇内容应该能给你一些可以直接抄的作业。我下面会从整体设计思路、核心细节、实操过程、问题排查几个维度展开中间会穿插我自己在Docker环境里搭Agent记忆系统时踩过的坑。内容基于常见工程实践补充具体参数和配置你可以根据自己的场景调整。2. 整体设计思路Agent记忆系统的分层与选型2.1 为什么不能只靠上下文窗口很多人做Agent的第一反应是把历史对话全塞进prompt里不就行了我试过结论是——能跑但跑不远。一个典型的客服Agent单次对话轮次超过20轮token消耗就奔着8000去了。如果用的是按token计费的模型成本直接起飞。更麻烦的是上下文越长模型对中间部分的注意力越弱这就是所谓的“lost in the middle”现象。你塞了100条历史模型可能只记得前5条和后5条。所以Agent Memory的第一个设计决策就是分层。我一般会分成三层工作记忆Working Memory当前对话的最近N轮直接放在prompt里保证响应速度。N一般取5到10看任务复杂度。短期记忆Short-term Memory本次会话的完整历史存在外部存储里需要的时候通过检索召回。长期记忆Long-term Memory跨会话的用户偏好、事实性知识比如“这个用户上次说他对花生过敏”。hindsight这个项目从名字推测重点可能在短期记忆和长期记忆的回溯机制上。它要做的不是简单地把历史存下来而是让Agent在需要的时候能“想起来”。2.2 MCP协议在记忆系统中的角色热搜词里MCP出现了很多次这里简单说一下我的理解。MCPModel Context Protocol本质上是一个标准化的工具调用协议让LLM能够以统一的方式访问外部资源。你可以把它理解成Agent世界的USB接口——不管你是数据库、文件系统还是API只要实现了MCP ServerAgent就能通过标准接口调用。在记忆系统里MCP的价值在于解耦。记忆的存储和检索逻辑封装成一个MCP ServerAgent本身不需要知道底层用的是Redis还是PostgreSQL只需要按照MCP协议发请求就行。这样换存储后端的时候Agent代码不用动。我自己的做法是把记忆的写入和检索分别做成两个MCP工具。写入工具接收content、metadata、timestamp检索工具接收query、top_k、time_range。Agent在对话过程中自动调用写入工具在需要回忆的时候调用检索工具。2.3 Docker化部署的考量热搜词里Docker相关的内容很多包括docker安装、docker desktop、docker网络不通等等。这说明很多人在本地跑Agent记忆系统的时候选择用Docker来管理依赖。我自己的经验是Agent记忆系统涉及多个组件——LLM服务、向量数据库、MCP Server、可能还有Redis做缓存。如果每个都手动装环境冲突能让人崩溃。Docker Compose一把梭所有服务定义在一个yaml文件里docker compose up就能跑起来省心。但Docker也有坑。Windows上装Docker Desktop经常遇到“Virtualization support not detected”的报错这个后面排查部分会细说。另外Docker网络不通也是高频问题特别是容器之间需要互相访问的时候。3. 核心细节解析记忆的写入、存储与检索3.1 记忆写入什么该记什么不该记这是我觉得最容易被忽视的环节。很多人做记忆系统上来就是“把所有对话都存下来”。我试过结果检索的时候噪音太大召回的内容一半是“好的”“谢谢”这种废话。我的做法是过滤摘要。具体来说用户的消息如果长度小于10个字符且不包含实体人名、地名、时间、数字直接丢弃。Agent的回复只保留包含事实性信息的部分比如“您的订单号是12345”而不是“很高兴为您服务”。每5轮对话让LLM做一次摘要把关键信息提取成结构化格式。结构化格式我一般用JSON字段包括subject主体、action动作、object客体、time时间。比如用户说“我上周在你们这买了个蓝色的杯子”提取出来就是{ subject: 用户, action: 购买, object: 蓝色杯子, time: 上周 }这样检索的时候即使用户换了一种说法比如“那个蓝色的杯子”通过语义相似度也能匹配到。3.2 存储选型向量数据库 vs 传统数据库热搜词里有“agent 存储 working memory”和“rag graphrag llm wiki 本体rag”说明大家在存储方案上有不同选择。我列一下我用过的几种方案和适用场景方案适用场景优点缺点Redis工作记忆、短期缓存快简单不支持语义检索PostgreSQL pgvector中小规模长期记忆事务支持好SQL查询灵活向量检索性能一般专用向量库如Milvus、Qdrant大规模语义检索检索性能强支持混合搜索运维复杂度高图数据库如Neo4j关系型记忆、知识图谱关系推理强学习曲线陡我自己的项目里工作记忆用Redis长期记忆用PostgreSQL pgvector。原因很简单数据量不大单个用户几千条记忆pgvector够用了而且不用额外维护一个向量数据库。如果数据量上到百万级再考虑迁移到Qdrant。3.3 检索策略不只是向量相似度很多人做记忆检索就是拿query去向量库里做相似度搜索返回top_k。我试过效果一般。问题在于向量相似度只能捕捉语义相似捕捉不了时间关系和逻辑关系。我的做法是混合检索语义检索用embedding做相似度搜索召回top_20。时间过滤如果query里包含时间词“上周”“昨天”按时间范围过滤。关键词匹配用BM25做关键词召回补充语义检索漏掉的结果。重排序用一个小的cross-encoder模型对召回结果重排序取top_5。这套流程下来召回质量明显提升。代价是延迟增加大概多200到300毫秒。对于非实时场景可以接受。3.4 记忆的遗忘机制这个点很少有人提但我觉得很重要。记忆系统不能只进不出否则检索噪音会越来越大。我设计了一个简单的遗忘策略超过30天未被检索到的记忆降权处理。超过90天未被检索到的记忆归档到冷存储。用户明确说“忘记这个”的记忆直接删除。降权的方式是在检索评分里乘一个时间衰减因子。比如import math def time_decay(last_access_time, now, half_life_days30): days (now - last_access_time).days return math.exp(-days / half_life_days)这样越久没用的记忆检索评分越低自然就被挤下去了。4. 实操过程从零搭建一个带记忆的Agent4.1 环境准备与Docker Compose配置我假设你已经装好了Docker和Docker Compose。如果没有Windows用户去官网下Docker DesktopLinux用户用包管理器装。这里不展开安装步骤重点说Compose配置。我的docker-compose.yml大概长这样version: 3.8 services: redis: image: redis:7-alpine ports: - 6379:6379 volumes: - redis_data:/data postgres: image: pgvector/pgvector:pg16 environment: POSTGRES_DB: agent_memory POSTGRES_USER: agent POSTGRES_PASSWORD: agent123 ports: - 5432:5432 volumes: - pg_data:/var/lib/postgresql/data mcp_server: build: ./mcp_server ports: - 8080:8080 depends_on: - redis - postgres environment: REDIS_URL: redis://redis:6379 DATABASE_URL: postgresql://agent:agent123postgres:5432/agent_memory volumes: redis_data: pg_data:几个关键点pgvector用官方镜像pgvector/pgvector:pg16省得自己编译。MCP Server用build而不是image因为需要自定义代码。环境变量里数据库连接用服务名postgres而不是localhost这是Docker网络的基本规则。注意如果你在Windows上跑确保Docker Desktop的WSL2后端已启用。否则容器之间网络可能不通。4.2 数据库表结构设计PostgreSQL里我建了两张表memories和memory_embeddings。CREATE EXTENSION IF NOT EXISTS vector; CREATE TABLE memories ( id SERIAL PRIMARY KEY, user_id VARCHAR(64) NOT NULL, session_id VARCHAR(64) NOT NULL, content TEXT NOT NULL, metadata JSONB DEFAULT {}, created_at TIMESTAMP DEFAULT NOW(), last_accessed_at TIMESTAMP DEFAULT NOW(), access_count INT DEFAULT 0 ); CREATE TABLE memory_embeddings ( memory_id INT REFERENCES memories(id) ON DELETE CASCADE, embedding vector(1536) ); CREATE INDEX ON memories (user_id, session_id); CREATE INDEX ON memories (created_at); CREATE INDEX ON memory_embeddings USING ivfflat (embedding vector_cosine_ops) WITH (lists 100);vector(1536)是因为我用的是OpenAI的text-embedding-3-small维度1536。如果你用别的embedding模型改这个数字就行。ivfflat索引的lists参数一般取sqrt(行数)。我预估单用户记忆量在10000条左右所以取100。4.3 MCP Server的实现MCP Server我用Python写框架用mcp这个包。核心是两个工具write_memory和search_memory。from mcp.server import Server from mcp.types import Tool, TextContent import asyncpg import redis.asyncio as redis 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}, session_id: {type: string}, content: {type: string}, metadata: {type: object} }, required: [user_id, content] } ), Tool( namesearch_memory, description检索记忆, inputSchema{ type: object, properties: { user_id: {type: string}, query: {type: string}, top_k: {type: integer, default: 5} }, required: [user_id, query] } ) ] app.call_tool() async def call_tool(name: str, arguments: dict): if name write_memory: return await handle_write(arguments) elif name search_memory: return await handle_search(arguments)handle_write的逻辑是生成embedding写入PostgreSQL同时把最近N条写入Redis作为工作记忆。handle_search的逻辑是生成query的embedding在PostgreSQL里做向量检索结合时间衰减因子重排序返回top_k。4.4 Agent侧的集成Agent侧我用的是LangChain但核心逻辑不依赖特定框架。关键是在对话循环里插入记忆的读写。async def chat_loop(user_id, session_id, user_input): # 1. 检索相关记忆 memories await mcp_client.call_tool(search_memory, { user_id: user_id, query: user_input, top_k: 5 }) # 2. 构建prompt memory_context \n.join([m[content] for m in memories]) prompt f 以下是相关历史记忆 {memory_context} 用户说{user_input} 请回复 # 3. 调用LLM response await llm.generate(prompt) # 4. 写入记忆 await mcp_client.call_tool(write_memory, { user_id: user_id, session_id: session_id, content: f用户{user_input}\n助手{response}, metadata: {type: dialogue} }) return response这里有个细节写入的时候我把用户输入和助手回复拼在一起存。检索的时候如果匹配到这条记忆模型能看到完整的对话上下文比只存用户输入效果好。4.5 参数计算embedding维度和检索阈值embedding维度取决于你用的模型。OpenAI的text-embedding-3-small是1536维3-large是3072维。维度越高表达能力越强但存储和检索成本也越高。我一般用1536够用了。检索阈值这个参数我试过几个值。余弦相似度阈值设0.7的话召回的内容比较相关但可能漏掉一些设0.5的话召回多但噪音大。我的经验是0.65比较平衡。当然这个值跟你的embedding模型有关建议在自己的数据上测一下。5. 常见问题与排查技巧实录5.1 Docker Desktop启动失败Virtualization support not detected这是Windows用户的高频问题。报错信息一般是“Virtualization support not detected”或者“Docker Desktop failed to start because virtualization support is not enabled”。排查步骤打开任务管理器看“性能”标签页CPU那一栏有没有“虚拟化已启用”。如果是“已禁用”进BIOS开启。如果BIOS里开了还是不行检查Hyper-V和WSL2是否冲突。管理员权限打开PowerShell运行bcdedit /set hypervisorlaunchtype auto然后重启。如果用的是Windows家庭版可能没有Hyper-V需要装WSL2后端。Docker Desktop设置里勾选“Use WSL 2 based engine”。我踩过的坑开了Hyper-V之后VMware用不了了。如果同时需要VMware得用WSL2后端别开Hyper-V。5.2 Docker网络不通容器之间无法互相访问这个问题的表现是MCP Server容器里连不上PostgreSQL容器报“connection refused”。排查思路首先确认两个容器在同一个network里。docker compose默认会创建一个network所有服务都在里面。然后确认连接字符串用的是服务名而不是localhost。在容器里localhost指的是容器自己不是宿主机。如果还是不通进容器里ping一下docker exec -it mcp_server ping postgres。如果不通检查防火墙。我遇到过一次是因为Windows防火墙拦了Docker的虚拟网卡。在防火墙里允许Docker Desktop就行。5.3 记忆检索召回质量差这个问题的表现是Agent检索出来的记忆跟当前对话不相关或者该召回的记忆没召回。排查方向检查embedding模型是否一致。写入和检索必须用同一个模型否则向量空间不对齐。检查文本预处理。如果写入的时候做了摘要检索的时候用原始query可能匹配不上。建议检索前也对query做一次改写。调整top_k和阈值。top_k太小可能漏太大可能引入噪音。加入重排序。纯向量检索的效果有限加一个cross-encoder重排序能明显提升。5.4 常见问题速查表问题可能原因解决方法Docker Desktop启动失败虚拟化未开启BIOS开启虚拟化或切换WSL2后端容器间网络不通不在同一network检查docker-compose网络配置记忆检索不相关embedding不一致确保写入和检索用同一模型检索延迟高向量索引未优化建ivfflat或hnsw索引记忆膨胀无遗忘机制加时间衰减和归档策略MCP工具调用失败schema不匹配检查inputSchema定义5.5 几个我踩过的坑第一个坑一开始我把所有对话都存了结果检索的时候噪音太大。后来加了过滤规则只存包含实体或事实的对话效果好了很多。第二个坑embedding生成是异步的但我一开始没做批量处理每条记忆单独调一次API写入100条记忆花了30秒。后来改成批量生成100条只要3秒。第三个坑Redis里的工作记忆没有设过期时间跑了一周发现内存爆了。后来加了TTL默认24小时过期。6. 记忆系统的扩展方向这套东西跑通之后我试过几个扩展方向简单说一下。一个是记忆的主动召回。现在的做法是每次对话都检索一次但有些对话根本不需要历史信息。我的优化是先用一个小模型判断当前query是否需要历史记忆需要才检索。这样能省不少token。另一个是跨用户记忆共享。有些场景下多个用户之间的记忆可以共享比如同一个团队的成员。实现方式是在memories表里加一个group_id字段检索的时候按group过滤。还有一个是记忆的可视化。我写了一个简单的Web界面用D3.js把记忆之间的关系画出来方便调试。这个不是必须的但对理解记忆系统的行为很有帮助。最后分享一个小技巧如果你用的是OpenAI的embedding API记得把dimensions参数设成1536默认是1536但有些模型默认不是。这个参数不设对向量维度对不上插入数据库会报错。
返回列表