hindsight 实战:为 LLM Agent 构建分层记忆系统与 MCP 集成
1. 从 hindsight 这个名字说起为什么 Agent Memory 值得单独造一个轮子第一次看到 hindsight 这个项目名我脑子里蹦出来的不是技术架构而是一句老话——事后诸葛亮。但恰恰是这个略带自嘲意味的词点破了当前 LLM Agent 落地时最要命的一个短板记忆。我们先把场景摆出来。你搭了一个基于 LLM 的 Agent接入了 MCP 协议跑在 Docker 里能调工具、能查数据库、能操作浏览器。单轮对话它表现惊艳可一旦对话拉长到十几轮或者隔了一天再回来接着聊它就开始失忆——昨天你告诉它我们公司的数据库端口是 5433 不是 5432今天它照样给你连 5432。你让它记住这个项目里所有金额单位都是万元它转头就按元来算。这不是模型笨是它压根没有一套像样的记忆机制。hindsight要解决的就是这件事。它本质上是一个面向 LLM Agent 的记忆层Memory Layer核心思路是把 Agent 的工作记忆working memory从易失的上下文窗口里剥离出来做成可持久化、可检索、可分层管理的独立组件。你可以把它理解成给 Agent 装了一个外挂大脑短期记忆放在内存里快速读写长期记忆落到数据库里慢慢沉淀需要的时候再按相关性捞回来塞进 prompt。为什么这件事值得单独做一个项目而不是在 Agent 框架里随手加几行代码因为记忆这件事远比想象中复杂。它涉及几个绕不开的硬问题存什么原始对话摘要结构化事实、怎么存向量图键值、怎么取语义相似度时间衰减重要性加权、怎么忘容量上限淘汰策略冲突消解。这四个问题每一个都能写一篇论文随手糊几行代码的结果就是——要么检索不准要么越存越乱要么成本爆炸。这篇文章我会把hindsight这类 Agent Memory 方案从设计思路到落地实操完整拆一遍。涉及 MCP 协议怎么对接、Docker 怎么部署、存储层怎么选型、检索策略怎么调参都会给到可直接抄作业的步骤。适合正在做 LLM Agent 落地、被记忆问题折磨过的开发者也适合刚接触 MCP 和 Agent 存储、想搞清楚这套东西到底怎么跑起来的朋友。哪怕你之前只听说过 LLM 和 Docker跟着走也能把一套可用的记忆系统搭起来。2. 整体设计思路Agent Memory 到底该怎么分层2.1 为什么不能只靠上下文窗口硬扛很多人第一反应是现在模型上下文都 128K 甚至 1M 了把历史对话全塞进去不就完了我实测过这条路在真实场景里走不通原因有三个。第一是成本。上下文越长每次请求的 token 消耗越大而且是线性甚至超线性增长。一个跑了 50 轮的对话如果每轮都把全部历史带上token 账单会难看到你想哭。第二是注意力稀释。模型对长上下文中间部分的召回能力是明显下降的这是公开研究里反复验证过的现象你塞进去的关键信息很可能被淹没。第三是无状态。上下文窗口是会话级的会话一结束就没了跨会话、跨设备的记忆根本无从谈起。所以正确的做法是上下文窗口只放当前最相关的一小撮记忆剩下的全部外置。这就是hindsight这类项目的立足点。2.2 三层记忆模型working / episodic / semantic我在设计自己的记忆层时参考了认知科学里比较经典的分层落到工程上大致是三层层级对应概念存储介质生命周期典型内容工作记忆Working Memory内存 / Redis单次会话当前任务状态、临时变量情景记忆Episodic Memory向量库 / 文档库中期历史对话片段、事件记录语义记忆Semantic Memory关系库 / 图库长期提炼后的事实、偏好、规则工作记忆是手边的小本本读写要快容量小会话结束就可以丢。情景记忆是日记本按时间线记录发生过什么检索时靠语义相似度。语义记忆是知识库是从大量情景里蒸馏出来的稳定事实比如用户偏好用中文回复这个项目的部署环境是内网。hindsight的价值就在于它把这套分层做成了开箱即用的组件而不是让你从零手搓。它处理了层与层之间的流转什么情况下把工作记忆固化进情景记忆什么情况下从情景记忆里提炼出语义记忆检索时三层怎么协同召回。2.3 存储选型向量、图、还是键值选型这块我踩过不少坑直接说结论。向量库适合情景记忆的语义检索这是标配。但纯向量有个致命问题它擅长模糊相似不擅长精确关系。你问张三的直属领导是谁向量检索可能给你捞出一堆提到张三和领导的段落但拼不出准确答案。图数据库适合语义记忆里的实体关系。把人物、项目、概念做成节点关系做成边查询张三 - 领导 - ?就是一次图遍历精确且高效。这也是热词里 LLM ontology 和 GraphRAG 火起来的原因——用本体ontology来约束记忆的结构。键值存储适合工作记忆和简单的事实缓存快、简单、便宜。我的建议是混合工作记忆用 Redis情景记忆用向量库比如 pgvector 或 Qdrant语义记忆用图库或带关系表的关系库。hindsight这类项目通常会抽象出一个统一的 Memory Interface底层可以插不同的后端你按需组合。提示不要一上来就上全套。先用向量库把情景记忆跑通验证检索质量再考虑加图库。过早引入图结构会让你的 schema 设计反复推翻重来。2.4 与 MCP 协议的关系记忆作为一种能力暴露出去MCPModel Context Protocol是这两年 Agent 生态里最重要的一个协议层。它的核心思想是把 Agent 能调用的能力工具、资源、提示模板标准化成 ServerAgent 作为 Client 去连接。热词里那一大串 playwright mcpchrome devtools mcpunity mcp同花顺 mcp 都是这个思路的产物。那记忆和 MCP 什么关系关系很大。把记忆系统做成一个 MCP Server意味着任何支持 MCP 的 Agent 都能即插即用地获得记忆能力不用改 Agent 本身的代码。你的记忆 Server 暴露几个工具memory_store存、memory_recall取、memory_forget删、memory_summarize提炼。Agent 在需要的时候调用这些工具记忆就自然融入了工作流。这是hindsight这类项目最聪明的设计选择——不绑定特定 Agent 框架而是通过 MCP 做能力解耦。你用的是 Trae、还是别的 IDE只要它支持 MCP就能接上。3. 核心细节解析记忆的存、取、忘三件事3.1 存什么原始对话、摘要还是结构化事实这是记忆系统第一个要拍板的决策直接决定后面所有环节的复杂度。原始对话最省事直接把 user/assistant 的消息对存下来。优点是信息无损缺点是噪声大、冗余高、检索时容易命中无关内容。摘要是折中方案用 LLM 把一段对话压缩成几句话。优点是密度高、检索准缺点是有信息损失而且摘要本身要花 token 和延迟。结构化事实最理想把对话里的事实抽成三元组或 JSON比如{user: 张三, preference: 中文回复}。优点是精确、可查询、可推理缺点是需要抽取逻辑抽取错了会污染记忆。我的实操经验是三者都要但分层存原始对话进情景记忆做兜底摘要进情景记忆做快速检索结构化事实进语义记忆做精确查询。hindsight里通常会有对应的 pipeline对话结束 - 触发摘要 - 触发事实抽取 - 分别落库。这里有个关键细节抽取时机。同步抽取会拖慢响应异步抽取又可能丢数据。我的做法是写入时先落原始对话保证不丢然后丢一个异步任务去做摘要和抽取用消息队列解耦。这样用户侧无感后台慢慢处理。3.2 怎么存向量化与元数据设计向量化这块选 embedding 模型是第一步。中文场景我一般用 BGE 系列或者通义系的 embedding英文场景 OpenAI 的 text-embedding-3 系列够用。维度上768 或 1024 是性价比比较高的选择1536 以上收益递减但存储和检索成本上升明显。但光有向量是不够的元数据metadata才是检索质量的关键。我一般会给每条记忆打上这些标签timestamp时间戳用于时间衰减和范围过滤session_id会话 ID用于隔离不同会话user_id用户 ID用于多用户隔离type记忆类型对话/摘要/事实importance重要性分数0-1用于加权entities涉及的实体列表用于精确过滤source来源标识便于溯源有了这些元数据检索时就能做混合过滤先按user_id和type做硬过滤再在候选集里做向量相似度排序最后按importance和时间做加权。这比纯向量检索的准确率高出一大截。注意元数据字段不要贪多。每加一个字段写入和索引的成本都会上升。我见过有人给每条记忆打二十几个标签结果写入慢得离谱检索时大部分字段根本用不上。控制在 6-8 个核心字段就够了。3.3 怎么取检索策略与重排序检索是记忆系统里最影响体验的环节。用户问一句话你要从成千上万条记忆里捞出最相关的那几条塞进 prompt捞错了 Agent 就答错。基础流程是query 向量化 - 向量库 ANN 检索 top-K - 重排序 - 取 top-N 塞进 prompt。这里的坑主要在 K 和 N 的取值以及重排序要不要做。K 一般取 20-50N 取 3-8。K 太小会漏太大重排序成本高。重排序rerank我强烈建议做用一个 cross-encoder 模型对候选做精排能把准确率提升 20% 以上。代价是延迟增加但记忆检索本来就不该在关键路径上卡太久可以接受。除了语义检索还有两条路要走时间检索最近 N 条记忆和实体检索涉及某实体的所有记忆。实际系统里通常是三者融合用 RRFReciprocal Rank Fusion之类的算法把多路结果合并。热词里提到的 LLM 的 token 三个点 key 我是谁、query 我在找什么、value 我能提供什么其实说的就是检索时的三元组思维你是谁身份上下文、你在找什么查询意图、你能提供什么候选记忆的价值。把这三者对齐检索质量就上来了。3.4 怎么忘淘汰、合并与冲突消解记忆系统最容易被忽视的就是遗忘。只存不忘库会越来越大检索越来越慢噪声越来越多。淘汰策略我一般用组合拳容量上限 时间衰减 重要性阈值。每个用户/会话设一个容量上限超了就按重要性 × 时间衰减因子排序淘汰末尾的。时间衰减因子用指数衰减比如exp(-λ * days)λ 取 0.01 到 0.05 之间让老记忆自然降权。合并是指把相似记忆归并。比如用户三次提到喜欢简洁回复可以合并成一条带计数的记忆而不是存三条。这能显著降低冗余。冲突消解是最难的。用户先说用 Python后说改用 Go两条记忆冲突了怎么办我的做法是保留时间戳检索时优先返回新的同时在语义记忆里做一次更新把旧事实标记为 superseded。不要直接删旧的因为有些场景需要追溯历史决策。4. 实操过程从零把 hindsight 跑起来4.1 环境准备Docker 与依赖安装先把基础环境搭好。我假设你在 Windows 或 macOS 上用 Docker Desktop 最省事。Windows 上装 Docker Desktop 有个经典报错virtualization support not detected或者docker desktop failed to start because virtualisation support wasnt detected。这不是 Docker 的锅是 BIOS 里虚拟化没开。进 BIOS 把 Intel VT-x 或 AMD-V 打开然后在 Windows 功能里确认虚拟机平台和适用于 Linux 的 Windows 子系统都勾上重启就好。装完之后验证docker --version docker compose version两个命令都能输出版本号环境就 OK 了。如果docker compose报找不到命令说明你装的是老版本用docker-compose带横杠试试或者升级到新版。4.2 用 Docker Compose 编排记忆服务hindsight这类记忆系统通常需要几个组件记忆服务本体、向量库、缓存、消息队列。用 Docker Compose 一把编排最省心。下面是我常用的一个模板version: 3.9 services: hindsight: image: hindsight:latest ports: - 8080:8080 environment: - VECTOR_BACKENDqdrant - QDRANT_URLhttp://qdrant:6333 - REDIS_URLredis://redis:6379 - EMBEDDING_MODELBAAI/bge-base-zh-v1.5 - MEMORY_TTL_DAYS90 depends_on: - qdrant - redis restart: unless-stopped qdrant: image: qdrant/qdrant:latest ports: - 6333:6333 volumes: - qdrant_data:/qdrant/storage redis: image: redis:7-alpine ports: - 6379:6379 volumes: - redis_data:/data volumes: qdrant_data: redis_data:几个参数说明一下。VECTOR_BACKEND选 qdrant 是因为它部署简单、性能好、支持元数据过滤。EMBEDDING_MODEL选 BGE 中文模型是因为中文场景下它比通用多语言模型效果好。MEMORY_TTL_DAYS是记忆的默认存活天数配合淘汰策略用。启动docker compose up -d docker compose logs -f hindsight看到服务正常监听 8080 就成功了。提示如果你遇到docker 网络不通先检查容器间是否在同一 network。Compose 默认会创建一个共享 network服务名就是主机名所以http://qdrant:6333能直接解析。如果手动docker run起的容器记得加--network。4.3 把记忆暴露成 MCP Server这是让记忆真正被 Agent 用起来的关键一步。MCP Server 一般用 stdio 或 SSE/WebSocket 两种传输方式。本地开发用 stdio 最简单远程共享用 SSE。一个最小的 MCP Server 配置以 stdio 为例{ mcpServers: { hindsight-memory: { command: docker, args: [ run, -i, --rm, --network, hindsight_default, hindsight-mcp:latest ], env: { HINDSIGHT_API: http://hindsight:8080 } } } }把它写进你 Agent 客户端的 MCP 配置文件里重启客户端就能看到hindsight-memory这个 Server 暴露的工具了。通常会有这几个memory_store存一条记忆参数是 content、type、importance、metadatamemory_recall检索记忆参数是 query、top_k、filtersmemory_forget删除记忆参数是 memory_id 或过滤条件memory_summarize对一段对话做摘要并存入Agent 在对话过程中会自动调用这些工具。比如用户说记住我用的是 PostgreSQL 15Agent 就会调memory_store存下来下次用户问我的数据库版本是多少Agent 会调memory_recall捞出来。4.4 参数计算容量、衰减与检索阈值这部分是很多人忽略的但直接决定系统能不能长期稳定跑。容量估算。假设你有 100 个活跃用户每人每天产生 50 条记忆每条记忆平均 200 token。一天的原始数据量是100 × 50 × 200 100 万 token。向量化后768 维 float32 是 3KB 左右加上元数据算 5KB一天 100 万条就是 5GB。这个量级用单机 Qdrant 完全扛得住但你要提前规划磁盘。时间衰减。我用的公式是score importance × exp(-λ × age_days) × similarity。λ 取 0.02 意味着记忆半衰期约 35 天。这个值要根据业务调客服场景记忆有效期短λ 可以取 0.05个人助理场景记忆长期有效λ 取 0.01。检索阈值。相似度低于某个值的记忆直接丢弃避免噪声。我一般设 0.6余弦相似度低于这个值的候选不进入重排序。这个阈值要用真实数据调太高会漏太低会引入噪声。重排序 top-N。塞进 prompt 的记忆条数我一般控制在 5 条以内每条不超过 300 token总预算 1500 token 左右。超过这个数prompt 里记忆部分就开始挤占其他内容了。4.5 实操现场一次完整的记忆读写我把一次完整的流程走一遍你能看到每个环节的实际输入输出。用户第一轮说我在做一个电商项目后端用 Go数据库用 PostgreSQL 15部署在阿里云。Agent 调用memory_store{ content: 用户在做电商项目后端技术栈 Go数据库 PostgreSQL 15部署在阿里云, type: fact, importance: 0.8, metadata: { user_id: u_123, entities: [电商项目, Go, PostgreSQL 15, 阿里云], timestamp: 2025-01-15T10:30:00Z } }后台异步任务会做两件事把这条记忆向量化存入 Qdrant同时触发事实抽取把后端Go数据库PostgreSQL 15等三元组存入语义记忆。三天后用户问我那个项目的数据库是哪个版本Agent 调用memory_recall{ query: 项目的数据库版本, top_k: 20, filters: { user_id: u_123, type: fact } }检索流程query 向量化 - Qdrant 按 user_id 和 type 过滤后做 ANN 检索 - 返回 20 条候选 - 重排序取 top 5 - 返回给 Agent。Agent 拿到PostgreSQL 15这条回答用户。整个过程用户侧感知不到但记忆已经跨会话生效了。5. 常见问题与排查技巧实录5.1 记忆检索不准的排查路径检索不准是最常见的问题排查要按顺序来。先看向量化是否正常。把 query 和一条已知相关的记忆分别向量化算余弦相似度。如果相似度低于 0.5说明 embedding 模型不适合你的语料换模型。再看元数据过滤是否过严。有时候你加了typefact的过滤但相关记忆存的是typesummary自然捞不到。把过滤条件放宽试试。然后看重排序是否帮倒忙。cross-encoder 模型如果和你的语料领域不匹配可能把相关结果排到后面。临时关掉重排序看原始向量检索的结果。最后看记忆本身是否存对了。有时候是写入环节就错了比如摘要把关键信息丢了。直接查库看原始记忆内容。5.2 Docker 部署常见报错速查报错信息原因解决virtualization support not detectedBIOS 虚拟化未开进 BIOS 开 VT-x/AMD-Vdocker 网络不通容器不在同一 network用 Compose 或手动加--networkport is already allocated端口被占用改端口或杀掉占用进程no space left on device磁盘满清理docker system pruneqdrant connection refused向量库没起来检查 depends_on 和健康检查5.3 记忆膨胀与性能下降的治理跑一段时间后你会发现检索变慢、结果变差。这是记忆膨胀的典型症状。治理手段有几个。定期归档把超过 90 天且重要性低于 0.3 的记忆移到冷存储主库只留热数据。去重合并用相似度阈值比如 0.95找出重复记忆合并成一条。重建索引向量库跑久了索引会碎片化定期重建能恢复性能。分层存储热数据放内存或 SSD冷数据放对象存储。我一般会写一个定时任务每周跑一次治理。别等到性能崩了才想起来。5.4 多用户隔离与隐私边界如果你的记忆系统服务多个用户隔离是红线。每条记忆必须带user_id检索时必须强制过滤。我见过有人忘了加过滤结果 A 用户问问题Agent 把 B 用户的记忆答出来了这是严重事故。除了user_id还要考虑会话隔离和租户隔离。会话隔离是同一用户不同会话之间的记忆要不要共享一般情景记忆共享、工作记忆隔离。租户隔离是不同组织之间的硬隔离通常用独立的库或独立的 collection。注意隐私相关的记忆比如用户明确说这个别记要有删除机制。GDPR 之类的合规要求下用户有权要求删除自己的数据你的系统要能按 user_id 一键清除。5.5 与 Agent 框架集成的踩坑记录最后说几个集成时的坑。工具调用时机。Agent 什么时候该调memory_recall太频繁会拖慢响应太少会漏掉上下文。我的做法是在 system prompt 里明确告诉 Agent在回答涉及用户历史信息的问题前先调用 memory_recall。同时给一个判断规则比如当问题包含之前上次我的等词时触发。记忆注入位置。检索到的记忆塞在 prompt 的哪个位置很讲究。放在 system prompt 后面、user message 前面效果最好。放在最前面容易被忽略放在最后又可能干扰当前问题。token 预算。记忆注入会占用 token要给它设上限。我一般给记忆留 1500-2000 token 的预算超了就截断或减少条数。失败降级。记忆服务挂了怎么办不能让整个 Agent 挂掉。我的做法是记忆检索失败时返回空Agent 按无记忆模式继续工作同时记录日志告警。6. 记忆系统的扩展方向与个人实践体会把基础版跑通之后hindsight这类系统还有不少可以深挖的方向。本体增强。热词里的 LLM ontology 和 GraphRAG 指向同一个方向用本体来约束记忆的结构。不是随便存文本而是按预定义的 schema 存实体和关系。这样检索时能做推理比如张三的领导的下属是谁这种多跳查询。代价是前期 schema 设计成本高适合领域明确的场景。空间记忆。spatial llm 这个热词提示了一个有趣的方向把记忆和空间位置关联。对于机器人、AR、地图类应用记忆不只是什么时候发生了什么还有在哪里发生了什么。这需要在元数据里加地理坐标检索时支持空间范围查询。记忆的自我演化。让 Agent 定期回顾自己的记忆发现矛盾、提炼规律、更新认知。这本质上是把语义记忆的维护也交给 LLM减少人工干预。风险是 LLM 可能提炼出错误结论需要人工审核环节。跨 Agent 记忆共享。多个 Agent 共享一套记忆协作完成任务。这需要解决并发写入、冲突消解、权限控制等问题是更复杂的工程挑战。我个人在实际操作中的体会是记忆系统的价值不在于技术多先进而在于和业务场景的匹配度。我见过用最朴素的键值存储做出体验极好的记忆系统也见过堆了一堆向量库图库但检索一塌糊涂的方案。关键是想清楚你的场景到底需要什么是精确的事实查询还是模糊的语义联想是短期的会话连续还是长期的用户画像想清楚了技术选型自然就清晰了。最后分享一个小技巧给记忆加一个来源字段记录这条记忆是从哪次对话、哪个文档来的。这在排查问题时极其有用——当 Agent 答错了你能顺着来源回溯到原始上下文快速定位是记忆存错了还是检索错了。这个字段成本极低但省下的排查时间难以估量。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →