尧图精选

LLM Agent记忆系统实战:基于MCP协议的分层记忆架构与Docker部署

🕒 发布时间:2026/10/1 4:55:01 📁 来源:尧图网络
1. 从“hindsight”说起为什么我们需要给Agent装上“后视镜”“hindsight”这个词本身很有意思字面意思是“事后的洞察力”也就是我们常说的“后见之明”。放在LLM Agent的语境里它指向一个非常具体且关键的问题Agent如何记住过去发生过的事情并在后续决策中有效地调用这些记忆如果你最近在折腾Agent相关的项目大概率会遇到这样的场景你花了不少时间搭建了一个基于LLM的对话助手或者任务执行Agent刚开始用着还行但聊得越久它越“健忘”——前面明确说过的偏好、约束、上下文过几轮就丢得干干净净。你不得不反复提醒它“我是谁”“我刚才说了什么”“我要你做什么”体验非常割裂。这不是模型能力的问题而是记忆架构的问题。当前主流LLM的上下文窗口虽然越来越大从4K到128K甚至1M token但“能塞进去”和“能有效利用”是两码事。上下文越长注意力稀释越严重关键信息被淹没的概率越高。更别说每次请求都携带完整历史token成本会线性膨胀。“hindsight”这个项目标题本质上就是在解决这个问题为LLM Agent构建一套持久化、可检索、可演进的记忆系统。它要做的不是简单地存聊天记录而是让Agent具备“回忆”和“反思”的能力——在需要的时候精准地调出相关的历史经验辅助当前决策。这套东西适合谁如果你正在做以下任何一件事它都值得你花时间研究构建多轮对话系统希望Agent能记住用户偏好和历史交互开发任务型Agent需要跨会话保持状态和上下文研究LLM的记忆机制想动手实现一套可落地的记忆架构使用MCP协议对接各类工具希望Agent能调用外部记忆存储接下来我会从架构设计、核心机制、实操部署、问题排查几个维度把这套东西拆开讲清楚。内容会涉及Docker部署、MCP协议对接、记忆存储结构设计等具体细节尽量做到你照着就能跑起来。2. 记忆系统的整体架构设计为什么不是简单的向量数据库2.1 从“上下文窗口”到“分层记忆”的范式转变很多人第一次接触Agent记忆第一反应是“搞个向量数据库不就行了”。把历史对话embedding后存进去需要的时候做相似度检索听起来很合理。但实际跑起来会发现几个致命问题第一检索精度不稳定。向量相似度匹配的是语义相近但Agent需要的往往是“事实相关”。比如用户说“我下周要去北京出差”三天后问“帮我看看那边的天气”向量检索可能召回一堆“北京”“出差”相关的片段但真正关键的是“下周”这个时间约束。纯向量检索对结构化约束的捕捉很弱。第二记忆没有生命周期。所有历史记录一视同仁地存着旧信息和新信息混在一起。用户上周说“我喜欢喝美式”这周说“我最近改喝拿铁了”如果两条记忆权重相同Agent很可能给出矛盾的建议。第三缺乏主动遗忘和 consolidation。人脑的记忆不是简单堆叠而是会经历编码、巩固、遗忘的过程。Agent记忆系统如果只增不减检索空间会越来越嘈杂信噪比持续下降。“hindsight”这类项目的核心思路是把记忆拆成多个层次每个层次承担不同职责记忆层级存储内容生命周期检索方式工作记忆当前会话的即时上下文单次会话直接拼接进prompt短期记忆近期交互的关键事实数小时到数天时间衰减加权检索长期记忆用户偏好、稳定事实持久结构化查询语义检索反思记忆对历史行为的总结归纳持久定期更新触发式调用这个分层不是拍脑袋定的而是对应了认知科学里关于人类记忆的基本模型。工作记忆容量有限但访问极快长期记忆容量近乎无限但需要线索激活。Agent的记忆系统本质上是在用工程手段模拟这套机制。2.2 为什么选择MCP作为对接层热词里反复出现“MCP协议”这不是偶然。MCPModel Context Protocol本质上是一套标准化的工具调用协议它让LLM能够以统一的方式访问外部资源——包括记忆存储。在没有MCP之前你要让Agent访问记忆库得自己写function calling的schema每个模型厂商的格式还不太一样。OpenAI一套、Anthropic一套、国内模型又一套维护成本很高。MCP把这些差异屏蔽掉定义了一套标准的“资源-工具-提示”模型。对于记忆系统来说MCP的价值在于解耦记忆存储可以作为独立的MCP Server运行Agent通过标准协议访问不依赖具体模型实现可组合同一个Agent可以同时挂载多个MCP Server记忆服务只是其中之一可替换底层存储从Redis换成Postgres只要MCP接口不变上层Agent无感知实际部署时记忆MCP Server通常会暴露这几个核心工具{ tools: [ { name: store_memory, description: 存储一条记忆片段, parameters: { content: string, memory_type: working|short_term|long_term|reflection, metadata: object } }, { name: retrieve_memory, description: 根据查询检索相关记忆, parameters: { query: string, memory_types: array, top_k: integer } }, { name: consolidate_memory, description: 触发记忆巩固将短期记忆合并为长期记忆, parameters: { session_id: string } } ] }这套接口设计的关键在于memory_type参数。它让Agent自己决定一条信息应该存到哪个层级而不是所有东西都往一个池子里扔。比如用户说“今天天气不错”这是工作记忆会话结束就丢用户说“我对花生过敏”这是长期记忆必须持久化。2.3 存储选型为什么是Redis向量库的组合热词里出现了“docker安装redis主从”“docker安装mysql8.0”说明大家在部署记忆系统时确实在纠结存储选型。我的经验是不要试图用一种存储解决所有问题。工作记忆和短期记忆的特点是读写频繁、时效性强、数据量可控用Redis非常合适。Redis的过期机制天然适合做短期记忆的自动清理主从复制保证了可用性。你可以给不同层级的记忆设置不同的TTL# 工作记忆30分钟过期 SET memory:working:{session_id}:{seq} {...} EX 1800 # 短期记忆7天过期 SET memory:short_term:{user_id}:{memory_id} {...} EX 604800 # 长期记忆不过期但需要显式删除 HSET memory:long_term:{user_id} {memory_id} {...}长期记忆和反思记忆需要语义检索能力这时候向量数据库就派上用场了。但注意向量库不是唯一选择。如果你的长期记忆规模不大比如单用户几千条Postgrespgvector完全够用还省去了维护额外组件的麻烦。数据量上到百万级再考虑专门的向量库。这里有个容易踩的坑不要把原始对话直接embedding后存进去。原始对话包含大量冗余信息直接embedding会导致检索时噪声很大。正确的做法是先做一轮信息抽取把对话中的关键事实、偏好、约束提取成结构化或半结构化的记忆单元再存进去。比如这段对话用户我下周三要去上海出差帮我看看那边的天气。 Agent好的上海下周三预计有雨气温18-22度。 用户那我把会议改到周四吧周四天气怎么样 Agent周四多云气温20-25度更适合出行。抽取后的记忆单元应该是{ user_id: u123, memory_type: short_term, content: 用户计划下周四在上海进行商务会议, metadata: { location: 上海, time: 下周四, event_type: 商务会议, preference: 偏好天气好的日期 }, source_session: s456, created_at: 2025-01-15T10:30:00Z }这样检索时即使用户问“我下周的行程安排”也能精准命中这条记忆而不是召回一堆天气查询的原始记录。3. 核心机制拆解记忆的写入、检索与巩固3.1 写入策略什么该记什么该忘记忆系统的第一个难题不是“怎么存”而是“存什么”。如果Agent把每句话都当记忆存下来系统很快就会被垃圾信息淹没。我见过不少项目在这里翻车——上线跑了一周记忆库膨胀到几十万条检索延迟从毫秒级涨到秒级效果反而比没有记忆更差。“hindsight”的思路是引入一个记忆重要性评分机制。每条候选记忆在写入前先经过一个轻量级评分模型可以是一个小LLM也可以是一组规则从几个维度打分信息密度这条信息是否包含具体的事实、偏好、约束还是只是寒暄时效性这条信息在多久之后还有用是“今天下午三点开会”还是“我习惯用中文交流”独特性这条信息是否与已有记忆重复如果用户第三次说“我喜欢喝美式”不应该存三条。情感强度用户表达强烈情绪的内容往往更重要比如“我非常讨厌等待超过5秒的响应”。评分高于阈值的才进入写入流程低于阈值的直接丢弃或只保留在工作记忆中。这个阈值需要根据实际场景调没有万能值。我的经验是初期设低一点宁滥勿缺跑一段时间后根据检索命中率和噪声比例再往上调。写入时还有一个关键决策记忆的粒度。太粗整段对话存一条会导致检索不精准太细每句话存一条会导致检索结果碎片化。比较合理的粒度是一个完整的事实或意图通常对应对话中的1-3个回合。3.2 检索机制多路召回重排序检索是记忆系统最核心也最难调的部分。单一检索策略很难同时满足精度和召回率的要求所以“hindsight”采用了多路召回重排序的架构。第一路是语义检索用query的embedding去向量库做相似度匹配。这条路负责召回语义相关的记忆但对结构化约束不敏感。第二路是关键词检索从query中抽取实体、时间、地点等关键要素去结构化索引里精确匹配。这条路负责召回那些“语义不太像但事实相关”的记忆。第三路是时间衰减检索按记忆的创建时间和最后访问时间排序优先召回近期活跃的记忆。这条路保证Agent不会“活在过去的辉煌里”总是优先考虑最近发生的事。三路召回的结果合并后进入重排序阶段。重排序模型可以是交叉编码器cross-encoder也可以是一组加权规则。加权规则通常考虑检索路数命中数三路都命中的记忆优先级最高记忆类型权重长期记忆短期记忆工作记忆时间衰减因子越新越重要但长期记忆衰减更慢访问频率被频繁调用的记忆更重要最终取top_k条记忆格式化成prompt注入到Agent的上下文中。这里有个细节注入位置很重要。放在system prompt里还是user message里效果差异很大。我的实测经验是与当前任务强相关的记忆放在user message开头背景性、偏好性的记忆放在system prompt末尾效果比较稳。3.3 记忆巩固从短期到长期的自动化迁移记忆巩固consolidation是“hindsight”区别于普通向量存储的关键机制。它的核心思想是短期记忆不会自动变成长期记忆而是需要经过一个“提炼”过程。触发巩固的时机通常有两个一是会话结束时二是短期记忆积累到一定数量时。巩固过程大致如下取出该用户所有未巩固的短期记忆用LLM对这批记忆做总结归纳提取出稳定的偏好、事实、模式将归纳结果写入长期记忆标记短期记忆为“已巩固”设置较短的过期时间比如24小时后清理举个例子用户在过去一周的多次会话中分别提到“不喜欢太长的回复”“希望代码示例用Python”“对性能问题很敏感”。这三条短期记忆单独看都是零散的但巩固过程会把它们合并成一条长期记忆“用户偏好简洁的Python代码示例关注性能”。这样做的好处是长期记忆的信噪比远高于短期记忆。检索时优先命中长期记忆既精准又省token。巩固过程中还有一个容易忽略的点冲突检测。如果新巩固出的长期记忆与已有长期记忆矛盾需要触发更新而不是简单追加。比如用户之前说“我住在北京”后来搬到上海了巩固时应该更新这条记忆而不是存两条矛盾的记录。实现上可以用LLM做一次冲突判断或者用简单的规则同一维度的记忆只保留最新。4. 实操部署从零搭建一套可用的Agent记忆服务4.1 环境准备与Docker编排假设你已经在本地或服务器上装好了Docker和Docker Desktop。如果还没装Windows下直接去官网下载安装包注意安装时勾选WSL2后端否则启动时可能报“virtualization support not detected”的错误。这个报错本质上是BIOS里虚拟化没开进BIOS把Intel VT-x或AMD-V打开就行。整个记忆服务由三个容器组成Redis短期记忆存储、Postgrespgvector长期记忆存储、记忆MCP Server业务逻辑。用docker-compose编排version: 3.8 services: redis: image: redis:7-alpine ports: - 6379:6379 command: redis-server --appendonly yes volumes: - redis_data:/data postgres: image: pgvector/pgvector:pg16 ports: - 5432:5432 environment: POSTGRES_DB: agent_memory POSTGRES_USER: memory_user POSTGRES_PASSWORD: memory_pass volumes: - pg_data:/var/lib/postgresql/data memory-mcp: build: ./memory-mcp ports: - 8080:8080 environment: REDIS_URL: redis://redis:6379 DATABASE_URL: postgresql://memory_user:memory_passpostgres:5432/agent_memory EMBEDDING_MODEL: text-embedding-3-small depends_on: - redis - postgres volumes: redis_data: pg_data:这里有几个配置细节值得说明。Redis开了appendonly持久化防止重启丢数据。Postgres用了pgvector官方镜像省去自己编译扩展的麻烦。memory-mcp服务通过环境变量注入连接信息不硬编码在代码里。启动命令很简单docker-compose up -d第一次启动时Postgres需要初始化vector扩展可以进容器手动执行docker exec -it postgres_container_id psql -U memory_user -d agent_memory -c CREATE EXTENSION IF NOT EXISTS vector;4.2 记忆表结构设计长期记忆的表结构设计直接影响检索效率。我的方案是主表向量索引分离CREATE TABLE memories ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id VARCHAR(64) NOT NULL, memory_type VARCHAR(32) NOT NULL, content TEXT NOT NULL, metadata JSONB DEFAULT {}, importance FLOAT DEFAULT 0.5, access_count INTEGER DEFAULT 0, created_at TIMESTAMPTZ DEFAULT NOW(), last_accessed_at TIMESTAMPTZ DEFAULT NOW(), expires_at TIMESTAMPTZ ); CREATE INDEX idx_memories_user_type ON memories(user_id, memory_type); CREATE INDEX idx_memories_metadata ON memories USING GIN(metadata); CREATE INDEX idx_memories_created ON memories(created_at DESC); CREATE TABLE memory_embeddings ( memory_id UUID PRIMARY KEY REFERENCES memories(id) ON DELETE CASCADE, embedding vector(1536) ); CREATE INDEX idx_embeddings_ivfflat ON memory_embeddings USING ivfflat (embedding vector_cosine_ops) WITH (lists 100);几个设计决策的理由metadata用JSONB不同记忆类型的元数据结构差异很大JSONB提供了灵活性同时GIN索引保证了查询性能。importance和access_count分开importance是写入时评分的access_count是运行时累加的。检索排序时两者加权使用。expires_at可空长期记忆不设过期时间短期记忆设置过期时间由定时任务清理。embedding单独建表向量数据体积大单独存表可以避免主表膨胀也方便后续换embedding模型时只重建向量表。4.3 MCP Server的核心实现记忆MCP Server是整个系统的中枢它接收Agent的调用请求协调Redis和Postgres完成记忆的读写。核心逻辑用Python实现关键部分如下import json import redis import asyncpg from datetime import datetime, timedelta from typing import List, Dict, Any class MemoryService: def __init__(self, redis_url: str, db_url: str): self.redis redis.from_url(redis_url, decode_responsesTrue) self.db_pool None self.db_url db_url async def init_db(self): self.db_pool await asyncpg.create_pool(self.db_url) async def store_memory(self, user_id: str, content: str, memory_type: str, metadata: dict None) - str: importance await self._score_importance(content, memory_type) if importance 0.3: return None memory_id str(uuid.uuid4()) if memory_type in (working, short_term): ttl 1800 if memory_type working else 604800 key fmemory:{memory_type}:{user_id}:{memory_id} self.redis.setex(key, ttl, json.dumps({ content: content, metadata: metadata or {}, importance: importance, created_at: datetime.utcnow().isoformat() })) else: embedding await self._embed(content) async with self.db_pool.acquire() as conn: await conn.execute( INSERT INTO memories (id, user_id, memory_type, content, metadata, importance) VALUES ($1, $2, $3, $4, $5, $6) , memory_id, user_id, memory_type, content, json.dumps(metadata or {}), importance) await conn.execute( INSERT INTO memory_embeddings (memory_id, embedding) VALUES ($1, $2) , memory_id, embedding) return memory_id async def retrieve_memory(self, user_id: str, query: str, memory_types: List[str] None, top_k: int 5) - List[Dict]: results [] if not memory_types or short_term in memory_types: results.extend(self._retrieve_short_term(user_id, query)) if not memory_types or long_term in memory_types: results.extend(await self._retrieve_long_term(user_id, query)) reranked self._rerank(results, query) return reranked[:top_k]这段代码有几个关键点。_score_importance方法可以用规则实现比如包含“我喜欢”“我习惯”“记住”等关键词的加分也可以调一个小模型。_retrieve_short_term从Redis里按时间倒序取最近N条再做简单的关键词过滤。_retrieve_long_term走向量检索结构化过滤。_rerank做多路结果的融合排序。4.4 与Agent的对接配置MCP Server跑起来后需要在Agent端配置连接。以常见的MCP客户端配置为例{ mcpServers: { agent-memory: { url: http://localhost:8080/mcp, transport: sse, tools: [store_memory, retrieve_memory, consolidate_memory] } } }Agent在每轮对话开始前先调用retrieve_memory获取相关记忆注入到prompt中。对话结束后调用store_memory写入本轮的关键信息。会话结束时触发consolidate_memory做记忆巩固。这里有个实操技巧不要每轮都检索全量记忆。可以根据对话的意图判断是否需要检索。比如用户说“你好”没必要检索记忆用户说“帮我安排一下下周的行程”这时候才触发记忆检索。判断逻辑可以放在Agent的system prompt里让模型自己决定是否调用记忆工具。5. 常见问题与排查技巧实录5.1 记忆检索不准确怎么办这是反馈最多的问题。表现是Agent调用了记忆但召回的内容跟当前话题关系不大甚至完全无关。排查思路按以下顺序来第一步检查embedding质量。如果你用的是通用embedding模型对某些垂直领域比如医疗、法律的语义捕捉可能不够准。可以试试在领域数据上微调一个小embedding模型或者换用领域专用的embedding服务。第二步检查记忆粒度。如果一条记忆包含太多信息比如整段对话embedding会变成一个“平均语义”检索时跟什么都有点像但又都不太像。把记忆拆细一条记忆只表达一个事实。第三步检查重排序权重。默认权重不一定适合你的场景。如果发现时间衰减太强旧的重要记忆总被新记忆挤掉把时间衰减因子调小。如果发现语义检索主导了结果结构化匹配的信号被淹没把关键词匹配的权重调高。第四步加一层LLM过滤。在检索结果注入prompt之前用一个轻量LLM做一次相关性判断把明显不相关的记忆剔除。这会增加一点延迟但对精度提升很明显。5.2 Docker网络不通导致MCP连接失败这个坑我踩过不止一次。表现是Agent端配置了MCP Server地址但一直连接超时。原因通常是容器网络配置问题。如果MCP Server跑在Docker里Agent跑在宿主机上Agent配置里不能用localhost要用宿主机的实际IP或者Docker的端口映射。如果Agent也跑在Docker里两个容器需要在同一个network下用服务名互相访问。排查命令# 查看容器网络 docker network ls docker network inspect network_name # 进容器测试连通性 docker exec -it agent_container ping memory-mcp docker exec -it agent_container curl http://memory-mcp:8080/health如果用了Docker Desktop注意它的网络模型跟Linux原生Docker有差异。Windows下有时候需要把localhost换成host.docker.internal。5.3 记忆库膨胀过快跑了一周发现记忆表几十万行检索越来越慢。这个问题通常出在写入策略太宽松。检查几个点importance阈值是不是设太低了调到0.5以上试试。短期记忆的TTL是不是太长了工作记忆30分钟、短期记忆7天是比较合理的默认值。巩固任务有没有正常跑如果短期记忆一直没被巩固和清理会无限堆积。有没有去重逻辑相同内容反复写入的情况很常见写入前先做一次相似度检查超过阈值的直接更新而不是新增。定期清理的SQL-- 清理过期记忆 DELETE FROM memories WHERE expires_at IS NOT NULL AND expires_at NOW(); -- 清理低价值且长期未访问的记忆 DELETE FROM memories WHERE importance 0.3 AND access_count 2 AND last_accessed_at NOW() - INTERVAL 30 days;5.4 记忆冲突导致Agent行为矛盾用户改了偏好但旧记忆还在Agent一会儿按新偏好来一会儿按旧偏好来。这个问题需要在巩固阶段做冲突检测。简单做法是给记忆的metadata加一个dimension字段标识这条记忆属于哪个维度比如“饮食偏好”“语言偏好”“时区”。同一维度下只保留最新的记忆旧的标记为superseded。UPDATE memories SET metadata metadata || {superseded: true} WHERE user_id $1 AND metadata-dimension $2 AND id ! $3;检索时过滤掉supersededtrue的记忆。5.5 常见问题速查表问题现象可能原因排查方向解决手段检索结果不相关embedding质量差/记忆粒度过粗检查embedding模型和记忆拆分逻辑换模型/拆细记忆/加LLM过滤MCP连接超时容器网络配置错误检查network和端口映射统一network/用服务名访问记忆库膨胀写入阈值低/TTL过长检查importance分布和过期策略调高阈值/缩短TTL/加去重行为矛盾新旧记忆冲突检查同维度记忆数量加dimension字段/标记superseded检索延迟高向量索引未建/数据量过大检查索引和表大小建IVFFlat索引/分表/加缓存巩固不生效定时任务未跑/LLM调用失败检查任务日志修复调度/加重试机制6. 记忆系统的演进方向与个人实践体会这套记忆架构跑通之后有几个方向可以继续深挖。一个是记忆的可解释性让Agent能说清楚“我为什么记得这件事”“我为什么在这个决策里引用了这条记忆”这对调试和建立用户信任都很重要。另一个是跨Agent的记忆共享多个Agent协作时如何共享和隔离记忆是个有意思的工程问题。我在实际项目里最大的体会是记忆系统的效果不取决于存储技术多先进而取决于写入和检索策略跟业务场景的匹配度。同样的架构在客服场景和编程助手场景下最优的参数配置可能完全不同。客服场景下用户偏好变化快短期记忆权重要高编程助手场景下技术栈和代码规范相对稳定长期记忆的价值更大。所以我的建议是先把基础架构搭起来然后花时间观察真实使用中的检索命中率和用户反馈持续调参。记忆系统不是一次性的工程而是一个需要持续运营的组件。定期review记忆库的内容分布清理噪声补充缺失的记忆类型这些日常维护工作比初始搭建更重要。另外一个小技巧给记忆加一个手动反馈接口。当用户说“你记错了”或者“这个不用记”的时候Agent能直接修正或删除对应记忆。这比事后去数据库里手动改要高效得多也让用户感觉到自己对Agent的记忆有控制权。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →