TextIn vs. DeepDoc性能測評:RAGFlow解析升級完整教程(附二開代碼)

兩個月前在星球的會員群中,有人推薦了TextIn這款解析工具。我當時也是第一次聽說,最近一段時間陸續在手頭項目上測試了些以往認為是 Corner Case 的復雜布局文檔后,發現居然都有不錯的表現。后續了解到TextIn背后的公司叫合合信息,看起來還是有點陌生,不過這家公司旗下另外一款叫做“掃描全能王”的產品各位應該聽說過或者用過。
本著 Garbage in, Garbage out 的理念,構建健壯企業級RAG應用涉及的眾多復雜組件集成與優化中,解析組件的選擇無疑是首當其沖的重點命題。我在之前的文章里已經介紹了幾期關于原生開發和使用 Langchian、Llamaindex 框架開發 RAG 應用的示例。為了針對性的評測 TextIn 的實際解析效果,這篇就結合 RAGFlow 框架(v0.20.4版本)來做個整體演示。
這篇試圖說清楚:
解析工具的開源和商業化產品分類、API 和本地部署的兩種調用方式、在三類場景(純文本、表格、圖片)下TextIn與 Deepdoc 的效果對比、TextIn在 RAGFlow中二開的兩種實現方式等。
1、解析工具分類
1.1是否開源
文檔解析工具分為開源和閉源兩大類,在開放性、可控性、成本和功能深度上存在明顯差別。開源工具例如 PyMuPDF、MinerU、Marker 等,商業化產品例如這篇要介紹的TextIn。
開源產品
開源產品的優勢首先肯定是免費,其次就是高透明度,可以根據需求進行二開。當然,活躍的開源社區可以貢獻代碼、修復漏洞、提供支持等。
但同時劣勢也很明顯,首先技術門檻高是繞不開的問題,對非技術團隊或資源有限的組織挑戰較大。同時,因為要依賴社區支持,響應速度和問題解決的專業性、保障性通常不如商業閉源產品的官方支持。最后也是最重要的,就是特定場景功能不足。針對特定行業場景(例如復雜的財務表格、醫療報告結構化)的預訓練模型或精細化處理能力,可能不如成熟的商業閉源產品。
商業化產品
商業化產品的劣勢在于使用成本與低透明度(無法直接修改核心代碼)。優勢也很明顯,就是開箱即用,易于集成。通常都會提供完善的前端界面、SDK、清晰的文檔和示例,集成相對簡單快捷,使用技術門檻低。技術支持、問題響應上也都比較成熟。其次,在深度優化與特定功能上往往都會比開源工具表現更為出色。 畢竟這些廠商投入大量資源進行核心算法研發、模型訓練(尤其在特定領域如法律合同、醫學文獻、復雜表格識別)和性能優化,總會在精度、特定場景覆蓋和功能深度上具備技術優勢。當然,還有個持續更新與維護的好處。
1.2調用方式
在使用方法這個維度,主要有 API 調用和本地化部署兩類。
API 調用方法可以快速啟動,零運維。通常按需付費,避免了前期高昂的硬件和軟件許可投資。這點對于中小企業來說比較友好。但同時,對于中大型企業或者特定行業(金融,軍工等)往往不符合企業的數據合規要求。
本地部署模式反之最直接的好處,是可以保障數據安全與合規性。 文檔數據不用出本地,更容易滿足嚴格的合規和監管要求。劣勢就是相應的前期高投入,運維負擔重。部署復雜和上線周期長等問題。
總體來說,具體選擇往往取決于具體需求和資源情況。接下來的三個場景演示中,考慮便捷程度,演示環節選擇直接在TextIn的官網和RAGFlow的知識庫頁面進行直接評測對比,在RAGFlow的二開環節選擇直接集成Textin的 API 方式。
2、三類場景測評效果
為了盡可能的完整對比 Deepdoc 和 TextIn 在不同場景下的解析效果表現,這里用我歷史做過的項目中的幾個真實的 case 中純文本、復雜表格、圖文混排等三種類型的文檔,進行一個具體的測試。
2.1合同中的條款
首先是常見的法律法規類的純文本合同。這類文檔的格式相對比較規范,一般不涉及圖表,更多的是如何在分塊上能夠比較穩定的進行按照條款級進行切分。

