资讯动态

Agent记忆层设计:hindsight工作记忆与长期记忆分层实战

发布时间:2026/10/2 3:11:30 来源:尧图企业网站定制
1. 项目缘起为什么“事后复盘”值得单独做成一个记忆层“hindsight”这个词本身的意思就是“事后的明白”中文里最贴切的翻译是“后见之明”。我第一次看到这个项目标题的时候脑子里蹦出来的不是哲学概念而是一个非常具体的工程问题大模型智能体在跑完一轮任务之后到底记不记得自己刚才干了什么、为什么这么干、哪一步其实走错了绝大多数做 agent 的团队早期都会把精力砸在“让 agent 能干活”上——接工具、写 prompt、调流程。等到 agent 真能跑起来才发现一个更棘手的问题它没有记忆。或者说它只有一种非常粗糙的记忆就是把整段对话历史塞回上下文窗口。这种做法在短任务里勉强能用一旦任务链条拉长到几十步上下文就会爆炸token 成本飙升而且模型对早期信息的注意力会明显衰减。hindsight 要解决的就是这件事。它不是又一个 agent 框架也不是又一个向量数据库而是一个专门面向 agent 的记忆层。你可以把它理解成给 agent 装了一个“事后复盘”的模块任务执行过程中发生了什么、哪些信息值得留下、下次遇到类似场景该怎么调用这些都由它来管。这个定位很关键。市面上做 agent memory 的方案大致分两派一派是“全量存、语义搜”把所有交互都扔进向量库靠 embedding 相似度召回另一派是“结构化存、按需取”把记忆拆成不同类型分别管理。hindsight 明显偏向后者它强调的是working memory工作记忆和长期记忆的分层这一点和认知科学里人类记忆的运作方式是对齐的。适合读这篇内容的人我大致分三类。第一类是在做 agent 产品、已经被上下文长度和 token 成本折磨过的工程师第二类是对 LLM 应用架构感兴趣、想搞清楚“记忆”这一层到底该怎么设计的技术负责人第三类是想动手跑一个完整 demo、通过 Docker 快速验证想法的独立开发者。不管你是哪一类下面这些内容都是从实际工程角度出发的不讲空话。2. 核心设计拆解hindsight 的记忆分层到底怎么切2.1 为什么不能只靠向量数据库先说说为什么“全量存向量库”这条路走不通。我早期做过一个客服 agent做法很朴素每轮对话都存进向量库下一轮把用户问题做 embedding召回 top-k 相关历史。跑了一周就发现两个致命问题。第一个问题是召回不准。向量相似度衡量的是语义接近但 agent 需要的往往不是“语义接近的历史”而是“当前任务状态下真正相关的那条信息”。比如用户说“还是用上次那个地址”向量检索很可能召回一堆提到“地址”的对话但真正有用的那条可能压根没出现“地址”这个词。第二个问题是没有时间维度。向量库里的记忆是扁平的它不知道哪条记忆是五分钟前的、哪条是上周的。而 agent 的任务执行是有严格时序的工作记忆必须能反映“当前这一步之前刚刚发生了什么”。hindsight 的思路是把记忆分成至少两层工作记忆和长期记忆。工作记忆是任务执行期间的临时状态生命周期短、结构清晰、访问频繁长期记忆是跨任务沉淀下来的知识生命周期长、需要索引和检索。这个分层直接决定了它的存储结构和召回策略。2.2 工作记忆agent 的“草稿纸”工作记忆这一层我个人的理解就是 agent 的草稿纸。它记录的是当前任务链条里的关键节点已经完成了哪些步骤、每步的输入输出是什么、当前处于什么状态、下一步计划做什么。这里有个设计上的取舍值得说。工作记忆到底该存“原始交互”还是“压缩摘要”两种做法各有代价。存原始交互信息无损但占空间存摘要省空间但会丢细节。hindsight 在这块的常见实践是混合近期几步保留原始内容更早的步骤做结构化摘要。这个策略和人类工作记忆的运作方式很像——你刚说的话记得很清楚十分钟前说的就只记得大意了。具体到实现工作记忆通常用一个有界的队列或者滑动窗口来管理。窗口大小需要根据任务复杂度和模型上下文长度来定。我一般会按这个公式估算工作记忆窗口步数 (模型上下文上限 - 系统提示占用 - 预留输出空间) / 单步平均 token 数举个例子假设模型上下文是 128k系统提示占 2k预留输出 8k单步平均消耗 1.5k token那么理论上能放 (128-2-8)/1.5 ≈ 78 步。但实际工程里我不会放这么满一般留 30% 余量所以窗口设在 50 步左右比较稳妥。2.3 长期记忆从“发生过”到“学到了”长期记忆这一层才是 hindsight 真正有意思的地方。它要解决的不是“记住发生了什么”而是“从发生过的事情里提炼出可复用的东西”。这里涉及一个关键概念热词里提到的LLM ontology本体其实和这件事高度相关。所谓本体就是一套描述“世界上有哪些东西、它们之间有什么关系”的框架。放到 agent 记忆里本体决定了你把一条记忆拆成哪些字段来存。一个粗糙的做法是只存文本。一个讲究的做法是存结构化的三元组也就是热词里那句很形象的话key 是“我是谁”query 是“我在找什么”value 是“我能提供什么”。这三者构成了记忆的基本单元。当 agent 需要某条信息时它拿自己的 query 去匹配记忆的 key然后取出对应的 value。hindsight 在长期记忆里通常会维护几类不同的记忆记忆类型存什么典型用途事实记忆客观知识、用户偏好回答“用户喜欢什么”经验记忆任务执行的成功/失败路径回答“这类任务怎么做”反思记忆对错误的归因和修正回答“上次为什么失败”关联记忆实体之间的关系回答“A 和 B 有什么关系”这个分类不是死的你可以根据业务场景增减。但核心思想是一致的不同类型的记忆用不同的结构存、走不同的召回路径而不是一锅烩进向量库。2.4 和 MCP 的关系记忆层怎么对外暴露能力热词里 MCP 出现频率极高这里必须说清楚。MCP 是一种软件协议全称是 Model Context Protocol它定义的是模型和外部能力之间怎么通信。你可以把它类比成 USB 协议——USB 规定了设备怎么和电脑对话MCP 规定了模型怎么和工具、数据源对话。hindsight 作为一个记忆层天然适合通过 MCP 暴露自己的能力。这样做的好处是解耦agent 不需要知道记忆层内部是用什么数据库、什么索引实现的它只需要通过 MCP 调用几个标准接口比如“写入一条记忆”“检索相关记忆”“更新某条记忆的状态”。我实测下来把记忆层做成 MCP server 有几个明显优势。第一是可替换今天用 hindsight明天想换别的实现只要 MCP 接口不变agent 侧代码不用动。第二是可组合记忆层可以和 browser use MCP、playwright MCP 这些工具型 MCP 并存agent 统一调度。第三是可观测MCP 的调用日志天然就是记忆读写的审计记录。不过要注意一个坑MCP 本身不解决记忆的语义问题它只是传输层。记忆怎么组织、怎么召回还是得在 hindsight 内部实现。别指望接上 MCP 就自动有了智能记忆。3. 动手实操用 Docker 把 hindsight 跑起来3.1 环境准备与 Docker 安装要点hindsight 这类项目通常依赖几个基础组件一个关系型数据库存结构化记忆一个向量库做语义检索可能还有一个缓存层。用 Docker Compose 编排是最省事的方式。先说 Docker 本身的安装。Windows 用户走 Docker Desktop 这条路但这里有个高频报错必须提前说virtualization support not detected。这个错误的根源是主板 BIOS 里的虚拟化功能没开。解决办法是重启进 BIOS找到 Intel VT-x 或者 AMD-V 选项打开。Windows 11 用户还要确认“虚拟机平台”和“适用于 Linux 的 Windows 子系统”这两个功能在“启用或关闭 Windows 功能”里是勾选状态。Linux 用户直接用包管理器装就行装完记得把当前用户加进 docker 组否则每次都要 sudosudo usermod -aG docker $USER newgrp docker验证安装是否成功跑一句docker run --rm hello-world看到欢迎信息就说明基础环境没问题了。3.2 用 Docker Compose 编排记忆层依赖hindsight 的典型依赖组合我一般这么配PostgreSQL 存结构化数据Redis 做工作记忆的缓存再加一个向量检索组件。下面是一份可以直接抄的 compose 文件骨架version: 3.8 services: postgres: image: postgres:16 environment: POSTGRES_USER: hindsight POSTGRES_PASSWORD: hindsight_dev POSTGRES_DB: hindsight ports: - 5432:5432 volumes: - pg_data:/var/lib/postgresql/data healthcheck: test: [CMD-SHELL, pg_isready -U hindsight] interval: 10s timeout: 5s retries: 5 redis: image: redis:7-alpine ports: - 6379:6379 command: redis-server --appendonly yes volumes: - redis_data:/data hindsight: build: . depends_on: postgres: condition: service_healthy redis: condition: service_started environment: DATABASE_URL: postgresql://hindsight:hindsight_devpostgres:5432/hindsight REDIS_URL: redis://redis:6379/0 ports: - 8080:8080 volumes: pg_data: redis_data:这份配置里有几个细节值得展开。healthcheck那段不是可有可无的它保证 hindsight 服务在数据库真正就绪之后才启动。我踩过的坑就是没加健康检查结果 hindsight 启动时数据库还在初始化直接连接失败退出。depends_on配合condition才能实现真正的启动顺序控制光写depends_on是不够的。Redis 开了appendonly yes这是为了工作记忆的持久化。工作记忆虽然生命周期短但 agent 任务可能跨进程重启不持久化的话重启就全丢了。3.3 数据库初始化与记忆表结构PostgreSQL 起来之后需要建表。hindsight 的记忆表结构我建议至少包含这几张CREATE TABLE working_memory ( id BIGSERIAL PRIMARY KEY, session_id VARCHAR(64) NOT NULL, step_index INT NOT NULL, content TEXT NOT NULL, summary TEXT, created_at TIMESTAMPTZ DEFAULT NOW(), expires_at TIMESTAMPTZ ); CREATE INDEX idx_working_session ON working_memory(session_id, step_index); CREATE TABLE long_term_memory ( id BIGSERIAL PRIMARY KEY, memory_type VARCHAR(32) NOT NULL, mem_key TEXT NOT NULL, mem_value TEXT NOT NULL, embedding VECTOR(1536), confidence FLOAT DEFAULT 1.0, access_count INT DEFAULT 0, last_accessed TIMESTAMPTZ, created_at TIMESTAMPTZ DEFAULT NOW() ); CREATE INDEX idx_ltm_type ON long_term_memory(memory_type); CREATE INDEX idx_ltm_embedding ON long_term_memory USING ivfflat (embedding vector_cosine_ops);working_memory表里的expires_at字段是清理策略的关键。工作记忆不该无限增长我一般设成任务结束后 24 小时过期用一个定时任务清理。long_term_memory表里的access_count和last_accessed用来做记忆的“热度”排序经常被访问的记忆权重更高长期不用的可以降权甚至归档。embedding字段用了 pgvector 扩展维度 1536 对应的是常见的 embedding 模型输出维度。如果你用的模型维度不同这里要改。建索引用ivfflat注意这个索引需要在表里有数据之后再建效果才好空表建索引意义不大。3.4 记忆写入与召回的代码实现记忆写入的核心逻辑我写一个 Python 版本的关键片段import hashlib from datetime import datetime, timedelta def write_working_memory(session_id, step_index, content, ttl_hours24): summary summarize(content) if len(content) 500 else None expires datetime.now() timedelta(hoursttl_hours) db.execute( INSERT INTO working_memory (session_id, step_index, content, summary, expires_at) VALUES (%s, %s, %s, %s, %s), (session_id, step_index, content, summary, expires) ) def write_long_term_memory(memory_type, mem_key, mem_value): embedding embed(mem_value) existing db.query( SELECT id, confidence FROM long_term_memory WHERE mem_key %s, (mem_key,) ) if existing: new_conf min(1.0, existing.confidence 0.1) db.execute( UPDATE long_term_memory SET mem_value %s, embedding %s, confidence %s, last_accessed NOW() WHERE id %s, (mem_value, embedding, new_conf, existing.id) ) else: db.execute( INSERT INTO long_term_memory (memory_type, mem_key, mem_value, embedding) VALUES (%s, %s, %s, %s), (memory_type, mem_key, mem_value, embedding) )这里有个设计决策要解释为什么写入长期记忆时要先查mem_key是否存在因为记忆是会重复的。同一个事实可能被多次观察到如果每次都插一条新记录长期记忆会迅速膨胀且充满冗余。用mem_key做去重重复观察时提升confidence这样记忆的置信度会随着反复验证而增强符合直觉。召回逻辑则要区分场景。工作记忆的召回是按 session 和时序取不需要语义检索def recall_working_memory(session_id, recent_n10): return db.query( SELECT step_index, content, summary FROM working_memory WHERE session_id %s AND expires_at NOW() ORDER BY step_index DESC LIMIT %s, (session_id, recent_n) )长期记忆的召回是混合检索既要语义相似又要考虑记忆类型和热度def recall_long_term_memory(query, memory_typeNone, top_k5): query_embedding embed(query) type_filter AND memory_type %s if memory_type else params [query_embedding] if memory_type: params.append(memory_type) params.append(top_k) return db.query( fSELECT mem_key, mem_value, confidence, 1 - (embedding %s) AS similarity FROM long_term_memory WHERE 11 {type_filter} ORDER BY (1 - (embedding %s)) * confidence * (1 LN(access_count 1) * 0.1) DESC LIMIT %s, params )这个排序公式是我自己调出来的把语义相似度、置信度和访问热度三者加权。LN(access_count 1)是为了让热度的影响呈对数增长而不是线性增长避免一条被访问很多次的记忆永远霸占召回结果。4. 踩坑实录记忆层最容易出问题的几个地方4.1 记忆污染与投毒防护热词里有个词叫agentpoison说的是通过污染记忆或知识库来攻击 agent。这不是危言耸听是真实存在的风险。如果你的 agent 记忆层允许外部输入直接写入长期记忆攻击者可以通过精心构造的输入往记忆里注入错误信息之后 agent 就会基于这些错误记忆做出错误决策。防护思路有几条。第一是来源标记每条记忆记录它的来源是用户直接说的、还是 agent 推理出来的、还是从外部文档读的。不同来源的记忆在召回时给不同权重用户直接说的权重高agent 推理的权重低。第二是写入审核长期记忆的写入不应该是无条件的高置信度的记忆才允许进长期层低置信度的先放工作记忆观察。第三是定期审计对长期记忆做抽样检查发现异常模式及时清理。我自己的做法是给每条记忆加一个source字段和一个verified标记。只有被多次验证或者来源可靠的记忆verified才为真召回时优先返回。4.2 上下文窗口与记忆召回的平衡这是最考验工程判断的地方。工作记忆窗口开多大、长期记忆召回几条这两个参数直接决定了 agent 的表现和成本。窗口开太小agent 会“失忆”做着做着忘了前面干了什么。窗口开太大token 成本高不说模型对中间部分的注意力还会下降出现“中间遗忘”现象。我的经验值是工作记忆保留最近 10 到 15 步的完整内容更早的用摘要替代。长期记忆每次召回 3 到 5 条太多会稀释相关性。还有一个技巧是记忆的按需加载。不要一开始就把所有相关记忆塞进上下文而是让 agent 在执行过程中主动查询。这需要 agent 有调用记忆检索工具的能力通过 MCP 暴露一个recall_memory接口就能实现。4.3 常见问题速查表问题现象可能原因排查方向agent 重复执行已完成步骤工作记忆未正确写入或召回检查 session_id 是否一致、expires_at 是否已过期召回的记忆不相关embedding 模型不匹配或索引未建确认写入和查询用同一模型检查向量索引记忆写入报错数据库连接池耗尽或字段超长看连接数配置检查 content 字段长度限制Docker 容器反复重启健康检查未通过或依赖未就绪看容器日志确认 depends_on 条件长期记忆膨胀过快缺少去重和过期策略检查 mem_key 去重逻辑加归档任务召回结果总是同几条热度权重过高调整排序公式里的 access_count 系数4.4 记忆的过期与归档策略工作记忆过期好办按expires_at清理就行。长期记忆的过期要复杂得多因为“什么算过时”很难定义。我的做法是分三级活跃30 天内被访问过、冷存30 到 180 天未访问、归档180 天以上未访问。活跃记忆正常参与召回。冷存记忆降低召回权重但不删除。归档记忆移到单独的归档表只在明确需要历史查询时才访问。这个分级用一个定时任务每天跑一次根据last_accessed更新状态。要注意的是有些记忆是永久有效的比如用户的核心偏好、系统的固定配置。这类记忆应该打上permanent标记不参与过期流程。判断标准是这条记忆如果丢了agent 会不会犯原则性错误。会就标永久。5. 记忆层的扩展方向与个人实践体会hindsight 这套记忆层跑通之后能扩展的方向其实不少。我最近在试的一个方向是记忆的跨 agent 共享。多个 agent 如果服务同一个用户它们的长期记忆其实可以共享这样用户跟 A agent 说过的事B agent 也能知道。实现上就是长期记忆表加一个owner_id字段召回时按 owner 过滤。另一个方向是记忆的主动反思。现在的记忆写入大多是被动的任务执行完就存。更高级的做法是让 agent 定期回顾自己的记忆主动发现矛盾、合并冗余、提炼规律。这需要额外跑一个反思任务成本不低但对长期运行的 agent 来说值得。还有一个我觉得很有潜力的方向是记忆的可解释性。当 agent 做出某个决策时能追溯它是基于哪几条记忆做出的。这对调试和建立信任都很重要。实现上就是在决策日志里记录召回了哪些记忆 ID形成一条完整的证据链。我个人在实际操作中的体会是记忆层这东西先跑通再优化比什么都重要。我见过太多团队在记忆的 schema 设计上纠结几周结果一行代码没写。正确的做法是先用一个最简版本跑起来哪怕就是一张表存文本然后在真实使用中发现问题、迭代结构。记忆的价值不在于设计得多精巧而在于它真的被用起来、真的帮 agent 记住了该记的东西。最后分享一个小技巧给记忆层加一个调试面板能实时看到当前 session 的工作记忆内容、长期记忆的召回结果和排序分数。这个东西在排查“agent 为什么这么做”的时候比看日志高效十倍。我现在的习惯是每接一个新 agent第一件事就是把记忆调试面板搭起来后面省下的时间远超这点投入。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