公司动态

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

📅 2026/8/9 11:14:42
文档上传后别急着向量化!解析和切片做错,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 pdfTXT 和 Markdown 本身接近文本但 PDF 不是简单文本文件。PDF 中可能包含分页、排版、字体、表格、换行和不可见字符。Embedding 模型需要的是文本而不是二进制文件。所以第一步必须解析。文档通常太长一份制度文档可能几千字一份项目手册可能几万字。如果把整篇文档一次性向量化会出现问题文本超过模型输入限制。向量表示过于粗糙。检索时只能命中整篇文档无法定位具体段落。后续拼 Prompt 成本很高。RAG 需要的是“找到最相关的一小段资料”而不是“把整篇文档都塞给模型”。引用来源需要片段粒度KnowHub 返回答案时不只返回模型生成的回答还会返回引用来源。如果系统只保存整篇文档那么引用来源只能告诉用户答案来自某个文件。但用户真正需要的是更细粒度的信息答案来自哪份文档的第几个片段 这个片段内容是什么 相似度是多少这就要求系统必须把文档拆成 chunk。02 文档解析器怎么设计文档解析的目标是输入文件路径或文件流 输出纯文本内容不同文件类型解析方式不同。TXT 可以直接读取文本。Markdown 可以按文本读取保留标题和正文。PDF 需要使用 PDFBox 这类库提取文本。为了让代码结构清晰KnowHub 使用统一接口抽象解析器。依赖引入本章的解析和切片发生在 task-service 中。原因很简单knowledge-service 负责接收上传请求、保存文件元数据、把原始文件放到 MinIO真正耗时的解析、切片、Embedding 和向量入库应该交给后台任务服务异步执行。PDF 解析需要引入 Apache PDFBoxorg.apache.pdfbox pdfbox 3.0.2DocumentParser 接口可以设计一个接口public interface DocumentParser { boolean supports(String fileType); String parse(Path path); }它表达两个意思。第一这个解析器支持什么类型。第二给它一个文件路径它返回解析后的文本。这样后续增加 Word、Excel、OCR 时不需要改所有业务代码只需要新增解析器实现。TXT / Markdown 解析TXT、Markdown 都可以先走普通文本解析器。它负责读取文件内容。处理编码。返回字符串。支持类型可以包括txt md markdownMarkdown 虽然有标题、列表、代码块等结构但第一版可以当作文本处理。后续如果想提高切片质量可以增加 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\nLinux 换行通常是\n系统可以统一转换成 \n方便后续处理。去掉过多空白连续多个空行可以压缩。行首行尾空格可以清理。但不要粗暴删除所有换行。因为换行往往代表段落边界对切片有价值。空文本校验如果解析后文本为空就不应该继续切片和向量化。常见原因包括PDF 是扫描件。文件内容本身为空。编码不正确。解析器不支持该文件。这种情况应该让任务失败并记录错误原因。保留必要结构Markdown 中的标题、列表、代码块对语义有帮助。第一版可以不做复杂结构解析但不建议把所有格式信息都暴力删除。例如标题## 报销制度它能帮助模型理解后面内容属于哪个主题。05 为什么要做文本切片文档解析完成后我们得到一大段文本。接下来要做切片。切片的英文通常叫 chunking。它的目标是把长文本拆成多个较短、语义尽量完整的片段检索需要局部片段用户提问通常只和文档中的一小部分相关。比如一本员工手册里有入职流程。考勤制度。年假规则。报销流程。离职手续。用户问年假时系统只需要召回年假相关片段。如果整本手册只有一个向量检索结果就很粗糙。降低 Prompt 成本如果每次问答都把整篇文档放进 Prompt成本会很高也容易超过上下文限制。切片后只把最相关的几个 chunk 放进 Prompt。这就是 RAG 比“整篇文档塞给模型”更实用的原因。提高引用准确性切片后系统可以告诉用户答案来自哪几个片段。这比只告诉用户“来自员工手册.pdf”更有价值。06 TextChunker 怎么设计TextChunker 是文本切片组件。最基础的切片策略是固定长度切片。它通常有两个核心参数chunkSize overlapchunkSizechunkSize 表示每个 chunk 的最大长度。比如chunkSize 800表示每个片段大约 800 个字符。chunk 太大会导致检索不够精确。Prompt 变长。召回片段包含太多无关内容。chunk 太小会导致语义不完整。一句话被拆断。模型看到的上下文不足。中文文档可以从 500 到 800 字符起步根据效果调整。overlapoverlap 表示相邻 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: 10007 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 chunkSizechunk_count 和实际不一致如果 document_info.chunk_count 和 document_chunk 实际数量不一致说明任务执行过程中状态更新有问题。排查chunk 是否批量写入成功。document_info 是否更新 chunk_count。任务是否中途失败。是否重复执行导致旧数据未清理。PDFBox 解析大文件时内存溢出现象task-service 在处理某些 PDF 时抛出 OutOfMemoryError、GC overhead limit exceeded或者服务突然变慢、频繁 Full GC。排查顺序确认 PDF 文件是否过大例如超过 50MB。确认 PDF 是否包含大量高清图片。确认 task-service 的 JVM 堆内存是否过小。确认上传入口是否已经限制 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.txtUTF-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 都变为 INDEXEDchunk_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 文件。预期结果任务状态变为 FAILEDerror_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.gitrag-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%免费】