ARTICLE DETAIL

资讯详情

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

基于MCP与Docker构建LLM Agent长期记忆系统:hindsight三层架构实战

基于MCP与Docker构建LLM Agent长期记忆系统:hindsight三层架构实战 1. 从“hindsight”说起为什么我们需要给Agent装一个“后视镜”第一次看到“hindsight”这个词我脑子里蹦出来的不是词典释义而是每次调完一个LLM Agent之后拍大腿的瞬间——刚才那轮对话里它明明已经拿到了正确线索结果下一轮又绕回原点像金鱼一样只有七秒记忆。hindsight这个词本身的意思是“事后聪明”也就是回头看时才明白当时该怎么做。把它用在Agent Memory这个领域其实非常精准我们想让Agent具备的正是这种“回头看”的能力把过去发生过的事情、踩过的坑、验证过的结论沉淀成下一次可以直接调用的经验。这个项目标题背后对应的核心领域是LLM驱动的自主Agent的记忆系统。热搜词里出现的agent memory、LLM、MCP、Docker基本勾勒出了它的技术轮廓用LLM做推理内核用MCP做工具与上下文协议用Docker做可复现的运行环境最终服务于Agent的长期记忆管理。它解决的问题很具体——当前大多数Agent框架的记忆是“会话级”的会话一关上下文清零即便有向量库也往往只是把历史对话粗暴地切片塞进去检索出来的东西和当前任务的相关性全靠运气。hindsight要做的是让记忆具备结构、具备时间维度、具备可回溯性。适合读这篇内容的人我大致分三类一是正在做Agent应用、被“记忆混乱”折磨过的开发者二是对MCP协议和LLM工程化感兴趣、想找一个完整落地案例的技术人三是想理解“Agent记忆”到底和普通RAG有什么区别的产品和研究者。不管你是哪一类我都会尽量把原理讲透、把操作步骤写细让你看完能自己动手搭一个最小可用的版本。2. 整体设计思路hindsight到底在架构上做对了什么2.1 核心命题记忆不是存储而是“可检索的经验”很多人一提到Agent Memory第一反应就是“上个向量数据库”。我早期也这么干过把每轮对话embedding之后丢进Chroma检索top-k拼进prompt。实测下来问题一大堆检索出来的片段经常是半句话时间顺序错乱同一个事实被重复存储十几次更致命的是——Agent根本不知道自己“曾经知道过什么”它只是被动接收一堆碎片。hindsight的设计思路我理解下来是把记忆拆成了三个层次这也是它和普通RAG最本质的区别原始事件层Episodic完整记录每一次交互的原始输入输出带时间戳、带会话ID、带工具调用记录。这一层不做任何加工就是“发生了什么”。语义提炼层Semantic从原始事件中抽取稳定的事实、偏好、结论。比如“用户偏好用Python而不是JavaScript”“这个API的rate limit是每分钟60次”。这一层是结构化的可以更新、可以失效。反思层Reflective这是hindsight最有意思的地方也是“后视镜”这个名字的来源。它会让LLM定期回看一段时间内的原始事件主动生成“经验总结”——哪些做法有效、哪些路径是死胡同、下次遇到类似情况应该优先尝试什么。提示这三层不是简单的叠加而是有明确的读写策略。原始层只写不删或按策略归档语义层可读写可失效反思层是周期性批量生成。搞混了这三层的职责系统就会退化成普通的向量检索。2.2 为什么选MCP而不是自己造轮子热搜词里MCP出现频率极高从mcp协议、mcp server到playwright mcp、蓝湖mcp说明这个协议正在快速成为Agent工具调用的事实标准。hindsight选择MCP作为记忆的对外接口我认为是个很聪明的决定理由有三第一解耦。记忆系统本身不应该关心调用它的是哪个Agent框架。用MCP暴露成server之后无论是Claude Desktop、还是自己写的Agent循环都能通过统一协议访问记忆不用为每个框架写适配层。第二工具化。MCP的tool定义天然适合记忆操作——store_memory、recall_memory、reflect_on_period这些都可以定义成标准toolAgent在推理过程中自主决定什么时候存、什么时候取。这比在prompt里硬塞上下文要优雅得多。第三生态复用。MCP server可以用任何语言写Docker打包之后随处可跑。热搜里docker安装、docker desktop、ubuntu安装docker这些词的高频出现也侧面说明大家越来越习惯用容器来交付这类服务。2.3 Docker在这套方案里的真实角色我不止一次看到有人把Docker当成“部署工具”就完事了但在hindsight这类项目里Docker承担的是环境一致性和状态持久化两个关键职责。记忆系统依赖向量库、依赖embedding模型、依赖LLM网关配置这些东西版本一乱行为就不可复现。用Docker Compose把记忆服务、向量库、可选的本地LLM网关编排在一起volume挂载持久化数据才能保证“今天跑出来的记忆行为下周还能复现”。下面这张表是我整理的三层记忆与对应技术选型的对照方便你快速定位每一层该用什么记忆层次存储介质写入时机检索方式典型工具原始事件层关系库/文档库每轮交互后按时间/会话IDPostgreSQL、SQLite语义提炼层向量库元数据异步提炼向量相似度过滤Qdrant、Chroma反思层文档库周期性批量按主题/时间段同上独立collection3. 核心细节拆解记忆的写入、检索与反思怎么落地3.1 写入策略什么时候该记记什么这是最容易做错的地方。我见过太多项目把用户说的每一句话都存进去结果向量库爆炸检索质量反而下降。hindsight的写入策略我总结成一句话原始层全量记语义层选择性记反思层批量记。原始层的写入很简单每轮交互结束后把{session_id, timestamp, role, content, tool_calls, tool_results}作为一个事件写入。这里有个细节要注意tool_calls和tool_results一定要关联存储因为Agent的很多“经验”其实藏在工具调用的成败里。比如它调用某个API失败了三次这个失败模式如果不记录反思层就无从总结。语义层的写入需要一次LLM调用做提炼。我的做法是给一个结构化的prompt让模型输出JSON格式的候选事实每条事实带confidence和ttl生存时间。confidence低的先不写入ttl用于后续失效判断。这里的关键是不要让模型自由发挥schema约束越严格后续检索越可靠。# 语义提炼的prompt骨架简化版 EXTRACT_PROMPT 从以下对话片段中抽取稳定的事实、用户偏好或结论。 只输出JSON数组每个元素包含 - fact: 一句话描述不超过50字 - confidence: 0到1之间的浮点数 - ttl_days: 建议的有效天数不确定填30 - tags: 字符串数组用于分类 对话片段 {episode} 反思层的触发我建议用时间事件数双阈值比如每积累50个原始事件或者每过24小时就触发一次反思。反思的prompt要让模型回看这批事件输出“经验条目”每条包含pattern模式描述、evidence支撑事件ID、suggestion下次建议。这些经验条目单独存一个collection检索时优先级高于普通语义记忆。3.2 检索策略怎么让Agent“想起该想起的”检索是记忆系统的价值出口。我的经验是单一向量检索不够用hindsight这类系统通常需要混合检索向量相似度处理语义模糊的查询比如“上次那个关于部署的问题”。关键词/BM25处理精确匹配比如某个具体的错误码、某个API名字。时间衰减越近的记忆权重越高但反思层经验可以豁免衰减。标签过滤如果当前任务明确属于某个领域先用tag缩小范围。具体实现上我习惯用Qdrant做向量库因为它原生支持payload过滤和混合检索。检索时先做一次粗排向量关键词各取top-20再用一个轻量rerank模型或者直接让LLM打分做精排最后取top-5注入上下文。注意注入上下文时一定要带上记忆的“来源”和“时间”。我踩过的坑是Agent看到一条记忆但不知道它是三天前的结果把一个已经失效的结论当成当前事实用。格式建议[记忆|2024-06-01|confidence:0.8] 用户偏好使用pnpm。3.3 MCP接口设计把记忆能力暴露成标准工具MCP server这边我建议至少暴露四个toolstore_episode写入原始事件参数是session_id、role、content等。recall检索记忆参数是query、top_k、time_range、tags。reflect手动触发反思参数是time_range或event_count。forget让某条语义记忆失效参数是memory_id。用Python写MCP server的话官方有mcp包可以直接用。核心是把每个tool的input schema定义清楚尤其是recall的过滤参数schema越明确Agent调用时越不容易传错。# MCP tool定义示例伪代码展示结构 server.tool() async def recall(query: str, top_k: int 5, time_range: str 7d, tags: list[str] None) - list[dict]: 检索Agent记忆。 Args: query: 检索查询 top_k: 返回条数 time_range: 时间范围如7d/30d/all tags: 标签过滤 # 混合检索逻辑 ...3.4 Docker编排让记忆服务可复现Docker Compose文件我一般这么组织一个memory-server服务跑MCP server一个qdrant服务跑向量库一个可选的postgres跑原始事件再加一个ollama或外部LLM网关配置。关键点是volume要挂对qdrant的数据目录、postgres的数据目录都要持久化否则容器一重启记忆就没了。# docker-compose.yml 骨架 services: memory-server: build: . ports: - 8080:8080 environment: - QDRANT_URLhttp://qdrant:6333 - DATABASE_URLpostgresql://user:passpostgres:5432/memory depends_on: - qdrant - postgres qdrant: image: qdrant/qdrant:latest volumes: - ./data/qdrant:/qdrant/storage postgres: image: postgres:16 volumes: - ./data/postgres:/var/lib/postgresql/data environment: - POSTGRES_PASSWORDpass4. 实操过程从零搭一个最小可用的hindsight记忆服务4.1 环境准备与依赖安装我假设你用的是Ubuntu或者macOSWindows的话建议走WSL2因为Docker Desktop在Windows上的网络配置偶尔会抽风热搜里virtualization support not detected那个报错就是典型。先确认Docker和Docker Compose装好docker --version docker compose version如果没装Ubuntu下用官方脚本装最省事。装完之后建议把当前用户加入docker组免得每条命令都要sudosudo usermod -aG docker $USER newgrp dockerPython环境我建议用3.11以上因为MCP的Python SDK对版本有要求。用venv隔离python3.11 -m venv venv source venv/bin/activate pip install mcp qdrant-client psycopg2-binary openai这里的openai包不是只能用OpenAI任何兼容OpenAI接口的LLM网关都能用热搜里llm网关这个词出现得很多说明大家普遍在用统一网关来管理多个模型。4.2 向量库与数据库初始化Qdrant起来之后需要建两个collection一个存语义记忆一个存反思经验。维度取决于你用的embedding模型比如text-embedding-3-small是1536维。from qdrant_client import QdrantClient from qdrant_client.models import VectorParams, Distance client QdrantClient(urlhttp://localhost:6333) client.create_collection( collection_namesemantic_memory, vectors_configVectorParams(size1536, distanceDistance.COSINE) ) client.create_collection( collection_namereflective_memory, vectors_configVectorParams(size1536, distanceDistance.COSINE) )Postgres这边建一张episodes表字段包括id、session_id、timestamp、role、content、tool_callsJSONB、tool_resultsJSONB。索引建在session_id和timestamp上方便按会话和时间范围查询。4.3 写入链路打通写入链路我建议做成异步的Agent调用store_episode之后立即返回后台任务再做语义提炼。这样不会阻塞Agent的主循环。用FastAPI的BackgroundTasks或者简单的asyncio队列都行。async def store_episode(session_id, role, content, tool_callsNone): # 1. 写原始层 await db.execute( INSERT INTO episodes (session_id, role, content, tool_calls) VALUES ($1,$2,$3,$4), session_id, role, content, json.dumps(tool_calls) ) # 2. 异步触发语义提炼 asyncio.create_task(extract_semantics(session_id, content))extract_semantics里调用LLM拿到JSON之后逐条判断confidence超过阈值我一般设0.7的才写入Qdrant同时把fact、confidence、ttl、tags作为payload存进去。4.4 检索链路与反思触发检索的时候我习惯先做一次query改写——让LLM把当前任务描述改写成更适合检索的查询尤其是当任务描述很长的时候。然后并行走向量检索和关键词检索合并去重后rerank。反思触发我用一个简单的计数器每次写入episode之后检查当前session的未反思事件数超过50就触发。反思任务本身也是异步的生成的经验条目写入reflective_memorycollectionpayload里带上evidence_ids方便追溯。实操心得反思的prompt里一定要明确要求模型“只总结有证据支撑的模式”否则它会编造一些听起来很有道理但实际没有依据的“经验”。我早期没加这个约束结果反思层里全是正确的废话。4.5 与Agent主循环集成最后一步是把MCP server接到你的Agent上。如果你用的是支持MCP的客户端比如某些桌面端工具直接在配置里加上server地址就行。如果是自己写的Agent循环用MCP的client SDK连接把recall的结果拼进system prompt或者作为tool result返回。我一般会在system prompt里加一段固定说明“你可以使用recall工具检索历史记忆。当用户提到‘上次’‘之前’‘以前’这类词时优先调用recall。”这样能显著提升记忆的调用率。5. 常见问题与排查技巧实录5.1 记忆检索不相关怎么办这是最高频的问题。排查顺序我建议这样走先看embedding模型是不是多语言的中文场景用英文模型效果会打折再看切片粒度原始事件如果太长embedding会稀释语义建议超过500字的事件先做一次摘要再embedding最后看rerank如果没做reranktop-5里混进无关条目很正常。还有一个隐蔽的坑query和记忆的表述风格不一致。用户问“怎么部署”记忆里存的是“容器化方案”向量相似度可能不高。解决办法是在写入时让LLM同时生成几个“可能的查询问法”作为额外payload检索时一起匹配。5.2 Docker网络不通的典型表现热搜里docker网络不通这个词很显眼我猜不少人卡在这。典型表现是memory-server容器里访问http://qdrant:6333超时。排查步骤docker compose ps确认两个容器都在同一network下。docker exec -it memory-server ping qdrant看能不能通。如果ping不通检查compose文件里有没有显式定义networks或者服务名有没有拼错。如果ping通但HTTP超时检查Qdrant的端口是不是6333HTTP而不是6334gRPC。注意在macOS上容器内访问宿主机服务要用host.docker.internal而不是localhost。这个坑我踩过不止一次。5.3 LLM请求报schema错误的处理热搜里llm request failed: provider rejected the request schema or tool payload这个错误通常出现在用MCP tool的时候。原因是tool的input schema定义和实际传参不匹配比如schema里写的是integerAgent传了个字符串。解决办法是在tool定义里尽量用宽松类型或者在server端做一次参数清洗。另外有些LLM网关对JSON schema的支持不完整遇到复杂嵌套schema会直接拒绝这时候把schema扁平化往往能解决。5.4 记忆膨胀与性能下降跑一段时间之后Qdrant里的点越来越多检索变慢。我的做法是语义记忆设TTL过期自动删除反思经验不设TTL但限制总量比如每个tag下最多保留100条超了就按时间淘汰最旧的。另外定期做一次“记忆合并”把高度相似的语义记忆合并成一条confidence取最大值。下面这张表是我整理的常见问题速查问题现象可能原因排查动作解决方向检索结果不相关embedding模型不匹配/切片过长检查模型语言支持、事件长度换多语言模型、先摘要再embed容器间访问超时网络未共享/端口错误ping测试、检查端口统一network、用对端口tool调用报schema错误schema与传参类型不符看server日志的入参扁平化schema、参数清洗记忆越来越多检索变慢无TTL/无淘汰策略统计collection点数设TTL、限制总量、定期合并反思层全是废话prompt约束不足抽查反思条目要求证据支撑、加few-shot5.5 几个我踩过的坑第一个坑是时间戳时区混乱。原始事件存UTC检索时按本地时间过滤结果差了几个小时。后来统一全部存UTC展示时再转本地。第二个坑是tool_calls的序列化。不同LLM返回的tool_calls结构不完全一样有的用function.arguments字符串有的直接是对象。存之前一定要统一成一种格式否则反思层解析时会炸。第三个坑是反思触发太频繁。早期我设的是每10个事件反思一次结果LLM调用量暴涨而且反思质量很差因为事件太少总结不出模式。后来改成50个事件或24小时效果好很多。6. 关于扩展方向的一些个人想法这套东西跑通之后我陆续试过几个扩展方向有的效果不错有的还在摸索。一个是把反思层和RAG的GraphRAG结合把经验条目之间的关系也建模成图检索时可以做多跳推理。另一个是给记忆加“置信度衰减”语义记忆的confidence随时间缓慢下降除非被反复命中这样能自动淘汰过时信息。还有一个我觉得很有潜力的方向是把hindsight的记忆层和MCP生态里的其他server联动。比如playwright mcp负责浏览器操作hindsight负责记住“上次这个网站的操作路径是什么”两者通过MCP协议组合Agent就能积累出特定网站的自动化经验。热搜里chrome devtools mcp和playwright mcp同时出现说明这个组合已经有人在尝试了。最后分享一个小技巧如果你在调试阶段想快速看记忆里到底存了什么别去翻数据库直接写一个MCP tool叫dump_memory按tag或时间范围返回可读的列表。这个工具在排查“为什么Agent想不起来”的时候特别有用比看日志直观得多。
返回列表