摄入主线:从一个 PDF 到可被问答的知识图谱
本章讲写路径:
process_document_complete被调用后,一份文档怎么一步步变成知识图谱里的节点和边。
这一节讲什么
先看整条主线的骨架,再逐阶段拆。核心难点只有一个:纯文本可以直接交给 LightRAG,但图表公式不行——它们得先被「翻译」成文字,而且翻译完还要小心地接回 LightRAG 的内部存储,不能破坏 LightRAG 自己维护的文档状态。
3.1 入口与主线骨架
process_document_complete(processor.py:1660,ProcessorMixin)是端到端入口。剥掉错误处理和回调,主线只有四步:
# 示意,浓缩自 processor.py:1708-1791
content_list, doc_id = await self.parse_document(file_path, ...) # ① 解析
text_content, multimodal_items = separate_content(content_list) # ② 分离
if text_content.strip():
await insert_text_content(self.lightrag, input=text_content, ids=doc_id, ...) # ③ 文本入库
if multimodal_items:
await self._process_multimodal_content(multimodal_items, file_name, doc_id) # ④ 模态处理
为什么文本和模态分两条路? 文本是 LightRAG 的「母语」,直接 ainsert 即可,LightRAG 会自己切块、抽实体、建图、写 doc_status。模态块没有这个待遇——它们要先被处理器翻译,再由 RAG-Anything 手动塞进 LightRAG 的各个存储。所以模态是一条平行的、自己搭的流水线。
3.2 阶段①:解析 → content_list
parse_document(processor.py:388)按文件扩展名分派到解析器的不同方法:PDF 走 parse_pdf、图片走 parse_image、Office/HTML 走 parse_office_doc,其余走通用 parse_document。解析跑在 asyncio.to_thread 里(解析是同步重活,不能阻塞事件循环)。
产物是 content_list —— 这是贯穿全系统的核心数据结构,一个保留文档顺序的字典数组。每项形如:
| type | 关键字段 | 说明 |
|---|---|---|
text | text, text_level, page_idx | 正文;text_level>0 表示是标题 |
image | img_path, image_caption, image_footnote, page_idx | 图片(绝对路径) |
table | table_body, table_caption, table_footnote | 表格(markdown 或行列) |
equation | latex / text, text_format | 公式 |
解析结果会被缓存(基于「文件路径 + mtime + 解析配置」的 MD5),见 _generate_cache_key(processor.py:50)。同一文件没改、配置没变,第二次直接命中缓存跳过解析。
doc_id 来自内容而非文件名。 _generate_content_based_doc_id(processor.py:202)把每个块的关键内容拼成一个签名串再做 hash,前缀 doc-。好处:同一份内容无论叫什么文件名都得到同一个 id,天然去重。
3.3 阶段②:separate_content 拆流
separate_content(utils.py:172)遍历 content_list,做两件事:
type=="text"的块:把text拼进一个大字符串(用\n\n连接)。- 其余块:原样收进
multimodal_items,并埋两个隐藏字段给后续用:_content_list_index:它在原数组里的位置(后面排序 chunk 用)。- 对图片额外算
_section_path(它所在的章节路径,如Introduction > Method)和_neighbor_text(前后几个文字块),帮 VLM 理解上下文。
这两个隐藏字段是个巧妙处:在分离阶段就顺手把「这张图在文档里的位置感」固化下来,因为此刻还能看到完整有序的 content_list,后面分块并发处理时就看不到全局了。_section_path 的算法是经典的「标题栈」——见 extract_section_path_from_content_list(utils.py:91)。
3.4 阶段④:模态批处理的 7 个子阶段
这是全章重点。_process_multimodal_content_batch_type_aware(processor.py:884)把模态块变成图谱内容,分 7 步:
怎么读这张图
从上到下是先后顺序。Stage 1 是并发的(每个模态块一个协程),其余是串行批处理。
Stage 1 并发生成描述 每个块 → 选对处理器 → 调 VLM/LLM → (描述, 实体信息)
│ (Semaphore 限流,默认 max_parallel_insert=2)
▼
Stage 2 套 chunk 模板 描述 + 原始数据 → 格式化成一段标准文本块
│
▼
Stage 3 写 chunk 存储 text_chunks(给抽取用) + chunks_vdb(给检索用)
│
▼
Stage 3.5 写「模态主实体」 把 "实验结果图 (image)" 这种实体写进 entities_vdb + 图谱 + full_entities
│
▼
Stage 4 抽实体关系 复用 LightRAG 的 extract_entities 从 chunk 里挖实体/关系
│
▼
Stage 5 加 belongs_to 关系 把抽出的子实体都「归属」到模态主实体名下(权重 10.0)
│
▼
Stage 6 合并入图谱 复用 LightRAG 的 merge_nodes_and_edges 落库
│
▼
Stage 7 更新 doc_status 把模态 chunk 的 id 并进文档的 chunks_list
Stage 1:为什么先只生成描述
注意处理器有两个方法:generate_description_only(只翻译,不入库)和 process_multimodal_content(翻译 + 入库)。批处理走的是前者——先把所有块的「翻译」并发跑完(这一步最慢,要等 LLM/VLM),拿到 (description, entity_info) 列表,再统一走后面的入库阶段。这样 LLM 调用能并发吃满,而对存储的写入保持有序批量。Stage 1 的并发协程见 process_single_item_with_correct_processor(processor.py:921)。
Stage 2:chunk 模板——模态块长什么样
模态块不是裸描述,而是套了模板的结构化文本。以图片为例(prompt.py:334,PROMPTS["image_chunk"]):
Image Content Analysis:
- Section Path: Introduction > Method
- Neighbor Text: 前后正文……
Image Path: /abs/path/fig3.jpg
Captions: Figure 3: 实验曲线
Footnotes: None
Visual Analysis: <VLM 生成的详细描述>
关键:Image Path: 这一行被原样保留进 chunk 文本。这不是冗余——查询阶段的 VLM 增强正是靠正则匹配这一行,才能把图片路径找回来(见 03-query.md)。模板套用逻辑在 _apply_chunk_template(processor.py:1109)。
Stage 5:belongs_to 是怎么回事
这是 RAG-Anything 把模态接进图谱的核心嫁接术。
问题:LightRAG 的 extract_entities 从图片描述里能挖出一堆普通实体(比如「准确率」「测试集」),但这些实体和「这张图本身」是断开的。RAG-Anything 想表达「这些实体都出自图 3」。
做法:为每个抽出的子实体,补一条指向模态主实体(如 实验结果图 (image))的 belongs_to 边,权重设成显眼的 10.0:
# 示意,见 processor.py:1437-1451 _batch_add_belongs_to_relations_type_aware
belongs_to_relation = {
"src_id": entity_name, # 子实体,如「准确率」
"tgt_id": modal_entity_name, # 模态主实体,如「实验结果图 (image)」
"keywords": "belongs_to,part_of,contained_in",
"weight": 10.0, # 高权重:检索时优先把同图实体聚到一起
"source_id": chunk_id,
}
效果:检索命中「准确率」时,顺着这条高权重边能把整张图的相关实体都拉出来,保持「同一张图的内容在检索结果里抱团」。这就是 README 说的 "Hierarchical Structure Preservation"。