微信扫码
添加专属顾问
我要投稿
RAGFlow v0.19版本带来图文混排新体验,深度解析预处理与增强案例。 核心内容: 1. RAGFlow v0.19版本更新亮点:Agent模块新增代码执行组件,Chat和Agent模块图片展示方式变革 2. 图文混合回答功能底层实现逻辑,与URL渲染方案对比分析 3. 基于业务语义驱动的PDF重组案例,精细化预处理文档,特别是表格内图片等复杂场景
RAGFlow在5/26 正式更新了v0.19版本,其中有两点值得关注。首当其冲的是在 Agent 模块(也就是工作流)新增了代码执行组件,这个被吐槽了很久了 RAGFlow工作流编排功能,终于可以处理更加复杂的任务了。
第二点比较大的更新,是这篇文章主要要讨论的,也就是在 Chat 和 Agent 模块中改变以往图片作为引用的展示方式,直接在正文中进行显示。进行初步测试之后发现,RAGFlow 实际是把我历史文章中提到的“占位符+映射替换”以及前端渲染的方法内置化和标准化了。但测试下来也明显发现这种原生方案同时带来了文档预处理的复杂性。
这篇试图说清楚:
RAGFlow v0.19版本图文混合回答功能的底层实现逻辑,和 URL 渲染方案的主要区别,以及如何基于业务语义驱动的PDF 重组案例,在保留 RAGFlow 原生图片显示能力(基于 img_id)的同时,对文档进行更精细化的预处理,特别是针对表格内图片这类复杂场景,以确保分块和图片关联符合预期。
以下,enjoy:
1
URL 方案的三种做法
在正式开始介绍前先快速回顾下,上述提到的我给出的三种历史 URL 方式解决方案。之前的思路是,在现有开源框架基础上,不改动核心代码,又要达到较好的流式图文问答体验,自然需要引入一些“变通方案”。本着“规避 LLM 弱点”、“模块化处理”、“逐步优化体验”的工程实践思路,我前后进行了以下三种方案的探索。
RAGFlow的v0.19版本更新前,正好在官方群里又人在讨论我历史文章中的图片显示方案
1.1
初步尝试
RAGFlow如何实现图片问答:原理分析+详细步骤(附源码)
最初通过独立的图片服务器容器化部署,把本地图片路径转换为 HTTP URL。前端通过挂载静态文件目录,从而图片可以通过 HTTP URL 直接访问,并在 Markdown 渲染中通过<img>标签或 Markdown 图片链接展示。
这种做法是在文档预处理时替换本地路径为 HTTP URL,依赖外部图片服务器进行实现。当然,最大的问题还是在于 LLM 会不可控的“自作聪明”修改 URL,这个方案不适合长期在生产环境使用。
1.2
进阶优化
dify+RAGFLow:基于占位符的图片问答升级方案(最佳实践)" data-itemshowtype="0" linktype="text" data-linktype="2">Dify+RAGFLow:基于占位符的图片问答升级方案(最佳实践)
针对 LLM 可能修改直接嵌入的图片 URL,导致加载失败的问题,我后续引入了“占位符+后端映射替换”的方案。具体做法是,把图片存储在 RAGFlow 自带的 MinIO 中,文档预处理环节在文本中插入特定格式的占位符(例如[IMG::filename.png]),同时生成一个包含占位符到 MinIO 图片实际 URL 映射关系的 map.json 文件。进而,在 Dify 这样的编排工具中,通过 HTTP 节点获取 map.json,再由 Code 节点在 LLM 输出后,根据映射关系把文本中的占位符替换为最终的 HTML <img>标签。
这种做法的核心是利用 MinIO 存储、占位符机制、后端(Dify Code 节点)替换逻辑,增强了 URL 的稳定性,但 Dify Code 节点批处理导致的非流式输出会带来明显的体验问题。(当时选择 RAGFlow+Dify 的拼接方案,主要是受制于 RAGFlow 的 agent 模块还不支持自定义的 code 节点。)
1.3
体验提升
究极方案:油猴脚本实现RAG问答前端图片流式体验
为了解决后端替换带来的延迟和非流式问题,我选择把图片占位符的替换工作从后端(Dify)转移到了用户浏览器前端。通过编写油猴脚本 (Tampermonkey script)在 Dify 聊天界面加载的时候,脚本异步获取 map.json 文件,然后实时监控 LLM 的流式文本输出,一旦检测到预设的图片占位符,就立即将其在前端动态替换为<img>标签,从而实现图片随文本流式同步显示。
选择这种做法的核心原因是可以非侵入式的修改 Dify 前端行为,免去了直接增强 RAGFlow 后端流式处理能力或前端渲染逻辑的修改风险。毕竟这种做法成本高,且影响框架升级。
2
v0.19 的图片显示逻辑
为了方便大家理解,这部分我从 API 文档作为切入点,再过渡到后端代码实现理解,最后通过实际操作中的部分截图来印证和展示效果。
注:为了 升级 v0.19,不需要重新部署。修改 ragflow/docker/.env 文件,更新 RAGFLOW_IMAGE 变量为你想要升级到的版本号。然后运行下述 Docker 命令会拉取 .env 文件中指定的新版本镜像,并使用新的镜像重新创建和启动容器。(由于数据卷是独立于容器的,新的容器会重新挂载已存在的数据卷,从而保留你的历史项目文件。)
# 修改env
RAGFLOW_IMAGE=infiniflow/ragflow:v0.19.0
#在docker目录下运行
docker compose -f docker/docker-compose.yml pull
docker compose -f docker/docker-compose.yml up -d
2.1
API 接口
打开 v0.19 的 Python API 文档,首先注意到一个关键变化:在与聊天助手或 Agent 交互后返回的 Message 对象中,引用的 Chunk 对象里增加了一个叫做为 img_id 的字符串类型。官方文档的解释是:
The ID of the snapshot of the chunk. Applicable only when the source of the chunk is an image, PPT, PPTX, or PDF file.
从这行内容可以明确看出来,img_id 正是 RAGFlow 用于标识和关联图片快照的核心 ID,它暗示了 RAGFlow 在后端对包含视觉信息的文档进行了处理,并为每个相关的文本块生成了可引用的视觉元素。那么进一步的问题就是,图片是如何被存储、如何通过这个 ID 最终在前端显示的?这个就要从后台代码中一探究竟。
2.2
查看源码
这部分主要介绍 RAGFlow 的异步任务处理模块 task_executor.py,这个脚本是理解文档解析和图片处理的关键。在整个脚本中可以找到三处和图片处理与 img_id 生成流程相关的片段,接下来我逐一做个说明:
图片格式统一与存储 (JPEG):
if not d.get("image"): # d 是一个 chunk 字典
_ = d.pop("image", None)
d["img_id"] = ""
docs.append(d)
continue
try:
output_buffer = BytesIO()
if isinstance(d["image"], bytes): # 如果图片已经是bytes
output_buffer = BytesIO(d["image"])
else: # 否则假定是PIL Image对象
d["image"].save(output_buffer, format='JPEG') # 保存为JPEG格式
st = timer()
# 上传到存储 (例如MinIO)
await trio.to_thread.run_sync(lambda: STORAGE_IMPL.put(task["kb_id"], d["id"], output_buffer.getvalue()))
el += timer() - st
except Exception:
logging.exception(
"Saving image of chunk {}/{}/{} got exception".format(task["location"], task["name"], d["id"]))
raise
在 build_chunks 函数的循环内,可以看到这样的逻辑:当从文档中提取的图片数据(可能是一个 PIL Image 对象)存在时,系统会将其保存到一个 BytesIO 缓冲中,并明确指定 format='JPEG'。这意味着,无论原始图片格式如何,RAGFlow 在处理流程中倾向于将其标准化为 JPEG 格式进行后续存储。
然后,这些处理后的图片字节流会通过 STORAGE_IMPL.put(...)上传到对象存储服务中(MinIO)。task["kb_id"]作为存储桶名称(或前缀),块的唯一 ID d["id"] 作为对象名称。
img_id 的生成与关联
d["img_id"] = "{}-{}".format(task["kb_id"], d["id"]) # 生成img_iddel d["image"] # 从chunk字典中删除原始图片数据,因为它已被存储docs.append(d)
在图片成功上传到 MinIO 之后,RAGFlow 会为这个图片生成一个 img_id。这个 img_id 的格式是 "{知识库 ID}-{块 ID}" ("{}-{}"_format(task["kb_id"], d["id"]))。这个 ID 唯一标识了与该文本块关联的存储图片。
生成 img_id 后,原始的图片数据(d["image"])会从内存中的 chunk 字典中删除,以节省资源,毕竟图片已经持久化存储了。 这个包含 img_id 的 chunk 信息最终会被索引到搜索引擎中。
FACTORY 字典的定义
FACTORY = { "general": naive, ParserType.NAIVE.value: naive, # ... 其他解析器 ... ParserType.PICTURE.value: picture, # 有专门的图片解析器 # ... 可能通过 naive 间接支持 Markdown}
RAGFlow 内部有一个解析器工厂(FACTORY),根据任务指定的 parser_id(文档类型或解析方式)来选择合适的 chunker 模块。对于直接上传的图片文件 (ParserType.PICTURE),会有专门的 picture 模块(rag.app.picture)来处理,它很可能会将整个图片作为一个块,并提取图片本身作为数据。
2.3
前端测试
以历史文章中经常使用的工程机械维保文档为例,为了更直观地理解 RAGFlow 前端是如何处理和显示图片的,我在浏览器的开发者工具对网络请求和页面元素进行了观察,以下结合相关截图进行说明。
知识库与检索测试
首先来看一下当 RAGFlow 在知识库中展示已解析文档的文本块时,图片信息是如何传递给前端的。下图左侧是‘解析块’界面,可以看到文本块‘故障名称:打着车老是熄火...’旁边有一个小小的图片预览。”
通过开发者工具(如第一张图右侧所示),捕获到了前端在加载这部分内容时,从后端获取到的数据。在返回的 JSON 响应中,可以清晰地看到一个名为 image_id 的字段,这个值正是与这个文本块关联的图片快照的唯一标识符。”
这个 image_id 就是我们之前在 API 文档和后端源码分析中多次提到的关键 ID。它由知识库 ID 和块 ID 组合而成,是 RAGFlow 内部用来索引和定位图片的核心。这证实了 RAGFlow 在前端展示知识库内容时,会首先通过 API 获取到包含 image_id 的元数据,为后续实际图片的加载做好准备。
聊天助手
对于聊天助手场景,如下图左侧所示,当助手在回答中引用包含图片的知识时,这些图片会以预览图的形式直接嵌入到对话流中实现图文混排。那嵌入的预览图是如何加载的呢?
通过开发者工具的‘Elements’面板(如图右侧所示),可以审查这些图片预览对应的<img>标签的 src 属性。可以发现,它并不是一个指向 MinIO 或其他静态文件服务器的直接图片 URL。相反,它的路径是类似/v1/document/image/{image_id}这样的格式。这清晰地表明,前端是通过调用 RAGFlow 后端的一个特定 API 接口来获取图片内容的,而 image_id 正是这个接口定位具体图片的关键参数。
注:通过 API 端点提供图片的方式,是更符合最佳实践实现精细化权限控制的推荐做法。 它不像直接暴露静态文件 URL 那样难以控制访问。每一次图片请求都需要经过后端的鉴权逻辑。
渲染逻辑总结
浏览器要加载并渲染一个外部资源(如图片、iframe 内容、通过 AJAX 获取的数据),它必须通过一个 URL 来定位这个资源。所以,可能会有盆友好奇缩略图和 Document Previewer 既然“不是直接的 MinIO URL”,是怎么在前端页面显示的?这个疑问的关键是理解“URL”的含义和浏览器的工作方式。
当我们说它“不是直接的 MinIO URL”时,指的是<img>标签的 src 属性或者 Document Previewer 加载内容的源,并不是一个指向 MinIO 存储桶中某个文件对象的永久、公开、任何人都可以直接访问的静态链接(比如 http://minio.example.com/mybucket/myimage.jpg这种)。以缩略图为例,其<img>标签的 src 属性值是 /v1/document/image/{image_id}。这本身就是一个合法的 URL (更准确地说,是一个相对 URL,浏览器会根据当前页面的域名和端口补全它,变成一个完整的 HTTP/HTTPS URL,例如 http://localhost:8080/v1/document/image/...)。
简单来说,/v1/document/image/{image_id}这些 URL 指向的是 RAGFlow 后端应用程序的 API 端点,而不是直接指向 MinIO 中的静态文件。这种实现方式既保证了图片能被网页正确渲染,又有效地集成了权限管理机制。
3
PDF 重组方案增强
RAGFlow 这次的图文混答方案有很多好处,但是 img_id 只有在源文档本身是图片、PPT、PPTX 或 PDF 文件时才会生成。所以随之而来的问题是,如何在保留 RAGFlow 原生图片显示能力(基于 img_id)的同时,对文档进行更精细化的预处理,特别是针对表格内图片这类复杂场景,以确保分块和图片关联符合预期。同样以上述演示的维保案例为例,下面提供一个通过 PyMuPDF 等工具对 PDF 进行预处理和重组的思路参考。
3.1
核心问题
PDF 分块不理想
原始 PDF 的文本和图片布局可能导致 RAGFlow 分块时图文分离
维修案例等结构化内容被错误拆分,影响检索效果
图文关联不准确
图片与相关文本内容在不同分块中,检索时无法准确关联
RAGFlow 的 img_id 机制需要合适的文档结构才能发挥最佳效果
3.2
解决方案
PDF 重组 + RAGFlow 原生处理
预处理阶段:使用 PyMuPDF 提取文本和图片,按业务逻辑重新组织
重组阶段:使用 reportlab 重新生成 PDF,确保每个业务单元(如维修案例)独立成页
RAGFlow 处理:利用 RAGFlow 原生的 DeepDoc 和 img_id 机制处理重组后的 PDF
优化配置:针对重组 PDF 的特点优化分块策略和助手配置
3.3
混合方案考量
通过 PyMuPDF+reportlab 的 PDF 重组策略,可以实现从"物理布局优先"到"业务语义优先"的转变。在预处理阶段就按照业务逻辑重新组织文档结构,确保每个业务单元(如维修案例)的完整性,让 RAGFlow 的原生能力在更合适的文档结构上发挥作用。
项目源码已发布在星球中
注:
1、上述列举的 PDF 重组策略是个基础演示,方案的有效性依赖于业务逻辑的清晰定义,各位需要结合具体场景和实践进行验证。
2、这次更新的 API 文档中 create_dataset 方法也存在调整,原脚本中的 language 参数、embedding_model 格式和 avatar 参数都有调整,各位更新脚本可以注意下。
4
写在最后
RAGFlow 的 img_id 方案作为原生内置功能,具有显著的工程优势。这种设计简化了用户端配置,提高了系统稳定性和集成度,从架构层面规避了 LLM 篡改图片链接的风险。统一的后端 API 调用为权限控制和图片资源管理提供了坚实基础,这对企业级应用的安全性和可维护性至关重要。
然而,原生方案也带来了文档预处理的复杂性。RAGFlow 的分块算法主要基于文本结构和版面分析,对于复杂的业务文档(如维修手册、技术规范等),往往难以准确识别业务语义边界,容易出现图文分离、上下文割裂等问题。特别是当图片与相关文本在物理布局上分散时,传统的分块策略很难保持它们的逻辑关联性。
上述演示的PDF重组方案,主要目的是在保留 RAGFlow 原生 img_id 机制的所有优势(稳定性、安全性、企业级特性)的同时,又通过符合业务逻辑的预处理解决复杂文档的前置打磨。它不是对原生方案的替代,更多是一种增强和优化,让 RAGFlow 在处理结构化业务文档时表现更佳。
53AI,企业落地大模型首选服务商
产品:场景落地咨询+大模型应用平台+行业解决方案
承诺:免费场景POC验证,效果验证后签署服务协议。零风险落地应用大模型,已交付160+中大型企业
2025-05-30
2025年GitHub上十大RAG框架深度解析:从技术原理到实战应用
2025-05-30
90%企业不知道的RAG优化秘籍:Dify原生集成RAGflow (2)
2025-05-30
RAG其实并没有你想的那么简单,Late Chunking vs Contextual Retrieval解决上下文难题
2025-05-30
基于Gemini与Qdrant构建生产级RAG管道:设计指南与代码实践
2025-05-30
RAG和向量数据库之间有什么关系?
2025-05-30
RAG相关术语快速了解
2025-05-29
超越基础:Agentic Chunking 如何彻底改变 RAG?
2025-05-29
用Milvus构建RAG系统,N8N VS dify 如何选?
2024-10-27
2024-09-04
2024-05-05
2024-07-18
2024-06-20
2024-06-13
2024-07-09
2024-07-09
2024-05-19
2024-07-07
2025-05-30
2025-05-29
2025-05-29
2025-05-23
2025-05-16
2025-05-15
2025-05-14
2025-05-14