尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

基于MCP与Docker的LLM Agent记忆系统架构设计与实现

基于MCP与Docker的LLM Agent记忆系统架构设计与实现 1. 从“hindsight”说起为什么我们需要给Agent装上记忆“hindsight”这个词本身很有意思字面意思是“事后的洞察力”也就是我们常说的“后见之明”。放在LLM Agent的语境里它指向一个非常具体且棘手的问题Agent如何记住过去发生过的事情并在后续决策中真正用上这些记忆。我接触过不少做Agent项目的团队大家一开始都信心满满觉得只要把LLM接上工具调用再套个ReAct循环一个能干的智能体就成型了。但跑上几天就会发现Agent的表现开始“飘”——同一个用户上周明确说过的偏好这周它忘得一干二净之前踩过的坑换个会话又踩一遍多轮任务做到一半上下文窗口塞满了早期关键信息被挤掉Agent直接“失忆”。这不是模型能力不行而是记忆架构缺位。“hindsight”这个项目标题结合热搜词里的agent memory、LLM、MCP、Docker我判断它要解决的核心问题是为LLM驱动的Agent构建一套可持久化、可检索、可演进的记忆系统并且这套系统要能通过MCP协议与外部工具链打通用Docker做标准化部署。换句话说它不是一个单纯的“聊天记录存储”而是一个让Agent具备“事后复盘能力”的基础设施。这篇文章适合谁看如果你正在做Agent应用开发被上下文窗口限制折磨过如果你在调研MCP协议怎么落地到实际项目如果你想知道Docker在LLM工程化里到底扮演什么角色——那这篇内容应该能给你一些可以直接抄作业的思路。我会从整体设计、核心细节、实操部署、问题排查四个维度展开尽量把每个技术选型背后的“为什么”讲透。2. 整体设计与思路拆解记忆系统到底该怎么分层2.1 为什么不能只靠上下文窗口做记忆很多人第一反应是现在LLM上下文窗口都到128K甚至1M了直接把历史对话全塞进去不就行了我实测过这条路走不通原因有三个。第一成本线性增长。每次请求都把全量历史带上token消耗是O(n)增长的一个跑了三个月的Agent单次请求成本能翻几十倍。第二注意力稀释。上下文越长模型对关键信息的召回率反而下降这是Transformer架构的固有特性业界叫“lost in the middle”现象。第三无法跨会话。上下文窗口是会话级的用户关掉页面再回来一切归零。所以“hindsight”这类项目必须做分层记忆。我的理解是至少分三层工作记忆Working Memory、情景记忆Episodic Memory、语义记忆Semantic Memory。工作记忆对应当前会话的短期上下文情景记忆存具体发生过的事件谁在什么时候做了什么语义记忆存抽象出来的知识和偏好。2.2 MCP协议在记忆系统中的角色定位热搜词里MCP出现频率极高这里得说清楚它是什么。MCP全称Model Context Protocol是一个标准化协议用来让LLM应用以统一方式连接外部数据源和工具。你可以把它类比成“AI领域的USB-C接口”——以前每个工具都要写一套适配代码现在只要实现MCP Server任何支持MCP的客户端都能直接调用。在“hindsight”的架构里MCP的价值在于把记忆系统做成一个独立的MCP Server。Agent不需要在自身代码里硬编码记忆读写逻辑而是通过MCP协议去调用记忆服务。这样做的好处是记忆系统可以独立升级、独立部署甚至可以被多个Agent共享。比如你有一个客服Agent和一个数据分析Agent它们可以通过同一个MCP记忆服务共享用户画像信息。2.3 Docker为什么是必选项LLM应用的依赖环境极其复杂——Python版本、CUDA驱动、向量数据库、各种SDK稍有不慎就是“在我机器上能跑”。Docker解决的是环境一致性问题。把记忆系统、向量库、MCP Server全部容器化用docker-compose编排一条命令拉起整套服务这对团队协作和后续迁移至关重要。我见过太多项目因为环境问题浪费几天时间最后发现只是某个依赖版本差了一个小版本号。Docker不是银弹但在LLM工程化场景下它是最务实的基建选择。2.4 整体架构草图基于以上分析“hindsight”的合理架构应该是这样的接入层MCP Server暴露记忆读写接口store、retrieve、forget、summarize逻辑层记忆管理器负责记忆的写入策略、检索策略、衰减策略存储层向量数据库存语义记忆 关系型数据库存情景记忆元数据 缓存存工作记忆模型层Embedding模型做向量化LLM做记忆摘要和提炼这个分层不是拍脑袋定的而是遵循了“关注点分离”原则。接入层只管协议转换逻辑层只管策略存储层只管持久化每层可以独立替换。比如你明天想从Chroma换成Milvus只需要改存储层适配器上层无感知。3. 核心细节解析与实操要点记忆的写入、检索与遗忘3.1 记忆写入不是所有对话都值得记新手最容易犯的错误是“全量存储”——把Agent和用户的每一句话都存进数据库。这样做的后果是检索时噪声极大真正有用的信息被淹没。我的经验是写入前必须做重要性评分。具体怎么做可以用一个轻量LLM做打分prompt大概是这样IMPORTANCE_PROMPT 请评估以下对话片段对长期记忆的重要性打分范围1-10。 评分标准 - 9-10分用户明确表达的偏好、身份信息、关键决策 - 6-8分任务相关的上下文、中间结论 - 3-5分一般性寒暄、确认性回复 - 1-2分无意义的填充词、重复内容 对话片段{segment} 只输出数字。 实测下来用GPT-4o-mini做这个打分成本极低但效果比规则匹配好太多。分数低于阈值的直接丢弃高于阈值的进入下一步。写入时还要做去重和合并。用户可能在不同时间说了同一件事比如“我不吃辣”说了三次。这时候不应该存三条而应该更新已有记忆的置信度和时间戳。我通常用向量相似度做初筛相似度高于0.9的走合并逻辑低于0.9的作为新记忆存入。注意合并逻辑要谨慎曾经遇到过把“我喜欢苹果”和“我喜欢苹果手机”合并成一条的尴尬情况。建议合并前用LLM做一次语义一致性判断。3.2 检索策略如何让Agent“想起”该想起的检索是记忆系统最核心的环节。简单用向量相似度检索是不够的因为相关性不等于有用性。一条记忆可能和当前query很相似但它是三个月前的过期信息。我的做法是混合检索重排序。第一步用向量检索召回Top-20第二步用BM25做关键词召回Top-20第三步合并去重后用Cross-Encoder做精排。精排时不仅看语义相关性还要看时间衰减因子和访问频率。时间衰减可以用指数衰减函数score relevance * exp(-λ * days_since_access) * (1 log(1 access_count))其中λ取值建议在0.01到0.05之间。λ太大记忆衰减太快Agent会“健忘”λ太小旧记忆一直占坑新信息进不来。我一般从0.02开始调根据实际效果微调。访问频率的加成也很重要。一条被反复检索到的记忆说明它确实有用应该获得更高的保留优先级。这其实模拟了人类大脑的“长时程增强”机制——经常被激活的神经连接会变强。3.3 遗忘机制主动删除比被动堆积更重要“hindsight”这个名字暗示了“事后洞察”但洞察的前提是信息经过筛选。一个不会遗忘的系统最终会被垃圾信息淹没。所以必须设计遗忘机制。遗忘分两种被动遗忘和主动遗忘。被动遗忘就是上面说的时间衰减分数低于阈值的记忆自动归档或删除。主动遗忘则是提供显式接口让Agent或用户可以主动删除某条记忆。比如用户说“忘掉我刚才说的地址”Agent应该调用MCP的forget接口。这里有个坑删除要软删除还是硬删除我的建议是软删除加一个deleted_at字段。因为有时候用户会反悔或者需要审计追溯。硬删除只在合规要求下才做。3.4 记忆摘要把长对话压缩成可复用的知识原始对话片段太长直接存进去检索效率低。所以需要做记忆摘要。我的做法是每N轮对话N10左右触发一次摘要用LLM把这段对话压缩成2-3句话保留关键实体、意图和结论。摘要的prompt设计很关键要明确要求保留人物、时间、地点、动作、结果、用户偏好。同时要求输出结构化格式方便后续解析{ summary: 用户张三在2024年3月询问了Docker安装问题最终通过启用虚拟化支持解决, entities: [张三, Docker, 虚拟化], intent: 问题排查, outcome: 已解决, preference: 用户使用Windows系统 }这样存进去的记忆检索时不仅能匹配语义还能按实体或意图做过滤精度提升明显。4. 实操过程与核心环节实现从零搭建一套记忆服务4.1 环境准备与Docker编排先说环境。我假设你用的是Linux服务器或者Windows上的WSL2。Windows原生Docker Desktop经常遇到“Virtualization support not detected”的问题本质是Hyper-V或WSL2没开。解决办法是在BIOS里启用虚拟化然后在“启用或关闭Windows功能”里勾选“虚拟机平台”和“适用于Linux的Windows子系统”。docker-compose.yml的核心配置如下version: 3.8 services: memory-mcp: build: ./mcp-server ports: - 8080:8080 environment: - VECTOR_DB_URLhttp://vector-db:8000 - REDIS_URLredis://redis:6379 - LLM_API_KEY${LLM_API_KEY} depends_on: - vector-db - redis vector-db: image: chromadb/chroma:latest volumes: - ./data/chroma:/chroma/chroma ports: - 8000:8000 redis: image: redis:7-alpine volumes: - ./data/redis:/data ports: - 6379:6379这里选Chroma做向量库是因为它轻量、易部署适合中小规模记忆场景。如果记忆量上亿建议换Milvus或Qdrant。Redis存工作记忆和热点缓存因为它的读写延迟在毫秒级。提示数据卷一定要挂载到宿主机否则容器重建数据就没了。我踩过这个坑一次docker-compose down -v把三个月的记忆全清了。4.2 MCP Server的实现要点MCP Server需要暴露几个核心工具Toolstore_memory写入记忆参数包括content、importance、metadataretrieve_memory检索记忆参数包括query、top_k、time_rangeforget_memory删除记忆参数包括memory_id或querysummarize_session触发会话摘要用Python实现的话可以用官方mcp库from mcp.server import Server from mcp.types import Tool, TextContent server Server(hindsight-memory) server.tool() async def store_memory(content: str, importance: int, metadata: dict) - str: if importance 5: return skipped: low importance embedding await get_embedding(content) memory_id await vector_db.insert(embedding, content, metadata) await redis.setex(fmem:{memory_id}, 3600, content) return fstored: {memory_id}这里有个细节importance低于5的直接跳过不浪费存储和检索资源。这个阈值可以根据业务调整客服场景可以调到6因为客服对话噪声更大。4.3 记忆检索的完整链路检索的完整流程我拆成五步Query改写用户当前输入可能很短比如“那个事怎么样了”直接检索效果很差。先用LLM把query改写成完整问句补全指代和省略。多路召回向量召回关键词召回并行执行。去重合并按memory_id去重保留最高分。精排用Cross-Encoder模型对Top-20做精排结合时间衰减和访问频率。组装上下文把Top-5记忆格式化成自然语言注入到Agent的system prompt里。Query改写的prompt示例REWRITE_PROMPT 根据对话历史将用户当前输入改写为独立完整的检索query。 对话历史{history} 当前输入{query} 只输出改写后的query。 这一步看似简单但效果提升非常明显。实测改写后检索命中率能提升30%以上。4.4 与Agent框架的集成如果你用的是LangChain或LlamaIndex集成方式很简单——把MCP Server包装成一个Tool注册到Agent的tool列表中。Agent在需要回忆时自动调用retrieve_memory在对话结束时调用store_memory。但更优雅的做法是在Agent的system prompt里注入记忆检索结果而不是让Agent自己决定何时检索。因为Agent往往“不知道自己不知道”它不会主动去查记忆。我的做法是在每轮对话开始前自动用当前输入去检索一次记忆把Top-3结果注入prompt。这样Agent“无感”地获得了历史信息。集成代码大概长这样async def chat(user_input, session_id): memories await mcp_client.call_tool( retrieve_memory, {query: user_input, top_k: 3} ) system_prompt f 你是一个有记忆的助手。以下是相关历史记忆 {format_memories(memories)} 请结合这些记忆回答用户问题。 response await llm.chat(system_prompt, user_input) await mcp_client.call_tool( store_memory, {content: fUser: {user_input}\nAssistant: {response}, importance: 6} ) return response5. 常见问题与排查技巧实录5.1 记忆检索不准的排查思路这是最高频的问题。排查顺序建议从下往上排查层级检查项常见原因解决方向数据层记忆是否真的写入了importance阈值过高全被过滤降低阈值检查日志向量层Embedding是否正常模型加载失败返回零向量检查模型服务健康状态检索层召回数量是否足够top_k设置过小增大top_k到20以上排序层精排模型是否生效模型未加载退化为随机排序检查Cross-Encoder加载日志注入层记忆是否进入prompt格式化函数报错被吞加异常捕获和日志我遇到过一次诡异情况检索结果明明有高分记忆但Agent就是不用。排查半天发现是prompt注入时记忆内容被放在了system prompt的最后而模型对system prompt末尾的注意力很弱。把记忆移到system prompt开头后问题解决。5.2 Docker网络不通的典型场景Docker网络问题在记忆系统部署中很常见因为涉及多个容器间通信。典型症状是memory-mcp容器访问vector-db时报“Connection refused”。排查步骤docker exec -it memory-mcp ping vector-db看能否解析主机名如果ping不通检查是否在同一个network下docker network inspect如果ping通但端口不通检查vector-db服务是否真的在监听docker logs vector-db如果服务正常但连接超时检查防火墙和端口映射最常见的原因是容器启动顺序。memory-mcp启动时vector-db还没就绪导致连接失败。解决办法是用healthcheckdepends_on的condition形式depends_on: vector-db: condition: service_healthy5.3 记忆膨胀导致性能下降跑了一段时间后向量库越来越大检索变慢。这时候需要做记忆归档。我的策略是超过90天且访问次数少于3次的记忆迁移到冷存储比如S3或本地文件向量库只保留热数据。归档不是删除检索时如果热数据没命中可以降级查冷存储。这样既控制了向量库规模又不丢失历史信息。5.4 LLM请求失败的排查热搜词里有个“llm request failed: provider rejected the request schema or tool payload”这是MCP场景下的高频错误。原因通常是tool定义的JSON Schema和实际传参不匹配。比如tool定义要求importance是integer但代码里传了string。解决办法是在MCP Server端加严格的参数校验用Pydantic做类型检查from pydantic import BaseModel, Field class StoreMemoryParams(BaseModel): content: str Field(..., min_length1, max_length10000) importance: int Field(..., ge1, le10) metadata: dict Field(default_factorydict)校验失败时返回明确的错误信息而不是让请求打到LLM才报错。这样排查效率高很多。5.5 记忆冲突的处理用户今天说“我喜欢蓝色”明天说“我讨厌蓝色”两条记忆冲突怎么办我的做法是时间优先置信度加权。新记忆的置信度默认高于旧记忆但如果旧记忆被多次确认过access_count高则保留旧记忆并标记冲突让Agent在回答时主动询问用户。具体实现是在检索时如果发现语义相似但结论相反的记忆在prompt里同时呈现并提示Agent“用户偏好可能发生变化建议确认”。6. 一些实操心得与扩展方向踩了这么多坑有几个心得值得单独说。第一记忆系统的核心不是存储而是检索和遗忘。很多人把精力花在怎么存更多结果检索质量一塌糊涂。我的建议是先把检索链路做扎实存储量反而是次要的。第二MCP协议的价值在于解耦不要把记忆逻辑写死在Agent里做成独立服务后你会发现复用场景比想象的多。第三Docker编排要留好数据卷这是血泪教训容器可以随便重建数据丢了就真没了。后续扩展的话我在考虑几个方向一是记忆的图结构化管理把实体和关系抽出来建成知识图谱检索时可以做多跳推理二是记忆的主动反思让Agent定期回顾自己的记忆自动发现矛盾和过时信息三是多Agent记忆共享不同Agent通过同一个MCP记忆服务交换信息形成群体记忆。这些方向目前还在实验阶段等跑出稳定结果再单独开一篇聊。如果你也在做类似的事情欢迎交流踩坑经验。
返回列表