首页
/ Hindsight Retain 深度解析:如何把对话与文档转化为可检索的结构化记忆

Hindsight Retain 深度解析:如何把对话与文档转化为可检索的结构化记忆

2026-09-15 00:00:25作者:郦嵘贵Just

导读

retain() 是 Hindsight 记忆系统的写入入口:调用它之后,对话记录与文档会被转化为结构化、可搜索、保留语义与上下文的记忆单元(memory unit),最终沉淀进独立的 Memory Bank,供后续 recall() 检索与 reflect() 反思使用。本文以官方开发者文档 skills/hindsight-docs/references/developer/retain.md 为骨架,结合仓库源码(hindsight-api-slim/hindsight_api)逐步拆解 retain 的抽取、实体解析、知识图谱构建、时间维度、内联附件、提取使命与观测整合机制,读完你既能掌握 retain() 的完整心智模型,也能用配置项和 API 对记忆写入做精细化控制。


Retain 做什么:从原始内容到记忆银行的五步流水线

文档用一张流程图概括了 retain 的整体链路:

graph LR
    A[Your Content] --> B[Extract Facts]
    B --> C[Identify Entities]
    C --> D[Build Connections]
    D --> E[Memory Bank]
  • Extract Facts:从内容中抽取值得长期记住的事实(而非逐字存储);
  • Identify Entities:识别并统一实体(人物、组织、地点、概念);
  • Build Connections:在实体、时间、语义、因果四个维度上建边,形成知识图谱;
  • Memory Bank:全部写入隔离的记忆银行,等待 recall()reflect() 消费。

在源码层面,这条流水线对应 hindsight-api-slim/hindsight_api/engine/retain 目录下的 orchestrator.py(编排)、fact_extraction.py(抽取)、fact_storage.py(落库)与 link_creation.py(建边)等模块,其中事实抽取与链接创建是下面重点展开的两个环节。


富事实抽取:存下"为什么"与"意味着什么"

Hindsight 不只是记录"说了什么",还捕获原因(why)、方式(how)与含义(what it means)。以文档中的例子为准:

"Alice joined Google last spring and was thrilled about the research opportunities",抽取结果为:

  • 核心事实:Alice 加入了 Google;这件事发生在去年春天;
  • 情绪与含义:她很兴奋;这代表一个重要的机会;
  • 推理:她选择 Google 是因为研究机会。

正是因为抽取阶段同时保留了情绪与动机,之后才能回答 "Why did Alice join Google?" 这样的问题,而不是只能得到一句干巴巴的 "she joined Google"。

上下文的保存:完整叙事而非碎片

传统系统会把一段对话拆成互不相连的碎片:

  • "Bob suggested Summer Vibes"
  • "Alice wanted something unique"
  • "They chose 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."

于是搜索结果返回的是带完整上下文的事实,而不是脱离语境的碎片。这一特性在抽取提示词中也有对应设计:基础提示模板要求抽取时做 共指消解(coreference resolution),例如把 "my roommate" + "Emily" 统一为 "Emily (user's roommate)",把 "the manager" + "Sarah" 统一为 "Sarah (the manager)",从而在事实文本层面消除指代歧义。相关实现见 fact_extraction.py_BASE_FACT_EXTRACTION_PROMPT

抽取的五个字段

文档中提到的核心事实结构,在提示模板中对应五个字段(详见 fact_extraction.py):

字段 含义 要求
what 核心事实 简洁但完整,1–2 句话以内
when 时间信息 未提及则为 "N/A",尽量给出具体日期
where 地点 无相关地点则为 "N/A"
who 相关人物及关系 仅针对一般信息时为 "N/A"
why 背景/重要性 仅当重要时填写,明显时省略

模板还强调"选择性"——只抽取值得长期记住的显著事实("Be SELECTIVE - only extract facts worth remembering long-term"),并用"一句好句子胜过三句平庸句子"来约束简洁度。


