RAGFlow框架優(yōu)化經(jīng)驗(yàn)分享(附代碼):圖文識(shí)別+動(dòng)態(tài)分塊 、API調(diào)優(yōu)+源碼修改
在這個(gè)過程中很高興能了解到很多之前沒涉獵過的行業(yè)和場(chǎng)景,其中經(jīng)常被集中問到的問題大概有四個(gè):中小企業(yè)做RAG知識(shí)庫落地選擇框架哪個(gè)比較好?如果選擇 RAGFlow 如何進(jìn)行定制化開發(fā)?如何對(duì)文檔中的圖片進(jìn)行識(shí)別檢索?如何對(duì)復(fù)雜文檔進(jìn)行動(dòng)態(tài)分塊等。
關(guān)于選擇什么框架的問題,仁者見仁,需要綜合對(duì)比的盆友可以參考我之前發(fā)的那篇文章企業(yè)RAG落地避坑指南:自主開發(fā) vs 三大框架,核心配置與選型全解析。這篇以 RAGFlow 框架為例,針對(duì)上述后三個(gè)問題結(jié)合目前團(tuán)隊(duì)實(shí)踐經(jīng)驗(yàn),給各位做個(gè)分享,大家辨證參考。
畢竟,RAG并沒有“一招鮮”的神奇魔法,傳說那幾個(gè)大廠手里掌握的RAG”核心技術(shù)“,私以為也是經(jīng)過了必要且復(fù)雜的“策略優(yōu)化-管道設(shè)計(jì)-訓(xùn)練-調(diào)優(yōu)-發(fā)布”等專業(yè)開發(fā)流程,不過成熟的開源應(yīng)用框架,無疑是更有想象空間的社會(huì)化大創(chuàng)新。
注:本篇會(huì)比較偏開發(fā)導(dǎo)向,非技術(shù)向盆友選擇性翻翻就好。
以下,enjoy:
1、優(yōu)化實(shí)施路線圖
以上篇介紹的機(jī)械加工行業(yè)的維保場(chǎng)景為例RAGFlow+DeepSeek-R1:14b落地案例分享(足夠詳細(xì)):機(jī)加工行業(yè)設(shè)備維保場(chǎng)景,推薦先采用官方的 Python API 進(jìn)行合理的優(yōu)化配置后,再修改項(xiàng)目源碼,最后根據(jù)業(yè)務(wù)目標(biāo)做必要的高級(jí)優(yōu)化拓展。此處附上一張個(gè)人項(xiàng)目實(shí)施過程中積累的些優(yōu)化要點(diǎn),供各位參考:
1.1 階段一:API 優(yōu)化配置
配置不同文檔類型的解析策略
調(diào)整檢索參數(shù)優(yōu)化語義搜索質(zhì)量
定制大模型提示詞以適應(yīng)機(jī)械行業(yè)特點(diǎn)
1.2 階段二:基礎(chǔ)源碼修改
實(shí)現(xiàn)專業(yè)術(shù)語處理模塊
開發(fā)查詢路由機(jī)制
增加上下文增強(qiáng)功能
1.3 階段三:高級(jí)優(yōu)化擴(kuò)展
實(shí)現(xiàn)多級(jí)索引結(jié)構(gòu)
開發(fā)高性能緩存機(jī)制
添加查詢?nèi)罩痉治鱿到y(tǒng)
2、6 個(gè)官方 Python API 優(yōu)化全解析
根據(jù) RAGFlow 官方 Python API 文檔,為大家完整梳理了所有可進(jìn)行 API 優(yōu)化的模塊和參數(shù)。這些 API 調(diào)優(yōu)可以不修改源碼,直接通過參數(shù)配置實(shí)現(xiàn)性能提升。
建議在看完之后還是去官網(wǎng)看下原文,親自動(dòng)手試過一遍會(huì)有不一樣的體感。原文出處:
https://ragflow.io/docs/dev/python_api_reference
2.1 數(shù)據(jù)集管理
創(chuàng)建數(shù)據(jù)集 create_dataset
RAGFlow.create_dataset(
name: str,
avatar: str = "",
description: str = "",
embedding_model: str = "BAAI/bge-large-zh-v1.5",
language: str = "English",
permission: str = "me",
chunk_method: str = "naive",
parser_config: DataSet.ParserConfig = None
)
優(yōu)化參數(shù):
- embedding_model: 選擇合適的嵌入模型,影響檢索質(zhì)量
中文場(chǎng)景推薦: "BAAI/bge-large-zh-v1.5"
英文場(chǎng)景可選: "BAAI/bge-large-en-v1.5"
- language: 選擇與文檔匹配的語言
- chunk_method: 關(guān)鍵參數(shù),根據(jù)文檔類型選擇最佳分塊策略
"naive": 通用文檔
"paper": 論文/設(shè)備手冊(cè)
"book": 結(jié)構(gòu)化書籍
"table": 表格數(shù)據(jù)
"qa": 問答格式文檔
"picture": 圖片文檔
"one": 整個(gè)文檔作為一個(gè)塊
"knowledge_graph": 知識(shí)圖譜
- parser_config: 精細(xì)調(diào)整解析配置
chunk_token_num: 控制分塊大小
delimiter: 自定義分隔符
layout_recognize: 是否啟用布局識(shí)別
raptor: 高級(jí)解析選項(xiàng)
更新數(shù)據(jù)集 DataSet.update
DataSet.update(update_message : dict)
優(yōu)化參數(shù):
embedding_model: 更換更適合的嵌入模型
chunk_method: 調(diào)整分塊策略
meta_fields: 更新元數(shù)據(jù)字段
2.2 文件管理 (FILE MANAGEMENT)
上傳文檔 DataSet.upload_documents
DataSet.upload_documents(document_list : list[dict])
優(yōu)化參數(shù):
display_name: 文件顯示名,方便檢索與管理
blob: 文件內(nèi)容
更新文檔 Document.update
Document.update(update_message : dict)
優(yōu)化參數(shù):
- chunk_method: 文檔分塊方法
- parser_config: 文檔解析配置
對(duì)于圖文文檔,可設(shè)置: "layout_recognize": True
對(duì)于表格文檔,可設(shè)置: "html4excel": True
解析文檔
DataSet.async_parse_documents(document_ids : list[str])
用于觸發(fā)文檔解析流程,支持批量處理。
2.3 分塊管理 (CHUNK MANAGEMENT)
添加分塊 Document.add_chunk
Document.add_chunk(content: str, important_keywords: list[str] = [])
優(yōu)化參數(shù):
important_keywords: 關(guān)鍵詞標(biāo)注,增強(qiáng)檢索相關(guān)性
更新分塊 Chunk.update
Chunk.update(update_message : dict)
優(yōu)化參數(shù):
content: 更新分塊內(nèi)容
important_keywords: 更新關(guān)鍵詞
available: 控制分塊可用性
檢索 RAGFlow.retrieve(關(guān)鍵API)
RAGFlow.retrieve(
question: str = "",
dataset_ids: list[str] = None,
document_ids: list[str] = None,
page: int = 1,
page_size: int = 30,
similarity_threshold: float = 0.2,
vector_similarity_weight: float = 0.3,
top_k: int = 1024,
rerank_id: str = None,
keyword: bool = False,
highlight: bool = False
)
優(yōu)化參數(shù) (最重要的檢索相關(guān)參數(shù)):
- similarity_threshold: 相似度閾值,影響召回范圍
- vector_similarity_weight: 向量相似度權(quán)重與關(guān)鍵詞匹配權(quán)重的比例
設(shè)置為 0-1 之間,值越大向量權(quán)重越高
工業(yè)領(lǐng)域建議參考0.3-0.5,平衡專業(yè)術(shù)語與語義理解
- top_k: 參與向量檢索的 chunk 數(shù)量,影響檢索范圍
- rerank_id: 重排序模型 ID,提升檢索精度
- keyword: 開啟關(guān)鍵詞匹配,對(duì)專業(yè)領(lǐng)域極其有用
- highlight: 高亮匹配內(nèi)容,幫助理解匹配原因
2.4 聊天助手管理
創(chuàng)建聊天助手 RAGFlow.create_chat
RAGFlow.create_chat(
name: str,
avatar: str = "",
dataset_ids: list[str] = [],
llm: Chat.LLM = None,
prompt: Chat.Prompt = None
)
優(yōu)化參數(shù):
- llm: LLM 模型配置
model_name: 模型名稱
temperature: 溫度,影響創(chuàng)造性
top_p: 詞匯采樣范圍
presence_penalty: 重復(fù)懲罰
frequency_penalty: 頻率懲罰
max_token: 最大輸出 token 數(shù)
- prompt: 提示詞配置
similarity_threshold: 相似度閾值
keywords_similarity_weight: 關(guān)鍵詞相似度權(quán)重
top_n: 提供給 LLM 的 chunk 數(shù)量
rerank_model: 重排序模型
top_k: 重排序參與的候選數(shù)量 empty_response: 無匹配時(shí)的回復(fù)
show_quote: 是否顯示引用來源
prompt: 系統(tǒng)提示詞內(nèi)容
更新聊天助手 Chat.update
Chat.update(update_message : dict)
優(yōu)化參數(shù): 同 create_chat 中的參數(shù)
2.5 會(huì)話管理 (SESSION MANAGEMENT)
創(chuàng)建會(huì)話 Chat.create_session
Chat.create_session(name: str = "New session")
提問 Session.ask
Session.ask(question: str = "", stream: bool = False, **kwargs)
優(yōu)化參數(shù):
stream: 流式輸出,提升用戶體驗(yàn)
**kwargs: 可傳遞給 prompt 中定義的變量
2.6 代理管理 (AGENT MANAGEMENT)
創(chuàng)建代理會(huì)話 Agent.create_session
Agent.create_session(id, rag, **kwargs)
代理提問 Session.ask
Session.ask(question: str = "", stream: bool = False)
與普通會(huì)話的ask方法類似。
3、調(diào)整項(xiàng)目源碼思路參考
3.1 專業(yè)術(shù)語處理
需要在檢索引擎層面添加工業(yè)領(lǐng)域同義詞和術(shù)語映射:
# 需要修改源碼的示例邏輯
class CustomTerminologyProcessor:
def __init__(self, terminology_mapping):
self.terminology_mapping = terminology_mapping # 同義詞映射表
def process_query(self, query):
# 專業(yè)術(shù)語標(biāo)準(zhǔn)化
# 車間俚語轉(zhuǎn)換為標(biāo)準(zhǔn)術(shù)語
processed_query = query
for slang, standard_term in self.terminology_mapping.items():
processed_query = processed_query.replace(slang, standard_term)
return processed_query
修改點(diǎn):
在查詢預(yù)處理階段添加定制的術(shù)語處理模塊
需要在 RAGFlow 的查詢管道中修改源碼添加此功能
3.2 多級(jí)索引結(jié)構(gòu)實(shí)現(xiàn)
需要定制 Milvus 索引策略,實(shí)現(xiàn)基礎(chǔ)索引層和語義索引層的混合索引:
# 這部分需要修改源碼,以下是概念性代碼
class CustomHybridIndexBuilder:
def __init__(self, vector_db_client):
self.client = vector_db_client
def create_scalar_indices(self, collection_name, fields):
# 創(chuàng)建設(shè)備編號(hào)、故障代碼等標(biāo)量索引
for field in fields:
self.client.create_index(collection_name, field, "scalar")
def create_vector_indices(self, collection_name, fields):
# 創(chuàng)建向量索引
for field in fields:
self.client.create_index(collection_name, field, {"index_type": "HNSW", "params": {"M": 16, "efConstruction": 200}})
修改點(diǎn):
修改 RAGFlow 的索引構(gòu)建模塊擴(kuò)展
Milvus 客戶端接口以支持多索引策略
3.3 查詢路由設(shè)計(jì)
需要實(shí)現(xiàn)定制化的查詢路由邏輯,識(shí)別不同類型的查詢并路由到最合適的檢索通道:
# 查詢路由器 - 需要源碼修改實(shí)現(xiàn)
class QueryRouter:
def route_query(self, query_text):
if self._is_equipment_code(query_text):
return "exact_match", {"field": "equipment_code"}
elif self._is_fault_code(query_text):
return "exact_match", {"field": "fault_code"}
elif self._is_parameter_query(query_text):
return "parameter_lookup", {"field": "parameter_name"}
else:
return "semantic_search", {"model": "embedding_model"}
修改點(diǎn):
在 RAGFlow 的查詢處理流程中添加查詢分類和路由機(jī)制
實(shí)現(xiàn)針對(duì)不同查詢類型的專用處理通道
3.4 上下文增強(qiáng)機(jī)制
增加查詢上下文增強(qiáng),融入設(shè)備信息、歷史記錄等:
# 上下文增強(qiáng)器 - 需要修改源碼實(shí)現(xiàn)
class ContextEnhancer:
def enhance_query(self, query, session_history, equipment_metadata=None):
# 添加設(shè)備上下文信息
if equipment_metadata:
query_context = f"設(shè)備型號(hào): {equipment_metadata['model']}, 生產(chǎn)年份: {equipment_metadata['year']}\n"
query_context += query
# 添加歷史查詢信息
if session_history:
relevant_history = self._extract_relevant_history(session_history, query)
query_context = f"參考?xì)v史信息: {relevant_history}\n" + query_context
return query_context
修改點(diǎn):
修改會(huì)話管理模塊,實(shí)現(xiàn)會(huì)話狀態(tài)跟蹤
增加設(shè)備元數(shù)據(jù)關(guān)聯(lián)機(jī)制
在查詢處理流程中加入上下文增強(qiáng)步驟
4、圖文結(jié)合文檔處理方案
依然是兩種方案,直接使用RAGFlow API方案優(yōu)勢(shì)是更簡(jiǎn)單,使用現(xiàn)有功能,無需額外的模型調(diào)用,也能夠直接顯示原始圖片,視覺效果更好,處理速度更快,不依賴外部 API,當(dāng)然成本也無疑更低。
但使用一個(gè)多模態(tài)模型進(jìn)行預(yù)處理的方案優(yōu)勢(shì)也很明顯,圖片內(nèi)容被轉(zhuǎn)換為文本,便于向量化和語義搜索,也可以依托多模態(tài)模型的能力,提供更豐富的圖片內(nèi)容解釋。
目前實(shí)際測(cè)試下來,采用兩種方案的組合,效果更加穩(wěn)定。
4.1 使用多模態(tài)預(yù)處理生成圖片描述
# 使用多模態(tài)模型生成圖片描述
processor = MultimodalDocumentProcessor(api_key="YOUR_API_KEY")
enhanced_docs = processor.process_pdf("設(shè)備手冊(cè).pdf")
4.2 使用 RAGFlow 處理和存儲(chǔ)原始圖片
# 配置保留圖片的數(shù)據(jù)集
dataset = rag_object.create_dataset(
name="圖文設(shè)備手冊(cè)",
chunk_method="paper",
parser_cnotallow={"layout_recognize": True}
)
# 上傳原始PDF文檔
with open("設(shè)備手冊(cè).pdf", "rb") as f:
dataset.upload_documents([{"display_name": "設(shè)備手冊(cè).pdf", "blob": f.read()}])
4.3 創(chuàng)建能夠提供文本描述和圖片引用的助手
assistant = rag_object.create_chat(
name="圖文設(shè)備專家",
dataset_ids=[dataset.id],
prompt=Chat.Prompt(
prompt="""你是設(shè)備維修專家。回答時(shí),請(qǐng)同時(shí)提供:
1. 文字描述解釋故障和解決方案
2. 引用相關(guān)圖片,包括圖片描述
3. 告訴用戶可以參考哪些圖片獲取更多信息
{knowledge}"""
)
)
4.4 源碼修改的一些建議
增強(qiáng)圖片提取和處理:
修改 PDF 解析器,更準(zhǔn)確地綁定文本和相關(guān)圖片
增加圖片內(nèi)容分析功能,自動(dòng)標(biāo)注圖片類型(如"故障圖"、"結(jié)構(gòu)圖"等)
實(shí)現(xiàn)圖文混合索引:
為圖片創(chuàng)建特殊索引,支持通過圖片內(nèi)容或相關(guān)文本檢索圖片
在檢索結(jié)果中包含圖片 URL 或直接嵌入圖片
改進(jìn)響應(yīng)生成:
修改聊天助手的響應(yīng)生成邏輯,自動(dòng)識(shí)別圖片引用
在生成的回答中包含相關(guān)圖片或圖片鏈接
5、動(dòng)態(tài)分塊策略參考
RAGFlow的chunk_method參數(shù)是在數(shù)據(jù)集級(jí)別或文檔級(jí)別設(shè)置的,不支持在單個(gè)文檔內(nèi)部動(dòng)態(tài)切換不同的分塊策略。但實(shí)際情況是,同一文檔中的不同部分可能需要不同的處理方式,比如針對(duì)段落、圖片、表格、圖表等,使用單一的分塊策略很難同時(shí)兼顧所有這些內(nèi)容類型的特點(diǎn)。
5.1 4種分塊策略對(duì)比
源碼修改方案:
修改 RAGFlow 的文檔解析器,使其能夠識(shí)別文檔中的不同部分并應(yīng)用不同的分塊策略,但這需要深入修改 RAGFlow 的核心處理邏輯,如果沒有深入理解RAGFlow的全局代碼,建議不要這么做。
文檔預(yù)處理方案:
在上傳到 RAGFlow 前預(yù)處理文檔,將其拆分成不同類型的子文檔。例如,將設(shè)備手冊(cè)拆分為純文本部分、表格部分、圖文部分等然后分別上傳到不同的數(shù)據(jù)集,每個(gè)數(shù)據(jù)集使用適合的分塊策略。
這個(gè)方案優(yōu)點(diǎn)是,可以充分利用RAGFlow針對(duì)不同內(nèi)容類型的專門分塊策略,但問題也很明顯,就是文檔上下文被拆分,可能影響整體理解。
自定義分塊方案:
不使用RAGFlow 的自動(dòng)分塊,而是手動(dòng)控制分塊,使用chunk_method="one"將整個(gè)文檔作為一個(gè)塊導(dǎo)入,然后使用自定義邏輯創(chuàng)建更細(xì)粒度的分塊。這種做法無疑可以實(shí)現(xiàn)最精細(xì)的控制、保留文檔完整性,當(dāng)然缺點(diǎn)就是實(shí)現(xiàn)上會(huì)相對(duì)麻煩。
混合模型方案:
創(chuàng)建多個(gè)使用不同分塊策略的數(shù)據(jù)集,將同一文檔上傳到所有這些數(shù)據(jù)集,在檢索時(shí)查詢所有數(shù)據(jù)集并合并結(jié)果。這種方法保持文檔完整性,但創(chuàng)建多個(gè)使用不同分塊策略的副本,會(huì)造成存儲(chǔ)冗余,檢索時(shí)需要合并多個(gè)結(jié)果集。
5.2 推薦自定義方案
這種方法使用RAGFlow的API但完全控制分塊過程,最靈活且無需修改源碼。
完整保留原始文檔:
使用 chunk_method="one"將文檔整體上傳保留文檔的完整性和上下文關(guān)系
自定義內(nèi)容識(shí)別:
使用 PyMuPDF 識(shí)別文檔中的不同內(nèi)容類型:
文本、段落、表格內(nèi)容圖片及其相關(guān)描述、章節(jié)標(biāo)題和結(jié)構(gòu)
動(dòng)態(tài)創(chuàng)建精細(xì)分塊:
文本塊: 基于段落和語義分界
表格塊: 保留表格結(jié)構(gòu)和行列關(guān)系
圖文塊: 關(guān)聯(lián)圖片和周圍的描述文本
添加分塊類型標(biāo)記:
每個(gè)分塊添加類型標(biāo)識(shí)符[text], [table], [image]等
方便檢索時(shí)區(qū)分不同類型的內(nèi)容
豐富關(guān)鍵詞提取:
為每個(gè)分塊提取相關(guān)關(guān)鍵詞
保留章節(jié)上下文信息
from ragflow_sdk import RAGFlow
import fitz # PyMuPDF
import re
import pandas as pd
import io
import logging
from typing import List, Dict, Any, Tuple
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
class DynamicChunker:
"""針對(duì)混合格式文檔的自定義分塊器"""
def __init__(self, api_key: str, base_url: str):
self.rag_object = RAGFlow(api_key=api_key, base_url=base_url)
def process_document(self, pdf_path: str, dataset_name: str = None) -> str:
"""處理PDF文檔并實(shí)現(xiàn)自定義分塊"""
# 1. 創(chuàng)建或獲取數(shù)據(jù)集
if dataset_name:
datasets = self.rag_object.list_datasets(name=dataset_name)
if datasets:
dataset = datasets[0]
logger.info(f"使用現(xiàn)有數(shù)據(jù)集: {dataset.name}, ID: {dataset.id}")
else:
dataset = self.rag_object.create_dataset(
name=dataset_name,
embedding_model="BAAI/bge-large-zh-v1.5",
language="Chinese",
chunk_method="one" # 使用"one"方式,保持文檔完整性
)
logger.info(f"創(chuàng)建新數(shù)據(jù)集: {dataset.name}, ID: {dataset.id}")
else:
dataset = self.rag_object.create_dataset(
name="混合文檔庫",
embedding_model="BAAI/bge-large-zh-v1.5",
language="Chinese",
chunk_method="one"
)
logger.info(f"創(chuàng)建新數(shù)據(jù)集: {dataset.name}, ID: {dataset.id}")
# 2. 上傳文檔
with open(pdf_path, "rb") as f:
document_blob = f.read()
docs = dataset.upload_documents([{
"display_name": pdf_path.split("/")[-1],
"blob": document_blob
}])
if not docs:
logger.error("文檔上傳失敗")
return None
doc = docs[0]
logger.info(f"文檔上傳成功, ID: {doc.id}")
# 3. 分析文檔結(jié)構(gòu)并創(chuàng)建自定義分塊
chunks = self._create_custom_chunks(pdf_path)
logger.info(f"創(chuàng)建了 {len(chunks)} 個(gè)自定義分塊")
# 4. 手動(dòng)添加分塊到文檔
chunk_ids = []
for chunk in chunks:
content = chunk["content"]
keywords = chunk["keywords"]
chunk_type = chunk["type"]
# 添加分塊類型標(biāo)記以便未來檢索
content_with_marker = f"[{chunk_type}]\n{content}"
# 添加分塊
new_chunk = doc.add_chunk(
cnotallow=content_with_marker,
important_keywords=keywords
)
chunk_ids.append(new_chunk.id)
logger.info(f"添加分塊: 類型={chunk_type}, ID={new_chunk.id}, 關(guān)鍵詞數(shù)量={len(keywords)}")
return doc.id
def _create_custom_chunks(self, pdf_path: str) -> List[Dict[str, Any]]:
"""分析PDF并創(chuàng)建自定義分塊"""
pdf_doc = fitz.open(pdf_path)
chunks = []
# 用于存儲(chǔ)當(dāng)前上下文
current_section = ""
current_text_chunk = ""
current_table_data = []
current_keywords = []
for page_idx, page in enumerate(pdf_doc):
# 提取頁面文本
text = page.get_text()
# 檢測(cè)章節(jié)標(biāo)題
section_headers = self._detect_section_headers(text)
if section_headers:
# 如果有積累的文本塊,先保存
if current_text_chunk:
chunks.append({
"content": current_text_chunk,
"keywords": list(set(current_keywords)),
"type": "text",
"section": current_section
})
current_text_chunk = ""
current_keywords = []
# 更新當(dāng)前章節(jié)
current_section = section_headers[0]
current_keywords.append(current_section)
# 檢測(cè)表格
tables = self._detect_tables(page)
if tables:
# 如果有積累的文本塊,先保存
if current_text_chunk:
chunks.append({
"content": current_text_chunk,
"keywords": list(set(current_keywords)),
"type": "text",
"section": current_section
})
current_text_chunk = ""
current_keywords = []
# 處理表格
for table in tables:
table_content = self._format_table(table)
table_keywords = self._extract_keywords_from_table(table)
chunks.append({
"content": table_content,
"keywords": list(set(table_keywords + current_keywords)),
"type": "table",
"section": current_section
})
# 檢測(cè)圖片
images = self._detect_images(page)
for img_idx, img in enumerate(images):
# 提取圖片周圍的文本作為上下文
img_context = self._extract_image_context(text, img["bbox"])
# 如果找到圖片相關(guān)文本
if img_context:
# 如果有積累的文本塊,先保存
if current_text_chunk:
chunks.append({
"content": current_text_chunk,
"keywords": list(set(current_keywords)),
"type": "text",
"section": current_section
})
current_text_chunk = ""
current_keywords = []
# 創(chuàng)建圖文塊
image_text_content = f"圖片描述: {img_context}\n圖片位置: 第{page_idx+1}頁"
image_keywords = self._extract_keywords(img_context)
chunks.append({
"content": image_text_content,
"keywords": list(set(image_keywords + current_keywords)),
"type": "image",
"section": current_section
})
# 防止重復(fù)處理同一段文本
text = text.replace(img_context, "", 1)
# 處理剩余文本
if text.strip():
# 按段落拆分
paragraphs = self._split_into_paragraphs(text)
for para in paragraphs:
if len(para.strip()) < 10: # 忽略太短的段落
continue
# 累積文本直到達(dá)到合適的大小
current_text_chunk += para + "\n\n"
current_keywords.extend(self._extract_keywords(para))
# 檢查是否應(yīng)該創(chuàng)建新的文本塊
if len(current_text_chunk) > 1500: # 大約300-500個(gè)詞
chunks.append({
"content": current_text_chunk,
"keywords": list(set(current_keywords)),
"type": "text",
"section": current_section
})
current_text_chunk = ""
current_keywords = []
# 處理最后一個(gè)文本塊
if current_text_chunk:
chunks.append({
"content": current_text_chunk,
"keywords": list(set(current_keywords)),
"type": "text",
"section": current_section
})
return chunks
def _detect_section_headers(self, text: str) -> List[str]:
"""檢測(cè)章節(jié)標(biāo)題"""
# 這是一個(gè)簡(jiǎn)化的實(shí)現(xiàn),可以根據(jù)實(shí)際文檔格式調(diào)整
header_patterns = [
r"^第[一二三四五六七八九十\d]+章\s*(.+)$",
r"^\d+\.\d*\s+(.+)$",
r"^[一二三四五六七八九十]+[、..]\s*(.+)$"
]
headers = []
for line in text.split("\n"):
line = line.strip()
for pattern in header_patterns:
match = re.match(pattern, line)
if match:
headers.append(line)
break
return headers
def _detect_tables(self, page) -> List[Any]:
"""檢測(cè)頁面中的表格"""
# 這里使用簡(jiǎn)化的檢測(cè)邏輯,實(shí)際應(yīng)用中可能需要更復(fù)雜的表格檢測(cè)算法
# 例如,可以尋找包含多個(gè)垂直和水平線的區(qū)域
tables = []
# 簡(jiǎn)單表格檢測(cè):查找水平線和垂直線的集中區(qū)域
# 這里僅作為占位示例,實(shí)際實(shí)現(xiàn)會(huì)更復(fù)雜
horizontal_lines = []
vertical_lines = []
for drawing in page.get_drawings():
for item in drawing["items"]:
if item["type"] == "l": # 線段
x0, y0, x1, y1 = item["rect"]
if abs(y1 - y0) < 2: # 水平線
horizontal_lines.append((x0, y0, x1, y1))
elif abs(x1 - x0) < 2: # 垂直線
vertical_lines.append((x0, y0, x1, y1))
# 簡(jiǎn)單的表格判定:有足夠多的水平線和垂直線
if len(horizontal_lines) >= 3 and len(vertical_lines) >= 2:
# 找出所有線的邊界,作為表格邊界
min_x = min([min(l[0], l[2]) for l in horizontal_lines + vertical_lines])
max_x = max([max(l[0], l[2]) for l in horizontal_lines + vertical_lines])
min_y = min([min(l[1], l[3]) for l in horizontal_lines + vertical_lines])
max_y = max([max(l[1], l[3]) for l in horizontal_lines + vertical_lines])
# 提取表格區(qū)域的文本
table_rect = fitz.Rect(min_x, min_y, max_x, max_y)
table_text = page.get_text("text", clip=table_rect)
# 簡(jiǎn)化的表格結(jié)構(gòu)化(實(shí)際應(yīng)用中需要更復(fù)雜的邏輯)
table_rows = table_text.split("\n")
table = []
for row in table_rows:
if row.strip():
cells = row.split()
if len(cells) >= 2: # 至少有2個(gè)單元格才視為有效行
table.append(cells)
if table:
tables.append(table)
return tables
def _detect_images(self, page) -> List[Dict[str, Any]]:
"""檢測(cè)頁面中的圖片"""
images = []
# 獲取頁面上的圖片對(duì)象
img_list = page.get_images(full=True)
for img_idx, img_info in enumerate(img_list):
xref = img_info[0]
base_image = page.parent.extract_image(xref)
if base_image:
# 尋找圖片在頁面上的位置
img_rects = []
for rect in page.get_image_rects(xref):
img_rects.append(rect)
if img_rects:
# 使用第一個(gè)找到的位置
bbox = img_rects[0]
images.append({
"bbox": [bbox.x0, bbox.y0, bbox.x1, bbox.y1]
})
return images
def _extract_image_context(self, text: str, bbox: List[float], context_size: int = 200) -> str:
"""提取圖片周圍的文本作為上下文"""
# 在文本中查找可能的圖片標(biāo)題,如"圖1","Figure 1"等
caption_patterns = [
r"圖\s*\d+[\..:]?\s*(.+?)(?:\n|$)",
r"Figure\s*\d+[\.:]?\s*(.+?)(?:\n|$)",
r"圖表\s*\d+[\..:]?\s*(.+?)(?:\n|$)"
]
for pattern in caption_patterns:
matches = re.finditer(pattern, text, re.IGNORECASE)
for match in matches:
return match.group(0)
# 如果找不到明確的圖片標(biāo)題,則嘗試提取圖片周圍的文本
# 這是一個(gè)簡(jiǎn)化實(shí)現(xiàn),實(shí)際應(yīng)用中可能需要更復(fù)雜的邏輯
lines = text.split("\n")
total_length = 0
start_line = 0
# 估計(jì)圖片在文本中的位置
# 這是一個(gè)非常粗略的估計(jì),實(shí)際應(yīng)用中需要更精確的方法
relative_position = bbox[1] / 1000 # 假設(shè)頁面高度為1000
target_position = int(len(text) * relative_position)
# 找到大致對(duì)應(yīng)的行
for i, line in enumerate(lines):
total_length += len(line) + 1 # +1 for newline
if total_length > target_position:
start_line = max(0, i - 2) # 從前兩行開始
break
# 提取上下文(當(dāng)前行及其前后幾行)
context_lines = lines[max(0, start_line):min(len(lines), start_line + 5)]
return "\n".join(line for line in context_lines if len(line.strip()) > 5)
def _format_table(self, table: List[List[str]]) -> str:
"""格式化表格為文本格式"""
if not table:
return ""
# 創(chuàng)建pandas DataFrame
df = pd.DataFrame(table[1:], columns=table[0] if len(table) > 1 else None)
# 轉(zhuǎn)換為字符串形式
result = io.StringIO()
df.to_csv(result, sep="|", index=False)
return result.getvalue()
def _extract_keywords_from_table(self, table: List[List[str]]) -> List[str]:
"""從表格中提取關(guān)鍵詞"""
keywords = []
# 表頭作為關(guān)鍵詞
if table and len(table) > 0:
keywords.extend(table[0])
# 第一列可能是行標(biāo)題,也加入關(guān)鍵詞
for row in table[1:] if len(table) > 1 else []:
if row and len(row) > 0:
keywords.append(row[0])
return keywords
def _split_into_paragraphs(self, text: str) -> List[str]:
"""將文本拆分為段落"""
# 按照多個(gè)換行符拆分
paragraphs = re.split(r"\n\s*\n", text)
return [p.strip() for p in paragraphs if p.strip()]
def _extract_keywords(self, text: str) -> List[str]:
"""從文本中提取關(guān)鍵詞"""
# 這是一個(gè)簡(jiǎn)化的關(guān)鍵詞提取方法
# 實(shí)際應(yīng)用中可以使用更復(fù)雜的NLP技術(shù),如TF-IDF、TextRank等
# 1. 移除停用詞和標(biāo)點(diǎn)
stop_words = {"的", "了", "和", "與", "或", "在", "是", "有", "被", "將", "把"}
cleaned_text = re.sub(r'[^\w\s]', ' ', text)
words = cleaned_text.split()
filtered_words = [w for w in words if w not in stop_words and len(w) > 1]
# 2. 簡(jiǎn)單詞頻統(tǒng)計(jì)
word_count = {}
for word in filtered_words:
if word in word_count:
word_count[word] += 1
else:
word_count[word] = 1
# 3. 選擇頻率最高的幾個(gè)詞作為關(guān)鍵詞
sorted_words = sorted(word_count.items(), key=lambda x: x[1], reverse=True)
return [word for word, count in sorted_words[:10] if count > 1]
# 使用示例
if __name__ == "__main__":
chunker = DynamicChunker(
api_key="YOUR_API_KEY",
base_url="http://YOUR_BASE_URL:9380"
)
doc_id = chunker.process_document("設(shè)備維修手冊(cè).pdf", "動(dòng)態(tài)分塊測(cè)試")
print(f"處理完成,文檔ID: {doc_id}")
(完)