上圖可以看到Textin 的條款識別是比較準確清晰的。同時,下圖可以看到 RAGFlow在Deepdoc 模式下對于條款的識別同樣沒什么問題。而且得益于 RAGFlow 提供了多種面向特定文檔類型的分塊策略(這里選擇是的 Law),所以針對這種典型的法規類文本可以精準的按照條款級進行分塊。這樣有利于保障每個分塊的語義獨立性和完整性。

從這個純文本的角度來看,Deepdoc 和 Textin 應該說是平分秋色,或者說 Deepdoc 作為原生集成的解析組件,使用起來無疑更加方便些。
2.2財報中的表格
緊接著來測試下更為常見的一些復雜表格的識別。這里選擇兩種情形。一種是涉及到跨頁的表格看下是否能夠正常的完成拼接。其次重要看下對于合并單元格的情形,看下是否會有文字錯位的情形。
跨頁表格
下面依次展示的是 Textin 和 Deepdoc 的解析結果,可以看到兩個解析效果依然是不分伯仲,都很好的完成了跨頁表格的重組。猜測二者都對這種情景做過單獨的優化。


合并單元格識別
以下展示的是 Deepdoc 與 Textin 的識別效果對比。二者在表格的主體內容上都完成了很準確的解析。包括表頭的合并單元的解析也都是準確的。但 Deepdoc 在這里犯了一個不小的錯誤是,把原本在“固定資產”和“應付賬款”兩行最后的一列備注(重大變動說明)內容進行了合并處理,而且還莫名的產生了合并單元格。而 Textin 則表現得依舊穩定。


Deepdoc 的這個錯誤看起來似乎不大,但是當用戶問及固定資產或者應付賬款的一些變動原因的時候,即使能夠召回這個分塊作為上下文,但是由于對應的說明內容被混在了一起,大模型可能無法準確的還原出財報上原本的解釋含義。
2.3圖文混排關聯
說完了文本和表格之后,肯定要來對比下圖片部分的處理效果了。為了提高測試難度,這部分以歷史文章中介紹過工程機械維保案例文檔(表格內嵌圖片)的一個頁面來看下。下圖依次是 Textin 和 Deepdoc 的回答,可以看出 Textin 比較完整的還原出了原始表格的布局。而 Deepdoc 則在圖示中丟失了原始的圖片,取而代之的是 v0.19 版本之后更新的原生快照。但這張快照是整個 PDF 頁面的完整截圖,而非案例中的具體故障部件圖片。


為了更好理解RAGFlow的原生圖文問答邏輯,手繪了下圖供各位做個參考。

進一步來說,當手動下載 Textin 解析好的MD文檔之后可以發現,其中的圖片是存到了Textin的服務器上,并通過一個公開訪問的鏈接回填到了文檔的對應位置。這和歷史文章中介紹過的通過預處理把文檔中的圖片先保存到 RAGFlow 的 Minio 實例上然后返回一個公開訪問 URL 鏈接的做法殊途同歸。

這種URL的方案關鍵優勢在于大模型生成答案會自然繼承源分塊中的<img>標簽,當包含 HTML 標簽的答案返回前端時,瀏覽器會自動解析<img>標簽并根據 URL 顯示圖片,最終實現圖文回答呈現。

簡單小結一下來說,Textin 作為一款成熟的商業解析工具,在復雜布局的圖表文檔上,相比與 RAGFlow 內置的 Deepdoc 而言是略勝一籌。關于在實際生產場景中如何使用的問題,肯定不是上述演示的那樣通過Textin 的官方或者本地調用Textin api 進行預處理之后再上傳到 RAGFlow 知識庫。更方便的方式還是進行系統層面的整合集成。以下演示置換和集成Deepdoc 的兩種做法。
3、Deepdoc 的解析器置換
如果你傾向于直接使用 TextIn 替換掉 Deepdoc,但又不想對 RAGFlow 的核心分塊邏輯進大改。關鍵是找到解析和分塊之間的解耦點,進行一次模塊置換。
3.1代碼溯源分析
為了找到這個解耦點,我深入分析了 RAGFlow 處理 PDF 的源碼,路徑位于 ragflow/rag/app/naive.py。整個流程的關鍵在于 chunk 函數。它通過調用 Pdf 類實例來獲得兩個核心變量:sections (文本段落列表) 和 tables (表格列表)。這兩個變量就是連接解析和分塊的數據總線。所以,理論上只要用 TextIn 生成同樣格式的 sections 和 tables,就能實現無縫替換。