两种事实类型:experience 与 world

每条事实都会按照视角被分类——它记录的是"拥有该记忆银行的智能体自己"还是"外部世界":

类型 捕获内容 示例
experience 智能体自身的行为、观察、交互——它的第一人称历史 "我向 Alice 推荐了 Python"
world 关于其他人、地点、事物与事件的事实 "Alice 在 Google 工作"

归属由"说话者"决定,而非语法

关键点:判断依据是谁在说话,而不是句子是否用了第一人称。只有当第一人称陈述的说话者就是银行所属的智能体时,它才是 experience;同样的措辞出自他人之口,则是对该人的 world 事实:

  • 智能体自己的日志:"I patched the auth bug" → experience(智能体做的);
  • 用户对智能体说:"I bought a Tesla" → world(关于用户的事实,与智能体无关)。

在源码的抽取提示词中,这一规则体现为 fact_type 的分类指令(fact_extraction.py):

  • world:客观/外部事实,包括用户的偏好、规则、纠正、约束、计划、特质与上下文。即使用户在与助手交互时陈述这些内容,也保持为 world(如 "User prefers browser_navigate over web_search");
  • assistant:助手/智能体实际执行的动作、经验与观察(如 "I changed X"、"I discovered Y")。用于智能体做、尝试、学习、决定、推荐、回应等行为。

用 context 引导分类

要正确归类,应在每个条目的 context描述说话者

  • 保留转写稿或第三方内容时,使用类似 "Customer Maria is speaking" 的上下文,确保她的第一人称陈述被存为关于 Maria 的 world 事实,而不是被误认为智能体自己的经历;
  • 处理智能体自己的日志时,使用类似 "The assistant is speaking" 的上下文,将其第一人称陈述归属为智能体的 experience 事实。

源码中的 narrator 机制与此完全对应:当提供 agent_name 时,提示词会追加 "Narrator" 段,声明第一人称陈述默认归为 assistant(智能体自身),但当 context 指明不同的第一人称说话者(如转写稿中的用户/客户)时,Context 优先,将其归为 world(见 fact_extraction.py)。

retain() 完成后,观测(observations)会在后台自动整合——该过程把新事实中浮现的模式综合进银行的知识库,见后文"Observation Consolidation"一节。


实体识别与解析:让同一实体多种叫法归一

Hindsight 自动识别并持续追踪实体(entities)——那些重要的人物、组织与概念:

  • 人物:"Alice"、"Dr. Smith"、"Bob Chen"
  • 组织:"Google"、"MIT"、"OpenAI"
  • 地点:"Paris"、"Central Park"、"California"
  • 产品与概念:"Python"、"TensorFlow"、"machine learning"

实体解析(Entity Resolution)

同一个实体以不同方式出现时,通过模糊名称匹配(fuzzy name matching)统一,并以共现(co-occurrence)与时间邻近作为强化信号:

  • "Alice" + "Alice Chen" + "Alice C." → 归并为同一个人。

因为解析依赖名称相似度,近似变体会被自动合并;而名称毫不相似的写法(如昵称与完全无关的正式名)不会仅凭名称合并,但共享的共现实体仍可能把它们关联起来。

为什么重要:你可以问 "What do I know about Alice?",即使她在某些对话里被写作 "Alice Chen",也能拿到关于她的全部信息。

解析本质上是一种判断,因此也可能走向另一方向:在一个历史很长、实体很多的银行里,一个新出现的短名称若与既有实体相似、且总与该实体已关联的实体一同出现,就可能被吸收进既有实体而不是自成新实体。如果你发现新人物的事实被挂到了不相关的实体上,可参阅 How entity resolution decides 了解它比较了什么、哪个设置能让匹配更严格。

上下文感知消歧(Context-Aware Disambiguation)

