ARTICLE DETAIL

资讯详情

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

Agent Memory 架构设计与 Docker 部署实战:从分层存储到 MCP 协议

Agent Memory 架构设计与 Docker 部署实战:从分层存储到 MCP 协议 1. 从“hindsight”说起为什么我们需要给 Agent 装上“后视镜”“hindsight”这个词本身很有意思字面意思是“事后的洞察力”也就是我们常说的“后见之明”。放在 LLM Agent 的语境里它指向一个非常具体且要命的问题Agent 的记忆到底该怎么存、怎么取、怎么用。你肯定遇到过这种情况——跟一个 AI 助手聊了半小时它突然忘了你五分钟前说过的关键约束或者一个自动化流程跑到第三步把第一步已经确认过的参数丢得一干二净。这不是模型不够聪明而是记忆机制没设计好。我最近在折腾 Agent Memory 相关的项目发现大家讨论最多的几个关键词——agent memory、LLM、MCP、Docker——其实串起来就是一条完整的落地路径。Agent Memory 解决“记什么、怎么记”的问题LLM 是推理和生成的核心引擎MCP 协议负责让 Agent 能调用外部工具和资源Docker 则是把这一整套东西打包成可复现、可迁移的运行环境。这四个东西凑在一起基本就是一个能跑起来的、有记忆能力的 Agent 系统的最小闭环。这篇文章适合谁看如果你正在做 LLM 应用开发尤其是涉及多轮对话、任务编排、工具调用的场景那 Agent Memory 的设计就是你绕不过去的坎。如果你刚接触 MCP 协议想知道它跟传统的函数调用有什么区别或者你已经在用 Docker 但还没想清楚怎么把 Agent 的存储层容器化那这篇内容也能给你一些可以直接抄作业的方案。我会从架构设计讲到具体实现从参数选择讲到踩坑记录尽量把每个决策背后的“为什么”说清楚。提示Agent Memory 不是简单的“把对话历史塞进上下文窗口”那样做既浪费 token 又容易丢失关键信息。真正的记忆系统需要分层、需要索引、需要遗忘机制。2. Agent Memory 的核心架构分层存储与检索策略2.1 为什么不能把所有东西都塞进 Context Window很多人一开始做 Agent 记忆第一反应就是把所有对话历史拼成一个长字符串每次请求都全量带上。这种做法在对话轮次少的时候勉强能用但一旦超过十轮问题就暴露了。首先是 token 成本线性增长其次是模型对长上下文的注意力衰减——中间部分的信息很容易被忽略这就是所谓的“lost in the middle”现象。更麻烦的是当你的 Agent 需要调用工具时工具返回的结果可能非常长比如一个 API 返回了几千字的 JSON你不可能每次都把它完整塞回上下文。所以我们需要分层。我自己的做法是把 Agent Memory 分成三层工作记忆Working Memory、短期记忆Short-term Memory和长期记忆Long-term Memory。工作记忆就是当前这一轮对话或任务执行中活跃的信息比如用户刚说的那句话、刚调用的工具返回结果。短期记忆是最近几轮对话的摘要或关键实体通常用滑动窗口加摘要的方式维护。长期记忆则是跨会话的、持久化的知识比如用户的偏好、历史任务的结论、领域知识库。这三层的存储介质和检索方式完全不同。工作记忆直接放在内存里随用随取短期记忆可以用 Redis 或 SQLite 做轻量级持久化长期记忆则需要向量数据库或者图数据库来支持语义检索。这里的关键决策点是你到底需要语义检索还是精确检索。如果你的 Agent 主要是做任务型对话比如订机票、查订单那精确检索按用户 ID、订单号就够了用关系型数据库完全没问题。但如果你要做知识问答、个性化推荐那向量检索就是刚需。2.2 记忆的写入与遗忘什么时候该记什么时候该忘记忆系统最难的部分不是“存”而是“决定存什么”和“决定忘什么”。我见过太多项目把每一轮对话都无脑写入数据库结果检索的时候噪音极大召回的相关性很差。我的经验是写入记忆需要触发条件。比如用户明确说“记住我喜欢靠窗座位”这是一个显式的记忆写入请求必须存。另一种是隐式的比如用户在多次对话中都提到了同一个项目名称系统可以自动提取为实体并关联到用户画像。遗忘机制同样重要。工作记忆在任务完成后就应该清空或归档短期记忆可以设置 TTL比如 24 小时长期记忆则需要定期做去重和合并。我试过用简单的 LRU最近最少使用策略来淘汰短期记忆效果还不错。但长期记忆的淘汰要谨慎因为有些信息虽然很久没用但一旦需要就是关键信息。我的做法是给每条长期记忆打一个“重要性分数”这个分数由写入时的上下文、用户反馈、检索命中率共同决定低于阈值的才进入冷存储。注意不要用 LLM 本身来做记忆的写入决策那样成本太高且不稳定。用规则引擎加轻量级分类模型就够了比如判断一句话是否包含“记住”“我喜欢”“下次”等关键词。2.3 记忆检索的三种模式与适用场景检索是记忆系统的出口直接决定了 Agent 的响应质量。我总结下来有三种模式精确匹配、语义检索和混合检索。精确匹配适合结构化数据比如用户 ID、订单号、时间戳用 SQL 的 WHERE 条件就能搞定速度快、准确率高。语义检索适合非结构化文本比如用户的历史评论、知识库文档用向量相似度来召回。混合检索则是两者结合先用精确条件缩小范围再在候选集里做语义排序。这里有个坑向量检索的 embedding 模型选择很关键。如果你用的是通用 embedding 模型对领域术语的区分度可能不够。比如医疗领域的“心梗”和“心肌梗死”在通用模型里可能很接近但在专业场景下需要区分。我的建议是如果领域数据量够大可以微调一个 embedding 模型如果不够至少要用领域语料做一下检索效果的评估别盲目上通用模型。检索模式适用数据类型典型工具响应速度准确率精确匹配结构化字段SQLite/PostgreSQL毫秒级极高语义检索非结构化文本向量数据库十毫秒级中等混合检索混合数据向量库关系库十毫秒级高3. MCP 协议在 Agent Memory 中的角色不只是工具调用3.1 MCP 到底是什么从“函数调用”到“标准化协议”MCP 最近热度很高但很多人对它的理解还停留在“让 LLM 调用外部 API”的层面。其实 MCP 的核心价值在于标准化。在没有 MCP 之前每个 LLM 框架都有自己的函数调用格式OpenAI 有一套Anthropic 有一套开源模型又有一套。你写了一个工具想在不同框架之间迁移得改一遍代码。MCP 的出现就是为了解决这个问题——它定义了一套标准的协议包括工具描述、参数 schema、调用结果格式任何支持 MCP 的客户端都能直接使用。在 Agent Memory 的场景里MCP 的作用更微妙。你可以把记忆的读写操作封装成 MCP 工具比如memory_write、memory_search、memory_forget。这样 Agent 在推理过程中可以自主决定什么时候写入记忆、什么时候检索记忆。这比在代码里硬编码记忆逻辑要灵活得多。比如用户说“帮我记一下下周要交报告”Agent 可以调用memory_write工具把这条信息存到长期记忆里并设置一个提醒时间。3.2 用 MCP 封装记忆服务的实操步骤我拿一个实际项目举例。假设你已经有了一个基于 SQLite 的短期记忆存储和一个基于 Chroma 的长期记忆存储现在想把它们暴露成 MCP 工具。首先你需要一个 MCP Server可以用 Python 的mcp库来写。核心代码如下from mcp.server import Server from mcp.types import Tool, TextContent import sqlite3 import chromadb app Server(memory-server) chroma_client chromadb.Client() collection chroma_client.get_or_create_collection(long_term_memory) app.list_tools() async def list_tools(): return [ Tool( namememory_write, description写入一条记忆到长期存储, inputSchema{ type: object, properties: { content: {type: string}, importance: {type: number}, tags: {type: array, items: {type: string}} }, required: [content] } ), Tool( namememory_search, description语义检索长期记忆, inputSchema{ type: object, properties: { query: {type: string}, top_k: {type: integer, default: 5} }, required: [query] } ) ] app.call_tool() async def call_tool(name: str, arguments: dict): if name memory_write: collection.add( documents[arguments[content]], metadatas[{importance: arguments.get(importance, 0.5), tags: ,.join(arguments.get(tags, []))}], ids[fmem_{hash(arguments[content])}] ) return [TextContent(typetext, text记忆已写入)] elif name memory_search: results collection.query( query_texts[arguments[query]], n_resultsarguments.get(top_k, 5) ) return [TextContent(typetext, textstr(results))]这个 Server 写好后你需要在 Agent 的配置里注册它。如果你用的是支持 MCP 的客户端比如 Claude Desktop 或者某些 IDE 插件直接在配置文件里加上这个 Server 的启动命令就行。如果是自己写的 Agent 框架那就需要实现 MCP 客户端逻辑通过 stdio 或 SSE 跟 Server 通信。提示MCP Server 的启动方式推荐用 stdio比 HTTP 更轻量适合本地开发。生产环境可以考虑 SSE 或 WebSocket但要注意连接管理和超时重试。3.3 MCP 与 Docker 的结合让记忆服务可移植MCP Server 写好了怎么保证它在不同机器上都能跑答案就是 Docker。把 MCP Server 和它的依赖SQLite、Chroma、Python 环境一起打包成镜像这样无论你是在本地开发还是在云服务器部署只要拉取镜像、挂载数据卷就能得到完全一致的行为。我自己的 Dockerfile 大概长这样FROM python:3.11-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . CMD [python, memory_server.py]构建和运行命令也很简单docker build -t agent-memory-mcp . docker run -d --name memory-server \ -v /host/data:/app/data \ -p 8080:8080 \ agent-memory-mcp这里的关键是数据卷挂载。记忆数据必须持久化到宿主机否则容器一重启所有记忆就丢了。我一般会把 SQLite 文件和 Chroma 的持久化目录都挂载出来。另外如果你用的是 Docker Desktop记得在设置里给容器分配足够的内存Chroma 做向量检索的时候比较吃内存2GB 是底线4GB 以上比较稳。4. Docker 环境下的 Agent Memory 部署实战4.1 环境准备Docker Desktop 安装与常见问题Windows 上装 Docker Desktop 是很多人的第一道坎。我见过最多的报错就是“Virtualization support not detected”和“Docker Desktop failed to start because virtualization is not enabled”。这两个问题的根源是一样的你的 CPU 虚拟化功能没在 BIOS 里打开。解决办法是重启电脑进 BIOS通常是 F2 或 Del 键找到 Intel VT-x 或 AMD-V 选项设为 Enabled。保存退出后在 Windows 的“任务管理器-性能-CPU”里应该能看到“虚拟化已启用”。另一个常见问题是 WSL2 没装好。Docker Desktop 现在默认用 WSL2 作为后端如果你之前没启用过 WSL需要先在 PowerShell 里跑wsl --install然后重启。装完之后在 Docker Desktop 的设置里确认“Use WSL 2 based engine”是勾选的。如果还是启动失败可以试试在设置里点“Reset to factory defaults”但注意这会清空所有镜像和容器操作前先备份重要数据。Ubuntu 上装 Docker 就简单多了官方脚本一行搞定curl -fsSL https://get.docker.com | sh sudo usermod -aG docker $USER装完之后记得重新登录让用户组权限生效。然后跑docker run hello-world验证一下能看到欢迎信息就说明环境没问题了。4.2 用 Docker Compose 编排记忆服务栈单独跑一个 MCP Server 容器不够你还需要数据库、缓存、可能还有反向代理。这时候 Docker Compose 就派上用场了。我一般会定义一个docker-compose.yml把 SQLite、Redis、Chroma 和 MCP Server 都编排在一起。SQLite 其实不需要单独容器直接挂载文件就行但 Redis 和 Chroma 建议独立容器方便管理和扩展。version: 3.8 services: redis: image: redis:7-alpine ports: - 6379:6379 volumes: - redis_data:/data command: redis-server --appendonly yes chroma: image: chromadb/chroma:latest ports: - 8000:8000 volumes: - chroma_data:/chroma/chroma environment: - IS_PERSISTENTTRUE memory-mcp: build: . depends_on: - redis - chroma volumes: - ./data:/app/data environment: - REDIS_HOSTredis - CHROMA_HOSTchroma - CHROMA_PORT8000 stdin_open: true tty: true volumes: redis_data: chroma_data:这个编排文件里stdin_open和tty是为了让 MCP Server 的 stdio 通信正常工作。如果你用的是 SSE 模式那就不需要这两个配置。另外depends_on只保证启动顺序不保证服务就绪所以 MCP Server 里最好加一个重试逻辑等 Redis 和 Chroma 真正可用了再开始监听。4.3 数据持久化与备份策略Agent Memory 的数据比代码值钱代码丢了可以重写记忆丢了就真没了。所以持久化和备份必须做好。我的策略是SQLite 文件每天凌晨做一次全量备份用sqlite3 .backup命令或者直接复制文件注意先停写Redis 开启 AOF 持久化每秒同步一次Chroma 的持久化目录每周做一次快照存到对象存储里。Docker 环境下数据卷的管理也要注意。docker volume ls可以列出所有卷docker volume inspect看具体挂载点。千万不要用docker volume prune随便清理那个命令会删掉所有未被使用的卷包括你的记忆数据。我一般会给数据卷起明确的名字比如agent_memory_redis_data这样一眼就能看出是干什么的。注意如果你在 Windows 上用 Docker Desktop数据卷实际存储在 WSL2 的虚拟磁盘里。想直接访问文件的话路径大概是\\wsl$\docker-desktop-data\data\docker\volumes但建议通过容器来操作别直接改文件。5. 常见问题与排查技巧实录5.1 记忆检索召回率低怎么办这是被问得最多的问题。召回率低通常有三个原因embedding 模型不合适、分块策略有问题、或者查询本身太模糊。我的排查顺序是这样的先拿几条典型的查询语句手动看看向量检索返回的结果如果明显不相关那就是 embedding 模型的问题。换一个在领域语料上表现更好的模型或者用text-embedding-3-large这种高维模型试试。如果 embedding 没问题那就看分块。很多人把一整段对话直接作为一个 document 存入这样向量会非常“平均”区分度很低。正确的做法是按语义单元分块比如一轮问答、一个工具调用结果、一条用户偏好分别作为独立的 document。块的大小控制在 200-500 token 之间比较合适太小了缺乏上下文太大了噪音多。查询本身的问题也常见。用户说“上次那个事”这个查询没有任何语义信息向量检索肯定召回不了。这时候需要 Agent 先做查询改写把“上次那个事”扩展成“用户上次提到的项目截止日期”然后再去检索。这个改写步骤可以用 LLM 来做成本不高但效果提升明显。5.2 Docker 容器间网络不通的排查思路Docker Compose 默认会创建一个 bridge 网络所有服务在同一个网络里可以通过服务名互相访问。但如果你手动docker run启动容器没有指定网络那容器之间就是隔离的。排查网络问题的第一步是docker network ls看看容器在哪个网络里。然后用docker exec -it container ping target测试连通性。如果 ping 不通检查目标容器是否在运行、端口是否监听。有时候容器起来了但服务没起来docker ps显示 Up但实际端口没开。用docker logs container看日志或者docker exec进去netstat -tlnp看监听端口。还有一个坑是防火墙Windows 上 Docker Desktop 的端口映射有时候会被系统防火墙拦临时关掉防火墙测试一下如果通了就是防火墙规则的问题。5.3 MCP 工具调用超时与重试MCP 工具调用走的是 stdio 或 SSE如果 Server 处理时间太长客户端可能会超时。默认超时时间一般是 30 秒对于记忆检索来说通常够用但如果你的向量库很大、检索很慢就可能超时。解决办法有两个一是优化检索性能加索引、减少候选集二是调整客户端的超时配置大部分 MCP 客户端都支持自定义超时。重试策略也要注意。不是所有失败都值得重试比如参数错误重试多少次都没用。我的做法是区分错误类型网络类错误连接断开、超时重试 3 次每次间隔 1 秒业务类错误参数不合法、资源不存在直接返回不重试。重试的时候要保证幂等性特别是写入操作别重试一次写两条。问题现象可能原因排查命令解决方案检索结果不相关embedding 模型不匹配手动测试向量相似度换领域模型或微调容器间 ping 不通不在同一网络docker network inspect加入同一自定义网络MCP 调用超时检索太慢或超时太短看 Server 日志耗时优化索引或调大超时记忆写入重复重试未做幂等检查写入逻辑加唯一 ID 或去重5.4 记忆膨胀导致性能下降跑了一段时间后你会发现记忆库越来越大检索越来越慢。这是正常的但需要主动管理。我的做法是定期做记忆压缩把低重要性的、长时间未命中的记忆归档到冷存储主库只保留活跃记忆。归档不是删除只是移到另一个表或另一个 collection需要的时候还能查回来。另一个技巧是给记忆加 TTL。短期记忆默认 7 天过期长期记忆默认 90 天但用户显式要求记住的永久保留。TTL 的实现可以用 Redis 的 EXPIRE或者用定时任务扫描数据库删除过期记录。注意删除的时候要软删除加一个deleted_at字段方便审计和恢复。6. 一些实操心得与后续扩展方向我在多个项目里落地 Agent Memory 之后最大的体会是别追求一步到位。一开始就用最简单的方案——SQLite 存对话历史关键词匹配做检索能跑起来就行。等业务量上来了再逐步引入向量检索、分层存储、MCP 封装。过早优化会让你陷入技术选型的泥潭反而忽略了核心的业务逻辑。另一个心得是记忆的“质量”比“数量”重要得多。与其存一万条无关紧要的对话不如精心维护一百条高价值的记忆。我现在的做法是每次写入长期记忆之前先让 LLM 做一次摘要和重要性评分只有评分超过阈值的才写入。这样虽然增加了一点成本但检索时的信噪比提升非常明显。后续如果要扩展我建议往这几个方向走一是引入图数据库来做实体关系推理比如 Neo4j可以支持“找出所有跟项目 A 相关的记忆”这种查询二是做记忆的版本控制每次修改都保留历史版本方便回溯三是把记忆服务做成独立的微服务通过 gRPC 或 HTTP 对外提供这样不同的 Agent 可以共享同一套记忆。最后分享一个小技巧在 Docker 里跑 Chroma 的时候如果数据量超过 10 万条向量建议把chroma_server_grpc_max_message_size调大默认值有时候不够用会导致写入失败。这个参数在 Chroma 的启动配置里设置具体值根据你的单条数据大小来定一般设成 100MB 比较保险。
返回列表