3.2兩步走的改造方案
整個解析器的置換過程分為兩步,全部圍繞著 ragflow/rag/app/naive.py 文件展開。在開始之前,先通過 docker exec -it ragflow-server /bin/bash 進入容器,并備份一下原始文件,以防萬一。
cp /ragflow/rag/app/naive.py /ragflow/rag/app/naive.py.bak改造 Pdf 解析類
首要任務是讓 RAGFlow 放棄調用自帶的 DeepDoc 解析流程。在 naive.py 文件中,找到 class Pdf(PdfParser): 這個類定義。這個類的 call 方法是 DeepDoc 執行 PDF 解析的入口。它原本包含了一系列復雜的步驟,如布局分析、表格識別等。沒必要全部刪除,只需要讓它提前結束即可。同時,可以保留第一步 self.images(...)的調用,因為 RAGFlow 在前端展示分塊對應的原文位置時,需要用到 PDF 的頁面截圖。將 call 方法修改如下:

# 文件路徑: /ragflow/rag/app/naive.py
class Pdf(PdfParser):
def init(self):
super().init()
def call(self, filename, binary=None, from_page=0,
to_page=100000, zoomin=3, callback=None, separate_tables_figures=False):
# ... (前面的代碼保持不變)
# 保留這一步,為前端提供頁面截圖,確保原文定位功能正常
self.images(
filename if not binary else binary,
zoomin,
from_page,
to_page,
callback
)
# ... (前面的日志和回調保持不變)
# 關鍵改造!直接返回空列表,讓DeepDoc的后續工作短路
return [], []
# ---- 以下所有DeepDoc的原生代碼都將被跳過,不再執行 ----
# start = timer()
# self._layouts_rec(zoomin)
# ...通過在中間插入一行 return [], [],就巧妙地架空了 DeepDoc。Pdf 這個類依然可以被正常調用,但它不再進行任何實質性的解析工作,只會返回兩個空的列表,為下一步注入 TextIn 的數據做好了鋪墊。
改造 chunk 調度函數
在同一個文件中,找到 def chunk(...) 函數,并定位到處理 PDF 的邏輯分支 elif re.search(r".pdf$", filename, re.IGNORECASE):,再找到 if layout_recognizer == "DeepDOC": 的內部代碼塊。這里是整個流程的總指揮室。就在這里注入調用 TextIn API 的代碼,然后寫一個適配器,把 API 返回的 JSON 數據,精確地轉換成 RAGFlow 內部流通的 sections 和 tables 數據格式。參考下述代碼,把 if layout_recognizer == "DeepDOC": 內部的代碼塊替換為以下內容:
# 文件路徑: /ragflow/rag/app/naive.py -> def chunk(...) -> if layout_recognizer == "DeepDOC":
pdf_parser = Pdf()
# 調用我們改造過的Pdf類,此時sections和tables都是空列表[]
sections, tables = pdf_parser(filename if not binary else binary, from_page=from_page, to_page=to_page, callback=callback)
# 【↓↓↓ TextIn注入模塊開始 ↓↓↓】
import requests
# 1. 設置API認證信息 (請替換成您自己的Key)
headers = {
'x-ti-app-id': 'YOUR_TEXTIN_APP_ID',
'x-ti-secret-code': 'YOUR_TEXTIN_SECRET_CODE',
'Content-Type': 'application/octet-stream'
}
# 2. 調用TextIn云端解析API
result = requests.post(
'https://api.textin.com/ai/service/v1/pdf_to_markdown',
data=binary,
headers=headers,
params={ # 這里可以按需調整參數
'paratext_mode': 'none',
'formula_level': 2, # 關閉latex公式識別,節省資源
'page_start': from_page + 1,
'page_count': to_page - from_page
}
)
# 3. 核心:構建適配器,將TextIn的JSON輸出轉換為RAGFlow內部格式
json_data = result.json()
detail = json_data.get('result', {}).get('detail', {})
# 清空從pdf_parser那里繼承來的空列表,準備填充新數據
sections = []
tables = []
for item in detail:
page_id = item.get('page_id')
text = item.get('text')
# 清洗Markdown格式,因為分塊模塊需要的是純文本
text = re.sub(r'\*\*(.+?)\*\*', r'\1', text) # 去除加粗
text = re.sub(r'_(.+?)_', r'\1', text) # 去除斜體
text = re.sub(r'!\[.*?\]\((.*?)\)', '', text) # 刪除圖片標記
type = item.get('type')
position = item.get('position')
# 坐標系轉換:TextIn的默認ppi是144,DeepDoc是72,需要除以2
x0, y0, x1, y1 = position[0]/2.0, position[1]/2.0, position[4]/2.0, position[5]/2.0
if type == 'paragraph':
# 按需篩選需要的文本類型,可以過濾掉頁眉頁腳等
if item.get('sub_type') not in ['text', 'text_title', 'table_title', 'sidebar']:
continue
# 偽裝成RAGFlow的sections格式: (文本, '@@頁碼\t坐標##')
sections.append((text, f'@@{page_id - from_page}\t{x0}\t{x1}\t{y0}\t{y1}##'))
elif type == 'table':
# 對表格內容進行一些預處理
text = text.replace('<br>', '').replace('border="1"', '')
# 偽裝成RAGFlow的tables格式: ((None, HTML文本), [(頁碼, 坐標)])
tables.append(((None, text), [(page_id-1, x0, x1, y0, y1)]))
callback(0.6, "TextIn parsing completed.")
# 【↑↑↑ TextIn注入模塊結束 ↑↑↑】
# 將由TextIn數據偽裝而成的sections和tables,無縫傳入原生的分塊函數
res = tokenize_table(tables, doc, is_english)
callback(0.8, "Finish parsing.")
# ... 后續代碼保持不變這段代碼的核心是 for 循環,它會逐字逐句地把 TextIn 的 JSON 翻譯成了 DeepDoc 的特定的 Python 列表和元組結構。通過這種方式,下游的 tokenize_table 和 tokenize_chunks 等分塊函數,完全感知不到上游的變化,可以繼續它們的工作。(注意,記得去Textin 官方獲取對應的訪問憑證,更新在上述腳本中。)

