当前位置: 首页 > news >正文

文档上传后别急着向量化!解析和切片做错,RAG 回答一定跑偏

上一章我们解决了文件保存问题:用户上传的原始文档保存到 MinIO,数据库中的 document_info.storage_path 记录对象路径,task-service 可以根据这个路径下载文件。

但文件下载下来之后,还不能直接用于 RAG。

因为 RAG 检索的不是“文件本身”,而是文件中的文本片段。

一份 PDF、Markdown 或 TXT 文档,需要先经历两步处理:

文档解析 -> 文本切片

文档解析负责把不同格式的文件变成纯文本。

文本切片负责把长文本拆成多个适合向量化和检索的小片段。

这一篇要讲清楚:

  • 为什么上传文件不能直接向量化。

  • DocumentParser

    接口如何设计。

  • TXT、Markdown、PDF 如何解析。

  • DocumentParserDispatcher

    如何选择解析器。

  • 为什么要做文本归一化。

  • 为什么要做切片。

  • chunkSize

    overlap分别解决什么问题。

  • document_chunk

    表如何保存切片结果。


01 为什么上传文件不能直接向量化

很多初学者会有一个疑问:既然 RAG 最后要把文档变成向量,为什么不直接把整个文件丢给 Embedding 模型?

原因有三个。

原始文件不是纯文本

用户上传的文件可能是:

txt md markdown pdf

TXT 和 Markdown 本身接近文本,但 PDF 不是简单文本文件。

PDF 中可能包含分页、排版、字体、表格、换行和不可见字符。

Embedding 模型需要的是文本,而不是二进制文件。

所以第一步必须解析。

文档通常太长

一份制度文档可能几千字,一份项目手册可能几万字。

如果把整篇文档一次性向量化,会出现问题:

  • 文本超过模型输入限制。
  • 向量表示过于粗糙。
  • 检索时只能命中整篇文档,无法定位具体段落。
  • 后续拼 Prompt 成本很高。

RAG 需要的是“找到最相关的一小段资料”,而不是“把整篇文档都塞给模型”。

引用来源需要片段粒度

KnowHub 返回答案时,不只返回模型生成的回答,还会返回引用来源。

如果系统只保存整篇文档,那么引用来源只能告诉用户:答案来自某个文件。

但用户真正需要的是更细粒度的信息:

答案来自哪份文档的第几个片段 这个片段内容是什么 相似度是多少

这就要求系统必须把文档拆成 chunk。


02 文档解析器怎么设计

文档解析的目标是:

输入:文件路径或文件流 输出:纯文本内容

不同文件类型解析方式不同。

TXT 可以直接读取文本。

Markdown 可以按文本读取,保留标题和正文。

PDF 需要使用 PDFBox 这类库提取文本。

为了让代码结构清晰,KnowHub 使用统一接口抽象解析器。

依赖引入

本章的解析和切片发生在 task-service 中。

原因很简单:knowledge-service 负责接收上传请求、保存文件元数据、把原始文件放到 MinIO;真正耗时的解析、切片、Embedding 和向量入库,应该交给后台任务服务异步执行。

PDF 解析需要引入 Apache PDFBox:

org.apache.pdfbox pdfbox 3.0.2

DocumentParser 接口

可以设计一个接口:

public interface DocumentParser { boolean supports(String fileType); String parse(Path path); }

它表达两个意思。

第一,这个解析器支持什么类型。

第二,给它一个文件路径,它返回解析后的文本。

这样后续增加 Word、Excel、OCR 时,不需要改所有业务代码,只需要新增解析器实现。

TXT / Markdown 解析

TXT、Markdown 都可以先走普通文本解析器。

它负责:

  • 读取文件内容。
  • 处理编码。
  • 返回字符串。

支持类型可以包括:

txt md markdown

Markdown 虽然有标题、列表、代码块等结构,但第一版可以当作文本处理。后续如果想提高切片质量,可以增加 Markdown 标题感知切片。