如果 "Alice" 多次与 "Google"、"Stanford" 一同出现,那么一个新提及 "Alice" 的上下文若同样包含这些实体,则很可能就是同一个人——Hindsight 利用共现模式来消解常见名字的歧义。

实体标签(Entity Labels):受控分类词表

你可以定义一组受控的 key:value 分类标签词表(如 pedagogy:scaffoldingengagement:active),在 retain 时被抽取出来并作为实体存储。由于标签也是实体,它们会自动把相关记忆在知识图谱中链接起来(两条带 pedagogy:scaffolding 的记忆会被互链),同时提升语义检索与 BM25 关键词检索的效果;标签还可以选择性地写入记忆单元的 tags,从而在 recall 与 reflect 阶段支持标准的标签过滤。

与普通实体不同,标签实体永远不会按名称相似度合并——不同的标签值必须保持彼此不同,因此它们只按精确匹配解析,并被完全排除在模糊名称匹配之外。

配置入口是 entity_labels in the bank config。每一条 entity_labels 是一个标签组(一个分类维度),支持多种取值类型(详见 memory-banks.md):

{
  "entity_labels": [
    {
      "key": "engagement",
      "description": "Student engagement level during the session",
      "type": "value",
      "optional": true,
      "values": [
        { "value": "active",  "description": "Student is actively participating" },
        { "value": "passive", "description": "Student is listening but not participating" }
      ]
    },
    {
      "key": "pedagogy",
      "description": "Teaching strategies used",
      "type": "multi-values",
      "values": [
        { "value": "scaffolding",          "description": "Breaking complex tasks into smaller steps" },
        { "value": "direct_instruction",   "description": "Explicit explanation by the teacher" },
        { "value": "socratic_questioning", "description": "Guiding through questions rather than answers" }
      ]
    }
  ]
}
字段 默认值 说明
key 标签组标识,成为 key:value 实体("map" 类型则为 key:field:value)的前缀
description "" 展示给 LLM 的说明,用于引导标签指派
type "value" "value" 单选枚举;"multi-values" 多选;"text" 自由文本;"multi-text" 任意数量自由文本;"map" 带命名字段的结构化分组

构建连接:四类边构成知识图谱

记忆不是孤立的——Hindsight 用四种类型的连接构建知识图谱。对应源码实现集中在 hindsight-api-slim/hindsight_api/engine/retain/link_creation.py,其中 create_temporal_links_batchcreate_semantic_links_batchcreate_causal_links_batch 分别负责三类边的批量创建,语义链接基于事实的 embedding 计算余弦相似度并施加最低阈值(threshold)。

实体连接(Entity Connections)

所有提及同一实体的事实被互相链接。

  • 启用:"Tell me everything about Alice" → 取回全部与 Alice 相关的事实。

时间连接(Time-Based Connections)

时间上相近的事实被连接,日期越近的链接越强。

  • 启用:"What else happened around then?" → 找到语境上相关的事件。

语义连接(Meaning-Based Connections)

语义相似的事实被链接,即使它们用了不同的措辞(由 create_semantic_links_batch 基于 embedding 相似度阈值实现)。

  • 启用:"Tell me about similar topics" → 找到主题相关的信息。

因果连接(Causal Connections)

因果关系被显式追踪。

  • 启用:"Why did this happen?" → 追溯推理链;
  • 示例:"Alice felt burned out" ← 起因 ← "She worked 80-hour weeks"。

因果关系的抽取与写入由 HINDSIGHT_API_RETAIN_EXTRACT_CAUSAL_LINKS 环境变量控制(见 config.py),每条事实可携带 causal_relations 列表,最终由 create_causal_links_batch 写入。


理解时间:双重时间维度

Hindsight 追踪两个时间维度,这是它支持历史查询与新鲜度排序的基础。

事件发生时间(When It Happened)

  • 对于事件(会议、旅行、里程碑),记录其发生时间:"Alice got married in June 2024" → 发生于 2024 年 6 月;
  • 对于一般事实(偏好、特质),没有具体的发生时间:"Alice prefers Python" → 持续性偏好。