重啟服務生效
代碼修改完成后,需要將其應用到正在運行的 RAGFlow 服務中。使用 docker cp 命令將修改后的文件復制到容器內,覆蓋原始文件。
docker cp /path/to/your/local/naive.py ragflow-server:/ragflow/rag/app/naive.py把 /path/to/your/local/ 替換成你本地 naive.py 文件所在的真實路徑。在 docker 文件夾下,執行以下命令,RAGFlow 會自動加載新的代碼。
docker compose restart重啟完成后,RAGFlow 在處理 PDF 時就已經用上了 TextIn 解析服務。
3.3兩個都要怎么辦
通過直接覆寫 DeepDOC 的邏輯分支,實現了對解析器的替換。雖然這很直接,但無疑也犧牲了靈活性。一個更完美的方案是,在 RAGFlow 的 UI 上增加一個“TextIn”的選項,用戶可以自由切換。要實現這一點,需要進行一次小型的全棧開發,包括后端邏輯的擴展和前端界面的修改。
后端改造
后端的修改依然是在 ragflow/rag/app/naive.py 文件中進行。思路是不再替換,而是新增。具體來說,首先把注釋或刪除的 DeepDOC 分支下的原生代碼恢復原狀。確保當 layout_recognizer == "DeepDOC"時,執行的是原生的 DeepDoc 解析流程。其次,新增 TextIn 的邏輯分支。在 if layout_recognizer == "DeepDOC":代碼塊之后,增加一個新的 elif 分支,專門用于處理 TextIn 的邏輯。修改后的代碼結構會是這樣。