PDF 解析

PDF 解析器可以基于 PDFBox。

PDF 解析是文件处理中比较容易出问题的地方,因为 PDF 可能存在:

  • 扫描件,没有文本层。
  • 复杂表格。
  • 页眉页脚干扰。
  • 换行错乱。
  • 加密或损坏。

所以 PDF 解析失败时,要记录明确错误,而不是让任务无声失败。

扫描版 PDF 通常只有图片,没有可直接提取的文本层。第一版不会自动 OCR,这种文件解析结果可能为空,应在后面的空文本校验中把任务标记为失败。

后续扩展

后续可以继续扩展:

  • Word 解析。
  • Excel 解析。
  • PPT 解析。
  • 图片 OCR。
  • 网页正文抽取。

但本系列主线先把 TXT、Markdown、PDF 跑通。这样足够覆盖 RAG 平台的核心文档处理流程。


03 解析器分发:不要写一堆 if-else

有了多个解析器后,业务代码不应该手动写一堆 if-else。

更好的方式是使用 DocumentParserDispatcher。

它的职责是:

根据文件类型选择合适解析器

流程如下:

输入 fileType -> 遍历所有 DocumentParser -> 找到 supports(fileType) = true 的解析器 -> 调用 parse -> 返回文本

如果没有解析器支持当前类型,就直接抛出业务异常。

比如用户上传:

video.mp4

系统应该明确返回:

不支持的文件类型

而不是等到后面 Embedding 时报错。

这种分发器设计的好处是可扩展。

以后新增 Word 解析器,只需要让它实现 DocumentParser 并注册到 Spring 容器,Dispatcher 就能自动使用。


04 为什么要做文本归一化

解析出来的文本不能完全不处理。

不同文件格式解析出来的文本可能存在很多问题:

  • 多余空格。
  • 连续空行。
  • Windows 和 Linux 换行不一致。
  • PDF 换行错乱。
  • 不可见字符。
  • 文本为空。

所以在切片前,需要做基础归一化。

统一换行

Windows 换行通常是:

\r\n

Linux 换行通常是:

\n

系统可以统一转换成 \n,方便后续处理。

去掉过多空白

连续多个空行可以压缩。

行首行尾空格可以清理。

但不要粗暴删除所有换行。

因为换行往往代表段落边界,对切片有价值。

空文本校验

如果解析后文本为空,就不应该继续切片和向量化。

常见原因包括:

  • PDF 是扫描件。
  • 文件内容本身为空。
  • 编码不正确。
  • 解析器不支持该文件。

这种情况应该让任务失败,并记录错误原因。

保留必要结构

Markdown 中的标题、列表、代码块,对语义有帮助。

第一版可以不做复杂结构解析,但不建议把所有格式信息都暴力删除。

例如标题:

## 报销制度

它能帮助模型理解后面内容属于哪个主题。


05 为什么要做文本切片

文档解析完成后,我们得到一大段文本。

接下来要做切片。

切片的英文通常叫 chunking。

它的目标是:

把长文本拆成多个较短、语义尽量完整的片段

检索需要局部片段

用户提问通常只和文档中的一小部分相关。

比如一本员工手册里有:

  • 入职流程。
  • 考勤制度。
  • 年假规则。
  • 报销流程。
  • 离职手续。

用户问年假时,系统只需要召回年假相关片段。

如果整本手册只有一个向量,检索结果就很粗糙。

降低 Prompt 成本

如果每次问答都把整篇文档放进 Prompt,成本会很高,也容易超过上下文限制。

切片后,只把最相关的几个 chunk 放进 Prompt。

这就是 RAG 比“整篇文档塞给模型”更实用的原因。

提高引用准确性

切片后,系统可以告诉用户:答案来自哪几个片段。

这比只告诉用户“来自员工手册.pdf”更有价值。


06 TextChunker 怎么设计

