传统RAG系统在处理纯文本应用场景中已展现出显著效果,然而现实世界的信息载体往往呈现多模态特征。文档中普遍包含图像、表格、图表等承载关键信息的视觉元素,这些多模态内容的有效处理正是多模态RAG系统的核心价值所在。
多模态RAG最优方案选择
经过系统性研究和实验验证,我们将介绍一个在RAG系统中处理多模态内容的最佳实现方案。该方案在性能表现、准确性指标和实现复杂度之间实现了优化平衡。
架构优势分析
架构采用模态特定处理与后期融合相结合的技术路线。相比其他技术方案,该架构具有以下显著优势:
首先,在模态信息保留方面,该方法避免了统一嵌入方法可能导致的模态特有信息丢失问题,通过针对各模态优化的专用工具实现精确的内容类型处理。其次,系统具备良好的灵活性和模块化特征,支持单独组件的升级优化(例如更换更高性能的图像理解模型),而无需重构整个系统架构。
在检索精度方面,研究数据表明,该方法在处理复杂多模态查询时的性能相比统一方法提升23%。同时,该架构基于广泛可用的开源工具和模型构建,确保了大多数组织的技术可达性和实施可行性。
多模态文档处理工作流程
以下详细阐述推荐工作流程的各个环节,说明各组件如何协同工作以构建统一的系统架构:
结构保留的文档分割
该模块的核心功能是将文档分解为可管理的片段,同时保持其逻辑结构和不同内容类型之间的关联关系。
结构感知分割对于系统性能至关重要,它确保相关内容(如图像及其标题)在分割过程中保持关联,这对准确理解和检索具有决定性作用。
python
importfitz # PyMuPDF
defsplit_pdf_by_structure(pdf_path):
"""根据PDF文档的逻辑结构进行拆分。"""
doc=fitz.open(pdf_path)
sections= []
# 提取文档结构(简化示例)
toc=doc.get_toc()
iftoc:
# 使用目录进行结构化拆分
fori, (level, title, page) inenumerate(toc):
next_page=toc[i+1][2] ifi<len(toc)-1elselen(doc)
section= {
"title": title,
"start_page": page-1, # 0 索引
"end_page": next_page-1,
"level": level
}
sections.append(section)
else:
# 回退到页面级拆分
foriinrange(len(doc)):
sections.append({
"title": f"Page {i+1}",
"start_page": i,
"end_page": i,
"level": 1
})
returnsections, doc
2、模态特定内容提取
该模块采用针对特定模态优化的专用工具处理各类内容(文本、图像、表格)。
不同内容类型需要采用相应的处理技术才能有效提取其信息内容,通用方法往往产生次优结果。
python
defextract_multimodal_content(sections, doc):
"""使用专用工具从每种模态中提取内容。"""
extracted_content= []
forsectioninsections:
section_content= {
"title": section["title"],
"level": section["level"],
"text_elements": [],
"images": [],
"tables": []
}
# 处理节中的每个页面
forpage_numinrange(section["start_page"], section["end_page"] +1):
page=doc[page_num]
# 使用 PyMuPDF 的文本提取功能提取文本
text_blocks=page.get_text("blocks")
forblockintext_blocks:
ifblock[6] ==0: # 文本块
section_content["text_elements"].append({
"text": block[4],
"bbox": block[:4],
"page": page_num
})
# 使用 PyMuPDF 的图像提取功能提取图像
image_list=page.get_images(full=True)
forimg_index, img_infoinenumerate(image_list):
xref=img_info[0]
base_image=doc.extract_image(xref)
image_data= {
"image_data": base_image["image"],
"extension": base_image["ext"],
"bbox": page.get_image_bbox(img_info),
"page": page_num
}
section_content["images"].append(image_data)
# 使用专门的表格提取工具提取表格
# 在此示例中,我们将使用简化方法
tables=extract_tables_from_page(page)
fortableintables:
section_content["tables"].append({
"data": table,
"page": page_num
})
extracted_content.append(section_content)
returnextracted_content
defextract_tables_from_page(page):
"""
使用专门的表格检测从页面中提取表格。
在生产系统中,您将使用专用的表格提取
库,如 Camelot、Tabula 或深度学习模型。
"""
# 为说明目的简化实现
tables= []
# 使用启发式或机器学习来识别表格区域
# 然后从这些区域提取结构化数据
returntables
3、关系保留的HTML转换
该模块将提取的多模态内容转换为结构化HTML格式,同时保留内容元素间的关联关系。
HTML作为标准化格式能够有效表示混合模态内容并保持结构完整性,为后续处理提供理想的数据基础。
python
frombs4importBeautifulSoup
importos
importbase64
defconvert_to_structured_html(extracted_content, output_dir):
"""将提取的多模态内容转换为保留关系的结构化 HTML。"""
os.makedirs(output_dir, exist_ok=True)
html_files= []
forsectioninextracted_content:
# 为此部分创建一个新的 HTML 文档
soup=BeautifulSoup("<article></article>", "html.parser")
article=soup.find("article")
# 添加节标题
header=soup.new_tag(f"h{section['level']}")
header.string=section["title"]
article.append(header)
# 按页面和位置对所有元素进行排序
all_elements= []
# 添加文本元素
fortext_eleminsection["text_elements"]:
all_elements.append({
"type": "text",
"data": text_elem,
"page": text_elem["page"],
"y_pos": text_elem["bbox"][1] # 用于排序的 y 坐标
})
# 添加图像
fori, img_data_iteminenumerate(section["images"]):
# 将图像保存到文件
img_filename=f"{section['title'].replace(' ', '_')}_img_{i}.{img_data_item['extension']}"
img_path=os.path.join(output_dir, img_filename)
withopen(img_path, "wb") asf:
f.write(img_data_item["image_data"])
all_elements.append({
"type": "image",
"data": {
"path": img_path,
"bbox": img_data_item["bbox"]
},
"page": img_data_item["page"],
"y_pos": img_data_item["bbox"][1] # 用于排序的 y 坐标
})
# 添加表格
fori, tableinenumerate(section["tables"]):
all_elements.append({
"type": "table",
"data": table["data"],
"page": table["page"],
"y_pos": 0 # 在生产环境中会使用实际位置
})
# 按页面然后按 y 位置对元素进行排序
all_elements.sort(key=lambdax: (x["page"], x["y_pos"]))
# 按正确顺序将元素添加到 HTML
foreleminall_elements:
ifelem["type"] =="text":
p=soup.new_tag("p")
p.string=elem["data"]["text"]
article.append(p)
elifelem["type"] =="image":
figure=soup.new_tag("figure")
img_tag=soup.new_tag("img", src=elem["data"]["path"])
figure.append(img_tag)
# 查找潜在的标题(图像正下方的文本元素)
idx=all_elements.index(elem)
ifidx+1<len(all_elements) andall_elements[idx+1]["type"] =="text":
next_elem=all_elements[idx+1]
ifnext_elem["page"] ==elem["page"] andnext_elem["y_pos"] -elem["y_pos"] <50:
# 这段文字很可能是一个标题
figcaption=soup.new_tag("figcaption")
figcaption.string=next_elem["data"]["text"]
figure.append(figcaption)
article.append(figure)
elifelem["type"] =="table":
# 将表格数据转换为 HTML 表格
table_tag=soup.new_tag("table")
forrow_datainelem["data"]:
tr=soup.new_tag("tr")
forcellinrow_data:
td=soup.new_tag("td")
td.string=str(cell)
tr.append(td)
table_tag.append(tr)
article.append(table_tag)
# 保存 HTML 文件
html_filename=f"{section['title'].replace(' ', '_')}.html"
html_path=os.path.join(output_dir, html_filename)
withopen(html_path, "w", encoding="utf-8") asf:
f.write(str(soup))
html_files.append(html_path)
returnhtml_files
4、关系保留的语义分块
HTML转换为多模态内容的标准化表示提供了统一的处理基础,同时保持了结构完整性。
该模块将HTML内容划分为语义完整的片段,同时维护不同元素间的关联关系。
有效的分块策略对检索质量具有决定性影响。过大的块会降低检索精度,而过小的块则会丢失重要的上下文信息。
python
frombs4importBeautifulSoup
importnetworkxasnx
defcreate_semantic_chunks_with_relationships(html_files, max_chunk_size=1000):
"""创建语义块,同时保留元素之间的关系。"""
chunks= []
relationship_graph=nx.DiGraph()
forhtml_fileinhtml_files:
withopen(html_file, "r", encoding="utf-8") asf:
html_content=f.read()
soup=BeautifulSoup(html_content, "html.parser")
# 提取节标题
section_title=soup.find(["h1", "h2", "h3", "h4", "h5", "h6"]).get_text()
section_id=f"section_{len(chunks)}"
# 将节节点添加到关系图
relationship_graph.add_node(section_id, type="section", title=section_title)
# 查找用于分块的语义边界
boundaries=soup.find_all(["h1", "h2", "h3", "h4", "h5", "h6", "section"])
iflen(boundaries) <=1:
# 没有内部分界线,处理整个部分
current_chunk= {
"id": f"chunk_{len(chunks)}",
"html": str(soup),
"text": soup.get_text(separator=" ", strip=True),
"parent": section_id
}
chunks.append(current_chunk)
relationship_graph.add_node(current_chunk["id"], type="chunk")
relationship_graph.add_edge(section_id, current_chunk["id"], relation="contains")
else:
# 处理每个子部分
foriinrange(len(boundaries) -1):
start=boundaries[i]
end=boundaries[i+1]
# 收集开始和结束之间的所有元素
elements= []
current=start.next_sibling
whilecurrentandcurrent!=end:
ifcurrent.name: # 跳过 NavigableString
elements.append(current)
current=current.next_sibling
# 从这些元素创建块
ifelements:
chunk_soup=BeautifulSoup("<div></div>", "html.parser")
chunk_div=chunk_soup.find("div")
# 添加标题
chunk_div.append(start.copy())
# 添加所有元素
forelementinelements:
chunk_div.append(element.copy())
# 检查块是否太大
chunk_text=chunk_div.get_text(separator=" ", strip=True)
iflen(chunk_text) >max_chunk_size:
# 进一步拆分此块
sub_chunks=split_large_chunk(chunk_div, max_chunk_size)
forsub_chunkinsub_chunks:
sub_id=f"chunk_{len(chunks)}"
sub_chunk_obj= {
"id": sub_id,
"html": str(sub_chunk),
"text": sub_chunk.get_text(separator=" ", strip=True),
"parent": section_id
}
chunks.append(sub_chunk_obj)
relationship_graph.add_node(sub_id, type="chunk")
relationship_graph.add_edge(section_id, sub_id, relation="contains")
else:
# 按原样添加块
chunk_id=f"chunk_{len(chunks)}"
chunk_obj= {
"id": chunk_id,
"html": str(chunk_div),
"text": chunk_text,
"parent": section_id
}
chunks.append(chunk_obj)
relationship_graph.add_node(chunk_id, type="chunk")
relationship_graph.add_edge(section_id, chunk_id, relation="contains")
# 为图像和表格添加特殊处理,以确保它们正确连接
process_special_elements(soup, chunks, relationship_graph)
returnchunks, relationship_graph
defsplit_large_chunk(chunk_div, max_chunk_size):
"""根据段落将大块拆分为较小的块。"""
# 为简洁起见,省略了实现细节
return [chunk_div] # 占位符
defprocess_special_elements(soup, chunks, graph):
"""处理图像和表格以确保正确的••关系。"""
# 为简洁起见,省略了实现细节
pass