源码的提示模板对此有精细约束(fact_extraction.py):

  • 用输入中的 "Event Date" 作为相对日期的参照,必须把全部相对时间表达转换为绝对日期写入事实文本("yesterday" → 写出解析后的日期,而不是 "yesterday" 这个词);
  • 事件需同时设置 occurred_startoccurred_end(点事件两者相同);
  • 粗粒度日期(只说了年份或月份)要覆盖整个区间:"in 2015" → 2015-01-01 至 2015-12-31,"in March 2026" → 2026-03-01 至 2026-03-31,绝不能坍缩到区间首日或 Event Date;
  • 对话类(conversation)事实不设置 occurred 日期。

得知时间(When You Learned It)

Hindsight 同时记录你是什么时候告诉它每条事实的。

为什么两者都要? 假设 2025 年 1 月有人告诉你 "Alice got married in June 2024"

  • 历史查询:"What did Alice do in 2024?" → 能找到这场婚礼;
  • 新鲜度排序:最近被提及的条目在搜索中获得更高权重;
  • 时间推理:"What happened before her marriage?" → 能找到更早的事件。

如果没有这个区分,旧信息要么无法按日期检索,要么会被当成无关内容处理。


为记忆打标签:可见性范围控制

标签用于可见性范围控制(visibility scoping)——当一个记忆银行服务多个用户、而每个用户只应看到相关记忆时尤为有用:

  • 条目标签(Item tags):为单条记忆打上特定范围的标签;
  • 文档标签(Document tags):为一批中的所有条目统一打标签;
  • 标签过滤(Tag filtering):在 recall/reflect 阶段按标签过滤。

代码示例见 Retain API,过滤选项见 Recall API。


内容中的图片与文件:内联附件

文档中大量信息其实存在于图片里——说明按钮位置的截图、承载升级路径的示意图、本身就是数据的图表。content 接受有序的块(blocks)列表,让图片待在它该在的位置,而不是事先被压缩成一句说明文字:

{
  "content": [
    {"type": "text",  "text": "To reset the VPN, click the button shown:"},
    {"type": "image", "source": {"type": "base64", "media_type": "image/png", "data": "..."}},
    {"type": "text",  "text": "...then reconnect."}
  ]
}

纯字符串仍然和以前完全一样——纯文本 retain 的行为没有任何改变。

关键在于位置(position)。抽取时文本与图片一起、按顺序发送给模型,模型会在引入某句话的截图旁边读到该截图。"Click the button shown below" 单独看毫无意义;配图之后,它就变成一条能说出按钮名称的事实。这一点在抽取提示词中有显式强化:含附件时会注入 "ATTACHMENTS" 段落,要求模型阅读每个内联展示的附件,并规定"只存在于附件中的事实——按钮标签、表格中的数值、示意图的方框——与正文中的事实一样可抽取",且要求把每个附件归属到它周围的句子上(见 fact_extraction.py)。

返回什么

事实读起来很自然——[image: image/png] 只标记附件曾经所在的位置(不是内容哈希)——每条记忆携带它抽取自哪些附件,并给出可拉取的 URL:

{
  "text": "The escalation path for a stuck sync begins by contacting Tier 3 Platform.",
  "attachments": [
    {"id": "c414cd0e204d", "kind": "image", "media_type": "image/png",
     "url": "/v1/default/banks/my-bank/attachments/c414cd0e204d"}
  ]
}

来自正文的事实没有 attachments,即使同一份文档满是图片。因此,某条记忆旁出现附件,意味着模型确实看过它才产出该记忆——这是证据,不是装饰。抽取时每条事实携带的 from_attachments 编号正是这一机制的落点:提示词要求"只有事实离开该附件就无法陈述时,才列出该附件"。

图表与表格会怎样

