Hindsight 记忆系统设计指南:打造只记住"该记的事"的 AI Agent
【免费下载链接】hindsightHindsight: Agent Memory That Learns项目地址: https://gitcode.com/GitHub_Trending/hindsight2/hindsight
本文基于 Hindsight 仓库中的设计指南 Designing AI Agents That Remember What Matters,系统讲解"选择性记忆"的设计原则:什么样的信息值得持久化、什么应保持局部、什么绝不该落盘,以及如何在 Hindsight 的 retain API、recall API 和 Memory Bank 上把这套原则落成可运行的保留策略、检索策略与质量评审流程。读完本文,你将掌握一套可直接复用的"记忆层设计 + 评估"方法论。
一、快速答案:记忆设计是选择性的,不是穷举式的
原文档给出的三条核心结论,也是整篇设计纪律的骨架:
- 好的记忆设计是选择性的(selective),而非穷举的(exhaustive)——目标不是让 Agent 记住一切,而是让它记住"那些会改变未来结果的事情";
- 系统应该保留能改善未来工作的事实、偏好和决策,其余的留在任务局部上下文中;
- 有用的记忆层要同时平衡三件事:召回质量(recall quality)、作用域(scope)、操作控制(operational control)。
这三点在 Hindsight 中有直接对应的实现机制:retain 侧的"提取模式 + mission"决定记什么,Memory Bank 与 tags 决定记在谁的范围里,recall 侧的 budget / max_tokens 决定取回多少。设计纪律不是口号,而是落到 API 参数上的。
二、为什么生产环境需要持久记忆
很多团队在还没有词汇描述它之前就已经遇到了这个问题:Agent 在单次会话里看起来能力很强,换到下一次会话却异常脆弱。这通常意味着系统依赖的是"提示词状态"(prompt state)而非"持久记忆"(durable memory)。
这正是从 demo 走向生产工作流时,"临时上下文"与"持久记忆"的区分至关重要的原因。一个实用的记忆层让 Agent 能够复用之前的工作成果,而不用把整个历史塞进每个提示词。原文档中提到的 Hindsight retain / recall 两个 API 就是这一模式的两端:
- 想存储持久信号时,用 retain API;
- 想稍后恢复正确的上下文时,用 recall API。
同一个模式在仓库的手把手示例中反复出现,例如 Claude Code 集成、Codex 集成 等,它们展示的是同一套"retain 存、recall 取"的工作流。
三、常见的三种记忆设计失败
原文档归纳了三类典型失败,它们单独看都不大,但会叠加放大:
- 什么都存,导致召回变吵(noisy recall):噪声记忆混进提示词,模型被无关上下文带偏;
- 保留得太少,连续性永远得不到改善:Agent 反复"重新入职"(repeated onboarding);
- 没人能解释记忆策略到底是什么:记忆层成为黑盒,出了问题无法定位是"没存进去"还是"没取回来"。
这些失败会层层堆叠:一点点的遗忘变成反复的重新入职,反复的重新入职变成返工(rework),返工最终变成信任度下降——用户不再相信 Agent 能把关键上下文带下去。
可验证的对应关系:Hindsight 把"策略可解释"做进了产品形态——保留策略是 bank 配置(retain_mission、retain_extraction_mode等显式字段),召回行为可通过trace: true查看各检索臂的过程,这正是针对第 3 类失败的直接解法。
四、更好的记忆层:四条设计要点
更好的设计是选择性的:不试图永远保留每一个 token,而是聚焦"能改善未来工作的信号",并在需要时可被找回。好的系统通常包含这四件事:
- 在扩大存储之前先定义保留规则(retention rules);
- 区分个人、项目、团队三类记忆(personal / project / team scope);
- 构建与 Agent 提问方式匹配的检索(retrieval that matches the questions the agent asks);
- 用真实工作流例子评审记忆质量。
所以架构比标签重要。一个产品可以宣称有"记忆",行为却像"挂了一个搜索引擎的长提示词"。真正有用的系统必须做到:存得好(retain well)、取得好(retrieve well)、并把结果干净地装回当前上下文。
下面逐条对应到 Hindsight 的实现。
五、保留规则:retain 侧的参数化记忆纪律
5.1 retain 请求的关键字段
Hindsight 的 retain API 一次调用接受一个或多个items,每个 item 是一段原始内容(对话、文档、笔记)。关键在于:原始文本不会被原样存储,Hindsight 会切块、送 LLM 提取结构化事实,存的是"事实"而不是"文本"——这正是"选择性"的第一层实现。
最小调用(取自 examples/api/retain.py 中docs:retain-basic片段):
client.retain( bank_id="my-bank", content="Alice works at Google as a software engineer" )对话场景则作为单个 item保留,消息格式化为姓名 (时间戳): 内容,让 LLM 能把事实归因到正确的说话人、并解析跨消息的时间引用:
# 取自 examples/api/retain.py 的 docs:retain-conversation 片段 conversation = "\n".join([ "Alice (2024-03-15T09:00:00Z): Hi Bob! Did you end up going to the doctor last week?", "Bob (2024-03-15T09:01:00Z): Yes, finally. Turns out I have a mild peanut allergy.", "Alice (2024-03-15T09:02:00Z): Oh no! Are you okay?", "Bob (2024-03-15T09:03:00Z): Yeah, nothing serious. Just need to carry an antihistamine.", "Alice (2024-03-15T09:04:00Z): Good to know. We'll avoid peanuts at the team lunch.", ]) client.retain( bank_id="my-bank", content=conversation, context="team chat", timestamp="2024-03-15T09:04:00Z", document_id="chat-2024-03-15-alice-bob", )逐字段拆解(均对应 RetainRequest 源码模型):
| 字段 | 作用与取值 |
|---|---|
content | 唯一必填字段。Hindsight 将其切块并送 LLM 提取事实,存储的是结构化事实而非原文。 |
timestamp | 三种形态:省略/null→ 取入库时间;ISO 8601 字符串 → 使用给定时间;"unset"→不带任何时间戳,适合参考文档、书籍等"无真实事件时间"的内容。时间戳会被注入事实提取提示词,作为解析"上周一"这类相对时间表达的锚点;也决定了时间召回查询(如"去年春天发生了什么")能否正确工作。 |
context | 简短的来源/场景标签,如"team meeting"、"slack"、"support ticket"。它会直接注入 LLM 提示词,主动塑造事实提取的方向。同一句话在不同 context 下会产生不同的记忆。文档明确说:一致地提供 context 是提高记忆质量杠杆率最高的手段之一。 |
metadata | 任意键值对(字符串存储),如{"source": "slack", "channel": "engineering"}。既参与提取提示词,也会挂在每个记忆单元上、随 recall 结果原样返回,可做客户端过滤。值为null的键会被丢弃。 |
document_id | 让 retain幂等的钥匙:若同 ID 文档已存在,旧文档及其关联记忆会先被删除再重新处理。会话增长后只需带着完整新内容和相同document_id再次 retain,即可安全覆盖而不会积累重复记忆。 |
operation_id | 客户端提供的 UUID,用作异步 retain 操作的身份标识:相同 ID 重提只返回原操作、不产生新工作,避免超时重试造成重复入库;复用他人 ID 返回 409。 |
5.2 提取模式:从"默认精选"到"零 LLM 成本"
原文档的第一条设计要点——"先定义保留规则再扩大存储"——在 Hindsight 中对应 bank 级的 retain 配置(环境变量与层级覆盖详见 configuration):
retain_mission:一段自然语言,描述"这个 bank 在提取时应该关注什么"。它与内置提取规则一起注入提示词,引导聚焦但不替代提取逻辑。文档给出的示例风格是:e.g. Always include technical decisions, API design choices, and architectural trade-offs. Ignore meeting logistics, greetings, and social exchanges.这就是"什么算值得记、什么该忽略"的策略落点。
retain_extraction_mode:控制事实提取的激进程度,五种模式各对应一种"存储 vs 成本 vs 选择度"的取舍:
| 模式 | 说明 |
|---|---|
concise(默认) | 选择性提取——只保留值得长期记忆的事实 |
verbose | 每条事实捕获更多细节;更慢、消耗更多 token |
custom | 通过retain_custom_instructions完全替换内置规则,适合需要严格"包含/排除"逻辑的场景 |
verbatim | 每个 chunk 原文存为一条记忆;LLM 只提取实体、日期等元数据 |
chunks | 每个 chunk 存为一条记忆且完全不调用 LLM,零 LLM 成本;只有调用方提供的实体可用 |
对应环境变量为HINDSIGHT_API_RETAIN_EXTRACTION_MODE,取值concise/verbose/verbatim/chunks/custom(默认concise),custom模式需搭配HINDSIGHT_API_RETAIN_CUSTOM_INSTRUCTIONS。从 configuration 文档 可见,这些配置还可以按 retain strategy 分层覆盖——不同来源的内容用不同的保留策略,是"个人/项目/团队"之外的第四维细分。
retain_chunk_size(默认3000)与retain_structured_chunk_size(默认继承前者):控制切块粒度。chunk 越大 LLM 调用越少但可能降低长文本提取质量;对 JSONL 日志、聊天记录这类"单条记录不应被切开"的结构化内容,可把retain_structured_chunk_size调大以保持整条记录完整。entity_labels:定义 retain 时提取的受控标签词表(key:value形式的实体)。标签成为实体后会自动在知识图谱里把同类记忆连起来,同时改善语义/BM25 检索;对设置了tag: true的组,提取出的标签还会写为记忆单元上的 tag,可在 recall 里用tags/tags_match过滤。
从源码结构看,这些 bank 配置字段与请求模型都在 hindsight-api-slim/hindsight_api/api/http.py 中定义,配置解析与校验集中在 API 层,保证了"策略必须显式、可审计"这一设计前提。
5.3 "绝不该持久化"的边界:Memory Defense
原文档说记忆设计需要一条规则:"什么绝不该持久化"。Hindsight 的 Memory Defense 模块就是这条边界的落地:它用一套 45 条正则模式从 retain 内容中清除密钥与 PII,每个命中项被替换为[REDACTED:type]标记后才进入记忆单元与文档正文,且按 bank 配置、默认关闭。
最小策略(配置在 bank 的memory_defense字段,创建时设置或之后通过PATCH /v1/{tenant}/banks/{bank_id}/config更新):
{ "memory_defense": { "enabled": true, "rules": [ { "on": "sensitive_data", "action": "redact" } ] } }开源版提供两种动作:
redact:把命中的密钥替换为[REDACTED:type]标记,存储清洗后的记忆——之后所有 recall、导出、reflect 都看不到原始秘密;block:整条丢弃任何包含命中的 item;若一次 retain 的所有 item 都被 block,调用返回422。
覆盖类别包括 AI/LLM 提供商密钥(anthropic_key、openai_key、google_api_key等)、云厂商凭据(aws_access_key等)、源码管理与 CI token(github_token、gitlab_pat、npm_token等)、支付处理方密钥(stripe_secret等)。
两个必须知道的边界:策略只影响设置后新的 retain 调用,不会回溯扫描已有记忆;若某 bank 订阅了对应 webhook,被 redact / block 的 item 会触发memory_defense.triggered事件(payload 含动作、document ID、命中的模式标签),可接 SIEM 或 Slack 告警。
六、作用域:个人 / 项目 / 团队记忆的隔离与共享
第二条设计要点——区分 personal、project、team 记忆——在 Hindsight 中由两层机制实现:
6.1 Memory Bank:硬隔离
Memory Bank 是完整隔离的存储单元,内含记忆、文档、实体、实体间关系(知识图谱)和 Directives(reflect 时 Agent 必须遵循的硬规则)。不同 bank 之间完全不可见——一个 bank 里存的记忆,另一个 bank 永远看不到。
两个实操细节值得注意:
- bank 无需预创建,首次写入时按默认配置自动创建;
- 读取不存在的 bank 返回
404(而非空结果)——bank_id拼错、改名或删除会被明确报错,而不是伪装成"这个 bank 没有记忆"。对监控系统尤其重要:对缺失 bank 的GET .../stats会响亮地失败,而不是返回一组看起来健康实为空银行的零值计数器。
典型映射:个人记忆 → 每个用户一个 bank;项目记忆 → 每个项目一个 bank;团队共享 → 团队级 bank。
6.2 Tags 与 tags_match:bank 内部的软作用域
同一 bank 内部还需要更细的作用域,这由 item 级tags和 recall 侧的tags_match组合完成。retain 示例 中docs:retain-with-tags片段展示了给每条记忆打作用域标签:
client.retain_batch( bank_id="my-bank", items=[ { "content": "User Alice said she loves the new dashboard", "tags": ["user:alice", "feedback"], "document_id": "user_feedback_001" }, { "content": "User Bob reported a bug in the search feature", "tags": ["user:bob", "bug-report"], "document_id": "user_feedback_002" } ] )RecallRequest 源码 中tags_match提供六种语义,这是"个人/项目/团队"三档在单 bank 内共享时的精确工具:
| 取值 | 语义 |
|---|---|
any(默认) | OR 匹配,包含无标签记忆 |
all | AND 匹配,包含无标签记忆 |
any_strict | OR 匹配,排除无标签记忆 |
all_strict | AND 匹配,排除无标签记忆 |
exact | 完整标签集合相等;不带 tags(或空列表)+exact选中"空全局作用域",只匹配无标签记忆(即 observation_scopes=shared 写入的共享记忆) |
tag_groups | 布尔复合过滤(AND 连接组,组内支持 and/or/not 叶子),还支持resolve='fuzzy'按三元组相似度模糊匹配,'typsecript'也能命中'typescript' |
注意tags与tag_groups互斥(模型校验器 直接拒绝同时提供),复合过滤场景只能用后者。
多 Agent 系统需要"共享但有作用域的上下文"——这正是原文档列举的典型工作流之一。落地形态是:一个团队 bank + 按用户/项目的 tag 作用域 + 需要全局共享的结论(如整合后的 observation)写无标签的全局作用域,再用exact精确取回。
七、与提问匹配的检索:recall 侧的参数化设计
第三条设计要点——"检索要匹配 Agent 会问的问题"——由 recall API 承担。recall 会并行运行四条检索臂——语义相似度、关键词(BM25)、图谱遍历、时间——再融合排序成单一排名列表,返回的是结构化事实而非原始文档。基本调用(取自 examples/api/recall.py):
response = client.recall(bank_id="my-bank", query="What does Alice do?") # response.results 中每条 RecallResult 含: # text(提取出的事实)、type(world/experience/observation)、 # context / metadata / tags / entities、 # occurred_start / occurred_end / mentioned_at、 # document_id / chunk_id关键参数与源码 RecallRequest 的对应关系:
| 参数 | 作用与默认值 |
|---|---|
query | 唯一必填。自然语言问题会同时驱动四条检索臂:嵌入后做语义检索、分词后做 BM25、作为图谱遍历的起点、解析时间表达。超过 500 token 的 query 会被拒绝。 |
types | 控制搜索哪类事实:world(客观事实)、experience(事件与对话)、observation(由多条记忆整合去重、有证据支撑的信念)。省略则三类全搜;每类独立跑完整四臂管线,缩小types同时降低结果集与查询成本。 |
prefer_observations | 默认false。开启后:若某 observation 由原始事实整合而来,则丢弃该原始事实、observation 取代它,腾出的名额由次优结果回填——"要全部"与"不重复"兼得。仅当同时请求observation与至少一个原始类型时生效。 |
budget | low/mid(默认)/high,控制检索深度与广度。快速简单查找用low,日常用mid,需要间接关联或穷尽覆盖用high。 |
max_tokens | 返回事实的合计 token 预算,默认4096,只对每条事实的text字段计数。重排后按相关度顺序装填直至预算耗尽;放不下剩余预算的事实会被跳过而不终止选择。设计哲学是"Hindsight 为 Agent 而生,Agent 以 token 思考而非结果条数"——max_tokens就是"你想把上下文窗口的多少分给记忆"。 |
tags/tags_match/tag_groups | 见上节 6.2,作用域过滤。 |
temporal_window | 显式指定时间臂的窗口(替代从 query 文本解析日期),适合日期选择器或 Agent 已自行解析"上季度"的场景。注意它是排序而非过滤:窗口外的记忆仍会返回。 |
min_scores | 分阶段分数下限(semantic/keyword/reranker/final)。reranker与final是重排后的硬过滤,适合做"拒答"判断;文档同时警告重排器绝对分数跨查询未校准,慎用。 |
trace | true时返回各检索臂的中间过程,用于评审"为什么取回/没取回这条"。 |
两个值得记住的边界行为(均来自 recall 文档 的说明):
- 有命中就不会返回空:即使最相关事实也超出预算,它会完整返回并超预算,而不是截断到句子中间——因为"空列表"会被读成"这个 bank 没有这条记忆",而"截断的事实"是记忆从未做出过的陈述。唯一的例外是
max_tokens=0,它表示"故意不要事实",是只取 chunks 的方式; - observations 的定位:observation 是后台在 retain 之后自动创建和维护的、带证据引用的整合信念,新证据到来时是"精化"而非"覆盖"。它是记忆系统"连续性随时间改善"的机制基础。
max_tokens与原文档评估框架第 5 条——"召回的上下文是否足够简洁,能帮助而非干扰"——是一一对应的:召回侧的简洁性由预算参数保证,而不是靠事后裁剪提示词。
八、原文档列举的三类高价值工作流
原文档指出影响最明显的工作流场景,对照 Hindsight 的能力可以给出更具体的落地形态:
- 产品团队从零设计记忆架构:先定 bank 划分(personal/project/team),再写
retain_mission与提取模式,最后调 recall 预算; - 工程团队从"仅提示词"系统迁移到持久工作流:把每次会话的关键输出 retain 到项目 bank,下一次会话开始时 recall 注入,替换手工维护的"上下文接力";
- 需要共享但有限作用域上下文的多 Agent 系统:单 bank + tag 作用域 + 全局共享 observation 的组合(见第六节)。
九、五步评估框架:在自己的技术栈上验证
原文档给出的评估框架是全文最可操作的部分,逐条对应到 Hindsight 的验证手段:
- 找出"一件 Agent 应该明天还记得、因为今天学到了"的东西——确定最小验证信号;
- 决定这个信号属于个人、项目还是共享记忆——映射到 bank 选择或 tag 作用域;
- 验证系统能有意地(intentionally)保留它——retain 后检查返回的提取结果,或调
trace确认事实确实落库、类型与时间正确; - 测试它在正确的后续工作流中能被取回——用目标工作流里真实会问的 query 做 recall,检查排名与覆盖;
- 检查召回的上下文是否足够简洁,能帮助而非干扰——调
budget/max_tokens直到注入提示词的体积可控,且不丢关键事实。
好的记忆系统之所以更容易被信任,正在于存储与召回模型清晰到可以被检查:策略是显式配置,行为可用trace复盘,作用域可用tags_match精确复现。
十、FAQ
Agent 应该记住多少?只记住"让未来工作变得更好或更正确"的信息。落到参数上就是retain_mission里写清包含/排除规则,配合concise这类选择性提取模式。
记忆设计应该从存储开始,还是从检索开始?从工作流开始,从"Agent 之后要回答的问题"开始。因为检索问题定义了保留什么才有意义——Hindsight 的文档组织也是如此:retain 架构 与 recall 架构 都以"信号 → 问题"为主线。
策略可以随时间演化吗?可以。强记忆系统会随着团队弄清"什么值得保留"而持续改进。Hindsight 的策略载体(bank 配置、retain strategy 分层覆盖、memory defense 规则)都是可热更新的配置,不需要重建存储。
结语:记忆层 = 保留策略 × 作用域 × 检索预算 × 可评审性
回看原文档的骨架,"记住重要的事"不是一个功能,而是一组可审计的纪律:先用 retain 侧的 mission、提取模式、memory defense 决定"记什么、不记什么";再用 bank 与 tag 作用域决定"记给谁";然后用 recall 的 budget、max_tokens、tags_match 决定"取回多少、取回哪份";最后用 trace 与真实工作流评审"记取得对不对"。这套纪律在 Hindsight 仓库中全部有显式的配置字段与 API 参数承载——策略写在配置里而不是散落在提示词里,正是它区别于"挂了搜索引擎的长提示词"的关键。
进一步阅读(均为仓库内路径):
- Retain API 参考 / Recall API 参考
- Retain 架构详解 / Recall 架构详解
- Memory Banks 参考 / 配置与层级覆盖
- Memory Defense / Quick Start
- 请求模型源码:RecallRequest、RetainRequest
- 可运行示例:examples/api/retain.py、examples/api/recall.py
【免费下载链接】hindsightHindsight: Agent Memory That Learns项目地址: https://gitcode.com/GitHub_Trending/hindsight2/hindsight
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考