# 文件路徑: /ragflow/rag/app/naive.py -> def chunk(...)
# ...
elif re.search(r"\.pdf$", filename, re.IGNORECASE):
layout_recognizer = parser_config.get("layout_recognize", "DeepDOC")
# ...
if layout_recognizer == "DeepDOC":
#
# 這里是RAGFLOW原生的、完整的DEEPDOC解析代碼
# 確保這部分代碼是未經修改的原始版本
#
pdf_parser = Pdf()
sections, tables = pdf_parser(filename if not binary else binary, ...)
res = tokenize_table(tables, doc, is_english)
# ...
elif layout_recognizer == "TextIn": # <--- 我們新增的分支
#
# 把我們之前編寫的TEXTIN API調用和適配器代碼
# 完整地復制到這里
#
callback(0.1, "Start to parse with TextIn.")
import requests
headers = { 'x-ti-app-id': '...', 'x-ti-secret-code': '...' }
# ... (完整的TextIn API調用和數據轉換邏輯) ...
sections = []
tables = []
for item in detail:
# ... (將JSON轉換為sections和tables)
res = tokenize_table(tables, doc, is_english)
callback(0.8, "Finish TextIn parsing.")
else: # 處理 "Plain Text" 或 VisionParser 等其他情況
#
# 保持這部分代碼不變
#
if layout_recognizer == "Plain Text":
# ...
# ...通過這樣的改造,后端現在具備了處理三種不同 layout_recognizer 值的能力:"DeepDOC", "TextIn", 和 "Plain Text"。只要前端能傳來正確的值,后端就能調用對應的解析邏輯。
前端改造
如圖找到 layout-recognize-form-field.tsx 這個文件,從名字也可以看出來是布局識別表單字段。它就是一個用來選擇布局識別方式的表單組件。enum DocumentType 是數據源,在文件的第 16-19 行,可以看到一個枚舉(enum)定義。 在前端開發中,enum 是定義一組固定選項的“標準答案”。這里明確定義了“PDF 解析器”目前只有兩種合法的值:DeepDOC 和 Plain Text。

這是需要修改的第一個地方:
export const enum DocumentType {
DeepDOC = 'DeepDOC',
PlainText = 'Plain Text',
}修改后:
export const enum DocumentType {
DeepDOC = 'DeepDOC',
TextIn = 'TextIn', // <-- 新增的選項,值必須與后端elif判斷的字符串一致
PlainText = 'Plain Text',
}const list = [...] 是選擇列表:在文件的第 28 行,可以看到這行代碼:
const list = [DocumentType.DeepDOC, DocumentType.PlainText].map(...)
label: x === DocumentType.PlainText ? t(camelCase(x)) : 'DeepDOC',這行代碼的作用是,從上面看到的 enum 標準答案中,取出 DeepDOC 和 PlainText 這兩個選項,然后用它們來生成一個列表(菜單),最終渲染成用戶在界面上看到的下拉選項。注意,需要同步修改下一行的 label 的生成邏輯,默認直接顯示選項的值本身,只對 PlainText 這個特例進行翻譯處理。修改后:
const list = [DocumentType.DeepDOC, DocumentType.TextIn, DocumentType.PlainText].map(...)
label: x === DocumentType.PlainText ? t(camelCase(x)) : x,完成以上兩處修改后,就成功地從代碼層面為 RAGFlow 增加了 TextIn 選項。注意為了讓修改生效,還要執行如下步驟:
- 在前端項目根目錄運行 npm install 確保依賴安裝完整(如果有 node 版本依賴沖突,就強制安裝 npm install --force)。
- 運行 npm run build 編譯前端代碼,生成最新的靜態文件。
- 將新生成的 dist 目錄下的內容,替換掉 RAGFlow 服務中對應的舊前端文件,并重啟 RAGFlow 服務。

以上修改的完整源碼見知識星球
4、寫在最后
RAG 系統的優化是一項環環相扣的工程。優質的文檔解析結果提供了系統運行的基礎,接下來,分塊策略也是影響 RAG 能力的重要因素。分塊策略目前業界也有很多思考,但其實際應用受制于輸入的結構化文件、上下文窗口長度等因素。這里也提出一些可能性,權當拋磚引玉。

- 保留文檔結構:通過目錄樹(Root/Heading/Text/Table 等節點)維護標題層級關系和語義上下文,實現標題層級遞歸切片,保留文檔內在邏輯結構的完整性。
- 動態處理長內容:超長文本/表格按固定窗口切分,標題節點合并子內容。
- 優化檢索效率:以最小內容單元(子段落)作為檢索主體,提升匹配精度。

