携带结构化数据的附件会被转写(transcribed)而非摘要(summarised):每一行、每个柱、每个带标签的值都变成独立的事实,带着它的标签、数值以及绘制方式。因此一张图表产生的记忆远多于一张截图——这是刻意为之:一张二十个柱的图表的摘要只保留三个值、静默丢掉其余十七个,而后续提问往往针对被丢掉的那几个。相应地,抽取提示词要求"数据就是文档":为每一行/柱/扇区/带标签的值各建一条事实,带上标签、精确数值与绘制方式(颜色、在序列中的位置),并且不要摘要整个序列,还要记录图表整体信息(标题、单位、覆盖周期、条目数量、图例每种颜色代表什么)。

非常密集的页面仍采用采样而非穷尽:单次抽取遍过一张列了一百个条目的信息图,会捕获其中很大一部分,而非全部。

前置要求

  • 需要一个能读图的模型。默认是银行的 retain 模型,但并非必须:HINDSIGHT_API_VLM_MODEL 指定一个视觉槽位(vision slot),只用于真正携带附件的那些块,所有纯文本块仍走 retain LLM。因此,文档以散文为主的银行可以保留便宜的文本模型,只在确有可看内容时才为视觉付费(环境变量定义见 config.py);
  • 如果该模型不能读图——或者 Hindsight 无法判断(混合目录的后端网关即属此类)——retain 会以 422 拒绝,而不是静默丢弃附件。相关开关是 HINDSIGHT_API_LLM_VISION(该变量为三态布尔:True/False 直接覆盖模型自身判断,见 config.py);
  • 批量 retain(HINDSIGHT_API_RETAIN_BATCH_ENABLED不能携带附件,同样以 422 拒绝。

这区别于 POST /files/retain:后者把整个文件转成 markdown 作为独立文档。扫描件想被解析时它仍然是正确工具——但它把文件与提及它的正文分开了,而这正是内联附件要避免的。存储、大小限制与接受的媒体类型见 Inline attachments in retain,相关环境变量包括 HINDSIGHT_API_RETAIN_ATTACHMENT_MAX_SIZE_MBHINDSIGHT_API_RETAIN_ATTACHMENT_MAX_COUNTHINDSIGHT_API_RETAIN_MAX_ATTACHMENTS_PER_CHUNKconfig.py)。


你得到什么

retain() 完成后,产出包括:

  • 结构化事实:保留含义、情绪与推理;
  • 统一实体:消解不同名称变体;
  • 知识图谱:实体、时间、语义、因果四类链接;
  • 时间锚定:同时支持历史查询与新鲜度排序;
  • 可选标签:供 recall 阶段过滤。

以上全部存入你的隔离记忆银行,随时可供 recall()reflect() 使用。


用 Mission 引导抽取

默认情况下 retain() 抽取内容中所有显著事实。你可以用retain missionretain_mission)收窄关注点——用自然语言描述这个银行应该关注什么:

e.g. Always include technical decisions, API design choices, and architectural trade-offs.
     Ignore meeting logistics, greetings, and social exchanges.

mission 会被注入抽取提示词(与内置规则并列),它引导 LLM 而不替换抽取逻辑。它适用于所有基于 LLM 的抽取模式(conciseverboseverbatimcustom),在 chunks 模式下被忽略。在源码中,mission 作为 retain_mission_section 占位符被注入基础提示模板(fact_extraction.py)。

抽取模式(Extraction Modes)

需要更细粒度控制时,可以切换抽取模式

模式 适用场景
concise (默认) 通用场景——有选择性、速度快
verbose 需要带完整上下文与关系的更丰富事实
custom 想完全自定义抽取规则
verbatim 需要保留原始块文本,同时由 LLM 抽取实体、日期等元数据
chunks 原样存储块,不做任何 LLM 调用、不抽取元数据