TextChunker 是文本切片组件。

最基础的切片策略是固定长度切片。

它通常有两个核心参数:

chunkSize overlap

chunkSize

chunkSize 表示每个 chunk 的最大长度。

比如:

chunkSize = 800

表示每个片段大约 800 个字符。

chunk 太大,会导致:

  • 检索不够精确。
  • Prompt 变长。
  • 召回片段包含太多无关内容。

chunk 太小,会导致:

  • 语义不完整。
  • 一句话被拆断。
  • 模型看到的上下文不足。

中文文档可以从 500 到 800 字符起步,根据效果调整。

overlap

overlap 表示相邻 chunk 之间重叠的内容长度。

比如:

chunkSize = 800 overlap = 100

第一个 chunk 是 0 到 800 字符。

第二个 chunk 不是从 800 开始,而是从 700 开始。

这样可以避免关键信息刚好被切在边界处。

chunkIndex

每个 chunk 都需要序号。

chunkIndex = 0 chunkIndex = 1 chunkIndex = 2

序号用于:

  • 保持文档顺序。
  • 前端展示引用来源。
  • 重建索引时覆盖旧数据。
  • 通过唯一约束避免重复写入。

边界处理

切片时要处理一些边界情况:

  • 文本长度小于 chunkSize。
  • overlap 大于或等于 chunkSize。
  • 最后一个 chunk 不足 chunkSize。
  • 文本为空。

如果 overlap 设置不合理,比如 overlap >= chunkSize,切片循环可能无法前进,甚至死循环。

所以启动时或配置读取时,应该校验参数合法性。

配置可以放到 task-service 的 application.yml 中:

knowhub: chunking: chunk-size: 800 overlap: 100

07 document_chunk 表怎么设计

切片结果需要保存到 MySQL。

表名可以是 document_chunk。

简化结构如下:

CREATE TABLE document_chunk ( id BIGINT PRIMARY KEY AUTO_INCREMENT, document_id BIGINT NOT NULL, kb_id BIGINT NOT NULL, chunk_index INT NOT NULL, content TEXT NOT NULL, content_hash VARCHAR(64), token_count INT, vector_id VARCHAR(128), created_at DATETIME NOT NULL, UNIQUE KEY uk_doc_chunk (document_id, chunk_index), INDEX idx_document_id (document_id), INDEX idx_kb_id (kb_id) );

几个字段要重点理解。

document_id 表示这个 chunk 来自哪份文档。

kb_id 表示这个 chunk 属于哪个知识库。

chunk_index 表示 chunk 在文档中的顺序。

content 保存 chunk 文本内容。RAG 问答时,检索到相关向量后,系统需要拿到 chunk 内容拼接 Prompt。

content_hash 可以保存内容哈希,有助于判断内容是否重复,或者后续做增量索引。

vector_id 可以记录向量表中的对应记录。KnowHub 使用 MySQL 存 chunk 元数据,PostgreSQL + pgvector 存向量数据,这样业务数据和向量数据职责更清晰。

document_id + chunk_index 应该有唯一约束。这样即使 RabbitMQ 重复投递任务,或者任务重试时重复写入,也可以通过数据库约束兜底。


08 从解析到切片的完整流程

现在把前面的内容串起来。

task-service 消费索引任务后,大致流程是:

1. 根据 documentId 查询 document_info 2. 从 MinIO 下载 storage_path 对应文件 3. 根据 file_type 选择 DocumentParser 4. 解析文件,得到原始文本 5. 文本归一化 6. 检查文本是否为空 7. 使用 TextChunker 切片 8. 删除旧 document_chunk 和旧向量 9. 批量写入新的 document_chunk 10. 后续调用 Embedding 并写入 pgvector

注意,第 8 步非常重要。

如果是重建索引,必须先删除旧 chunk 和旧向量,否则会出现同一文档多份索引数据。

这一条主线跑通后,再进入 Embedding 和 pgvector,会更容易理解后续章节。


