ARTICLE DETAIL

资讯详情

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

Agent Memory 工程实践:从写入、检索到遗忘的完整设计

Agent Memory 工程实践:从写入、检索到遗忘的完整设计 1. 从“hindsight”这个词说起为什么记忆是Agent落地的最后一公里“hindsight”这个词本身很有意思字面意思是“事后的洞察力”也就是我们常说的“后见之明”。把这个词拿来命名一个跟 agent memory 相关的项目命名的人显然是想表达一层更深的含义一个真正好用的智能体不应该只是对当前输入做出反应它得能回头看、能记住之前发生过什么、能从历史交互里提炼出对当下有用的东西。这恰恰是当前 LLM 应用从“玩具”走向“生产可用”过程中最容易被低估、也最难做扎实的一环。我接触过不少团队做智能体模型选的是顶配工具链接了一大堆MCP 协议也玩得很溜Docker 部署得整整齐齐但一上真实场景就露馅——用户第三天回来问“上次那个方案你帮我改了吗”智能体一脸茫然。问题不在模型能力而在记忆层。大模型的上下文窗口再大它也是无状态的每次请求对它来说都是“第一次见面”。你要让它有连续性就必须在模型之外自己搭一套记忆系统。hindsight 这个项目本质上就是在解决这件事给 LLM 驱动的 agent 装上一套可检索、可更新、可遗忘的长期记忆。这篇文章我想聊的不是某个具体 API 怎么调而是围绕 agent memory 这个核心命题把背后的设计逻辑、工程取舍、以及我在实际搭建过程中踩过的坑完整地摊开来讲。适合正在做 LLM 应用、准备给智能体加记忆能力、或者单纯想搞明白“agent 存储 working memory 到底该怎么设计”的开发者。不管你是刚接触 Docker 和 MCP 的新手还是已经在调 LLM 框架的老手这里面的思路应该都能对上号。先说结论性的判断记忆系统的核心不是“存”而是“取”和“忘”。存谁都会存往数据库一塞就完事难的是在正确的时机把正确的那条记忆捞出来以及在它过时、矛盾、冗余的时候果断把它清理掉。hindsight 这类项目的价值就在于它把这三个动作存、取、忘做成了一个可复用的抽象层而不是让每个业务方自己从零造轮子。2. Agent Memory 的三种记忆形态working memory、episodic memory 与 semantic memory在动手写代码之前得先把概念理清楚。很多人一上来就问“记忆用什么数据库存”这其实是个伪问题因为不同类型的记忆存储和检索策略完全不同。我习惯把 agent 的记忆分成三层这个分法借鉴了认知科学但在工程上非常好用。2.1 Working Memory当前这轮对话的“草稿纸”Working memory 就是当前会话的上下文也就是你塞进 prompt 里的那部分内容。它的特点是生命周期短、容量有限、访问速度极快。你可以把它理解成人的“短期记忆”——你现在正在想的事情。在工程实现上working memory 通常就是对话历史数组加上一些临时的中间状态比如工具调用的结果、当前任务的进度。这里有个常见的误区很多人把 working memory 当成唯一的记忆觉得“我把历史对话全塞进 context 不就行了”。短会话确实没问题但一旦对话轮次上去token 消耗会爆炸而且模型对超长上下文的注意力会稀释中间部分的信息经常被忽略。我实测过一个场景把 50 轮对话全塞进去模型对第 10 轮提到的关键约束的召回率明显下降。所以 working memory 必须有一个“压缩”或“摘要”机制把老旧的对话浓缩成要点腾出空间给新内容。2.2 Episodic Memory发生过什么事的“事件日志”Episodic memory 记录的是具体发生过的事件——“用户在 3 月 5 日要求把报告格式改成 PDF”“上一次调用天气工具返回了暴雨预警”。它是有时间戳、有因果关系的。这类记忆的检索方式通常是按时间范围或按事件类型来查。工程上episodic memory 一般落在关系型数据库或者带时间索引的文档库里。我见过有人用 MySQL 存也见过用 Redis 的 sorted set 按时间戳排序。关键是要给每条记忆打上足够丰富的元数据时间、会话 ID、用户 ID、事件类型、涉及的工具或实体。没有这些元数据后面检索就是大海捞针。2.3 Semantic Memory沉淀下来的“知识”Semantic memory 是从大量事件中提炼出来的、去掉了时间属性的稳定知识。比如“这个用户偏好简洁的回复风格”“这个项目的代码规范要求用 4 空格缩进”。它不关心“什么时候知道的”只关心“知道什么”。这类记忆最适合用向量数据库来存因为检索方式天然是语义相似度匹配。你把知识转成 embedding 存进去查询时用 query 的 embedding 去最近邻搜索。这也是当前 agent memory 方案里最主流的一层。但要注意semantic memory 的写入不能太随意否则会积累大量低质量、互相矛盾的条目反而干扰检索。后面我会专门讲怎么控制写入质量。把这三层分清楚之后你会发现很多设计问题迎刃而解working memory 用内存或 Redis 缓存episodic memory 用带时间索引的数据库semantic memory 用向量库。它们各自独立又通过一个统一的检索层对外提供服务。3. 记忆的写入、检索与遗忘一套可落地的工程闭环概念讲完了进入实操。这一节我按“写入—检索—遗忘”三个环节来讲每个环节都会给出具体的做法和取舍理由。3.1 写入不是所有对话都值得记新手最容易犯的错是把每一轮对话都无脑写进记忆库。结果就是记忆库迅速膨胀检索出来的全是“你好”“谢谢”这种废话。写入必须做过滤和提炼。我的做法是分两步走。第一步用一个轻量的规则过滤器把明显无意义的轮次纯寒暄、纯确认、工具调用的中间态挡掉。第二步对通过过滤的内容调用一次 LLM 做“记忆抽取”让它输出结构化的记忆条目。这个抽取的 prompt 很关键我一般会要求模型输出类似这样的结构{ type: semantic, content: 用户偏好用表格形式呈现对比数据, confidence: 0.85, source_turn: 12, entities: [用户偏好, 表格] }这里有个经验抽取时一定要让模型给出 confidence 分数并且明确要求它“只抽取对未来交互有长期价值的信息”。我试过不加这个约束模型会把“用户说今天天气不错”也抽成记忆纯属噪音。另外抽取本身是一次额外的 LLM 调用有成本所以可以异步做不要阻塞主对话流程。3.2 检索token 的三个点——key、query、value 的映射关系热词里有一条我印象很深“llm 的 token 三个点 key 我是谁、query 我在找什么、value 我能提供什么”。这个类比非常精准它其实就是注意力机制的核心也是记忆检索的核心。在记忆系统里key 是记忆条目的索引特征query 是当前对话的检索意图value 是记忆的实际内容。检索的难点在于用户的 query 和记忆的 key 往往不是字面匹配的。用户问“上次那个方案”字面上跟“项目 A 的架构设计文档”没有任何重叠词但语义上高度相关。这就是为什么向量检索是主力。但纯向量检索也有问题它对精确匹配比如某个具体的订单号、某个专有名词不敏感。所以工业级的做法是混合检索——向量相似度 关键词匹配BM25 之类 元数据过滤三路结果做融合排序。我实际用的融合策略是加权求和权重根据场景调。对话类场景向量权重高一些0.6带明确实体查询的场景关键词权重高一些0.5。融合之后取 top-k再交给一个 rerank 模型做精排。rerank 这一步很值它能把真正相关的条目从一堆“看起来相关”的里面挑出来实测召回准确率能提升 20% 以上。还有一个细节检索出来的记忆不能直接全塞进 prompt得做去重和压缩。同一个事实可能被多次抽取内容略有差异这时候要合并。我一般用 embedding 相似度做去重阈值设 0.92 左右超过就认为是重复的保留 confidence 最高的那条。3.3 遗忘比记住更重要的能力遗忘机制是区分“能用”和“好用”的分水岭。记忆库只增不减迟早会变成垃圾场。遗忘策略我一般分三种时间衰减给每条记忆一个“新鲜度”分数随时间递减。检索时新鲜度作为加权因子。这样老旧的、不再被引用的记忆会自然沉底。冲突消解当新记忆和旧记忆矛盾时比如用户先说“我喜欢 Python”后说“我现在主要用 Go”要能识别并标记旧记忆为失效。这个可以用 LLM 做判断也可以基于实体属性做规则匹配。容量淘汰给每类记忆设一个容量上限超了就按“新鲜度 × 引用次数 × confidence”排序淘汰末尾的。我踩过的一个坑是遗忘策略太激进把一些低频但关键的记忆比如用户的安全约束、合规要求给淘汰了。后来我加了一个“保护标记”某些类型的记忆安全、偏好、身份不参与容量淘汰只做冲突消解。这个细节在文档里通常不会写但实际生产环境必须考虑。4. 用 Docker 把记忆服务跑起来环境搭建与常见故障排查聊完设计得让它跑起来。hindsight 这类记忆服务通常是一个独立的进程对外提供 HTTP 或 gRPC 接口主应用通过 MCP 协议或者直接 HTTP 调用它。用 Docker 部署是最省心的方式但 Docker 本身在 Windows 上的坑不少我按实际流程走一遍。4.1 环境准备Docker Desktop 安装与虚拟化检查Windows 11 上装 Docker Desktop第一步不是下载安装包而是确认虚拟化支持。我遇到过好几次“virtualization support not detected”的报错折腾半天发现是 BIOS 里没开虚拟化。检查方法很简单任务管理器 → 性能 → CPU看右下角“虚拟化”是不是“已启用”。没启用就去 BIOS 开Intel 平台叫 VT-xAMD 平台叫 SVM。装完 Docker Desktop 之后建议把 WSL2 后端打开默认就是性能比 Hyper-V 后端好很多。然后配置镜像加速不然拉镜像能等到天荒地老。配置位置在 Settings → Docker Engine加一行 registry-mirrors 就行。验证安装是否成功docker --version docker compose version docker run hello-world三条都通过环境就算齐了。这里提醒一句Docker Desktop 和 Docker Engine 是两回事前者是带 GUI 的桌面版后者是纯命令行。Windows 上用桌面版最方便Linux 服务器上直接装 Engine。4.2 用 Docker Compose 编排记忆服务及其依赖记忆服务一般不是孤立的它要连向量库、关系库、缓存。用 Docker Compose 一把梭最省事。下面是我常用的一个编排骨架version: 3.8 services: memory-api: image: hindsight/memory-api:latest ports: - 8080:8080 environment: - VECTOR_DB_URLhttp://vector-db:6333 - RELATIONAL_DB_URLpostgresql://user:passpostgres:5432/memory - REDIS_URLredis://redis:6379 depends_on: - vector-db - postgres - redis networks: - memory-net vector-db: image: qdrant/qdrant:latest volumes: - vector-data:/qdrant/storage networks: - memory-net postgres: image: postgres:16 environment: - POSTGRES_PASSWORDpass - POSTGRES_DBmemory volumes: - pg-data:/var/lib/postgresql/data networks: - memory-net redis: image: redis:7-alpine networks: - memory-net volumes: vector-data: pg-data: networks: memory-net: driver: bridge这个编排里vector-db 用 Qdrant轻量且 API 友好postgres 存 episodic memory 和元数据redis 做 working memory 的缓存和会话状态。三个依赖都挂在同一个自定义网络上服务之间用服务名互相访问不用管 IP。启动命令docker compose up -d docker compose logs -f memory-api-d是后台运行logs -f跟踪日志。第一次启动会拉镜像耐心等。4.3 网络不通、端口冲突、数据卷权限三个高频故障的排查链路Docker 部署最烦的就是“明明配置都对就是连不上”。我按排查顺序列一下最常见的三类问题。网络不通。症状是 memory-api 日志里报“connection refused”或“timeout”。排查步骤先docker compose ps看所有容器是不是都 Up再docker exec -it memory-api ping vector-db看容器间能不能通如果 ping 不通多半是没在同一个 network 里检查 compose 文件里每个服务的 networks 配置。还有一个隐蔽的坑如果你在 Windows 上用 localhost 去连容器内的服务是连不上的得用服务名或者宿主机的实际 IP。端口冲突。症状是启动时报“port is already allocated”。用netstat -ano | findstr 8080Windows或lsof -i:8080Linux/Mac查是谁占了端口。常见的是之前没清理干净的容器还在跑docker ps -a找到后docker rm -f掉。改端口也行把 compose 里的8080:8080改成8081:8080。数据卷权限。Linux 上跑 Postgres 容器经常遇到“permission denied”写不进数据目录。原因是容器内的 postgres 用户 UID 和宿主机挂载目录的属主不匹配。解决办法是提前chown -R 999:999 ./pg-data999 是 postgres 镜像里的默认 UID或者干脆用命名卷named volume而不是绑定挂载bind mount让 Docker 自己管权限。提示排查 Docker 问题时docker compose logs和docker inspect是两个最常用的命令。前者看应用日志后者看容器的网络、挂载、环境变量等底层配置。养成先看日志再动手改的习惯能省很多瞎试的时间。5. MCP 协议接入让记忆服务成为 Agent 的“标准外设”记忆服务跑起来了接下来要让它能被 agent 调用。当前最优雅的方式是通过 MCP 协议接入。MCP 是软件协议不是硬件协议——热词里有人问“mcp 是软件协议硬件协议那个概念叫什么来着”硬件那边对应的概念一般叫总线或接口标准比如 USB、PCIeMCP 在软件层的定位类似就是给 LLM 应用提供一套标准化的“外设接口”。5.1 MCP 到底解决了什么问题在没有 MCP 之前每个 LLM 应用要接一个外部工具都得自己写适配代码。A 框架接数据库是一套写法B 框架接同一个数据库又是另一套。MCP 的价值就是把这层适配标准化工具方只需要实现一个 MCP Server暴露标准的工具描述和调用接口应用方只需要一个 MCP Client就能对接所有实现了 MCP 的工具。对记忆服务来说这意味着你不需要为每个 agent 框架单独写集成代码。记忆服务实现一个 MCP Server暴露store_memory、retrieve_memory、forget_memory这几个工具任何支持 MCP 的 agent 都能直接调用。这也是为什么热词里 MCP 相关的内容这么多——它正在成为事实标准。5.2 把记忆服务包装成 MCP Server 的关键步骤实现一个 MCP Server核心是定义工具清单和对应的处理函数。工具描述要写得足够清晰因为 LLM 是靠读描述来决定什么时候调用哪个工具的。我一般这样定义{ name: retrieve_memory, description: 根据当前对话上下文检索相关的长期记忆。当用户提到过去发生过的事情、之前的偏好、或者需要历史信息来回答时调用此工具。, inputSchema: { type: object, properties: { query: { type: string, description: 检索意图的自然语言描述 }, memory_types: { type: array, items: {type: string, enum: [semantic, episodic]}, description: 要检索的记忆类型不填则检索全部 }, top_k: { type: integer, default: 5, description: 返回的记忆条数上限 } }, required: [query] } }描述里那句“当用户提到过去发生过的事情”非常重要它是在教模型什么时候该用这个工具。我见过很多 MCP 工具描述写得太技术化模型根本不知道什么时候该调结果工具形同虚设。处理函数里就是调前面说的混合检索逻辑返回结构化的记忆列表。注意返回内容要控制长度别一次返回几十条把 context 撑爆。5.3 接入 Codex、Dify 等平台时的授权与配置坑不同平台接入 MCP 的方式略有差异。Codex 类工具接入时常见的问题是“无法找到 mcp”多半是 MCP Server 的启动命令或路径配错了。检查配置文件里的 command 和 args确保路径是绝对路径环境变量也传对了。Dify 接入浏览器类 MCP 时要注意它和 Playwright MCP 的区别——前者通常是封装好的浏览器操作工具后者是更底层的浏览器自动化。如果你只是想让 agent 读个网页用前者更省事如果要精细控制页面交互才需要后者。还有一个高频问题MCP Server 启动超时。有些平台对 MCP Server 的启动时间有限制如果你的记忆服务初始化时要加载大量向量索引可能超时。解决办法是把索引加载做成懒加载或者让 MCP Server 先返回一个“就绪”状态后台再慢慢加载。注意MCP 工具的调用是有 token 成本的每次调用都会把工具描述和返回结果算进 context。所以工具描述要精炼返回结果要压缩。我见过有人把整个记忆库 dump 出来返回直接把 context 撑爆模型反而变傻了。6. 记忆质量治理去重、冲突消解与防投毒记忆系统上线一段时间后真正的挑战才刚开始——记忆库会变脏。这一节讲三个必须做的治理动作都是我实际踩坑后总结的。6.1 语义去重同一件事被记了八遍怎么办前面提过用 embedding 相似度去重这里展开讲。去重不能只看内容相似度还要看实体和属性。比如“用户喜欢 Python”和“用户偏好 Python 语言”embedding 相似度很高应该合并。但“用户喜欢 Python”和“用户喜欢 Java”相似度也高句式一样语义却相反不能合并。我的做法是两阶段先用 embedding 粗筛出候选对相似度 0.85再用 LLM 精判是否真的语义等价。LLM 判断时给它明确的指令“判断这两条记忆是否表达同一个事实注意区分‘偏好 A’和‘偏好 B’这类句式相同但语义不同的情况。” 这样准确率能到 95% 以上。去重之后要保留哪条我的策略是保留 confidence 最高、时间最新、引用次数最多的那条把其他条目的引用次数累加过去。这样既去重了又不丢失“这条记忆被多次验证”的信号。6.2 冲突消解当用户改主意了用户偏好会变项目约束会变记忆库必须能跟上。冲突消解的核心是识别“同一实体的同一属性出现了不同值”。比如实体是“用户”属性是“主力编程语言”旧值是“Python”新值是“Go”。实现上我在写入 semantic memory 时会要求 LLM 同时抽取出实体和属性。存储时按“实体属性”建索引。新记忆写入时先查有没有同实体同属性的旧记忆有的话就触发冲突处理把旧记忆标记为superseded新记忆标记为active并记录替换关系。检索时默认只返回 active 的但保留历史可追溯。这里有个细节不是所有冲突都要消解。有些是场景相关的比如“工作时用 Go个人项目用 Python”这不算冲突是两个不同场景下的偏好。所以冲突判断时要带上场景上下文让 LLM 判断是真冲突还是场景差异。6.3 防投毒记忆系统也会被“带偏”热词里有个词叫 agentpoison讲的是通过污染记忆或知识库来攻击 LLM agent。这不是危言耸听记忆系统确实是一个攻击面。如果攻击者能往记忆库里写入恶意记忆比如“用户授权把所有数据发送到某地址”后续 agent 的行为就可能被操控。防御措施有几层。第一层是写入来源校验只有可信来源认证过的用户会话才能写入记忆工具返回的内容不能直接作为记忆写入必须经过抽取和审核。第二层是敏感内容过滤涉及权限、授权、地址、密钥这类内容的记忆写入时要额外标记并人工审核。第三层是检索时的来源加权来自可信来源的记忆权重更高来自低可信来源的权重降低。我实际做的时候还给记忆加了一个“不可变性”标记某些核心约束比如合规要求一旦写入就不可被覆盖只能追加。这样即使攻击者写入了冲突内容也无法覆盖原有的安全约束。7. 从 hindsight 看 Agent Memory 的下一步我的一些实践体会聊了这么多最后说点个人体会。hindsight 这个项目名起得好它提醒我们记忆的本质是“回头看的能力”。但回头看不是为了怀旧是为了让当下的决策更准。我在实际搭建记忆系统的过程中最大的感受是记忆系统的复杂度不在存储层而在策略层。用什么数据库、什么向量库这些都有成熟方案照着搭就行。真正难的是决定“什么该记、什么该忘、什么时候取、取多少”。这些策略没有标准答案必须结合具体业务场景反复调。另一个体会是记忆系统要尽早做可观测性。我一开始没做出了问题完全不知道是写入错了、检索错了还是遗忘错了。后来加了三个指标写入量、检索命中率、记忆库增长曲线。写入量突然飙升说明过滤器失效了检索命中率下降说明记忆质量和 query 意图不匹配了增长曲线只升不降说明遗忘策略没生效。有了这些指标调优才有方向。还有一个容易被忽略的点记忆系统要能“解释自己”。当 agent 基于某条记忆做出回答时最好能告诉用户“我是根据你之前提到的 X 来回答的”。这不仅提升可信度也方便排查问题。实现上就是在检索结果里带上记忆的来源和置信度让 agent 在回复时可以选择性引用。至于后续扩展我觉得有几个方向值得关注。一是多模态记忆现在大部分记忆系统只处理文本但用户发的图片、语音里也有大量信息。二是记忆的跨 agent 共享多个 agent 协作时记忆能不能互通。三是记忆的隐私保护用户能不能查看、编辑、删除自己的记忆。这些方向目前都还没有特别成熟的方案但需求已经很明显了。如果你正准备给自己的 agent 加记忆能力我的建议是先用最简单的方案跑起来一个向量库 一个关系库就够了把写入和检索的闭环打通然后再逐步加去重、冲突消解、遗忘这些治理逻辑。别一上来就追求完美架构记忆系统的很多问题只有真实数据跑起来才会暴露。
返回列表