源码中允许的模式集合为 ("concise", "verbose", "custom", "verbatim", "chunks"),默认 "concise";非法的模式值会被校验并回退到默认值(config.pyconfig.py)。custom 模式读取 retain_custom_instructionsHINDSIGHT_API_RETAIN_CUSTOM_INSTRUCTIONS),verbose/verbatim 模式各自使用专属的响应 Schema(fact_extraction.py)。

配置方式

retain_missionretain_extraction_mode 可通过 bank config API 配置,也可用环境变量 HINDSIGHT_API_RETAIN_MISSIONHINDSIGHT_API_RETAIN_EXTRACTION_MODE(两者分别对应 config.py 中的 ENV_RETAIN_EXTRACTION_MODEENV_RETAIN_MISSION,在 config.py 读取)。

当 mission 排除了文档中的一切

mission 收窄了"什么能成为记忆"——而产不出事实的内容就不会产生任何记忆。文档本身仍被存储,但 recallreflect 检索的是记忆,因此一份零记忆的文档无法被两者中的任何一个找到。收紧 mission 因而牺牲的是对原始来源的检索能力,而不只是事实创建。

这是正常结果,不是错误:retain 成功,操作被报告为已完成。两个信号能告诉你它发生了:

位置 看什么
retain.completed webhook data.memory_unit_count: 0
Metrics hindsight.retain.documents.total{outcome="no_facts"}

你还可以事后审计:GET /documents 按文档返回 memory_unit_countapi/http.py),过滤为 0 即可列出当前全部不可达的文档。

抽取并非完全确定——一份边缘文档可能这次跑出事实、下次跑出零条。把零视为"这份文档需要再跑一遍",而不是永久判决。

要恢复文档,放宽 mission 后重新处理即可——存储的文本会被重新抽取,无需重新上传:

POST /v1/default/banks/{bank_id}/documents/{document_id}/reprocess

该端点对应源码中的 reprocess_document 操作(api/http.py)。


Observation Consolidation:后台观测整合

retain() 完成后,Hindsight 会自动在后台触发观测整合(observation consolidation)。该过程:

  1. 将新事实与既有观测对比分析;
  2. 模式浮现时创建新观测;
  3. 用新证据精化既有观测;
  4. 追踪每条观测由哪些事实支撑。

整个过程异步进行——你的 retain() 调用立即返回,整合在后台运行。细节见 Observations


记忆防御与来源溯源(Memory Defense and Source Provenance)

receipt_uri(可选)

类型:string

指向外部收据或共同签名系统的可选指针。原样存储,并会出现在该条目任何 Memory Defense 决策的 security_events.receipt_uri 中。

422 —— Memory Defense 违规

当目标银行启用了 Memory Defense、且批次中每条条目都被策略拦截时,请求返回 422 并携带违规列表:

{
  "detail": {
    "violations": [
      { "index": 0, "detector": "prompt_injection", "severity": "high", "message": "..." }
    ]
  }
}

部分拦截的批次返回 200,未拦截的条目照常处理;被拦截的条目静默地从结果中移除,其决策记录在 security_events 中。完整指南见 Memory Defense


下一步

  • Observations —— retain 之后知识如何被整合
  • Recall —— 多策略检索如何取回相关记忆
  • Reflect —— 智能体循环如何使用观测
  • Retain API —— 代码示例与参数说明
登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
34
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.21 K
2.82 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
949
1.87 K
docsdocs
暂无描述
Markdown
906
5.84 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
537
612
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
864
1.36 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
4.29 K
1.04 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.39 K
1.48 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
550
401
flutter_flutterflutter_flutter
本仓库是 Flutter SDK 与 Flutter Engine 的 OpenHarmony 适配版本,由 CPF-Flutter 团队维护。开发者可使用熟悉的 Flutter 技术栈开发 OpenHarmony 应用,3.35.7 及以后的适配版本可基于本仓库源码构建支持 OpenHarmony 的 Flutter Engine。
Dart
1.19 K
348