09 常见问题排查

PDF 解析失败

常见原因:

  • PDF 文件损坏。
  • PDF 加密。
  • PDF 是扫描图片,没有文本层。
  • PDFBox 版本不兼容。

处理方式:捕获异常,任务状态置为 FAILED,记录 error_message,后续可扩展 OCR 处理扫描件。

解析结果为空

如果解析结果为空,不应该继续切片。

排查:

  • 原文件是否为空。
  • 文件类型是否正确。
  • 解析器是否选对。
  • PDF 是否为扫描件。
  • 编码是否正确。

文本乱码

TXT 文件最容易遇到编码问题。

如果系统默认按 UTF-8 读取,但文件是 GBK,就可能乱码。

学习项目可以先约定 UTF-8,后续再扩展自动识别。

chunk 太大

现象:检索结果命中一个很长片段,Prompt 变得很长。

解决:调小 chunkSize。

chunk 太小

现象:检索到的片段只有半句话,模型无法回答完整问题。

解决:调大 chunkSize,或增加 overlap。

overlap 配置错误

如果 overlap 大于等于 chunkSize,切片逻辑可能无法前进。

配置校验中应该禁止这种情况:

overlap < chunkSize

chunk_count 和实际不一致

如果 document_info.chunk_count 和 document_chunk 实际数量不一致,说明任务执行过程中状态更新有问题。

排查:

  • chunk 是否批量写入成功。
  • document_info 是否更新 chunk_count。
  • 任务是否中途失败。
  • 是否重复执行导致旧数据未清理。

PDFBox 解析大文件时内存溢出

现象:task-service 在处理某些 PDF 时抛出 OutOfMemoryError、GC overhead limit exceeded,或者服务突然变慢、频繁 Full GC。

排查顺序:

  1. 确认 PDF 文件是否过大,例如超过 50MB。
  2. 确认 PDF 是否包含大量高清图片。
  3. 确认 task-service 的 JVM 堆内存是否过小。
  4. 确认上传入口是否已经限制 PDF 文件大小。

学习阶段先限制 PDF 上传大小。后续如果要支持大 PDF,可以扩展分批解析、临时文件解析或更专业的文档解析服务。


10 本章新增类清单

本章落到 task-service 中,主要新增这些类:

task-service/src/main/java/.../parser/ DocumentParser.java 解析器接口 PlainTextDocumentParser.java TXT/Markdown 解析 PdfDocumentParser.java PDF 解析 DocumentParserDispatcher.java 解析器分发 task-service/src/main/java/.../chunking/ TextNormalizer.java 文本归一化 TextChunker.java 文本切片 ChunkResult.java 切片结果 POJO ChunkingConfig.java 切片配置类

读者动手时可以先按这个目录建类,再把本章代码填进去。这样项目结构不会散,也方便后续第 9 章的 RabbitMQ 消费任务调用。


11 动手验证:从解析到切片结果入库

学完本章后,可以用三类文件验证解析和切片链路是否生效。

步骤一,准备三个测试文件。

test.txt:UTF-8 编码,约 2000 字符 test.md:包含标题、列表和普通段落 test.pdf:包含可复制文本的 PDF,确保不是扫描件

步骤二,通过 Gateway 分别上传三个文件到同一个知识库,记录每个返回的 documentId。

POST /kb/{kbId}/documents/upload Authorization: Bearer Content-Type: multipart/form-data file: test.txt / test.md / test.pdf

步骤三,确认 task-service 已启动,并确认 RabbitMQ 消息已经被消费。

可以查看 task-service 日志中是否出现类似信息:

开始解析文档 文档解析完成 索引任务执行完成

步骤四,查询 document_info 表。

SELECT id, file_name, file_type, index_status, chunk_count, error_message FROM document_info WHERE id IN (文档1, 文档2, 文档3);

预期结果:三个文档的 index_status 都变为 INDEXED,chunk_count 大于 0。

