跳到主要内容

模态处理器:把图 / 表 / 公式翻译成「带实体的文字」

本章讲 modalprocessors.py:四个处理器是怎么工作的,以及它们最脏最关键的活——把 LLM 吐的自由文本可靠地解析成结构化结果

这一节讲什么

先看四个处理器的共同骨架(都继承自一个基类,只换 prompt),再深入两个真正难的点:从 LLM 响应里抠出 JSON 的多级容错,和 reasoning 模型的 <think> 标签污染。

4.1 四个处理器,一个基类

所有处理器继承 BaseModalProcessor(modalprocessors.py:366)。差异极小——它们只是换了 prompt 和实体类型:

处理器用哪个模型默认 prompt默认 entity_type
ImageModalProcessorVLM(vision_model_func)vision_promptimage
TableModalProcessorLLMtable_prompttable
EquationModalProcessorLLMequation_promptequation
GenericModalProcessorLLMgeneric_prompt传入的 content_type

选哪个处理器由 get_processor_for_type(utils.py:422)按 type 字段路由,未知类型一律落到 generic。处理器在 RAGAnything._initialize_processors(raganything.py:204)里按配置开关创建——注意图片处理器优先用 vision_model_func,没有才退回 llm_model_func

每个处理器只需实现两个方法:

  • generate_description_only:翻译,返回 (描述, 实体信息)。批处理用它。
  • process_multimodal_content:翻译 + 入库一体。逐个兜底时用它。

4.2 一次翻译长什么样(以图片为例)

ImageModalProcessor.generate_description_only(modalprocessors.py:860)的核心步骤:

# 示意,浓缩自 modalprocessors.py:880-955
image_path = content_data.get("img_path")
context = self._get_context_for_item(item_info) # 取周边文字做上下文
vision_prompt = PROMPTS["vision_prompt_with_context"].format( # 有上下文就用带上下文版
context=context, section_path=..., image_path=image_path, captions=..., ...)
image_base64 = self._encode_image_to_base64(image_path) # 图片转 base64
response = await self.modal_caption_func( # 调 VLM
vision_prompt, image_data=image_base64,
system_prompt=PROMPTS["IMAGE_ANALYSIS_SYSTEM"])
enhanced_caption, entity_info = self._parse_response(response, entity_name) # 解析

重点看 prompt 怎么要求结构化输出。 vision_prompt(prompt.py:85)明确要 VLM 返回一段 JSON,含两个字段:detailed_description(详细描述)和 entity_info(实体名/类型/摘要)。表和公式的 prompt 结构同理。换句话说,翻译的同时也让 LLM 顺手做了实体抽取的第一步

prompt 里还藏了一条经验:"do not return file names or figure numbers such as figure_30_1 unless they are the actual title"——逼模型给图起个语义化的名字(如「实验结果对比图」),而不是 figure_30_1,因为这个名字会变成图谱里的实体节点名。

4.3 难点:从 LLM 自由文本里可靠抠出 JSON

LLM 不一定听话——可能把 JSON 包在 ```json 代码块里、可能加前言、可能用智能引号、可能漏逗号。_robust_json_parse(modalprocessors.py:577)用四级降级对付:

怎么读这张图

从上到下是尝试顺序,任一级成功就返回;全失败才到最后的正则兜底。

候选提取 _extract_all_json_candidates:
先剥 <think>/<thinking> 标签
再用三种方式找 JSON 片段:① ```json 代码块 ② 配平的大括号 ③ 贪婪 {.*}


级别 1 直接 json.loads 每个候选
│ 失败

级别 2 基础清理(智能引号→直引号、去尾逗号)后再 loads
│ 失败

级别 3 渐进式修转义(给 \alpha 这种裸反斜杠补转义)后再 loads
│ 失败

级别 4 正则直接抠 detailed_description / entity_name / entity_type / summary 字段

级别 4 是最后防线 _extract_fields_with_regex(modalprocessors.py:687):哪怕整段不是合法 JSON,也用正则把几个关键字段单独捞出来,保证流程不中断。

为什么级别 3 专门修反斜杠? 因为公式场景里 LaTeX 满是 \frac\alpha,这些裸反斜杠在 JSON 字符串里是非法转义,json.loads 会直接报错。_progressive_quote_fix(modalprocessors.py:672)把 \ 后跟字母的情况补成 \\。这是被公式数据逼出来的针对性补丁。

4.4 难点:reasoning 模型的 标签

DeepSeek-R1、Qwen-think 这类模型会先吐一段 <think>…</think> 的思维链再给答案。如果 JSON 解析失败、直接拿原始响应当描述存进图谱,就会把模型的「内心独白」也存进去,污染知识库。

_strip_thinking_tags(modalprocessors.py:553)用正则把 <think><thinking> 块整段删掉,只留最终答案。这个清理在两处发生:候选提取前(_extract_all_json_candidates,modalprocessors.py:611)和解析彻底失败的兜底分支里(如 _parse_response,modalprocessors.py:1065)。

4.5 实体名的小处理

解析成功后(如 _parse_response,modalprocessors.py:1054),实体名会被加上类型后缀:

# 示意,见 modalprocessors.py:1054
entity_data["entity_name"] = entity_data["entity_name"] + f" ({entity_data['entity_type']})"
# 例:"实验结果对比图" → "实验结果对比图 (image)"

好处:在知识图谱里一眼能区分「这个节点是图/表/公式」,也降低不同模态间同名实体撞车的概率。这个带后缀的名字就是 01-pipeline.md 里说的「模态主实体」名。

4.6 入库:_create_entity_and_chunk

逐个处理路径走 _create_entity_and_chunk(modalprocessors.py:471):建 chunk → 写 text_chunks + chunks_vdb → 建实体节点 → 写 entities_vdb → 调 _process_chunk_for_extraction 抽取实体关系并补 belongs_to。批处理路径则把这些拆成 01-pipeline.md 的 Stage 2-6 分别批量做。两条路最终都落到 LightRAG 的同一套存储。

关键细节 / 坑

  • 每个方法都有 fallback 实体:翻译或解析全挂时,处理器不抛异常,而是造一个 f"image_{hash}" 之类的兜底实体把流程接住(如 modalprocessors.py:959)。宁可降级也不让一张坏图毁掉整篇文档的摄入。
  • 表格内容的别名兼容:不同解析器给表的字段名不一(table_body vs table_data),get_table_body(utils.py:27)按优先级兜住;format_table_body(utils.py:36)还能把「行列二维数组」渲染成 markdown 表再喂给 LLM。
  • 公式不把描述拼进公式体:get_equation_text_and_format(utils.py:63)刻意只取公式本体,描述走模板里单独的 enhanced_caption 槽,避免污染公式文本。