跳到主要内容

跨模态知识图谱:从段文本到实体网络,再反查回片段

本章讲实体通道的两端:索引期怎么把段文本抽成图谱(extract_entities),查询期怎么从命中实体反查回视频段(_find_most_related_segments_from_entities)。图谱抽取整体复用了微软 GraphRAG 的范式(prompt.py:2 注明)。

4.1 实体抽取:靠「分隔符协议」让 LLM 输出可解析

它要解决的小问题: 让 LLM 从一段文本里抽出结构化的实体和关系,又不依赖脆弱的 JSON。

思路: 用一套自定义分隔符当输出格式,比 JSON 更抗模型「话痨」。提示词(prompt.py:9)要求模型输出形如:

("entity"<|>实体名<|>类型<|>描述)##
("relationship"<|>源<|>目标<|>关系描述<|>强度分)##
<|COMPLETE|>

其中 <|> 是 tuple 分隔、## 是记录分隔、<|COMPLETE|> 是结束符(prompt.py:138-140)。解析时按这些标记切开(_op.py:405split_string_by_multi_markers),再正则抠出括号内内容(_op.py:413)。默认实体类型只有四类:organization/person/geo/event(prompt.py:137)。

4.2 Gleaning:多轮「还有没漏的吗」补抽

它要解决的小问题: LLM 一次抽取往往漏实体。

思路(_op.py:390): 抽完第一遍后,带着历史对话再追问「上次漏了很多实体,按同样格式补上」(entiti_continue_extractionprompt.py:127),最多补 entity_extract_max_gleaning=1 轮(videorag.py:99)。中间还会问一句「还有没有遗漏?答 YES/NO」(entiti_if_loop_extractionprompt.py:132),答非 yes 就提前停(_op.py:402)。

# 示意,源自 _op.py:390 — 带历史多轮补抽,结果累加
for i in range(entity_extract_max_gleaning):
glean = await use_llm_func(continue_prompt, history_messages=history)
final_result += glean # 补抽的实体也算进来
if 模型回答不再有遗漏: break

所有 chunk 的抽取是 asyncio.gather 并发跑的(_op.py:448)。

4.3 合并与摘要:同名实体如何归一

同一个实体可能在多个 chunk 被抽到,描述各异。_merge_nodes_then_upsert_op.py:252)做归并:

  • 类型:取出现次数最多的那个 type(Counter 投票,_op.py:270)。
  • 描述:所有描述去重后用 <SEP> 拼接(GRAPH_FIELD_SEPprompt.py:6)。
  • 来源:把所有 source_id(即 chunk key)用 <SEP> 拼起来——这是反查片段的关键线索
  • 过长则摘要:描述 token 超 entity_summary_to_max_tokens=500videorag.py:100)就调便宜模型压缩(_handle_entity_relation_summary_op.py:181)。

边的合并(_merge_edges_then_upsert_op.py:299)类似,但权重是累加的(_op.py:321)——同一对实体被多次关联,关系越强。图是无向图:合并时对端点排序去重(_op.py:459)。最后实体名+描述拼起来也写进实体向量库(_op.py:476),供查询时语义召回。

4.4 反查:从实体回到视频段(实体通道的「最后一公里」)

查询命中若干实体后,要落回视频段。_find_most_related_segments_from_entities_op.py:487)的链路:

命中实体 node_datas
├─ 每个实体的 source_id(拆 <SEP>) → 它来自哪些文字 chunk
├─ 取每个实体的一跳邻居 get_node_edges → 邻居也带各自的 source chunk
└─ 给每个 chunk 算「relation_counts」
= 该 chunk 同时出现在「实体邻居的源 chunk」里的次数(共现强度)
└─ 按 relation_counts 降序取 top_k(=retrieval_topk_chunks=2)
└─ 收集这些 chunk 的 video_segment_id → 得到相关视频段集合

核心打分逻辑(_op.py:519):一个 chunk 如果同时被「命中实体的多个一跳邻居」引用,说明它在这个实体的关系网里很中心,得分高:

# 示意,源自 _op.py:519 — 用一跳邻居的源 chunk 共现给候选 chunk 打分
for e in this_edges: # 该实体的每条边
if e[1] in 邻居源chunk表 and c_id in 邻居源chunk表[e[1]]:
relation_counts += 1 # 邻居也引用了这个 chunk → 加分

最后从高分 chunk 的 video_segment_id 收集出片段集合(_op.py:540)——这就是实体通道交给主流程的「相关片段集 A」。

这一步把「文字图谱」和「视频段」缝在了一起: 图谱里全是文字实体,但每个实体记得自己来自哪个 chunk,而 chunk 在索引期(01 章 2.3 节)记了自己由哪些视频段组成。一条 source_id → video_segment_id 的链路就把语义检索的结果落回了画面。

代码地图

主题文件符号
实体抽取主流程videorag/_op.pyextract_entities
单条实体解析videorag/_op.py_handle_single_entity_extraction
单条关系解析videorag/_op.py_handle_single_relationship_extraction
节点合并/摘要videorag/_op.py_merge_nodes_then_upsert
边合并(权重累加)videorag/_op.py_merge_edges_then_upsert
描述过长摘要videorag/_op.py_handle_entity_relation_summary
实体反查片段videorag/_op.py_find_most_related_segments_from_entities
抽取/补抽提示词videorag/prompt.pyPROMPTS["entity_extraction"], PROMPTS["entiti_continue_extraction"]
图存储(默认)videorag/_storage/gdb_networkx.pyNetworkXStorage