m-flow Episodic Retrieval:图数据库、向量数据库、Bundle Scoring 与 Keyword Bonus
这篇文章只讲episodic retrieval。
目标不是泛泛介绍“什么是图数据库”或“什么是向量数据库”,而是回答下面几个更具体的问题:
- 在 m-flow 里,图数据库到底存什么
- 在 m-flow 里,向量数据库到底存什么
- 检索时这两个库各自负责哪一步
bundle scoring到底在算什么keyword bonus到底怎么工作,为什么它不是一条独立检索链
如果只先记一句话,可以记这个:
向量数据库负责“先把可能相关的点捞出来” 图数据库负责“把这些点放回结构里,判断它们属于哪一个 Episode” bundle scoring 负责“给每个 Episode 算总代价并排序” keyword bonus 负责“在向量召回结果上做一层规则加权”还有一个非常重要的前提:
episodic retrieval 里的 score 本质上是 distance distance 越小越好 所以所谓 bonus,很多时候其实是“把距离减小”0. 如果你没用过向量数据库和图数据库,先建立最小心智模型
这一节是给“没真正用过这两类库”的读者准备的。
如果只先记最短版本,可以记下面这三句话:
普通数据库擅长按字段精确查 向量数据库擅长按语义相似查 图数据库擅长按关系结构查0.1 三类数据库分别回答什么问题
假设你有下面这件事:
Episode: API 流式返回改造 Facet: 变更边界 FacetPoint: 鉴权中间件不能改 Entity: /chat/completions如果你问的是:
id = E1 的 Episode 是谁type = Facet 的节点有几个
这种更像是普通数据库擅长的问题,因为它本质上是按字段过滤。
如果你问的是:
和“API 流式返回那个任务有哪些约束”语义最像的是哪几条文本和“变更边界”意思接近的是哪些记录
这种更像是向量数据库擅长的问题,因为它本质上是相似度搜索。
如果你问的是:
“鉴权中间件不能改”这个点属于哪个 Facet这个 Facet 又属于哪个 Episode“API”这个实体和哪些 Episode 相连
这种更像是图数据库擅长的问题,因为它本质上是在问节点之间怎么连。
所以你可以先把三者理解成:
普通数据库:按列查 向量数据库:按“像不像”查 图数据库:按“连不连、怎么连”查0.2 向量数据库最小是怎么用的
如果不讲任何厂商细节,向量数据库最小就是四步:
- 选一段要检索的文本
- 把它变成 embedding 向量
- 连同
id / text / payload一起存进某个 collection - 搜索时把 query 也变成向量,做最近邻检索,返回 top-k 命中
你可以把它理解成:
输入一句话 -> 变成一串浮点数 -> 去找“向量空间里离它最近”的记录 -> 返回 id、text、score一个最小例子:
collection = Facet_anchor_text record: id = F2 text = 鉴权中间件保持不变,只改响应层和测试。 payload = {type: Facet, field: anchor_text} query: API 流式返回那个任务有哪些约束? search result: id = F2 score = 0.10这里的意思不是“它完全相同”,而是:
- 这条文本和 query 在语义空间里比较近
- 所以它值得进入下一步候选
向量数据库擅长的是:
- 同义表达
- 模糊表达
- 不同措辞但语义接近
向量数据库不擅长的是:
- 判断这条命中属于哪个 Episode
- 判断两个命中之间是不是父子关系
- 沿着
Point -> Facet -> Episode这种结构往回走
换句话说,向量数据库很擅长先回答:
什么像 query但它不擅长独立回答:
这些命中最后该归到谁头上0.3 图数据库最小是怎么用的
如果不讲任何厂商细节,图数据库最小也是四步:
- 建节点
- 建边
- 给边起关系名并带上属性
- 查询邻居、triplet 或局部子图
在这个项目里,一个极简图大概像这样:
Episode(E1: API 流式返回改造) --has_facet--> Facet(F2: 变更边界) --has_point--> FacetPoint(P1: 鉴权中间件不能改) Episode(E1) --involves_entity--> Entity(EN1: API)当向量库先命中P1或F2后,图数据库就可以继续回答:
P1连着哪个Facet- 这个
Facet连着哪个Episode - 这个
Episode还连着哪些Entity
你可以把图库理解成一个特别擅长回答下面这类问题的系统:
给你一个点 把它周围怎么连的也找出来图数据库擅长的是:
- 父子归属
- 多跳关系
- 局部结构恢复
- 路径解释
图数据库不擅长的是:
- 直接拿一句自然语言做高质量语义相似检索
- 靠“文本像不像”在整图上做第一轮粗召回
换句话说,图数据库很擅长回答:
这个点属于谁 它和谁相连 从它走几跳能回到哪个 Episode0.4 为什么 m-flow 里必须把两者一起用
如果只用向量库,你会得到:
- 一堆“看起来像 query”的文本命中
- 但不知道它们是否属于同一件事
如果只用图库,你会得到:
- 很清楚的结构关系
- 但不知道该先从哪几个点开始找
所以 m-flow 的组合方式本质上是:
向量库先负责“找入口” 图库再负责“还原归属关系”也可以更口语一点:
向量库先撒网 图库再收网1. 为什么 m-flow 不只用向量库
如果只有向量库,系统能回答的问题只有一种:
哪一段文本和 query 最像?
这个能力当然有用,但它不够。
因为 episodic memory 要解决的问题不是“找最像的一段文本”,而是:
这些命中的局部证据,最终应该收敛到哪一个 Episode?
一个用户问题经常会同时碰到不同粒度的内容:
- 有时先命中一个
Episode.summary - 有时先命中一个
Facet.search_text - 有时先命中一个
FacetPoint.search_text - 有时只命中一个
Entity.name
如果没有图结构,系统只能看到一堆分散的文本命中,看不到这些命中之间的归属关系。
这就是 m-flow 同时使用图数据库和向量数据库的原因:
- 向量数据库负责语义相似度
- 图数据库负责结构归属和多跳连接
1.1 这套逻辑不绑定某一家数据库
这个项目的 retrieval 代码不是直接写死在某个具体后端上,而是走 adapter 抽象:
- 图侧有
graph provider - 向量侧有
vector provider
因此从代码结构上说,它支持的不是单一组合。
可以先把它理解成:
- 图侧常见的是
Kuzu - 向量侧常见的是
LanceDB
但 retrieval 主逻辑真正依赖的不是“Kuzu 特性”或“LanceDB 特性”,而是这两个抽象能力:
- 图侧能按 id 投影局部子图
- 向量侧能按 collection 做 embedding search
2. 在这个项目里,图数据库到底存什么
2.1 图数据库存的是“结构化记忆图”
对 episodic memory 来说,图数据库里保存的不是孤立文本,而是节点和边。
最核心的节点有四类:
EpisodeFacetFacetPointEntity
最核心的边有几类:
Episode --has_facet--> FacetFacet --has_point--> FacetPointEpisode --involves_entity--> EntityFacet --involves_entity--> Entity
可以先把它想成这样:
Episode ├─ Facet │ └─ FacetPoint ├─ Entity └─ Chunk / 其他证据边2.2 图数据库关心的是“谁和谁连着”
图数据库最重要的职责不是文本相似,而是结构关系。
例如:
Episode: API 流式返回改造 ├─ Facet: 实施方案 ├─ Facet: 变更边界 │ └─ FacetPoint: 鉴权中间件不能改 ├─ Entity: API └─ Entity: /chat/completions在图数据库视角里,关键不是这些节点各自长什么样,而是:
变更边界属于哪个 Episode鉴权中间件不能改属于哪个 FacetAPI同时挂在哪些 Episode 或 Facet 上
2.3 边在这个系统里也有语义
这点很关键。
m-flow 里的边不是完全“哑”的关系线。边上通常还带有:
relationship_nameedge_text
这意味着系统不只知道:
Episode 和 Facet 连着还知道这条连接的关系文本是什么。
后面你会看到,这些边文本也会进入向量索引,参与检索和打分。
2.4 这个项目实际上怎么使用图数据库
如果你没用过图库,这里最重要的是分清:
- 本项目不是把图数据库当“全文搜索引擎”来用
- 而是把图数据库当“结构恢复器”来用
对 episodic retrieval 来说,图数据库最常做的是这几类事:
- 写入节点和边
- 例如写入
Episode / Facet / FacetPoint / Entity - 再写入
has_facet / has_point / involves_entity
- 例如写入
- 按 id 取节点、取边、取邻居
- 按
relevant_ids投影一个局部子图 - 把这个局部子图物化成内存里的
MemoryGraph
这意味着图库在这条链路里的使用方式不是:
拿一句 query -> 直接让图库在整库里做模糊语义匹配而更像是:
向量库先告诉我哪些 id 值得看 -> 图库把这些 id 周围的结构拉出来 -> 程序在这块局部图上继续算所以对这篇文章来说,你可以把图库理解成:
一个负责保存结构、恢复邻接关系、支持局部投影的后端3. 在这个项目里,向量数据库到底存什么
3.1 向量数据库存的不是整张图,而是“可嵌入字段”
图数据库存的是节点和边。
向量数据库存的是这些节点或关系上的某些文本字段的 embedding 索引。
在这个项目里,collection 名字遵循:
{NodeType}_{field}所以你会看到这些集合:
Episode_summaryFacet_search_textFacet_anchor_textFacetPoint_search_textEntity_nameRelationType_relationship_name
3.2 每个 collection 实际存的是什么
可以直接按表理解:
| Collection | 实际存的文本 | 用途 |
|---|---|---|
Episode_summary | Episode.summary | 从整件事角度粗召回 |
Facet_search_text | Facet.search_text | 用短标题召回某个 Facet |
Facet_anchor_text | Facet.anchor_text | 用富语义文本召回某个 Facet |
FacetPoint_search_text | FacetPoint.search_text | 用细粒度断言召回 |
Entity_name | Entity.name | 用实体名召回 |
RelationType_relationship_name | 边的edge_text / relationship_name | 给路径上的边补语义分数 |
3.3 它不是“把整条图边整条存进去”,而是给边语义做索引
RelationType_relationship_name容易误解。
它不是说:
- 图里每一条边都原样复制成一条独立向量记录
更准确地说,它会把边上的语义标签拿出来做索引:
- 优先用
edge_text - 没有时退回
relationship_name
检索时先查这些关系文本,再按文本匹配回图边,给实际路径上的边补vector_distance。
所以这层的职责是:
不是召回终点,而是给“路径本身”提供语义成本。
3.4 向量库里一条记录大概长什么样
你可以把每条向量记录理解成:
id = 某个节点或关系标签的 id text = 被嵌入的文本 vector = embedding 向量 payload = 原始字段对 episodic retrieval 来说,最关键的是:
id- 后面要回图
text- 后面要做
keyword bonus
- 后面要做
score- 检索后会成为 node cost 或 edge cost 的输入
3.5 这个项目实际上怎么使用向量数据库
如果你没用过向量库,可以把它想成“多张专门做语义检索的表”。
这个项目实际用到的能力很克制,核心只有几件事:
- 建 collection
- 例如
Episode_summary、Facet_search_text
- 例如
- 把某个字段写成向量记录
- 一条记录至少要能回出
id / text / payload
- 一条记录至少要能回出
- 按
query_text或query_vector做 search - 返回
VectorSearchHit- 里面至少有
id / score / payload / raw_distance / collection_name
- 里面至少有
- 在必要时做 batch search
所以它的工作方式更像:
把多个“可检索字段”分别建索引 搜索时每个 collection 都查一下 把命中的 id 和 distance 带回来而不是:
把整张图整个塞进一个大向量库 然后只靠向量库做最终排序对这篇文章最重要的一点是:
- 向量库负责返回候选命中
- 但不会独立决定最终哪个 Episode 胜出
真正的最终归因,还要等:
best_by_idedge_hit_map- 图投影
bundle_scorer
4. 这个项目里图数据库和向量数据库怎么配合
这条链路可以压成下面这五步:
1. 用向量库多集合召回 2. 把命中的 node id 放回图里 3. 在图里补齐相邻节点和边 4. 对每个 Episode 计算 bundle score 5. 取分数最小的 Episode bundles 输出把它翻成更口语一点的话就是:
向量库先说:“这些点像” 图数据库再说:“这些点其实属于这些 Episode” bundle scorer 最后说:“哪个 Episode 最能解释这些命中”5. 一条真实检索链是怎么跑的
这一节按真实顺序讲一次。
继续沿用前面的例子,假设用户问:
API 流式返回那个任务有哪些约束?5.1 第一步:query 预处理
系统会先得到一个PreprocessedQuery,里面最重要的是:
originalvector_querykeywordhybrid_reasonuse_hybrid
其中:
vector_query- 是拿去做 embedding 搜索的 query
keyword- 是后面做
keyword bonus用的规则关键词
- 是后面做
例如这句 query 里同时有中文和英文API,所以会触发:
hybrid_reason = mixed_langkeyword = API
这里没有 LLM。
5.2 第二步:多集合向量召回
然后系统会同时查多个集合:
Episode_summaryFacet_search_textFacet_anchor_textFacetPoint_search_textEntity_nameConcept_nameRelationType_relationship_name
注意:
- 这一步还没有图推理
- 只是把可能相关的节点和边文本先广撒网捞出来
假设召回结果像这样:
FacetPoint_search_text: P1 = 鉴权中间件不能改 score=0.08 Facet_search_text: F2 = 变更边界 score=0.12 Facet_anchor_text: F2 = 鉴权中间件保持不变,只改响应层和测试 score=0.10 Entity_name: EN1 = API score=0.18 Episode_summary: E1 = API 流式返回改造 score=0.36 E2 = 首页文案交付 score=0.625.3 第三步:先在向量结果上打 bonus
这一步仍然没有图。
系统会对这些向量结果做两类规则调整:
keyword bonusexact match bonus
例如 query 里的keyword = API,命中某些结果文本后,系统会把对应 score 再减一点。
所以某个结果可能从:
0.18 -> 0.03注意这不是“相似度加分”,而是“距离减小”。
5.4 第四步:生成best_by_id
同一个节点可能在多个集合里都命中。
例如Facet F2既可能命中:
Facet_search_text- 又命中
Facet_anchor_text
系统会把同一节点在所有集合里的分数取最小值,生成:
best_by_id[node_id] = min(all collection scores)这样后续图计算只看这个节点目前最强的一次命中。
5.5 第五步:生成edge_hit_map
对于边文本集合RelationType_relationship_name,系统也会做类似的事情:
edge_hit_map[edge_text] = best score之后如果图里某条边的edge_text命中了 query,这条边在路径计算里就更便宜。
如果没有命中,就用默认罚分edge_miss_cost。
5.6 第六步:把这些命中投影回图数据库
现在系统手里只有:
- 命中的 node ids
- 命中的 edge texts
- 各自的分数
它还不知道这些命中之间怎么连。
所以接下来会去图数据库里做两阶段投影:
- 先按命中的 relevant ids 投一个局部子图
- 再把相邻节点扩一跳,拿到更完整的局部结构
这里还有一个很容易忽略的点:
- 图数据库里的原始数据不会被
bundle_scorer直接逐条扫描 - 程序会先把这一小块相关结构投影成一个内存里的
MemoryGraph - 后面的关系整理、路径计算、bundle scoring 都发生在这个局部投影上
这一层可以把它理解成:
先用向量库找到“哪些点像” 再用图数据库把这些点周围的关系网拉出来5.7 第七步:构建 RelationshipIndex
子图拿回来后,程序不会直接在原始图对象上瞎搜。
它会先整理出几张关系表:
facets_by_episodepoints_by_facetentities_by_episodeentities_by_facet- 以及各种边的 lookup 表
这样后面的bundle_scorer就可以非常快地做路径计算。
6. bundle_scorer 到底在做什么
6.1 最短理解
bundle_scorer并不是在做“高级语义理解”。
它做的是一个很工程化的事情:
对每个 Episode,穷举几条允许的图路径,算出总代价,取最小值。
这个最小值,就是这个 Episode 的bundle score。
6.2 EpisodeBundle 是什么
最终每个候选 Episode 会得到一个EpisodeBundle,里面最重要的是:
episode_idscorebest_pathbest_support_idbest_facet_idbest_point_idbest_entity_id
你可以把它理解成:
这个 Episode 为什么会被召回 它是通过哪条最佳路径赢出来的 最佳证据落在 Facet、Point 还是 Entity 上6.3 它允许哪些路径
当前实现允许的主路径有五种:
direct_episode- 直接命中 Episode
facet- 直接命中 Facet,再回到 Episode
point- 先命中 FacetPoint,再回到 Facet,再回到 Episode
entity- 先命中 Episode 直接关联的 Entity,再回到 Episode
facet_entity- 先命中 Facet 关联的 Entity,再回到 Facet,再回到 Episode
所以它本质上是在做:
命中点 -> 沿图往上走 -> 最后落到 Episode6.4 Facet cost 怎么算
在算 Episode 之前,系统会先算每个 Facet 的最小代价。
公式可以写成:
FacetCost(fid) = min( 直接命中 Facet, 命中其下 Point 后回到 Facet, 命中其下 Entity 后回到 Facet )如果展开一点,就是:
FacetCost(fid) = min( facet_direct(fid), point_direct(pid) + edge_cost(fid-pid) + hop_cost, entity_direct(en) + edge_cost(fid-en) + hop_cost )其中:
facet_direct(fid)best_by_id[fid]
point_direct(pid)best_by_id[pid]
entity_direct(en)best_by_id[en]
edge_cost- 如果这条边的文本命中过 query,就用命中分数
- 否则用
edge_miss_cost
hop_cost- 每多跳一次就加一点固定成本
6.5 Episode bundle score 怎么算
然后对每个 Episode,再算:
EpisodeScore(ep) = min( direct_episode, via_facet, via_entity )展开后大致是:
direct_episode = episode_direct(ep) + direct_episode_penalty via_facet = FacetCost(fid) + edge_cost(ep-fid) + hop_cost via_entity = entity_direct(en) + edge_cost(ep-en) + hop_cost这里有三个最重要的超参数:
edge_miss_cost = 0.9hop_cost = 0.05direct_episode_penalty = 0.3
最短理解是:
- 边没命中就比较贵
- 多跳比少跳贵
- 直接命中 Episode 会额外吃一个罚分
6.6 为什么要给 direct Episode 一个 penalty
这点很多人第一次看会觉得反直觉。
原因是:
Episode.summary通常比较长,也比较泛- 它很容易“看起来相关”
- 但这种相关可能不够尖锐
所以系统故意做了一个偏好:
如果 query 能非常精确地命中某个 Point、Facet 或 Entity,就优先相信那条更具体的路径,而不是宽泛的 Episode summary。
这就是direct_episode_penalty的作用。
6.7 为什么不是把所有命中平均,而是只取最小路径
bundle scoring 当前采用的是:
这个 Episode 的最好路径是什么而不是:
这个 Episode 所有路径的平均分是多少这样做的意思是:
只要一个 Episode 有一条非常强的证据链,它就应该被召回。
否则一个 Episode 下挂了很多不相关 Facet 时,平均值会把真正重要的那条路径淹掉。
6.8 一个完整例子
假设图里有:
Episode E1 = API 流式返回改造 └─ Facet F2 = 变更边界 └─ Point P1 = 鉴权中间件不能改 Episode E2 = 首页文案交付当前 query 的命中结果是:
P1 score = 0.08 F2 score = 0.12 E1 score = 0.36 E2 score = 0.62再假设路径成本如下:
edge_cost(F2-P1) = 0.04 edge_cost(E1-F2) = 0.05 hop_cost = 0.05 direct_episode_penalty = 0.3那么:
FacetCost(F2) = min( 0.12, 0.08 + 0.04 + 0.05 ) = 0.12 EpisodeScore(E1) = min( 0.36 + 0.3, 0.12 + 0.05 + 0.05 ) = min(0.66, 0.22) = 0.22这意味着:
- 虽然
Episode.summary自己也命中了 - 但更强的解释路径其实是
Point -> Facet -> Episode
所以E1.best_path = point,而且它会排得很靠前。
7. keyword bonus 到底怎么工作
7.1 最短理解
keyword bonus不是:
- LLM 拆 query
- 不是 BM25
- 不是一条单独的 keyword search
- 也不是直接在图数据库里做过滤
它的真实位置是:
在向量召回结果出来之后,对命中的候选再做一层纯规则 rerank。
7.2 它发生在 bundle_scorer 之前
顺序必须分清:
query 预处理 -> 多集合向量召回 -> keyword / exact bonus 调整 score -> best_by_id / edge_hit_map -> 图投影 -> bundle scoring所以:
keyword bonus不属于bundle_scorer- 它只是给
bundle_scorer提供更好的输入分数
7.3 keyword 是怎么抽出来的
系统先根据 query 判断是否开启 hybrid:
mixed_langnumbershort_query
然后再按原因提取keyword:
情况 A:mixed_lang
如果 query 同时含中文和英文,就只抽英文部分。
例如:
API 流式返回那个任务有哪些约束?抽出来的keyword就是:
API情况 B:number
如果 query 里有数字,就优先抽数字和单位。
例如:
40万预算那个方案后来怎么改了?抽出来的 keyword 更接近:
40万情况 C:short_query
如果问题去掉疑问词后太短,就直接保留核心 query。
7.4 keyword bonus 的计算方式
这里最容易误解的一点是:
keyword bonus 不是再用 keyword 检索一次真实顺序是:
1. 从原始 query 里抽出 keyword 2. 仍然用 vector_query 去向量库检索 3. 向量库先返回一批候选结果 4. 再检查这些候选文本里有没有 keyword 5. 命中 keyword 的候选,把 score 减去 keyword_match_bonus也就是说:
向量库先召回候选 keyword bonus 只在候选内部改分数具体规则很硬:
- 把
keyword归一化- 转小写
- 去掉空格和常见标点
- 把候选结果文本也做同样归一化
- 做一次 substring 判断
- 命中就把 score 减去
keyword_match_bonus
当前默认值:
keyword_match_bonus = 0.15这个值的意思是:
如果候选文本包含 keyword,就把它的 distance 减少 0.15所以如果一个候选原始分数是:
0.22命中 keyword 后可能变成:
0.07这就是为什么前面一直强调:
score 是 distance bonus 其实是把 distance 往下减最短理解就是:
命中关键字 -> score 变小 score 越小 -> 越相关7.5 一个具体例子
query:
API 流式返回那个任务有哪些约束?预处理后:
keyword = API hybrid_reason = mixed_lang use_hybrid = True然后系统用vector_query先查向量库。假设向量库返回了三个候选:
候选 A: text = 鉴权中间件保持不变。 score = 0.22 候选 B: text = API 流式返回改造的变更边界。 score = 0.24 候选 C: text = 首页 hero 文案交付时间。 score = 0.30接着才进入keyword bonus。
候选 A 归一化后:
keyword_norm = api text_norm = 鉴权中间件保持不变不包含api,所以不改分:
score = 0.22候选 B 归一化后:
keyword_norm = api text_norm = api流式返回改造的变更边界包含api,所以命中 keyword bonus。
于是:
old_score = 0.24 new_score = 0.24 - 0.15 = 0.09候选 C 归一化后:
keyword_norm = api text_norm = 首页hero文案交付时间不包含api,所以不改分:
score = 0.30最后排序会更接近:
候选 B: 0.09 候选 A: 0.22 候选 C: 0.30之后这个更小的分数会进入best_by_id,再影响后面的 bundle scoring。
7.6 为什么它只是一层 bonus,不是主检索逻辑
原因很简单:
- 主检索仍然靠 embedding 多集合召回
- keyword bonus 只是在已经召回出来的候选上做排序修正
它解决的问题不是“把完全没召回的结果捞回来”,而是:
当 query 里有特别关键的英文词、数字、短词时,让包含这些精确信号的候选更靠前。
8. 图数据库、向量数据库、bundle_scorer、keyword bonus 的关系
这四者的关系可以压成一张图:
用户 query -> preprocess_query -> vector_query -> keyword -> 向量数据库多集合召回 -> node hits -> edge label hits -> keyword bonus / exact bonus -> 调整 node hit score -> best_by_id / edge_hit_map -> 图数据库投影局部子图 -> Episode / Facet / Point / Entity / edges -> build_relationship_index -> bundle_scorer -> 为每个 Episode 计算最小路径代价 -> top bundles -> output assembly -> 注入回答模型也就是说:
- 向量数据库负责“召回候选”
- keyword bonus 负责“修正候选分数”
- 图数据库负责“还原结构关系”
- bundle_scorer 负责“把局部命中收敛成 Episode 排名”
9. 常见误解
9.1 误解一:图数据库负责检索,向量数据库只是存 embedding
不对。
更准确地说:
- 向量数据库负责第一轮 semantic recall
- 图数据库负责后续结构归因和多跳连接
两个都参与 retrieval,只是职责不同。
9.2 误解二:bundle_scorer 是某种 LLM 推理器
不对。
bundle_scorer是纯规则路径计算。
它没有 prompt,也不调用 LLM。
9.3 误解三:keyword bonus 是另一套 keyword search
不对。
它不是独立召回器,而是向量结果上的后置加权。
9.4 误解四:Episode 命中了就一定最强
不对。
当前实现刻意压低 direct Episode 的优先级,鼓励更具体的Point / Facet / Entity路径获胜。
9.5 误解五:edge collection 是可有可无的
不对。
RelationType_relationship_name的作用不是决定最终返回什么,而是给路径本身补语义成本。
没有这一层,系统只能按“节点像不像”走,无法区分:
- 这两个节点虽然都像 query
- 但它们之间的连接是否也和 query 相关
10. 最后用一句话收束
如果把 episodic retrieval 压成一句工程语言,可以写成:
m-flow 先用向量数据库在多个语义入口上广撒网召回节点和边文本, 再把这些命中放回图数据库恢复结构, 最后用 bundle_scorer 计算“哪一个 Episode 拥有最低成本的证据链”, 而 keyword bonus 只是这条链路前半段的一个规则型分数修正器。如果继续往下展开:
向量库回答:什么像 图数据库回答:它属于谁 bundle scorer 回答:谁最能解释这些命中 keyword bonus 回答:哪些命中应该再往前提一点