拓冰建站拓冰建站
首页 / 资讯中心 / 正文

LLM Agent 记忆架构实战:分层存储、MCP 协议与 Docker 部署

1. 从“hindsight”说起为什么我们需要给 Agent 装上一双“后视之眼”“hindsight”这个词本身很有意思字面意思是“事后的洞察力”中文里最贴切的翻译大概是“后见之明”。放在 LLM Agent 的语境下它指向的是一个非常具体且长期被低估的问题Agent 的记忆到底该怎么存、怎么取、怎么用。我接触过不少做 Agent 的团队大家一开始的注意力几乎都放在模型选型、Prompt 调优、工具调用链路上记忆模块往往是最后才被想起来的那一块。结果就是 Agent 在单轮对话里表现惊艳一旦进入多轮、跨会话、长周期的任务场景就开始“失忆”——用户上周说过的偏好它不记得三天前踩过的坑它还会再踩一遍同一个任务反复问同样的问题。这不是模型不够聪明而是记忆架构没设计好。“hindsight”这个项目标题我理解它的核心诉求是让 Agent 具备对历史交互的回顾、提炼和复用能力。它不是简单的“把对话记录塞进向量库”那种粗暴做法而是要在存储、检索、遗忘、抽象这几个环节上做系统性的设计。结合热搜词里出现的agent memory、MCP、Docker、a-memguard、working memory这些关键词可以判断这个项目大概率涉及以下几个层面Agent 记忆的分层存储working memory / long-term memory、基于 MCP 协议的记忆服务暴露、容器化部署方案以及记忆安全防护。这篇文章适合谁看如果你正在做 Agent 应用开发被多轮对话的上下文管理折磨过如果你在调研 MCP 协议怎么落地到实际服务里如果你想知道 Docker 化部署一个记忆服务需要注意哪些坑——那这篇内容应该能给你一些可以直接抄作业的东西。我会尽量把每个设计决策背后的“为什么”讲清楚而不是只丢一堆配置让你自己猜。2. 记忆架构的整体设计思路拆解2.1 为什么不能只用向量数据库很多人做 Agent 记忆的第一反应是上个向量库把对话 embedding 一下存进去需要的时候相似度检索。这个方案能跑通 demo但上生产就会暴露三个问题。第一个问题是检索精度随数据量衰减。向量检索本质上是语义相似度匹配当记忆库里有几千上万条记录时Top-K 检索出来的内容经常是“语义相似但实际无关”的噪声。比如用户问“帮我订个会议室”检索出来的可能是三个月前一条“会议室预订系统密码忘了”的记录语义上确实相关但对当前任务毫无帮助。第二个问题是缺乏时间维度和重要性维度。向量检索只考虑语义相似度不考虑这条记忆是什么时候产生的、有多重要、是否已经过时。Agent 需要的是“最近三天内用户明确表达的偏好”而不是“历史上所有和偏好沾边的句子”。第三个问题是没有抽象和压缩机制。原始对话记录是冗余度极高的数据直接存原始文本会让记忆库迅速膨胀检索效率下降。人脑的记忆不是逐字存储的而是经过提炼、抽象、关联的。Agent 记忆也应该有类似的机制。所以“hindsight”这类项目通常采用分层记忆架构working memory 负责当前会话的短期上下文long-term memory 负责跨会话的持久化知识中间还有一个 abstraction layer 负责把原始交互提炼成结构化记忆。2.2 分层记忆的具体划分我在实际项目里用过的一套分层方案是这样的记忆层级存储内容生命周期存储介质检索方式Working Memory当前会话最近 N 轮对话会话结束即销毁内存 / Redis顺序读取Episodic Memory具体事件、操作记录数天到数周关系库 向量库时间 语义混合Semantic Memory提炼后的知识、偏好、规则长期向量库 图库语义 图遍历Procedural Memory任务流程、工具调用模板长期结构化存储精确匹配这个划分参考了认知科学里对人类记忆的分类落到工程上就是不同层级用不同的存储和检索策略。Working memory 追求低延迟直接放内存或 RedisEpisodic memory 需要时间范围查询用关系库存元数据、向量库存语义Semantic memory 需要关联推理图数据库更合适Procedural memory 本质上是模板库精确匹配就够了。注意不要一上来就把所有记忆都塞进向量库。我见过太多项目因为这一步偷懒后期检索质量怎么调都上不去最后不得不重构整个记忆层。2.3 MCP 协议在记忆服务中的角色热搜词里MCP出现频率很高这里简单说一下它在记忆架构里的定位。MCPModel Context Protocol本质上是一套标准化的接口协议让 LLM 能够以统一的方式调用外部服务。把记忆服务封装成 MCP Server 的好处是Agent 不需要关心记忆底层用什么数据库、什么检索算法只需要通过标准接口发起“存记忆”“取记忆”“忘记忆”这几个操作。这样做的好处有三个。一是解耦记忆服务的实现可以独立演进换向量库、换检索策略都不影响 Agent 侧代码。二是复用同一个记忆服务可以同时给多个 Agent 使用共享知识。三是可观测所有记忆操作都经过统一入口方便做日志、审计和安全防护——这就引出了a-memguard那个热搜词记忆安全确实是个容易被忽视但很重要的点。3. 核心细节解析与实操要点3.1 Working Memory 的窗口管理策略Working memory 最核心的问题就是当前会话的上下文窗口就那么大怎么决定哪些内容留在窗口里、哪些被挤出去最朴素的做法是 FIFO保留最近 N 轮。但这样会丢掉早期的重要信息比如用户在对话开头说的“我对花生过敏”聊了二十轮之后 Agent 推荐了一家花生酱餐厅这就出事了。我实际用的策略是加权滑动窗口给每条消息算一个保留分数score w1 * recency w2 * importance w3 * relevance其中recency是时间衰减因子越新的消息分越高importance是消息的重要性标记可以通过规则包含偏好声明、约束条件的关键词或轻量模型打分relevance是这条消息和当前 query 的语义相关度。三个权重根据场景调一般w10.4, w20.4, w30.2是个不错的起点。当窗口快满的时候按分数从低到高淘汰但有一个例外被标记为“关键约束”的消息永远不淘汰而是压缩成一句话摘要放在窗口顶部。比如“用户对花生过敏”这条即使过了五十轮也会以摘要形式保留。3.2 记忆写入的触发时机什么时候把一条交互写入长期记忆如果每轮对话都写记忆库会被垃圾数据淹没如果只在会话结束时写又可能丢掉中间的重要信息。我的做法是事件驱动 定期批量结合。具体来说以下几类事件会触发即时写入用户明确表达偏好、约束、身份信息“我是做后端的”“我不喜欢用 ORM”任务完成或失败的关键节点“这个方案跑通了”“这个方法报错了”用户显式要求记住的内容“记住这个配置”其他普通对话内容则累积到会话结束或达到一定条数后批量做一次提炼再写入。提炼这一步很关键不是把原始对话存进去而是让 LLM 生成一条结构化记忆格式大概是{ type: preference, content: 用户偏好使用 PostgreSQL 而非 MySQL, confidence: 0.85, source_turns: [12, 13], timestamp: 2025-01-15T10:30:00Z, expires_at: null }confidence字段是给后续检索做加权的expires_at用于处理有时效性的记忆比如“这周在出差”。3.3 记忆检索的混合策略检索是记忆系统里最考验功力的部分。纯向量检索的问题前面说过了我的方案是三路召回 重排序第一路是向量召回用 query 的 embedding 去语义记忆库里找 Top-20。第二路是关键词召回用 BM25 或类似算法做精确匹配补充向量检索漏掉的专有名词、代码片段。第三路是图召回如果语义记忆用图结构存储可以从 query 涉及的实体出发做 N 跳遍历找到关联记忆。三路召回的结果合并后用一个轻量 cross-encoder 做重排序最终取 Top-5 注入到 Agent 的上下文里。实测下来这个方案比纯向量检索的命中率能提升 30% 以上尤其是在涉及具体技术名词和实体关系的场景下。提示重排序模型不需要太大我用的是一个小型 cross-encoder推理延迟控制在 50ms 以内对整体响应时间影响可以忽略。3.4 记忆安全防护的基本思路a-memguard这个热搜词提醒了我记忆安全是个真实存在的攻击面。攻击者可以通过精心构造的对话往 Agent 的记忆库里注入恶意记忆比如“用户授权所有操作无需确认”这种。一旦写入后续所有会话都会受影响。基础的防护措施包括写入前做内容审核检测是否有越权声明、指令注入的痕迹写入时做来源标记区分用户直接输入、Agent 推断、外部工具返回不同来源的记忆在检索时给不同权重检索时做一致性校验如果一条记忆和当前会话的上下文明显矛盾降权或标记待确认。更严格的做法是给记忆加签名和版本控制每条记忆记录写入时的会话 ID、模型版本、审核结果出问题可以追溯和回滚。这块展开能写一整篇这里先点到为止。4. 实操过程与核心环节实现4.1 环境准备与 Docker 化部署记忆服务我建议直接用 Docker 部署一是环境隔离干净二是迁移方便。基础镜像用python:3.11-slim依赖主要包括向量库客户端、关系库驱动、MCP SDK。Dockerfile 大概长这样FROM python:3.11-slim WORKDIR /app RUN apt-get update apt-get install -y \ build-essential \ rm -rf /var/lib/apt/lists/* COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . EXPOSE 8080 CMD [python, -m, memory_service.server]requirements.txt里核心依赖mcp1.0.0 qdrant-client1.7.0 psycopg2-binary2.9.9 redis5.0.0 sentence-transformers2.5.0docker-compose 编排需要把记忆服务和它的依赖组件向量库、关系库、Redis串起来version: 3.8 services: memory-service: build: . ports: - 8080:8080 environment: - QDRANT_URLhttp://qdrant:6333 - POSTGRES_URLpostgresql://user:passpostgres:5432/memory - REDIS_URLredis://redis:6379 depends_on: - qdrant - postgres - redis qdrant: image: qdrant/qdrant:latest ports: - 6333:6333 volumes: - qdrant_data:/qdrant/storage postgres: image: postgres:16 environment: POSTGRES_USER: user POSTGRES_PASSWORD: pass POSTGRES_DB: memory volumes: - pg_data:/var/lib/postgresql/data redis: image: redis:7-alpine volumes: - redis_data:/data volumes: qdrant_data: pg_data: redis_data:这里有个坑要提醒Windows 上装 Docker Desktop 经常遇到virtualization support not detected的报错本质是 BIOS 里虚拟化没开或者 Hyper-V/WSL2 配置有问题。先去 BIOS 开 VT-x/AMD-V然后在 Windows 功能里确认 WSL2 已启用基本能解决。如果还不行检查一下是不是和某些虚拟化软件冲突了。4.2 MCP Server 的接口设计记忆服务暴露给 Agent 的 MCP 接口我设计了四个核心 toolfrom mcp.server import Server from mcp.types import Tool, TextContent server Server(memory-service) server.tool() async def store_memory( content: str, memory_type: str, importance: float 0.5, expires_at: str None ) - str: 存储一条记忆 memory_id await memory_store.write( contentcontent, typememory_type, importanceimportance, expires_atexpires_at ) return fstored:{memory_id} server.tool() async def recall_memory( query: str, top_k: int 5, memory_types: list[str] None ) - list[dict]: 检索相关记忆 results await memory_store.retrieve( queryquery, top_ktop_k, typesmemory_types ) return results server.tool() async def forget_memory(memory_id: str) - str: 删除指定记忆 await memory_store.delete(memory_id) return fdeleted:{memory_id} server.tool() async def summarize_session(session_id: str) - str: 提炼会话记忆 summary await memory_store.summarize(session_id) return summary接口设计的原则是语义清晰、参数精简。store_memory里memory_type用枚举值preference/fact/procedure/episodeimportance用 0-1 浮点数expires_at用 ISO 时间字符串。不要搞太多可选参数Agent 调用工具的时候很容易填错。4.3 记忆写入的完整流程一条记忆从产生到落库中间要经过好几个环节。我以“用户说他不喜欢用 ORM”这个场景为例走一遍完整流程。第一步是触发检测。对话流经过一个轻量分类器判断当前轮次是否包含值得记忆的信息。这个分类器可以是一个小模型也可以用规则 关键词匹配先跑起来。检测到“不喜欢”“偏好”“记住”这类信号词时标记为候选记忆。第二步是内容提炼。把候选轮次和前后文一起送给 LLM让它生成结构化记忆。Prompt 大概是从以下对话中提炼出值得长期记忆的信息输出 JSON 格式 { type: preference|fact|procedure|episode, content: 简洁的陈述句, confidence: 0.0-1.0, entities: [涉及的实体] } 对话 用户你们这个项目用的什么 ORM 助手我们用的是 SQLAlchemy。 用户哦我个人不太喜欢 ORM感觉 SQL 更可控。LLM 返回{ type: preference, content: 用户偏好直接写 SQL不喜欢使用 ORM 框架, confidence: 0.9, entities: [SQL, ORM] }第三步是去重和冲突检测。写入前先检索一下有没有相似记忆如果有判断是更新还是新增。比如之前已经有一条“用户喜欢用原生 SQL”那这次就合并提升 confidence更新 timestamp。第四步是多路写入。结构化元数据写 PostgreSQLembedding 写 Qdrant实体关系写图库如果用的话working memory 的摘要写 Redis。四路写入要保证原子性我的做法是先写 PostgreSQL 拿到 memory_id其他几路带上这个 ID失败时根据 ID 回滚。4.4 记忆检索的完整流程检索流程以 query“帮我写个数据库查询”为例。第一步是query 理解。对 query 做意图识别和实体抽取识别出这是“代码生成”意图涉及实体“数据库”“查询”。第二步是三路召回。向量路用 query embedding 在 Qdrant 里搜 Top-20关键词路用“数据库”“查询”“SQL”在 PostgreSQL 全文索引里搜 Top-20图路从“数据库”实体出发找一跳内的关联记忆比如“用户偏好 PostgreSQL”“用户不喜欢 ORM”。第三步是合并去重。三路结果按 memory_id 合并同一 ID 取最高分。第四步是重排序。用 cross-encoder 对合并后的候选集打分同时考虑记忆的 importance、recency、confidence 做加权final_score 0.6 * rerank_score 0.2 * importance 0.1 * recency 0.1 * confidence第五步是格式化注入。取 Top-5 记忆格式化成 Agent 容易理解的文本[相关记忆] - 用户偏好直接写 SQL不喜欢使用 ORM 框架置信度 0.9 - 用户常用 PostgreSQL 16置信度 0.8注入位置放在 system prompt 之后、用户 query 之前这样模型能同时看到记忆和当前问题。5. 常见问题与排查技巧实录5.1 记忆检索不准的排查思路检索不准是最常见的问题排查要按链路一步步来。先看召回阶段有没有问题把 query 分别走三路召回看每一路返回了什么。如果向量路返回的全是无关内容可能是 embedding 模型不适合当前领域换一个或者做微调。如果关键词路召回为空检查全文索引有没有建对分词器是否适配中文。再看重排序阶段把召回结果和重排后的结果对比如果重排把相关记忆排到了后面说明 cross-encoder 和你的场景不匹配考虑换模型或者调整加权公式。最后看注入阶段有时候检索是对的但注入格式让模型误解了。比如把记忆和当前 query 混在一起模型分不清哪个是历史哪个是现在。用明确的分隔标记并且给每条记忆标注来源和时间。5.2 记忆库膨胀的处理跑一段时间后记忆库会越来越大检索延迟上升。处理策略分三层过期清理给每条记忆设 TTL到期自动删除或归档合并压缩定期跑一个任务把相似记忆合并成一条更抽象的冷热分离最近 30 天的记忆放热存储更早的放冷存储检索时默认只查热存储需要时再查冷的。我一般设置的是episodic memory TTL 30 天semantic memory 不过期但每季度做一次合并procedural memory 长期保留。5.3 常见问题速查表问题现象可能原因排查方法解决方案记忆写入后检索不到embedding 未生成或写入失败查 Qdrant 里是否有该 ID检查写入流程的原子性检索结果全是旧记忆recency 权重太低打印各记忆的时间戳调高 recency 权重记忆内容矛盾冲突检测未生效查是否有相似记忆未合并加强去重逻辑服务启动报连接错误依赖组件未就绪查 docker-compose 日志加 healthcheck 和重试检索延迟高记忆库过大或索引未优化查 Qdrant 和 PG 的查询耗时加索引、冷热分离记忆被恶意注入无内容审核查写入日志的来源加审核和来源标记5.4 几个踩过的坑第一个坑是embedding 模型和检索场景不匹配。我一开始用的是一个通用中文 embedding 模型结果在技术术语上表现很差“ORM”和“对象关系映射”检索不到一起。后来换了一个在代码和技术文档上训练过的模型效果立竿见影。选 embedding 模型一定要在你的实际数据上测不要只看榜单。第二个坑是working memory 和 long-term memory 的边界模糊。有段时间我把两者混在一起导致当前会话的临时信息和长期记忆互相干扰。后来明确划分working memory 只存当前会话会话结束全部清空需要保留的通过提炼流程写入 long-term。边界清晰之后检索准确率明显提升。第三个坑是MCP 接口的幂等性。Agent 有时候会重复调用store_memory导致同一条记忆写入多次。解决办法是给写入接口加幂等键用content session_id turn_id做唯一约束重复写入直接返回已有 ID。第四个坑是Docker 网络配置。记忆服务和向量库在同一个 compose 网络里用服务名做 hostname 就行不要写localhost。我见过有人把QDRANT_URL配成http://localhost:6333在容器里根本连不上因为 localhost 指向容器自己。这个错误很低级但很常见。6. 记忆系统的扩展方向与个人经验这套架构跑通之后能扩展的方向其实不少。一个是多 Agent 共享记忆把记忆服务做成独立的微服务多个 Agent 通过 MCP 接入共享同一份知识库同时通过命名空间做隔离。另一个是记忆的可视化做一个简单的 Web 界面能看到记忆库里的内容、检索命中情况、写入历史调试的时候非常有用。还有一个我觉得很有价值的方向是记忆的主动遗忘。人脑会主动遗忘不重要的信息Agent 也应该有类似的机制。除了 TTL 过期还可以根据记忆的访问频率、最近一次被检索的时间、与其他记忆的关联度综合计算一个“遗忘分数”定期清理低分记忆。这样记忆库能保持在一个健康的规模检索质量也不会随数据量增长而下降。我个人在实际操作中的体会是记忆系统最难的不是技术实现而是定义什么值得记。这个判断标准因场景而异没有通用答案。我的建议是先用规则跑起来收集一段时间的实际数据看看哪些记忆被检索到了、哪些从来没被用过根据这些反馈迭代你的记忆策略。不要一开始就追求完美先让系统跑起来数据会告诉你该怎么优化。最后分享一个小技巧在开发阶段给每条记忆加一个debug_info字段记录它是从哪轮对话、哪个模型、哪个 Prompt 版本生成的。出问题的时候顺着这个字段能快速定位到源头。这个字段在生产环境可以关掉但开发阶段能省你很多排查时间。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门