ARTICLE DETAIL

资讯详情

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

基于hindsight的Agent长期记忆架构:MCP与Docker实践

基于hindsight的Agent长期记忆架构:MCP与Docker实践 1. 从“hindsight”说起为什么我们需要给Agent装上“后视镜”“hindsight”这个词本身很有意思字面意思是“事后的洞察力”也就是我们常说的“后见之明”。放在Agent Memory这个领域里它指向一个非常具体且关键的问题当一个大模型驱动的Agent完成了一轮任务之后它能不能回头看看自己刚才做了什么、哪些做对了、哪些做错了并且把这些经验存下来下次遇到类似场景时直接调用这个问题听起来简单但真正动手做过Agent系统的人都知道让Agent“记住东西”和让Agent“聪明地记住东西”完全是两码事。前者只需要一个向量数据库加一个检索接口后者则涉及记忆的写入策略、检索时机、遗忘机制、冲突消解、跨会话持久化等一系列工程难题。我最近在折腾一个基于LLM的Agent项目核心诉求就是让Agent具备跨会话的长期记忆能力。过程中试过不少方案也踩了不少坑最终形成了一套以“hindsight”为核心理念的记忆架构。这篇文章就把整个设计思路、技术选型、实操步骤和踩坑经验完整地分享出来适合正在做Agent Memory相关工作的开发者、对LLM应用架构感兴趣的技术人以及想了解MCP协议和Docker部署实践的读者参考。整篇文章会围绕几个关键词展开agent memory、LLM、MCP、Docker。如果你对这几个词中的任何一个感到陌生不用担心我会在对应的章节里用最直白的方式解释清楚。2. 整体架构设计hindsight记忆系统的核心思路2.1 为什么传统的“存-取”模式不够用大部分Agent记忆系统的第一版实现都很朴素把对话历史或者任务结果做embedding塞进向量数据库需要的时候用相似度检索捞出来。这个方案在Demo阶段完全够用但一旦进入真实场景问题就暴露了。最典型的问题是记忆污染。Agent在某次任务中因为工具调用失败产生了一条错误结论比如“API X不可用”这条记忆被存进去之后后续所有涉及API X的任务都会检索到这条记录导致Agent直接放弃尝试。但实际上API X可能只是当时网络抖动早就恢复了。第二个问题是记忆冗余。同一个事实被反复存储只是表述略有不同检索时返回一堆语义重复的内容白白消耗token。第三个问题是缺乏时间维度。Agent不知道一条记忆是什么时候产生的也不知道它是否已经过时。hindsight的核心思路就是解决这三个问题在记忆写入时做质量评估在记忆检索时做时效性加权在记忆维护时做冲突检测和淘汰。简单说就是让Agent不仅“能记住”还要“会忘记”和“会更新”。2.2 分层记忆架构的设计我把整个记忆系统分成三层这个分层方式参考了认知科学里人类记忆的经典模型但在工程实现上做了简化第一层Working Memory工作记忆这一层对应的是当前会话的上下文窗口。它不需要持久化生命周期就是一次会话。所有当前对话的消息、工具调用的中间结果、临时的推理链都放在这里。实现上就是维护一个消息列表配合token计数做窗口截断。第二层Episodic Memory情景记忆这一层存储的是“发生过什么”。每一次任务执行结束后系统会生成一条结构化的情景记录包含任务描述、执行步骤摘要、最终结果、成功/失败标记、时间戳。这些记录持久化到数据库中支持按时间范围和语义相似度检索。第三层Semantic Memory语义记忆这一层存储的是“我知道了什么”。从多条情景记忆中提炼出来的通用知识比如“用户偏好用Python而不是JavaScript”、“某个API的认证方式是Bearer Token”。语义记忆是跨任务、跨会话的更新频率低但价值密度高。三层之间的流转关系是这样的工作记忆在会话结束时经过摘要和评估写入情景记忆情景记忆积累到一定数量后通过聚类和提炼生成或更新语义记忆语义记忆在每次新会话开始时被加载到工作记忆中作为背景知识。2.3 技术选型背后的考量存储层选型情景记忆和语义记忆都需要持久化存储。我最终选了PostgreSQL配合pgvector扩展。原因有三第一pgvector的向量检索性能在千万级数据量下完全够用第二PostgreSQL的JSONB字段可以灵活存储结构化的记忆元数据第三运维成本低不需要额外引入专门的向量数据库。Embedding模型选型用的是BGE-M3主要看中它对中文和英文的混合支持比较好而且维度适中1024维检索速度和精度平衡得不错。如果你主要做英文场景可以考虑用text-embedding-3-small成本更低。Agent框架没有用LangChain或者AutoGPT这类重型框架而是自己写了一个轻量的Agent Loop。原因是我需要精确控制记忆的写入和检索时机重型框架的抽象层反而会增加调试难度。MCP协议的角色MCP在这里的作用是标准化Agent与外部工具之间的通信。记忆系统本身也可以封装成一个MCP Server这样任何支持MCP的Agent都可以接入这套记忆能力而不需要把记忆逻辑硬编码在Agent内部。Docker的角色整个系统涉及多个组件——PostgreSQL、Embedding服务、MCP Server、Agent运行时。用Docker Compose编排是最省心的方式一条命令拉起所有依赖环境隔离也做得好。3. 核心细节解析记忆的写入、检索与维护3.1 记忆写入什么时候存存什么怎么存记忆写入的时机很关键。我的做法是在每次任务执行结束后触发一个“反思”流程这个流程分三步第一步生成任务摘要。把整个任务的对话历史和工具调用记录喂给LLM让它生成一段200字以内的摘要同时输出一个结构化的JSON包含任务类型、涉及的工具、最终状态成功/失败/部分成功、关键决策点。第二步质量评估。不是所有任务都值得记住。我设计了一个简单的评分机制从三个维度打分任务复杂度步骤数、结果确定性是否有明确成功/失败信号、信息新颖度与已有记忆的语义距离。综合评分低于阈值的直接丢弃不写入长期记忆。第三步冲突检测。在写入之前先用摘要的embedding去语义记忆中检索最相似的几条记录。如果相似度超过0.95说明这条记忆和已有记忆高度重复直接跳过写入。如果相似度在0.8到0.95之间触发一次LLM判断新记忆是否与旧记忆矛盾如果矛盾根据时间戳决定是覆盖还是保留两条并标记冲突。这里有个实操细节写入时的embedding应该用摘要而不是原始对话。原始对话里噪音太多直接做embedding会导致检索精度下降。摘要经过LLM提炼信息密度高embedding质量明显更好。# 记忆写入的核心逻辑简化版 async def write_memory(task_result: TaskResult): # 1. 生成摘要和结构化元数据 summary, metadata await llm_summarize(task_result) # 2. 质量评分 score evaluate_quality(task_result, summary) if score QUALITY_THRESHOLD: return # 质量不够丢弃 # 3. 冲突检测 similar await search_similar_memories(summary, top_k5) for mem in similar: if mem.similarity 0.95: return # 高度重复跳过 elif mem.similarity 0.8: conflict await llm_check_conflict(summary, mem.content) if conflict: await resolve_conflict(summary, mem) return # 4. 写入数据库 embedding await get_embedding(summary) await db.insert_memory( contentsummary, embeddingembedding, metadatametadata, created_atnow() )3.2 记忆检索不只是相似度排序检索环节是hindsight系统里最需要精细调优的部分。单纯的向量相似度检索有三个明显缺陷忽略了时间衰减、忽略了记忆的重要性权重、忽略了检索结果的多样性。我的检索策略是一个多路召回加重排序的流程第一路语义相似度召回。用query的embedding去检索最相似的N条记忆N一般取20。第二路时间近因召回。取最近M条记忆M一般取10。这一路保证Agent不会完全忽略近期发生的事情。第三路重要性召回。按记忆的重要性评分排序取前K条K一般取5。重要性评分在写入时由LLM给出比如“用户明确表达的偏好”比“一次偶然的工具调用失败”重要性更高。三路召回的结果合并去重后进入重排序阶段。重排序的公式是final_score α × semantic_similarity β × time_decay γ × importance其中time_decay是一个指数衰减函数半衰期设为7天。α、β、γ三个权重根据具体场景调我的经验值是α0.6β0.25γ0.15。如果你的场景对时效性要求更高可以把β调到0.35左右。重排序之后还有一个多样性过滤步骤如果前5条结果中有3条以上语义高度相似两两相似度0.9只保留其中分数最高的一条。这个步骤能有效避免检索结果被同一类记忆霸占。3.3 记忆维护遗忘也是一门技术很多人做Agent Memory只关注“怎么存”和“怎么取”忽略了“怎么删”。但一个没有遗忘机制的记忆系统用不了多久就会变成一个充满过时信息和矛盾信息的垃圾场。我的遗忘策略分三种被动遗忘时间衰减每条记忆都有一个“新鲜度”分数随时间指数衰减。当新鲜度低于阈值时记忆不会被删除但会在检索时被降权。这样既保留了历史信息又不会让过时信息干扰当前决策。主动遗忘冲突消解当检测到两条记忆矛盾时根据时间戳和置信度决定保留哪一条。如果新记忆的置信度明显更高旧记忆会被标记为“已废弃”不再参与检索。容量淘汰LRU变体每个用户或每个Agent实例的记忆总量设一个上限。当达到上限时按“最后访问时间 × 重要性评分”排序淘汰分数最低的记忆。注意这里用的是最后访问时间而不是创建时间因为一条经常被检索到的老记忆可能比一条从未被访问的新记忆更有价值。实操心得遗忘机制的参数不要拍脑袋定建议先用真实数据跑一周观察记忆的检索命中率和Agent的任务成功率再反过来调参数。我一开始把时间衰减的半衰期设成3天结果发现很多有用的长期知识被过早降权了后来调到7天才合适。4. 实操过程从零搭建一套hindsight记忆系统4.1 环境准备与Docker Compose编排整个系统需要以下组件组件用途镜像PostgreSQL pgvector记忆持久化存储pgvector/pgvector:pg16Embedding服务文本向量化自建FastAPI服务MCP Server记忆能力的标准化接口自建Node.js服务Agent运行时执行任务的AgentPython 3.11Docker Compose文件的核心配置如下version: 3.8 services: postgres: image: pgvector/pgvector:pg16 environment: POSTGRES_DB: agent_memory POSTGRES_USER: memory_user POSTGRES_PASSWORD: ${DB_PASSWORD} ports: - 5432:5432 volumes: - pgdata:/var/lib/postgresql/data healthcheck: test: [CMD-SHELL, pg_isready -U memory_user] interval: 10s timeout: 5s retries: 5 embedding: build: ./embedding_service ports: - 8001:8001 deploy: resources: reservations: devices: - driver: nvidia count: 1 capabilities: [gpu] mcp_server: build: ./mcp_server ports: - 3001:3001 environment: DB_HOST: postgres EMBEDDING_HOST: embedding depends_on: postgres: condition: service_healthy volumes: pgdata:几个关键点说明pgvector镜像选择直接用pgvector官方提供的镜像不要自己从postgres基础镜像装扩展编译过程容易出问题。pg16版本对pgvector的支持最稳定。Embedding服务的GPU配置如果你用BGE-M3这类模型CPU推理也能跑但速度慢。Docker Compose的deploy.resources.reservations.devices配置可以让容器访问宿主机的GPU。注意这个配置在Docker Desktop for Windows上需要WSL2后端才生效。健康检查postgres的healthcheck很重要因为mcp_server依赖数据库就绪后才能启动。不加healthcheck的话mcp_server可能在数据库还没初始化完成时就尝试连接导致启动失败。4.2 数据库表结构设计记忆系统的表结构设计直接影响检索效率和维护成本。我的设计如下CREATE EXTENSION IF NOT EXISTS vector; CREATE TABLE memories ( id BIGSERIAL PRIMARY KEY, content TEXT NOT NULL, embedding vector(1024), memory_type VARCHAR(20) NOT NULL, -- episodic or semantic importance FLOAT DEFAULT 0.5, confidence FLOAT DEFAULT 1.0, access_count INT DEFAULT 0, last_accessed_at TIMESTAMPTZ DEFAULT NOW(), created_at TIMESTAMPTZ DEFAULT NOW(), metadata JSONB DEFAULT {}, is_deprecated BOOLEAN DEFAULT FALSE ); CREATE INDEX idx_memories_embedding ON memories USING ivfflat (embedding vector_cosine_ops) WITH (lists 100); CREATE INDEX idx_memories_type_created ON memories (memory_type, created_at DESC); CREATE INDEX idx_memories_importance ON memories (importance DESC) WHERE is_deprecated FALSE;ivfflat索引的lists参数这个参数影响检索速度和精度。经验公式是lists rows / 1000对于100万条记忆lists设为1000。但注意ivfflat索引需要在数据插入后重建才能达到最佳效果所以建议先插入一批数据再建索引。is_deprecated字段软删除标记。被废弃的记忆不参与检索但保留在数据库中用于审计和回溯。access_count和last_accessed_at每次检索到某条记忆时更新这两个字段用于后续的容量淘汰策略。4.3 MCP Server的实现要点MCPModel Context Protocol在这里的角色是提供一个标准化的接口让Agent可以通过统一的协议调用记忆的读写能力。MCP Server需要暴露以下几个工具memory_write写入一条新记忆memory_search检索相关记忆memory_update更新已有记忆memory_forget废弃一条记忆用Node.js实现MCP Server的核心代码结构import { Server } from modelcontextprotocol/sdk/server/index.js; import { StdioServerTransport } from modelcontextprotocol/sdk/server/stdio.js; const server new Server({ name: hindsight-memory, version: 1.0.0 }, { capabilities: { tools: {} } }); server.setRequestHandler(tools/list, async () ({ tools: [ { name: memory_search, description: 检索与query相关的记忆, inputSchema: { type: object, properties: { query: { type: string }, top_k: { type: number, default: 5 }, memory_type: { type: string, enum: [episodic, semantic, all] } }, required: [query] } } ] })); server.setRequestHandler(tools/call, async (request) { if (request.params.name memory_search) { const { query, top_k, memory_type } request.params.arguments; const results await searchMemories(query, top_k, memory_type); return { content: [{ type: text, text: JSON.stringify(results) }] }; } }); const transport new StdioServerTransport(); await server.connect(transport);注意事项MCP Server的传输方式有stdio和HTTP两种。stdio方式适合本地Agent调用配置简单但无法跨机器。HTTP方式适合分布式部署但需要额外处理认证和限流。我一开始用stdio后来因为Agent和记忆服务部署在不同机器上改成了HTTP方式。4.4 Agent侧的集成方式Agent侧需要做两件事在任务开始前检索相关记忆并注入上下文在任务结束后触发记忆写入。检索注入的代码逻辑async def prepare_context(user_input: str, session_id: str): # 1. 检索语义记忆长期知识 semantic_memories await mcp_client.call_tool( memory_search, {query: user_input, top_k: 3, memory_type: semantic} ) # 2. 检索情景记忆近期类似任务 episodic_memories await mcp_client.call_tool( memory_search, {query: user_input, top_k: 5, memory_type: episodic} ) # 3. 组装成系统提示的一部分 memory_context format_memories(semantic_memories, episodic_memories) system_prompt f你是一个具有长期记忆的Agent。 以下是你过去积累的相关知识和经验 {memory_context} 请基于以上记忆来辅助当前任务的决策。如果记忆与当前情况不符以当前情况为准。 return system_prompt这里有个容易忽略的细节记忆注入的位置。我试过把记忆放在system prompt里和放在user message前面两种方式实测下来放在system prompt里的效果更好因为模型对system prompt的遵循度更高。但要注意控制记忆内容的长度一般不超过500 token否则会挤占正常对话的上下文空间。5. 常见问题与排查技巧实录5.1 记忆检索不准确怎么办这是最常见的问题。表现是Agent检索到的记忆和当前任务不相关或者相关的记忆没被检索到。排查思路先确认embedding质量。拿几条典型query和对应的期望记忆手动算一下余弦相似度。如果相似度低于0.7说明embedding模型不适合你的场景考虑换模型或者对文本做预处理比如去掉无关的格式标记。如果embedding没问题检查检索策略的权重配置。我遇到过一次情况是时间衰减权重设得太高导致一条三天前的高相关记忆被一条刚刚写入的低相关记忆挤掉了。把β从0.4降到0.2之后问题解决。还有一个隐蔽的坑是索引未重建。pgvector的ivfflat索引在数据量变化较大时需要重建否则检索精度会下降。建议每周跑一次REINDEX INDEX idx_memories_embedding。5.2 记忆写入过于频繁导致token消耗大写入记忆需要调用LLM做摘要和评估每次都有token成本。如果Agent的任务频率很高这部分开销会很可观。优化方案加一个写入频率限制。同一个session内最多每5分钟触发一次记忆写入。另外对于简单的问答类任务可以直接跳过记忆写入只对涉及多步推理或工具调用的复杂任务做记忆持久化。我实测下来加了频率限制之后记忆写入的LLM调用量下降了约60%而Agent的任务成功率没有明显变化。5.3 Docker环境下PostgreSQL连接不稳定这个问题在Windows上用Docker Desktop时特别常见。表现是Agent偶尔报数据库连接超时但重启容器后又恢复正常。根因Docker Desktop的网络栈在Windows上有时候会出现端口映射不稳定。另外如果PostgreSQL的max_connections设得太低默认100并发高的时候连接池会耗尽。解决方案第一在Docker Compose里给postgres服务加上restart: unless-stopped容器异常退出时自动重启。第二把max_connections调到200同时在Agent侧用连接池管理连接。第三如果条件允许把数据库部署在Linux宿主机上Windows只跑Agent和MCP Server。5.4 常见问题速查表问题现象可能原因排查方法解决方案检索结果不相关embedding质量差手动计算相似度换模型或预处理文本相关记忆未被检索时间衰减权重过高检查β参数降低β值检索精度下降索引未重建检查索引状态定期REINDEX写入token消耗大写入频率过高统计LLM调用次数加频率限制数据库连接超时连接池耗尽查看pg_stat_activity调大max_connectionsMCP工具调用失败传输方式不匹配检查stdio/HTTP配置统一传输方式记忆冲突未消解冲突检测阈值不当检查相似度阈值调整0.8/0.95阈值独家避坑技巧在开发阶段建议把每次记忆写入和检索的详细日志都打到文件里包括query、检索到的记忆内容、相似度分数、最终注入到prompt里的内容。这样出问题的时候可以完整回溯。我一开始没做这个排查一个检索不准的问题花了整整两天后来加了日志之后类似问题十分钟就能定位。6. 记忆系统的扩展方向与个人体会这套hindsight记忆系统跑了一段时间之后我发现还有几个值得继续折腾的方向。多Agent共享记忆目前每个Agent实例有独立的记忆空间。但在多Agent协作的场景下Agent之间需要共享一部分记忆。比如一个负责搜索的Agent发现某个数据源不可用这个信息应该让负责分析的Agent也知道。实现上可以加一个“共享记忆池”所有Agent都可以读写但需要加权限控制和冲突消解。记忆的可解释性现在Agent检索到记忆之后直接注入prompt用户看不到Agent到底“想起了什么”。在一些需要审计的场景下这是不够的。可以考虑在Agent的输出里附带引用来源标明哪些结论是基于哪条记忆做出的。与RAG的融合记忆系统和传统的RAG系统其实有很多重叠。记忆可以看作是一种特殊的知识库只不过它的内容是由Agent自己生成的而不是从外部文档导入的。未来可以考虑把两者统一到一个检索框架里用同一套重排序逻辑处理。我个人在实际操作中的体会是Agent Memory这个方向难点不在于技术实现而在于产品化的取舍。存多少、记多久、什么时候忘、检索几条、注入多少token这些参数没有标准答案必须根据具体场景反复调。我建议刚开始做的时候不要追求大而全先把“写入-检索”这个最小闭环跑通用真实任务验证效果再逐步加冲突检测、遗忘机制、多路召回这些高级特性。一上来就搞复杂架构大概率会在调试阶段就耗尽耐心。最后分享一个小技巧如果你用Docker Compose编排整个系统建议把数据库的volume挂载到宿主机的一个固定目录并且定期备份。我有一次手贱跑了docker compose down -v把积累了半个月的记忆数据全删了那种感觉就像失忆了一样。
返回列表