跳到主要内容

CORE 检索 — 意图路由 + 6 种查询处理器 + 压缩重排

本章讲什么: 读出侧。当一个 agent 问「John 的电话是多少」或「上周 CORE 项目有啥进展」,CORE 怎么从图谱里取到对的东西。核心思想:不同类型的问题走不同的路

1. 直觉:为什么不能一套向量搜索打天下

朴素做法:把查询嵌入,向量搜最相似的事实,返回。但这对很多问题很烂:

  • 「John 的电话号码」→ 你要的是实体的一个属性,不是「相似的句子」。
  • 「John 和 Mike 怎么认识的」→ 你要的是两实体间的连接路径
  • 「上周我聊过哪些话题」→ 你要的是一份枚举清单,根本不用读对话内容。

所以 CORE 先给问题分类,再分流。这就是 search-v2 的两段式:路由(router)→ 处理器(handler)

自然语言查询


① 路由 routeIntent ──► 标签向量召回 + LLM 抽取
│ └─► {queryType, aspects, entityHints, temporal, selectedLabels}

② 按 queryType 分流到 6 个处理器之一

③ 压缩(同会话→compact 文档)+ Cohere 重排 ──► 结果

2. 路由:先分类,再取数

routeIntent(apps/webapp/app/services/search-v2/router.ts:299-340)两步:

Step 1 标签向量召回 —— searchLabels(:173-230)。CORE 给每个「话题标签(label)」存了嵌入;先用查询嵌入召回最相关的标签(top 8),作为 LLM 的候选上下文。

Step 2 LLM 抽取 —— extractAspects(:237-293)把查询连同候选标签喂给 LLM,结构化输出这些字段:

字段含义
queryType6 类之一(见下)
aspects涉及哪些 aspect(Goal/Identity/Event…)
entityHints提到的实体名(John、CORE…)
temporal时间窗(recent/range/before/after/all)
selectedLabels选中哪些候选标签
lookupMode实体查询是要「属性」还是「泛查」
shouldSearch这查询到底要不要搜(「你好!」就不搜)

防幻觉细节: prompt 反复强调「selectedLabels 只能从给定候选里选,没候选就返回空数组,绝不编造标签名」(:41-43:80)。

3. 6 种查询类型与处理器

routeToHandler(handlers.ts:1410-1557)按 queryType 分流:

queryType典型问题处理器干什么
aspect_query「我的健身目标是什么」按 标签+aspect 取 episode(最常见)
entity_lookup「John 的电话」/「John 是谁」属性模式直取属性;泛查模式取实体相关 episode
temporal「上周 CORE 有啥进展」按时间窗 + 标签取 episode
temporal_facets「上周我聊了哪些话题/人」只枚举话题/实体/aspect,不读内容
exploratory「CORE 里的搜索实现」按标签取整段会话的压缩文档
relationship「John 和 Mike 怎么认识」找连接两实体的语句

3.1 多路并行 + 兜底

大多数处理器并行跑多条取数路再合并去重。以最常见的 handleAspectQuery(handlers.ts:249-298)为例:

┌─ 标签+aspect 路:getEpisodesForAspect ─┐
查询 ──┤ ─ 实体提示路:getEpisodesViaEntityHints ├─► mergeEpisodes(按 uuid 去重)
└─ 向量兜底路:仅当没匹配到标签时启用 ──┘

第三路是兜底(:281-283):只有当标签一个都没匹配上,才退化成纯向量搜 episode——保证「冷启动 / 标签没建好」时也有结果。

3.2 entity_lookup 的「属性优先」

handleEntityLookup(:316-470)体现了「按问题取数」的精髓。先向量搜出实体,然后:

  • 若是 attribute 模式(问「电话」「邮箱」)→ 直接在实体的 attributes 里找那个键(大小写不敏感、支持包含匹配,:413-420),找到就只返回实体,不去翻对话
  • 找不到那个属性 → 回退到 broad 模式,取实体相关的 episode(:444-447)。

3.3 temporal_facets:枚举而非阅读

handleTemporalFacets(:1169-1357)很特别——它回答「上周有什么」时,不返回对话内容,只返回三个维度的清单:话题(带 episode 数)、实体(带提及数)、aspect(带语句数),三路并行用专门的图查询(getTopicsForFacets 等)聚合。适合做「每周摘要」「晨间简报」。

4. 压缩与重排:把结果收拾干净

取到一堆 episode 后,两道收尾:

4.1 用压缩文档替换零散 episode