步骤五,查询 document_chunk 表。

SELECT document_id, chunk_index, CHAR_LENGTH(content) AS content_len, LEFT(content, 80) AS preview FROM document_chunk WHERE document_id = 文档ID ORDER BY document_id, chunk_index;

预期结果:每个文档都有多条 chunk 记录,chunk_index 从 0 开始递增,内容长度大致符合 chunkSize 配置,相邻 chunk 之间能看到少量重叠内容。

步骤六,上传一个空白 TXT 文件。

预期结果:任务状态变为 FAILED,error_message 中包含“解析后文本为空”或类似提示。

步骤七,上传一个扫描版 PDF。

预期结果:如果 PDF 没有文本层,任务应变为 FAILED,并记录合理错误信息,而不是一直卡在 INDEXING。

如果切片结果和预期不一致,先检查 application.yml 中的 chunkSize 和 overlap 配置,再检查 TextChunker 的边界处理逻辑,尤其是 overlap < chunkSize 这个约束是否生效。


本章小结

这一章我们讲了文档解析与文本切片。

原始文件不能直接进入 RAG 检索。系统必须先通过解析器把 TXT、Markdown、PDF 等文件变成纯文本,再通过 TextChunker 把长文本切成多个适合向量化和检索的 chunk。

DocumentParser 接口让不同文件类型有统一解析入口,DocumentParserDispatcher 负责根据文件类型选择合适解析器。文本解析后还要进行基础归一化和空文本校验。

切片时最重要的两个参数是 chunkSize 和 overlap。chunkSize 控制片段大小,overlap 解决边界信息丢失问题。切片结果保存到 document_chunk 表,并通过 document_id + chunk_index 唯一约束防止重复写入。

下一章,我们会进入 RabbitMQ 消息队列与索引任务,讲清文档上传后如何把解析、切片、Embedding 和向量入库放到后台异步执行。


作者有话说

如果这篇文章对你有帮助,欢迎点个关注。

这个专栏会持续更新KnowHub / RAG 平台实战内容,后面会继续把 RabbitMQ 索引任务、Embedding、pgvector 向量检索和 RAG 问答闭环拆开讲清楚。

如果你想对照代码学习,可以结合下面两个仓库:

rag-demo-monolith:单体版源码

适合先理解 RAG 核心闭环,把业务链路跑通。

仓库地址:

https://gitee.com/MrLuoBin/rag-demo-monolith.git

克隆命令:

git clone https://gitee.com/MrLuoBin/rag-demo-monolith.git

rag-platform:微服务版源码

适合继续学习 Gateway、Auth、Knowledge、Task 的企业级拆分方式。

仓库地址:

https://gitee.com/MrLuoBin/rag-platform.git

克隆命令:

git clone https://gitee.com/MrLuoBin/rag-platform.git

如果你在做 AI 知识库或 RAG 项目,解析和切片不要糊弄。检索质量差,很多时候不是模型不行,而是文档在进入向量库之前就已经处理坏了。

学AI大模型的正确顺序,千万不要搞错了

🤔2026年AI风口已来!各行各业的AI渗透肉眼可见,超多公司要么转型做AI相关产品,要么高薪挖AI技术人才,机遇直接摆在眼前!

有往AI方向发展,或者本身有后端编程基础的朋友,直接冲AI大模型应用开发转岗超合适!

就算暂时不打算转岗,了解大模型、RAG、Prompt、Agent这些热门概念,能上手做简单项目,也绝对是求职加分王🔋

📝给大家整理了超全最新的AI大模型应用开发学习清单和资料,手把手帮你快速入门!👇👇

学习路线:

