Hindsight retain() 深度解析:事实抽取、实体消解与知识图谱构建的完整记忆写入链路
【免费下载链接】hindsightHindsight: Agent Memory That Learns项目地址: https://gitcode.com/GitHub_Trending/hindsight2/hindsight
本文以 Hindsight 开发者文档 retain.md 为主体,系统讲解retain()如何将对话与文档转化为结构化、可检索的记忆:包括富事实抽取(保留情绪、动机与推理链)、experience/world 双事实类型、实体识别与模糊消解、四类知识图谱连接(实体/时间/语义/因果)、双时间维度建模,以及retain_mission定向抽取、观察整合(Observation Consolidation)与 Memory Defense 安全拦截。读完并对照仓库源码后,你可以理解每一次retain()调用在引擎内部经过的完整处理阶段,并能正确配置抽取模式、实体标签与召回过滤,做出可验证的调优决策。
retain() 在 Hindsight 中的定位
当你调用retain()时,Hindsight 会将你的内容转化为保留语义与上下文的、结构化且可检索的记忆。原文档给出的整体流程是一个五步流水线:
从源码结构看,这条流水线落在 retain 编排模块中:该模块(共 4000 余行)负责协调分块、事实抽取、实体处理、链接创建、预算控制等全部 retain 子模块。其目录 engine/retain/ 下的文件与文档中的概念一一对应:
| 文档概念 | 对应源码模块 |
|---|---|
| Extract Facts(事实抽取) | fact_extraction.py、fact_storage.py |
| Identify Entities(实体识别/消解) | entity_processing.py、entity_labels.py、entity_resolver.py |
| Build Connections(建立连接) | link_creation.py、link_utils.py |
| 嵌入与向量索引 | embedding_processing.py |
| 分块与文档存储 | chunk_storage.py |
富事实抽取:不只记录"说了什么"
Hindsight 的抽取不止于字面陈述——它同时捕获why(动机)、how(方式)与what it means(含义)。以原文档的示例:
retain "Alice joined Google last spring and was thrilled about the research opportunities"
Hindsight 会抽取三个层次的信息:
核心事实:
- Alice 加入了 Google
- 这件事发生在去年春天
情绪与含义:
- 她非常兴奋(thrilled)
- 这代表一个重要机会
推理动机:
- 她是为了研究机会而选择的
这种富抽取的价值在于:你之后可以问 "Why did Alice join Google?",得到的不只是 "she joined Google",而是有意义的因果回答。
源码印证:五维抽取 Schema 与因果约束
在 fact_extraction.py 中,抽取结果由 Pydantic 结构化 Schema 约束,每条事实(ExtractedFact)要求模型输出以下维度:
what— 完整、详细的描述("COMPLETE, DETAILED description with ALL specifics");when/where/who— 时间与地点上下文;fact_type— 事实类型(见下文 experience/world 一节);entities— 命名实体、对象以及抽象概念的纯字符串数组;causal_relations— 因果链接,每条事实最多 2 条,且target_index必须小于当前事实的索引(即只能指向"更早的事实")。
# fact_extraction.py 中因果关系的 Schema 定义(节选) causal_relations: list[FactCausalRelation] | None = Field( default=None, description="Causal links to PREVIOUS facts only. target_index MUST be less than this fact's position. " "Example: fact #3 can only reference facts 0, 1, or 2. Max 2 relations per fact.", )同时,抽取系统提示词明确定位为 "Extract SIGNIFICANT facts ... Be SELECTIVE - only extract facts worth remembering long-term"——即抽取是有选择的,只保留值得长期记忆的事实。批量(Batch API)路径同样适用该 Schema,保证直连调用与批量提交两种模式输出一致。
保留完整叙事上下文
传统系统会把信息打碎成碎片:
- "Bob 提出了 Summer Vibes"
- "Alice 想要一些独特的"
- "他们选了 Beach Beats"
而 Hindsight 保留完整叙事:
"Alice and Bob discussed naming their summer party playlist. Bob suggested 'Summer Vibes' because it's catchy, but Alice wanted something unique. They ultimately decided on 'Beach Beats' for its playful tone."
这意味着搜索结果携带完整上下文,而非互不相关的碎片。从源码结构看,这一点由分块(chunking)层支撑:retain 流水线在保留文档原文(documents.original_text)的基础上按语义边界切块,且对 JSON 数组形式的对话有专门保护逻辑——orchestrator.py 中的merge_json_array_parts会把分片重新合并为单个合法 JSON 数组再交给分块器,避免追加写入时 JSON 被换行拼接破坏而丧失说话人归属。
两种事实类型:experience 与 world
每条事实都会按它从谁的角度被记录来分类——是拥有该 bank 的 agent 自己,还是外部世界:
| 类型 | 记录内容 | 示例 |
|---|---|---|
| experience | bank 自己的 agent 在行动、观察或互动——它的第一人称历史 | "I recommended Python to Alice" |
| world | 关于其他人、地点、事物和事件的事实 | "Alice works at Google" |
关键规则是:划分依据是说话者身份,而非语法。第一人称陈述只有当说话者就是该 bank 的 agent 时才是experience;同样的话出自他人之口,则是关于那个人的world事实:
- Agent 自己的日志——"I patched the auth bug" →experience(agent 做的);
- 用户对 agent 说——"I bought a Tesla" →world(关于用户的事实,而非 agent 的)。
实操建议:在每个条目的context字段中描述说话者身份,以正确引导分类。保留转录或第三方内容时,使用如"Customer Maria is speaking"的 context,可确保她的第一人称陈述被存为关于 Maria 的world事实,而不是误判为 agent 自身的 experience;对 agent 自己的日志,则用"The assistant is speaking"将其第一人称陈述归属为 agent 的experience。
源码印证:types.py 中ProcessedFact.fact_type的注释即为"world", "experience", "observation",其中observation是整合阶段派生的高层知识(见"观察整合"一节)。抽取提示词中还有对应的分类规则:用户偏好、规则、修正、约束、特质等客观事实一律标world,只有 agent 实际执行的动作或经历才标 agent 侧类型。
注意:观察(Observations)会在
retain()操作完成后在后台自动整合。该整合过程把新事实中的模式合成到 bank 的知识库中。
实体识别与消解
Hindsight 会自动识别并跟踪实体——重要的人物、组织与概念。
识别范围
- 人物:"Alice"、"Dr. Smith"、"Bob Chen"
- 组织:"Google"、"MIT"、"OpenAI"
- 地点:"Paris"、"Central Park"、"California"
- 产品与概念:"Python"、"TensorFlow"、"machine learning"
从源码 Schema 看,实体抽取的定义比文档更广:entities字段要求包含"命名实体、对象以及抽象概念"(如 "friendship"、"career growth"),并明确要求"抽取任何有助于把相关事实链接起来的内容"。
实体消解(Entity Resolution)
同一实体被不同方式提及时,通过模糊名称匹配统一,并由共现与时间邻近性强化:
- "Alice" + "Alice Chen" + "Alice C." → 同一个人
由于消解基于名称相似度,相近变体会自动合并。名称不相似的(例如昵称与一个无关的正式名)不会仅凭名称统一,尽管共享的共现实体仍可将它们关联起来。
为什么重要:你可以问 "What do I know about Alice?",即使她某些对话中被称为 "Alice Chen",也能检索到全部内容。
消解是判断性决策,也可能反向出错:在历史很多的 bank 中,一个新出现的短名称如果与既有实体相似——且与既有实体已关联的实体共同出现——可能被吸收进既有实体,而不是成为独立实体。如果你发现关于新人的事实被挂到了无关实体上,实体消解决策机制(configuration 文档中 "How entity resolution decides" 小节)解释了比较的是什么、以及哪个设置可以让匹配更严格。
源码级实现细节:仓库使用 PostgreSQL 的pg_trgm三元组相似度做候选预筛,相关阈值在 config.py 中有明确定义与注释:
HINDSIGHT_API_ENTITY_TRGM_SIMILARITY_THRESHOLD(默认0.15):pg_trgm相似度下限,控制名称需要多接近才会被视为候选(更低=召回更多近似匹配但 CPU 成本更高,更高=更严格更省资源);HINDSIGHT_API_ENTITY_INTRABATCH_MERGE_SIMILARITY(默认0.5):同一批次内两个全新名称合并为同一实体的阈值——pg_trgm 忽略非字母数字字符,因此装饰性变体得分约 1.0、大小写/后缀变体约 0.75,而真正不同的名称约 0.30,0.5 恰好落在这个空档;- 另有合并最小相似度门槛(
ENTITY_MERGE_MIN_SIMILARITY等),防止新名称仅因共现/新鲜度分数就被并入无关既有实体(源码注释指出这是针对"把新人事实挂到错误实体"问题的门禁)。
上下文感知消歧
如果 "Alice" 多次与 "Google" 和 "Stanford" 一起出现,那么一个新提到的 "Alice" 若也提及这些实体,很可能就是同一个人。Hindsight 利用共现模式对常见名称做消歧。
实体标签(Entity Labels)
你可以定义一套受控的key:value分类标签词表(例如pedagogy:scaffolding、engagement:active),它们在 retain 时被抽取并作为实体存储。因为标签会成为实体,它们会自动在知识图谱中链接相关记忆,并同时改善语义检索与关键词检索。标签还可以可选地写入记忆单元的 tags,从而在 recall 与 reflect 中支持标准基于标签的过滤。
与普通实体不同,标签实体从不按名称相似度合并——不同标签值必须保持不同,因此它们只做精确匹配,完全排除在模糊名称匹配之外。
完整配置参见 bank 配置中的 entity_labels 小节。源码中 entity_labels.py 负责解析标签配置、构建标签抽取 Schema(含map、multi-values、multi-text等嵌套字段类型),并在 fact_extraction.py 的_extract_map_entities中递归把 LLM 返回的 map 实体展平为key:field:value字符串。
构建连接:知识图谱的四类边
记忆不是孤立的——Hindsight 创建一个包含四类连接的知识图谱。link_creation.py 的模块文档明确写道:"Handles creation of temporal, semantic, and causal links between facts",与文档描述一一对应:
实体连接(Entity Connections)
所有提及同一实体的事实互相链接。
支撑能力:"Tell me everything about Alice" → 检索所有与 Alice 相关的事实。
基于时间的连接(Time-Based Connections)
时间上接近的事实被连接,日期越近链接越强。
支撑能力:"What else happened around then?" → 找到上下文相关的事件。对应实现为create_temporal_links_batch(按时间邻近批量建链)。
基于语义的连接(Meaning-Based Connections)
语义相似的事实被链接,即使措辞不同。
支撑能力:"Tell me about similar topics" → 找到主题相关信息。对应实现为create_semantic_links_batch,基于嵌入向量的余弦相似度阈值建链,并支持用预计算的 ANN(近似最近邻)结果替代事务内查询以提升性能。
因果连接(Causal Connections)
因果关系被显式追踪。
支撑能力:"Why did this happen?" → 追踪推理链。示例:"Alice felt burned out" ← caused by ← "She worked 80-hour weeks"
源码实现上,因果边由 link_creation.py 的create_causal_links_batch写入,注释说明 retain 只写入规范的caused_by关系类型(数据库与检索路径同时识别历史因果类型,保证导入的旧记忆仍可遍历)。因果链接是否抽取由配置开关retain_extract_causal_links(环境变量HINDSIGHT_API_RETAIN_EXTRACT_CAUSAL_LINKS)控制;启用后,抽取提示词会追加 "CAUSAL RELATIONSHIPS" 小节,教会模型以caused_by类型输出链接("Lost job → couldn't pay rent → moved apartment")。
时间建模:两个时间维度
Hindsight 追踪两个时间维度:
事件发生时间(When It Happened)
对事件(会议、旅行、里程碑),Hindsight 记录其发生时间:
- "Alice got married in June 2024" → occurred in June 2024
对一般性事实(偏好、特征),没有具体发生时间:
- "Alice prefers Python" → 持续性偏好
何时得知(When You Learned It)
Hindsight 同时追踪你告知它每条事实的时间。
为什么两者都要?设想 2025 年 1 月有人告诉你 "Alice got married in June 2024":
- 历史查询可用:"What did Alice do in 2024?" → 找到这场婚礼;
- 新鲜度排序可用:近期提及在搜索中获得优先;
- 时间推理可用:"What happened before her marriage?" → 找到更早的事件。
若没有这种区分,旧信息要么无法按日期检索,要么被当作无关。
源码印证:fact_extraction.py 在每条事实落库时设置mentioned_at = event_date(即对话/文档发生的时间),而事件的发生时间来自 LLM 抽取的occurred_start/occurred_end。此外还有一层兜底:_infer_temporal_date在 LLM 未提供occurred_start时,用正则识别 "last night"、"yesterday"、"last week" 等相对时间表达并换算为绝对日期,避免时间信息静默丢失。
记忆打标签(Tagging Memories)
标签实现可见性范围控制(visibility scoping)——当一个 memory bank 服务多个用户、但每个用户只应看到相关记忆时非常有用:
- 条目标签(Item tags):用特定 scope 标记单条记忆;
- 文档标签(Document tags):将标签应用到批次内的所有条目;
- 标签过滤(Tag filtering):在 recall/reflect 时按标签过滤。
代码示例参见 Retain API,过滤选项参见 Recall API。源码中,RetainBatchRequest数据类(types.py)同时携带contents(各条目可带独立 tags)与document_tags(应用于全部条目的文档级标签),对应上述两种粒度。
retain() 完成后的产出
一次retain()完成后,你得到:
- 结构化事实——保留含义、情绪与推理;
- 统一实体——消解了不同名称变体;
- 知识图谱——含实体、时间、语义与因果链接;
- 时间锚定——同时支持历史查询与新鲜度查询;
- 可选标签——用于 recall 时的过滤。
全部存储在你的隔离memory bank中,随时可供recall()与reflect()使用。
用 Mission 引导抽取
默认情况下,retain()会抽取内容中所有显著事实。你可以用retain mission(retain_mission)收窄这个焦点——一段自然语言,描述这个 bank 应关注什么:
e.g. Always include technical decisions, API design choices, and architectural trade-offs. Ignore meeting logistics, greetings, and social exchanges.Mission 与内置规则一起注入抽取提示词——它引导 LLM 而不替换抽取逻辑,并且对任意抽取模式(concise、verbose、custom)都有效。
源码实现:Mission 为什么放在用户消息里
这是一个值得注意的工程决策。fact_extraction.py 中_retain_mission_preamble的注释解释:每个 bank 的 mission 刻意不烘焙进系统提示词,否则系统提示词将变成 bank 专属,迫使 Gemini 上下文缓存为每个 mission 单独建缓存。因此系统提示词保持 bank 无关(一份CachedContent服务所有 bank),mission 通过_retain_mission_preamble()以 "FOCUS — What to retain for this bank (takes priority over the general guidelines)" 前导块的形式,放在每次请求的用户消息中。
抽取模式(Extraction Mode)
需要更细粒度控制时,可更换抽取模式:
| 模式 | 适用场景 |
|---|---|
concise(默认) | 通用——有选择、快速 |
verbose | 需要更丰富的、带完整上下文与关系的事实 |
custom | 想完全自己编写抽取规则 |
通过 bank 配置 API 的 retain 配置小节 或环境变量HINDSIGHT_API_RETAIN_MISSION设置retain_mission与retain_extraction_mode。源码中对应配置项见 config.py:ENV_RETAIN_MISSION = "HINDSIGHT_API_RETAIN_MISSION"、ENV_RETAIN_EXTRACTION_MODE、ENV_RETAIN_CUSTOM_INSTRUCTIONS(custom 模式使用的自定义指令)与ENV_RETAIN_EXTRACT_CAUSAL_LINKS。另外注意一个隐藏模式:当llm_provider设为none时,配置层会强制retain_extraction_mode = "chunks"并禁用 observations/consolidation(仅做分块存储,reflect 将返回 HTTP 400)。
当 Mission 排除了文档中的全部内容
Mission 收窄的是"什么会成为记忆"——不产出任何事实的内容就完全不产出记忆。文档本身仍然被存储,但recall和reflect检索的是记忆,因此零记忆的文档两者都找不到。收紧 mission 因此牺牲的不只是事实创建,还有原始来源的可检索性。
这是正常结果而非错误:retain 会成功,操作会被报告为已完成。两个信号可以识别这种情况:
| 位置 | 观察什么 |
|---|---|
retain.completedwebhook | data.memory_unit_count: 0 |
| Metrics | hindsight.retain.documents.total{outcome="no_facts"} |
也可以事后审计:GET /documents会返回每个文档的memory_unit_count,过滤出0即可列出当前不可达的所有文档。
抽取并非完全确定——边界文档可能这次跑出事实、下次跑不出。请把零事实当作"这份文档需要再处理一遍",而不是永久定论。
要恢复一份文档,放宽 mission 后重新处理即可——已存储的文本会被重新抽取,无需重新上传:
POST /v1/default/banks/{bank_id}/documents/{document_id}/reprocess观察整合(Observation Consolidation)
retain()完成后,Hindsight 自动在后台触发观察整合。该过程:
- 将新事实与既有观察比对分析;
- 当模式浮现时创建新观察;
- 用新证据精化既有观察;
- 追踪哪些事实支撑每条观察。
这是异步发生的——retain()调用立即返回,整合在后台运行。实现位于 engine/consolidation/ 目录,详细机制参见 Observations 文档。
Memory Defense 与来源溯源
receipt_uri(可选)
类型:string。
指向外部回执或共签(co-signature)系统的可选指针。按原样存储,并在针对该条目的任何 Memory Defense 决策中,通过security_events.receipt_uri暴露。
422 —— Memory Defense 违规
当目标 bank 启用了 Memory Defense 且批次中所有条目都被策略拦截时,请求返回 422 并附违规列表:
{ "detail": { "violations": [ { "index": 0, "detector": "prompt_injection", "severity": "high", "message": "..." } ] } }部分拦截的批次返回 200,未被拦截的条目正常处理;被拦截的条目从结果中静默丢弃,其决策记录在security_events中。完整指南参见 Memory Defense。
源码印证:orchestrator.py 定义了BlockedViolation(字段恰为index、detector、message)与MemoryDefenseAllBlockedError("all N items blocked by Memory Defense policy"),即 422 响应体的直接来源;同文件的redact_document_body还处理了超大文档被切分时的一个安全细节——完整原文不经过逐条 screening,因此切分前先用策略对全文做一次脱敏,防止原文绕过审查直接落入documents.original_text。注释中明确强调"这是一项安全控制,fail-open 是错误的默认值"。
延伸阅读
- Observations — retain 之后知识如何被整合;
- Retrieval(Recall) — 多策略搜索如何检索相关记忆;
- Reflect — agent 循环如何使用观察;
- Retain API — 完整参数与代码示例。
【免费下载链接】hindsightHindsight: Agent Memory That Learns项目地址: https://gitcode.com/GitHub_Trending/hindsight2/hindsight
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考