Dify+RAGFLow:基于占位符的圖片問答升級方案
4/2 號時(shí)寫了一篇 RAGFlow 實(shí)現(xiàn)圖片問答的原理解析,后續(xù)在知識星球內(nèi)有星友陸續(xù)反饋在使用我提供的源碼復(fù)現(xiàn)時(shí),會出現(xiàn)知識庫中能渲染圖片,但回答中圖片無法正常加載的問題。
知識庫預(yù)覽和引用文件部分是直接展示原始數(shù)據(jù)或進(jìn)行簡單渲染,它們能正確顯示圖片,證明原始上傳的增強(qiáng)文本中的 URL 是正確的,并且圖片服務(wù)器和網(wǎng)絡(luò)配置是通的,問題出在聊天內(nèi)容生成這個(gè)環(huán)節(jié)。
我當(dāng)時(shí)提供了三種選擇選擇,分別是:更強(qiáng)的 prompt 指令、更強(qiáng)的 LLM 和”占位符+后處理“的方案,鑒于后續(xù)大家反饋前兩種效果依然不夠穩(wěn)定,這篇我來系統(tǒng)的分享下如何實(shí)現(xiàn)第三種更符合實(shí)際生產(chǎn)場景的 RAG 富文本處理方案。
這篇試圖說清楚:
占位符方案如何將圖片 URL 幻覺問題,從一個(gè)難以控制的“語義幻覺”轉(zhuǎn)變?yōu)橐粋€(gè)相對更容易處理的“格式遵循”問題,從而極大降低圖片顯示錯(cuò)誤的概率。它不是銀彈,但通常是處理富文本(圖文混排)RAG 中最實(shí)用的工程方法之一。
以下,enjoy:
1、占位符方案的好處
LLM 的核心行為是生成文本,而非精確復(fù)制特定格式的字符串,尤其是當(dāng)這些字符串看起來像可解釋內(nèi)容時(shí)。
原始問題:
LLM 看到的是`<img src="http://{server_ip}:8000/images/..." alt="..." width="...">`這樣的標(biāo)簽,它看起來像是有意義的 HTML 和 URL。LLM 嘗試"理解"上下文,并可能基于文本內(nèi)容(比如“燃油噴射器泄漏圖”)重新生成一個(gè)它認(rèn)為更合適的 src(例如改成 .../fuel_injector_leak.png),導(dǎo)致 URL 失效。
占位符方案:
LLM 看到的是 [IMG::page1_img1_uuid.png] 這樣的特殊標(biāo)記。這種格式更像是一個(gè)代碼片段或元數(shù)據(jù)標(biāo)簽,而不是自然語言或標(biāo)準(zhǔn) HTML。LLM 被明確指示(通過 Prompt)要將其視為特殊標(biāo)記,如果需要引用就原樣復(fù)制。它"創(chuàng)造性地"修改這種標(biāo)記的動(dòng)機(jī)和可能性都大大降低了。它不太可能去"理解" page1_img1_uuid.png 并把它改成 fuel_injector_leak.png。
1.1降低 LLM 修改風(fēng)險(xiǎn)
占位符通常是簡單的、非描述性的文本字符串(如 [IMG::page1_img1_eef46633] 或類似的格式)。LLM 在生成回答時(shí),更有可能將這些占位符視為需要原樣保留的特殊標(biāo)記或代碼片段,而不是像 HTML <img> 標(biāo)簽?zāi)菢尤菀妆弧袄斫狻焙汀爸貥?gòu)”的內(nèi)容。
1.2解耦內(nèi)容與表示
將圖片的存在(通過占位符表示)與其具體的網(wǎng)絡(luò)位置(URL)分離。LLM 專注于基于文本內(nèi)容(包括占位符)生成連貫的回答,而圖片渲染的細(xì)節(jié)則由后續(xù)步驟處理。
1.3更強(qiáng)的控制力
通過后處理步驟,我們可以精確地控制最終呈現(xiàn)給用戶的 <img> 標(biāo)簽的格式,確保 URL 和其他屬性(如 alt, width)的準(zhǔn)確性。
2、后處理步驟是關(guān)鍵
這個(gè)方案的核心在于,在 LLM 從 RAGFlow 獲得包含占位符的回答后,必須有一個(gè)后處理步驟。這個(gè)步驟負(fù)責(zé)查找回答中的所有占位符,并使用之前存儲的映射關(guān)系將其替換回完整的 HTML <img> 標(biāo)簽。大致有以下兩種實(shí)現(xiàn)方式:
2.1直接使用 ragflow_sdk (不推薦)
優(yōu)點(diǎn)是,我們可以完全控制整個(gè)流程,集成度高。在獲取到 chat.answer() 的結(jié)果(包含占位符的文本)后,可以立即執(zhí)行 Python 代碼進(jìn)行替換,然后再將最終結(jié)果呈現(xiàn)給用戶或傳遞給下游系統(tǒng)。
缺點(diǎn)也很明顯,這種方式通常意味著我們需要自行構(gòu)建用戶界面 (UI) 或者將 RAG 邏輯嵌入到一個(gè)更大的后端應(yīng)用中。標(biāo)準(zhǔn)的 RAGFlow Web UI 是與其自身后端緊密集成的,不太可能直接支持你在調(diào)用鏈中插入自定義的 Python 后處理步驟。而修改 RAGFlow UI 來支持這一點(diǎn)會非常復(fù)雜。
2.2使用 Dify 工作流(推薦)
Dify 提供了強(qiáng)大的可視化工作流編排能力,我們可以清晰地看到數(shù)據(jù)流轉(zhuǎn)。將 RAGFlow 作為知識庫(通過其 API)接入,LLM 調(diào)用作為另一個(gè)節(jié)點(diǎn),而后處理邏輯則封裝在 Dify 的 "Code" 節(jié)點(diǎn)中,職責(zé)清晰。
Dify+RAGFlow:1+1>2的混合架構(gòu),詳細(xì)教程+實(shí)施案例
總結(jié)來說,通過 Dify 工作流中的 Code 節(jié)點(diǎn)實(shí)現(xiàn)后處理邏輯是完全可行的,并且可能是更靈活、更易于管理的方案,特別是如果你希望利用現(xiàn)成的或 Dify 提供的 UI 時(shí)。
3、映射關(guān)系的存儲和訪問
我們需要一種方便且穩(wěn)定的方式來存儲和檢索占位符 -> URL 的映射。 存儲環(huán)節(jié),在處理 PDF 時(shí),將生成的映射關(guān)系與增強(qiáng)文本一起保存,例如存為一個(gè) JSON 文件,其名稱與知識庫或文檔相關(guān)聯(lián)。 檢索環(huán)節(jié),在進(jìn)行聊天并需要進(jìn)行后處理時(shí),需要能夠加載與當(dāng)前知識庫/助手相關(guān)的映射文件。具體實(shí)現(xiàn)上我試過如下三種方式,推薦最后一種。
3.1映射關(guān)系作為知識庫
這個(gè)做法是我最先想到的一種。就是將映射關(guān)系保存為 TXT 文件并上傳到 RAGFlow 單獨(dú)知識庫,利用 RAGFlow 的 "One" 分塊策略來確保整個(gè)映射文件作為一個(gè)單元被檢索,避免了映射關(guān)系被分割的問題。
但仔細(xì)想了下意識到,實(shí)際測試在本地預(yù)處理腳本(如 process_pdf.py)運(yùn)行時(shí),我們無法知道 RAGFlow 將為即將上傳的原始文檔(例如,從 PDF 生成的增強(qiáng)文本文件)分配的 document_id。 document_id 是在調(diào)用 dataset.upload_documents() 之后由 RAGFlow 系統(tǒng)生成的。本地腳本在調(diào)用此 API 之前運(yùn)行,因此無法預(yù)先獲取這個(gè) ID。
3.2共享文件系統(tǒng)方案
這里需要先解釋一個(gè) Docker 卷掛載的概念,這是一種將宿主機(jī)(運(yùn)行 Docker 的機(jī)器)上的目錄或文件鏈接到 Docker 容器內(nèi)部指定路徑的機(jī)制。它允許容器訪問宿主機(jī)上的文件,并且宿主機(jī)和容器之間的數(shù)據(jù)是實(shí)時(shí)同步的(在掛載的目錄內(nèi))。
當(dāng)我們運(yùn)行 process_pdf.py 腳本會生成映射文件 (.json) 并保存在宿主機(jī)的一個(gè)特定目錄下(例如 ~/ragflow_project/mappings)。 Dify 應(yīng)用在其工作流的 Code 節(jié)點(diǎn)里需要讀取這些 .json 文件。 通過卷掛載,我們可以把宿主機(jī)上的 ~/ragflow_project/mappings 目錄映射到 Dify 容器內(nèi)部的一個(gè)路徑(例如 /data/mappings)。 這樣,Dify 容器內(nèi)的 Code 節(jié)點(diǎn)就可以像訪問本地文件一樣,通過路徑 /data/mappings/{unique_id}.json 來讀取映射文件了。
但是這個(gè)方案我測試下來沒有成功,docker exec 一直顯示 "No such file or directory",這意味著在 docker-compose.yaml 中為 api 服務(wù)定義的 volume 掛載沒有成功??赡苁且?yàn)?Docker Desktop 正在使用 WSL 2 后端,WSL 2 有更原生的方式訪問 Windows 文件系統(tǒng)(通常通過 /mnt/c, /mnt/d 等路徑),不再依賴舊的 Hyper-V 文件共享設(shè)置。一時(shí)半會沒研究明白,再加上這并不是一個(gè)適合在生產(chǎn)場景使用的方案,就果斷棄了。
3.3阿里云 OSS(或其他云對象存儲)
在考慮技術(shù)方案時(shí),必須充分考慮實(shí)際生產(chǎn)場景,尤其是多用戶并發(fā)訪問和部署架構(gòu)帶來的影響。OSS 方案更適合生產(chǎn)環(huán)境,原因有以下幾點(diǎn):
- 解耦存儲: 將映射文件存儲在獨(dú)立、高可用的云存儲中,而不是依賴本地文件系統(tǒng)。
- 避免本地環(huán)境問題: 不再受 Windows 路徑、WSL 2 掛載、Docker Desktop 配置等本地復(fù)雜性的影響。
- 可擴(kuò)展性好: OSS 可以輕松處理大量文件和高并發(fā)訪問。
- 部署更簡單: Dify 容器不再需要特殊的文件系統(tǒng)掛載,只需要配置好訪問 OSS 的網(wǎng)絡(luò)和憑證即可。
阿里云OSS訪問地址:https://oss.console.aliyun.com/overview
4、OSS 配置流程
4.1獲取 OSS 訪問憑證
Access Key ID、Access Key Secret、Endpoint、Bucket Name (你用于存儲映射文件的存儲桶名稱) ,以及一個(gè)用于存放映射文件的目錄前綴 (例如 mappings/)
4.2交互方式選擇
關(guān)于 Dify 中 code 節(jié)點(diǎn)和 OSS 的交互方式,最初我是默認(rèn)選擇使用 OSS SDK 的方式,結(jié)果發(fā)現(xiàn)報(bào)錯(cuò):ModuleNotFoundError: No module named 'oss2' ,這說明 Dify 為 Code 節(jié)點(diǎn)提供的沙箱環(huán)境沒有包含阿里云 OSS 的 SDK。這和 Coze 一樣是一個(gè)常見的限制,沙箱環(huán)境通常只包含一組有限的基礎(chǔ)庫。
所以使用 HTTP Request 節(jié)點(diǎn)是快速驗(yàn)證核心邏輯的好方法, 既然 Dify Code 節(jié)點(diǎn)無法直接與 OSS SDK 交互,我們可以讓 Dify 的 HTTP Request 節(jié)點(diǎn)來負(fù)責(zé)獲取映射文件的內(nèi)容,然后將獲取到的內(nèi)容傳遞給 Code 節(jié)點(diǎn)進(jìn)行處理。
4.3配置 Bucket 自定義域名
在確定了使用 http 節(jié)點(diǎn)訪問映射文件后,我們還有個(gè)問題需要克服。就是阿里云 OSS 出于安全考慮,對于使用 OSS 默認(rèn)域名 (如 xxx.oss-cn-shanghai.aliyuncs.com)或傳輸加速域名訪問時(shí),會強(qiáng)制在返回頭中增加 x-oss-force-download: true 和 Content-Disposition: attachment。這會導(dǎo)致瀏覽器或 HTTP 客戶端(如 Dify 的 HTTP 節(jié)點(diǎn))強(qiáng)制下載,即使你設(shè)置了對象的 Content-Disposition: inline 元數(shù)據(jù)。
因此 Dify HTTP 節(jié)點(diǎn)會將響應(yīng)體識別為一個(gè)文件,因此 body 字段為空,而將文件信息放入了 files 數(shù)組中。它實(shí)際上已經(jīng)成功下載了文件內(nèi)容,只是沒有直接放在 body 里。官方推薦的方法是為你的 OSS Bucket 綁定一個(gè)你自己的、已備案的域名(正好有一個(gè)剛做好 ICP 備案)。
如果你沒有備案過的域名,為了完成測試可以使用template節(jié)點(diǎn)中直接填寫映射關(guān)系的內(nèi)容,實(shí)測這樣和http節(jié)點(diǎn)效果是一致的。
然后要修改 Dify HTTP 節(jié)點(diǎn)中的請求 URL,使用這個(gè)自定義域名來訪問你的 JSON 文件,而不是使用 OSS 的默認(rèn)域名。 訪問通過自定義域名的文件時(shí),OSS 通常會尊重你在對象元數(shù)據(jù)中設(shè)置的 Content-Disposition: inline,從而允許預(yù)覽而不是強(qiáng)制下載。
在OSS點(diǎn)擊域名管理
在域名控制臺解析到OSS存儲位置
5、工作流解析
文檔預(yù)處理: 提取 PDF 中的圖片和文本。在生成的文本中,用唯一的占位符(例如 [IMG::page1_img1_uuid.png])標(biāo)記圖片的位置,而不是直接插入 <img> 標(biāo)簽。
映射關(guān)系存儲: 將"占位符"到"實(shí)際圖片訪問 URL"(指向獨(dú)立的圖片服務(wù)器)的映射關(guān)系保存為一個(gè) JSON 文件,并將其上傳到阿里云 OSS 對象存儲中,以便后續(xù)訪問。
獨(dú)立圖片服務(wù): 運(yùn)行一個(gè)獨(dú)立的 Docker 容器作為圖片服務(wù)器,負(fù)責(zé)托管從 PDF 中提取的圖片文件,并通過 HTTP 提供訪問(例如 http://<your-server-ip>:8000/images/...)。
RAG 流程: RAGFlow 知識庫僅存儲包含占位符的純文本。
后端處理 (Dify): 使用 Dify 工作流編排 RAG 查詢和 LLM 調(diào)用。在 LLM 生成包含占位符的回答后:
使用 Dify 的 HTTP Request 節(jié)點(diǎn)從 OSS 獲取對應(yīng)的映射關(guān)系 JSON 文件(通過自定義域名訪問)。
使用 Dify 的 Code 節(jié)點(diǎn)解析 JSON,查找回答中的占位符,并將其替換回完整的 <img> HTML 標(biāo)簽,其 src 指向圖片服務(wù)器的 URL。
6、QA/故障排除
Q: Dify Code 節(jié)點(diǎn)報(bào)錯(cuò) ModuleNotFoundError: No module named 'oss2
A: Dify Code 節(jié)點(diǎn)的沙箱環(huán)境不包含 oss2 庫。解決方案是改用 Dify 的 HTTP Request 節(jié)點(diǎn)來獲取 OSS 上的映射文件內(nèi)容(需要文件可公開訪問或使用預(yù)簽名 URL),然后將獲取到的字符串內(nèi)容傳遞給 Code 節(jié)點(diǎn)進(jìn)行 JSON 解析和處理。
Q: Dify HTTP Request 節(jié)點(diǎn)訪問 OSS URL 返回空 body,但在 files 數(shù)組中有內(nèi)容
A: 這是因?yàn)?OSS 返回了 Content-Disposition: attachment 頭,Dify 將其視為文件下載。解決方案是在上傳映射文件到 OSS 時(shí)(在 process_pdf.py 中),明確設(shè)置 headers={'Content-Disposition': 'inline'}。
Q: 瀏覽器訪問圖片服務(wù)器 URL (http://:8000/images/...) 返回 "Not Found",但 curl http://localhost:8000 成功
A: 這通常是 Docker Volume 掛載問題 (尤其在 Windows/WSL2)。正在運(yùn)行的 image-server 容器內(nèi)的 /app/images 沒有同步宿主機(jī) ./images 目錄的最新內(nèi)容。解決方案:停止并刪除舊的 image-server 容器 (docker stop/rm),然后使用正確的 -v <宿主機(jī)絕對路徑>:/app/images 參數(shù)重新運(yùn)行 docker run 命令啟動(dòng)新容器。
7、寫在最后
7.1殘余風(fēng)險(xiǎn)
占位符方案能否完全解決 LLM 幻覺?答案是不能完全保證,但是測試下來能極 大降低特定錯(cuò)誤的概率。 需了解的殘余風(fēng)險(xiǎn)包括:
- 完全省略占位符: 如果它認(rèn)為圖片不重要或與回答關(guān)系不大。
- 輕微修改占位符: 比如不小心加了空格、改變了括號 [ IMG:: ... ],或者截?cái)嗔宋募_@可能導(dǎo)致后續(xù)的 Code 節(jié)點(diǎn)正則匹配失敗。
- 在錯(cuò)誤的位置插入占位符。
7.2、圖片分頁問題
如何確保圖片鏈接與其所屬頁面的主要文本內(nèi)容在同一個(gè)塊中,并且最好位于該塊的開頭或明確關(guān)聯(lián)的位置。這個(gè)問題我三天前在知識星球?qū)iT寫過一篇回復(fù),大致有兩種方式:
1、改變圖片鏈接的插入時(shí)機(jī)和位置,圖片鏈接緊隨頁面開始標(biāo)記,與該頁文本強(qiáng)關(guān)聯(lián)。即使默認(rèn)分塊器在頁面中間分割,圖片鏈接也更有可能和頁面開頭的文本在一起。添加的 --- Page X Start/End --- 分隔符為后續(xù)更精細(xì)的分塊(方案二)打下基礎(chǔ)。
2、通過 paragraph_separator="\n--- Page ",強(qiáng)制分塊器在頁面邊界(或你定義的分隔符)處進(jìn)行切割,最大程度保證頁面內(nèi)容的完整性。
需要確保方案一中的分隔符 --- Page X Start/End --- 能夠被 \n--- Page 這個(gè)模式匹配到并作為切割點(diǎn)。
7.3正本清源
通過HTTP請求獲取并展示圖片(例如替換占位符)能夠顯著提高最終答案的可讀性和用戶體驗(yàn),但這屬于 RAG 流程末端(生成與呈現(xiàn)) 的優(yōu)化。輸出格式的優(yōu)化(如加入圖片)雖然重要,但屬于錦上添花,必須建立在核心檢索和生成能力達(dá)標(biāo)的基礎(chǔ)上。
而從構(gòu)建一個(gè)高質(zhì)量、高效率 RAG 項(xiàng)目的基礎(chǔ)來看,數(shù)據(jù)預(yù)處理和分塊策略優(yōu)化,絕對是更早期、更核心、也往往是決定 RAG 系統(tǒng)性能上限的關(guān)鍵工作。
后續(xù)幾篇我會回過頭來,從數(shù)據(jù)預(yù)處理(例如 MinerU、Mistral OCR等橫評)、動(dòng)態(tài)分塊策略進(jìn)一步寫些實(shí)踐經(jīng)驗(yàn)。