replaceWithCompacts(handlers.ts:867-982):同一个会话(session)往往有很多条 episode。CORE 在摄取时已为会话生成了压缩文档(compact,一段总结)。检索时若某会话命中超过 2 条 episode,就用那份压缩文档替换它们(:939),用命中的最高分作为该压缩文档的相关度:

命中:会话X 的 ep1, ep2, ep3, ep4 (零散、重复)
│ replaceWithCompacts(>2 条才替换)

返回:会话X 的压缩文档(1 段总结) + relevanceScore=该会话最高分

这让返回给 agent 的上下文更紧凑、不重复。

4.2 Cohere 重排,失败回退向量

applyEpisodeReranking(:754-821):有 COHERE_API_KEY 就用 Cohere rerank-v3.5 按查询给 episode 打分、过滤低分;没 key 或失败时回退到向量相似度重排(applyVectorReranking,:707-748),再不行就保持原序。多层降级保证「永远有结果」。

4.3 broad recall 兜底

还有一层 augmentEpisodesWithBroadRecallBackstop(:1140-1163):用旧版 SearchService 再做一次宽召回,把没覆盖到的候选补进来(受 env 开关控制)。这是「宁可多召回也别漏」的保险。

4.4 失效事实也一并带回

normalizeToRecallResult(:1027-1065)在返回结果时,会额外把这些 episode 上已失效的事实抽出来(extractInvalidatedFacts,:988-1021)单独放在 invalidatedFacts 里。这样消费方既知道「现在如何」,也能看到「曾经如何」——呼应 §2 的双时间轴设计。

5. 巧妙之处

  • 先分类再取数:6 种 queryType 各走最优路径,避免「一套向量搜索打天下」的低质量。
  • 属性查询不读对话:问电话就从 attributes 直取,快且准。
  • 枚举型查询不读内容:temporal_facets 只聚合计数,做摘要时省大量 token。
  • 层层降级:Cohere→向量→原序;标签→向量兜底;broad recall 兜底——任何外部依赖挂掉都还能出结果。
  • 压缩替换 + 带回失效事实:上下文既紧凑又完整(含历史)。

6. 边界与局限(整组文档收口)

诚实说几点:

  • 重度依赖 LLM:摄取要 normalize/extract/reflect/classify 多次调用,检索要路由调用——延迟和成本都不低,所以才有同步/异步分离、稀疏输出、diff-only 等省钱设计。
  • 去重/矛盾判定的对错取决于 LLM:召回缩小了候选,但最终裁决是 LLM,误判会让记忆变脏或丢失。代码里大量「失败回退保留原样」就是对此的防御。
  • 存储绑定多后端:Neo4j(图)+ Postgres(Aspects/Document/队列)+ 向量库(pgvector/qdrant/turbopuffer 可选,packages/providers/src/vector/)。自托管要把这套都拉起来。
  • provider 抽象但默认 Neo4j:ProviderFactory.getGraphProvider() 是接口,但本仓只见 Neo4j 实现(packages/providers/src/graph/neo4j/)。

7. 横向对比

同 shelf 的 memory-context 兄弟项目里,CORE 的取舍辨识度很高:

  • 对比「上下文窗口/摘要型」记忆(如 Letta/MemGPT 思路):那类把记忆当「可分页的上下文」,靠 agent 自己读写内存块;CORE 走的是外部化的时序知识图谱——事实是结构化节点、带双时间轴、有显式失效语义,更像一个「带历史的数据库」而非「更大的上下文」。
  • 强项:时间演化(失效而非删除)、世界/心声分存、检索按意图分流。
  • 代价:工程更重(多 LLM 阶段 + 多存储),不是「丢进 prompt」那么轻。

8. 代码地图

主题文件符号
意图路由(分类+抽取)apps/webapp/app/services/search-v2/router.tsrouteIntent / extractAspects
标签向量召回apps/webapp/app/services/search-v2/router.tssearchLabels
处理器分流apps/webapp/app/services/search-v2/handlers.tsrouteToHandler
aspect 查询(多路并行)apps/webapp/app/services/search-v2/handlers.tshandleAspectQuery
实体查询(属性/泛查)apps/webapp/app/services/search-v2/handlers.tshandleEntityLookup
时间窗枚举apps/webapp/app/services/search-v2/handlers.tshandleTemporalFacets
关系查询apps/webapp/app/services/search-v2/handlers.tshandleRelationship
压缩替换apps/webapp/app/services/search-v2/handlers.tsreplaceWithCompacts
Cohere/向量重排apps/webapp/app/services/search-v2/handlers.tsapplyEpisodeReranking / applyVectorReranking
带回失效事实apps/webapp/app/services/search-v2/handlers.tsextractInvalidatedFacts