✅大模型基础认知—大模型核心原理、发展历程、主流模型(GPT、文心一言等)特点解析
✅核心技术模块—RAG检索增强生成、Prompt工程实战、Agent智能体开发逻辑
✅开发基础能力—Python进阶、API接口调用、大模型开发框架(LangChain等)实操
✅应用场景开发—智能问答系统、企业知识库、AIGC内容生成工具、行业定制化大模型应用
✅项目落地流程—需求拆解、技术选型、模型调优、测试上线、运维迭代
✅面试求职冲刺—岗位JD解析、简历AI项目包装、高频面试题汇总、模拟面经

以上6大模块,看似清晰好上手,实则每个部分都有扎实的核心内容需要吃透!

我把大模型的学习全流程已经整理📚好了!抓住AI时代风口,轻松解锁职业新可能,希望大家都能把握机遇,实现薪资/职业跃迁~

这份完整版的大模型 AI 学习资料已经上传CSDN,朋友们如果需要可以微信扫描下方CSDN官方认证二维码免费领取【保证100%免费

http://www.jsqmd.com/news/1358939/

相关文章:

  • MySQL数据误操作恢复:binlog实战指南
  • 改进灰狼算法在电力系统多目标优化调度中的应用
  • 2026年8月空调机组厂家推荐指南:远程射流空调机组,恒温恒湿空调机组,柜式空调机组,转轮热回收空调机组,组合式空调机组公司优选! - 品牌商讯
  • OpenAI Astra(GPT-6)多模态AI模型:核心能力、接入准备与测试指南
  • 062、顶会注意力机制复现:PKINet上下文先验注意力适配YOLOv12的R-ELAN模块,手把手代码与COCO mAP对比
  • AI Agent网页抓取实战:绕过CORS与动态内容困境
  • 化工仪表物联网系统(PAIMS)架构设计与预测性维护实践
  • 散点图实战指南:从基础到商业分析应用
  • Windows防休眠工具终极指南:告别自动锁屏的智能解决方案
  • 基于YOLOv8/YOLOv10/YOLOv11/YOLOv12与SpringBoot的扑克牌识别检测系统(千问+DeepSeek智能分析+web交互界面+前后端分离+YOLO数据)
  • AI 漫剧剧情容易烂尾?知漫剧分集剧本生成实战教程
  • 揭秘中国建设银行内部网站:揭秘其功能与价值,探索中国建设银行内部网站如何赋能员工高效办公
  • 2026合肥理工学校怎么报名?应往届初中毕业生均可报,附正规报名流程与联系方式 - 最新资讯
  • 数据库系统原理核心考点与SQL优化实战
  • 5分钟快速上手:Reloaded-II游戏Mod管理器完整指南
  • 突破性Emby高级功能解锁方案:零成本享受完整媒体服务器体验
  • OriginLab在XRD半峰宽计算中的高效应用
  • 电力施工单位(0.4KV、10KV、35KV)怎么选?一文读懂如何挑选真正有实力的服务商! - 甄选测评馆
  • Kali Linux命令行实战指南:从基础操作到渗透测试高效工作流
  • Mac效率提升:一键预览与快速打开Markdown文件的两种实用方案
  • # 投票小程序免费制作怎么选?4款热门工具横评实测 - 资讯报道
  • Ubuntu桌面安全体检:使用ClamTk进行病毒扫描与防护
  • VibeCoding实时同步工作台:AI编程助手的结构化协作新范式
  • 线性卷积的分段计算方法:重叠相加与重叠保留法详解
  • 月付10元内!Docker一键部署幻兽帕鲁私服全攻略
  • Lunar-Javascript:企业级农历公历转换架构设计与高性能实现
  • 2026 三亚房屋漏水渗水修缮选择指南:厨卫、外墙、屋顶、飘窗阳光房渗漏怎么高效处理 - 筑宅安
  • 隔壁公司用AI工具3分钟做了一套拼多多主图+详情页的套图
  • 2026合肥理工学校就业怎么样?对口就业率97%以上,毕业直通蔚来、比亚迪等龙头企业 - 最新资讯
  • Flutter+OpenHarmony实现动态字体调节技术解析