第10篇-文档加载与分割:让AI读到你的资料
📝 作者说:前两篇我们搞定了向量数据库和 Embedding,但一直是在代码里手写字符串当知识库。真实场景中,知识库可能是 PDF、Word、Markdown、HTML——这篇就解决一个问题:怎么把这些文件喂给 AI。
一、为什么需要文档加载和分割?
1.1 RAG的”消化系统”
回顾一下 RAG 的完整流程:
原始文档 → 加载(DocumentLoader)→ 分割(DocumentSplitter)→ 向量化(Embedding)→ 存储(EmbeddingStore)↓用户提问 → 查询向量化 → 相似度检索 → 拼入 Prompt → LLM 生成回答
前两篇讲了”向量化”和”存储”环节。这篇讲前两步:加载和分割。
为什么不能直接把整个文档丢给 Embedding?
<br class="Apple-interchange-newline"><div></div>一个100页的PDF ≈ 5万字Embedding模型的输入上限通常是 512~8192 Token5万字 ≈ 25000 Token → 远超上限!即使模型能吃下,效果也很差:- 向量是整个文档的"平均语义",丢失了局部细节- 检索时无法精确定位到某一段
所以必须先加载,再分割,最后逐段向量化。
你可能会问:不分割会怎样?我把整个PDF直接做 Embedding 不行吗?
技术上可以,但效果就像——把一锅牛肉土豆番茄汤扔进搅拌机打成泥。你问AI”牛肉在哪?”,它尝了一口说”嗯…好像有肉的影子?”。向量表示的是全文的「平均语义」,每个知识点的特色都被抹平了。所以,必须切。
1.2 一个直觉类比
把文档分割想象成吃西瓜:
整个西瓜 → 你不可能一口吞下去切西瓜刀 → DocumentSplitter切成小块 → TextSegment每块大小要适中 → 太大塞不进嘴,太小吃不到肉
二、DocumentLoader:加载各类文档
2.1 LangChain4j的文档加载体系
FileSystemDocumentLoader(文件系统加载器)├── loadDocument(Path, DocumentParser) → 加载单个文件└── loadDocuments(Path, DocumentParser) → 加载整个目录DocumentParser(文档解析器,按格式选)├── TextDocumentParser → .txt/.md/.csv 等所有纯文本(万能兜底)├── ApachePdfBoxDocumentParser → .pdf(轻量,只支持文本型PDF)└── ApacheTikaDocumentParser → .pdf/.doc/.docx/.pptx/.html(万能,但重)(注:MarkdownDocumentParser 仅在 1.x-beta 中可用,1.0.0 稳定版用 TextDocumentParser 即可)
核心思路:加载和解析是分离的。FileSystemDocumentLoader 负责读文件,DocumentParser 负责把内容转成 Document 对象。
2.2 加载纯文本
最简单的情况,不需要额外依赖:
import dev.langchain4j.data.document.Document;import dev.langchain4j.data.document.DocumentParser;import dev.langchain4j.data.document.parser.TextDocumentParser;import java.nio.file.Path;import static dev.langchain4j.data.document.loader.FileSystemDocumentLoader.loadDocument;// 加载TXT文件DocumentParser parser = new TextDocumentParser();Document doc = loadDocument(Path.of("knowledge/faq.txt"), parser);System.out.println(doc.text()); // 文档全文System.out.println(doc.metadata()); // Metadata(文件名、来源等)
2.3 加载PDF
需要引入 Apache PDFBox 依赖:
<dependency><groupId>dev.langchain4j</groupId><artifactId>langchain4j-document-parser-apache-pdfbox</artifactId><version>${langchain4j.version}</version></dependency>
import dev.langchain4j.data.document.parser.apache.pdfbox.ApachePdfBoxDocumentParser;DocumentParser pdfParser = new ApachePdfBoxDocumentParser();Document pdfDoc = loadDocument(Path.of("knowledge/user-manual.pdf"), pdfParser);System.out.println("PDF页数估算:" + pdfDoc.text().length() / 500 + " 页");
⚠️ PDF解析的限制:Apache PDFBox 只能提取文本型 PDF 的内容。如果你的 PDF 是扫描件(图片型),文字提取出来是空的。扫描件需要 OCR 预处理,这超出了 LangChain4j 的范围。
那扫描件怎么办?有人可能会想”那我全用 Tika 不就行了?”——抱歉,Tika 也搞不定扫描件。扫描件本质是图片,需要先用 OCR(比如 Tesseract)把图片变成文字,再喂给 LangChain4j。这就好比一个只会认字的老师面对一张照片——他得先有人帮他把照片上的字”抄”下来,他才能教。
2.4 万能解析器:Apache Tika
如果你要支持 Word、PPT、Excel、HTML 等各种格式,用 Tika:
<dependency><groupId>dev.langchain4j</groupId><artifactId>langchain4j-document-parser-apache-tika</artifactId><version>${langchain4j.version}</version></dependency>
import dev.langchain4j.data.document.parser.apache.tika.ApacheTikaDocumentParser;DocumentParser tikaParser = new ApacheTikaDocumentParser();// Tika 自动识别格式,不管什么后缀都能解析Document doc1 = loadDocument(Path.of("knowledge/report.docx"), tikaParser);Document doc2 = loadDocument(Path.of("knowledge/slides.pptx"), tikaParser);Document doc3 = loadDocument(Path.of("knowledge/page.html"), tikaParser);
Tika 的代价:引入后 JAR 包增加约 80MB。如果你只需要解析 PDF,用 PDFBox 就够了。杀鸡焉用牛刀,除非你真的需要那把牛刀。
2.5 加载Markdown
💡 Markdown 本质就是纯文本,直接用 TextDocumentParser 加载即可。Markdown 中的
标题会作为文本内容的一部分,配合 DocumentByParagraphSplitter(按空行分段)或 recursive 分割器,就能自然地按标题段落分块。
// Markdown 文件用 TextDocumentParser 加载,无需额外依赖DocumentParser parser = new TextDocumentParser();Document mdDoc = loadDocument(Path.of("knowledge/api-guide.md"), parser);// 输出包含完整的 Markdown 原始内容(含 # 标题标记)
⚠️ 扩展知识:LangChain4j 在 1.x-beta 版本中提供了独立的 MarkdownDocumentParser(langchain4j-document-parser-markdown),它能提取标题层级作为 Metadata,方便后续按标题过滤。但 1.0.0 稳定版暂不包含此模块,用 TextDocumentParser 完全够用。
2.6 批量加载整个目录
import static dev.langchain4j.data.document.loader.FileSystemDocumentLoader.loadDocuments;// 加载 knowledge/ 下所有文件List<Document> docs = loadDocuments(Path.of("knowledge/"), tikaParser);System.out.println("共加载 " + docs.size() + " 个文档");// 带过滤:只加载PDFList<Document> pdfDocs = loadDocuments(Path.of("knowledge/"),path -> path.toString().endsWith(".pdf"),tikaParser);
2.7 Maven依赖汇总
| 格式 |
artifactId |
大小 |
推荐场景 |
|
纯文本/Markdown |
(核心自带) |
0 |
.txt/.md/.csv 文件 |
|
|
langchain4j-document-parser-apache-pdfbox |
3MB |
只需要PDF |
|
全格式 |
langchain4j-document-parser-apache-tika |
80MB |
Word/PPT/HTML/PDF混用 |

三、DocumentSplitter:把文档切成片段
文档加载后是一个完整的 Document 对象。我们需要把它切成多个 TextSegment,才能逐段做 Embedding。
3.1 LangChain4j内置分割器一览
LangChain4j 提供了 DocumentSplitters 工厂类,内置了4种分割器:
importdev.langchain4j.data.document.DocumentSplitter;importdev.langchain4j.data.document.splitter.DocumentSplitters;
| 分割器 |
工厂方法 |
分割依据 |
适用场景 |
|
Recursive |
DocumentSplitters.recursive() |
按段落→换行→句号递归分割 |
⭐通用推荐,大多数场景 |
|
Paragraph |
new DocumentByParagraphSplitter() |
按空行分段 |
有明显段落结构的文档 |
|
Sentence |
new DocumentBySentenceSplitter() |
按句号分割 |
需要精确到句子级别的检索 |
|
Character |
new DocumentByCharacterSplitter() |
按字符数硬切 |
没有明显结构的纯文本 |
|
Line |
new DocumentByLineSplitter() |
按行分割 |
日志、CSV等行结构数据 |
3.2 推荐分割器:recursive
这是 LangChain4j 官方推荐的通用分割器,也是你90%场景下的首选:
DocumentSplitter splitter = DocumentSplitters.recursive(300, // maxSegmentSizeInTokens:每个片段最大Token数30, // maxOverlapSizeInTokens:相邻片段重叠Token数new OpenAiTokenCountEstimator("gpt-4o") // Token计数器(OpenAI兼容));
递归分割的原理:
输入:一段2000字的文档第1轮:按双换行(段落边界)切├── 段落A(600字)→ 超过300 Token,继续切├── 段落B(200字)→ ✅ 小于300,成为一个片段└── 段落C(1200字)→ 超过300 Token,继续切第2轮(对超长的段落):按单换行切├── 行1(300字)→ 超过300,继续切└── 行2(200字)→ ✅ 成为一个片段第3轮(对超长的行):按句号切第4轮(对超长的句子):按空格切最后兜底:按字符硬切
为什么叫”递归”? 因为它会一级一级地尝试更细粒度的分隔符,直到片段满足大小要求。这比直接按字符硬切好得多——递归分割尽量保留完整的语义单元。
有人可能会想:如果一段文字没有段落、没有句号、没有空格(比如一串 A1B2C3D4E5F6… 的连续ID),recursive 会怎么办?答案是——它就”摆烂”了,退化成按字符硬切。这就像你问一个厨师”这道菜怎么切?”他看了看说”这玩意儿没有纹理,随便剁吧”。所以如果你知道自己的数据长什么样,选对分割器很重要。
3.3 按段落分割
如果你的文档有清晰的段落结构(比如空行分隔),可以按段落分割:
import dev.langchain4j.data.document.splitter.DocumentByParagraphSplitter;DocumentSplitter splitter = new DocumentByParagraphSplitter(300, // maxSegmentSizeInTokens30 // maxOverlapSizeInTokens);
段落分割的行为:尽量把完整的段落放在同一个片段里。如果一个段落太大,就用子分割器(默认是 DocumentBySentenceSplitter)继续切。
3.4 按句子分割
精确到句子级别:
import dev.langchain4j.data.document.splitter.DocumentBySentenceSplitter;DocumentSplitter splitter = new DocumentBySentenceSplitter(300, // maxSegmentSizeInTokens30, // maxOverlapSizeInTokensnew DocumentByCharacterSplitter(100, 10) // 子分割器:句子太长时按字符切);
3.5 按字符硬切
最简单粗暴的方式——直接按字符数切:
import dev.langchain4j.data.document.splitter.DocumentByCharacterSplitter;DocumentSplitter splitter = new DocumentByCharacterSplitter(500, // maxSegmentSizeInChars(注意这里是字符数,不是Token数)50 // maxOverlapSizeInChars);
⚠️ 字符分割不保证语义完整。可能在句子中间、甚至单词中间切断。只在其他分割器都不适用时使用。
3.6 使用分割器
所有分割器的使用方式都一样:
// 加载文档Document doc = loadDocument(Path.of("knowledge/faq.txt"), new TextDocumentParser());// 分割List<TextSegment> segments = splitter.split(doc);System.out.println("原文长度:" + doc.text().length() + " 字符");System.out.println("分割后:" + segments.size() + " 个片段");for (int i = 0; i < Math.min(3, segments.size()); i++) {System.out.printf(" 片段%d(%d字):%s...%n",i + 1,segments.get(i).text().length(),segments.get(i).text().substring(0, Math.min(50, segments.get(i).text().length())));}
四、分割粒度对检索效果的影响
这是 RAG 系统调优中最容易被忽略、但影响最大的参数。
4.1 太短:上下文丢失
chunk_size = 50 字符片段1:"退货政策:自收到商品之日起7天"片段2:"内可申请无理由退货。退货时请"片段3:"保持商品完好,不影响二次销"片段4:"售。退款将在3-5个工作日内到账"
用户搜”怎么退货”:
·每个片段都是半句话,LLM 拿到后根本拼不出完整意思
·语义被切碎,向量表示的是半个句子的语义,不是完整知识点
这就像你把一封信撕成碎片塞进信封——对方收到后只知道”里面好像说了退货”,但具体怎么退,鬼知道。
4.2 太长:噪声多
chunk_size = 5000 字符片段1包含:- 退货政策(3行)- 换货政策(3行)- 物流说明(10行)- 优惠券规则(5行)- 客服时间(2行)
用户搜”怎么退货”:
·向量是5个知识点的平均语义,”退货”的权重被稀释
·检索可能把包含”退货”的巨型片段排在后面,反而不如精确的小片段
4.3 合适的粒度
chunk_size = 300~500 Token(约 200~350 个中文字)片段1:"退货政策:自收到商品之日起7天内可申请无理由退货。退货流程:联系客服 → 填写退货单 → 寄回商品 → 3-5个工作日退款。"片段2:"换货政策:自收到商品之日起15天内可申请换货。换货流程:联系客服 → 填写换货单 → 寄回商品 → 重新发货。"
每个片段包含一个完整的知识点,检索时语义精确,LLM 拿到后也能直接用。
4.4 调参建议
| 参数 |
推荐值 |
说明 |
|
maxSegmentSize |
300500 Token |
FAQ类用300,技术文档用500 |
|
maxOverlapSize |
maxSegmentSize 的 10%15% |
30~75 Token |
|
分割器 |
recursive |
大多数场景首选 |
💡 OverLap 的作用:相邻片段有重叠,防止知识点被切断后丢失上下文。比如第1个片段末尾和第2个片段开头有30个Token的重叠。
那 overlap 越大越好吗?当然不是。想象你切西瓜,每块都和前后块有一大半重吾——切10块西瓜,实际只有3块是”新”的。overlap 设到 chunk_size 的 50%,你的向量库里一半都是重复数据,检索时同一内容出现三四次,浪费存储又浪费 Token。10%~15% 是黄金比例,刚好接住断句就行。
4.5 一个直观对比
用同一个文档,三种粒度分割后检索”怎么退货”:
📄 测试文档:某电商售后政策(共5000字)粒度 片段数 检索到的内容 评分50字 120个 "内可申请无理由退" ❌ 半句话,不知所云300字 20个 "退货政策:7天无理由退货..." ✅ 完整的退货流程3000字 2个 "退货政策+换货政策+物流..." ⚠️ 包含退货,但噪声也多
五、EmbeddingStoreIngestor:一键完成加载→分割→向量化→存储
前面讲了加载和分割,然后在第9篇讲了 Embedding 和存储。如果每步都手动写,代码会比较冗长。LangChain4j 提供了一个管道工具把四步串起来:
5.1 手动方式(分步)
// Step 1: 加载Document doc = loadDocument(Path.of("knowledge/faq.txt"), new TextDocumentParser());// Step 2: 分割DocumentSplitter splitter = DocumentSplitters.recursive(300, 30, new OpenAiTokenCountEstimator("gpt-4o"));List<TextSegment> segments = splitter.split(doc);// Step 3: 向量化List<Embedding> embeddings = embeddingModel.embedAll(segments).content();// Step 4: 存储store.addAll(embeddings, segments);
5.2 Ingestor方式(一键)
import dev.langchain4j.store.embedding.EmbeddingStoreIngestor;EmbeddingStoreIngestor ingestor = EmbeddingStoreIngestor.builder().documentSplitter(DocumentSplitters.recursive(300, 30, new OpenAiTokenCountEstimator("gpt-4o"))).embeddingModel(embeddingModel).embeddingStore(store).build();// 一行搞定ingestor.ingest(doc);// 也支持批量ingestor.ingest(List.of(doc1, doc2, doc3));
Ingestor 的好处:
-
·代码简洁,不用手动拼接四步
-
·统一的异常处理
-
·后续加 DocumentTransformer(文本清洗)也很方便
说白了就是——手动拼四步像自己买菜、洗菜、切菜、炒菜,Ingestor 是预制菜套装,拆开加热就能吃。效果一样,省心省力。

六、实战:加载技术文档并智能分割存储
6.1 需求
把一份技术文档(Markdown格式)加载、分割、向量化、存储,然后验证检索效果。
6.2 准备测试文档
先创建一份模拟的技术文档 spring-boot-guide.md:
# Spring Boot 快速入门指南## 项目创建Spring Boot 项目可以通过 Spring Initializr 快速生成。访问 start.spring.io,选择依赖后下载即可。推荐使用 Maven 构建,JDK 17+ 环境。## 配置文件Spring Boot 使用 application.yml 或 application.properties 进行配置。常用配置包括:服务端口(server.port)、数据库连接(spring.datasource)、日志级别(logging.level)。配置文件放在 src/main/resources 目录下。## 数据库访问Spring Data JPA 是最常用的数据库访问方案。定义一个接口继承 JpaRepository,即可获得增删改查能力,无需写 SQL。复杂查询可以用 @Query 注解自定义 JPQL。## REST API开发使用 @RestController 和 @GetMapping 等注解可以快速定义 REST 接口。参数绑定用 @RequestParam(查询参数)和 @PathVariable(路径参数)。返回值自动序列化为 JSON,无需手动转换。## 异常处理全局异常处理用 @ControllerAdvice + @ExceptionHandler。可以统一封装错误响应格式,避免每个 Controller 重复写 try-catch。建议定义一个通用的 ApiResponse 类,包含 code、message、data 三个字段。## 安全认证Spring Security 是 Spring 生态的安全框架。常用方案:JWT Token 认证。用户登录后签发 Token,后续请求携带 Token 验证身份。注意:密码必须加密存储,推荐 BCryptPasswordEncoder。## 部署打包命令:mvn clean package -DskipTests生成的 JAR 文件用 java -jar app.jar 启动。生产环境建议使用 Docker 容器化部署,配合 Nginx 反向代理。
6.3 完整代码
import dev.langchain4j.data.document.Document;import dev.langchain4j.data.document.DocumentParser;import dev.langchain4j.data.document.DocumentSplitter;import dev.langchain4j.data.document.parser.TextDocumentParser;import dev.langchain4j.data.document.splitter.DocumentSplitters;import dev.langchain4j.data.embedding.Embedding;import dev.langchain4j.data.segment.TextSegment;import dev.langchain4j.model.embedding.EmbeddingModel;import dev.langchain4j.model.openai.OpenAiEmbeddingModel;import dev.langchain4j.model.openai.OpenAiTokenCountEstimator;import dev.langchain4j.store.embedding.EmbeddingMatch;import dev.langchain4j.store.embedding.EmbeddingSearchRequest;import dev.langchain4j.store.embedding.EmbeddingSearchResult;import dev.langchain4j.store.embedding.EmbeddingStore;import dev.langchain4j.store.embedding.EmbeddingStoreIngestor;import dev.langchain4j.store.embedding.inmemory.InMemoryEmbeddingStore;import java.nio.file.Path;import java.util.List;import static dev.langchain4j.data.document.loader.FileSystemDocumentLoader.loadDocument;/*** 文档加载与分割实战** 演示:加载Markdown文档 → 分割 → 向量化 → 存储 → 检索*/public class DocumentSplitDemo {public static void main(String[] args) {// ========== 1. 初始化模型和存储 ==========EmbeddingModel embeddingModel = OpenAiEmbeddingModel.builder().apiKey(System.getenv("ZHIPU_API_KEY")).baseUrl("https://open.bigmodel.cn/api/paas/v4").modelName("embedding-3").build();EmbeddingStore<TextSegment> store = new InMemoryEmbeddingStore<>();// ========== 2. 创建Ingestor(加载→分割→向量化→存储 一键完成)==========EmbeddingStoreIngestor ingestor = EmbeddingStoreIngestor.builder().documentSplitter(DocumentSplitters.recursive(200, // 每个片段最大200 Token20, // 重叠20 Tokennew OpenAiTokenCountEstimator("gpt-4o"))).embeddingModel(embeddingModel).embeddingStore(store).build();// ========== 3. 加载文档 ==========DocumentParser parser = new TextDocumentParser();Document doc = loadDocument(Path.of("src/main/java/the_10/spring-boot-guide.md"), parser);System.out.println("📄 文档加载完成");System.out.println(" 原文长度:" + doc.text().length() + " 字符");// ========== 4. 手动分割(演示用,看分割效果)==========DocumentSplitter splitter = DocumentSplitters.recursive(200, 20, new OpenAiTokenCountEstimator("gpt-4o"));List<TextSegment> segments = splitter.split(doc);System.out.println("\n🔪 分割结果(共 " + segments.size() + " 个片段):");System.out.println("-".repeat(60));for (int i = 0; i < segments.size(); i++) {String text = segments.get(i).text();String preview = text.length() > 60 ? text.substring(0, 60) + "..." : text;System.out.printf(" 片段%02d(%d字):%s%n", i + 1, text.length(), preview);}// ========== 5. Ingestor一键入库 ==========System.out.println("\n📥 开始向量化并存储...");ingestor.ingest(doc);System.out.println("✅ 入库完成");// ========== 6. 检索测试 ==========System.out.println("\n" + "=".repeat(60));System.out.println("🔍 检索效果测试");System.out.println("=".repeat(60));String[] queries = {"怎么连接数据库","如何处理全局异常","怎么打包部署到生产环境"};for (String query : queries) {System.out.println("\n📝 查询:「" + query + "」");System.out.println("-".repeat(40));Embedding queryEmbedding = embeddingModel.embed(query).content();EmbeddingSearchRequest request = EmbeddingSearchRequest.builder().queryEmbedding(queryEmbedding).maxResults(2).minScore(0.3).build();EmbeddingSearchResult<TextSegment> result = store.search(request);List<EmbeddingMatch<TextSegment>> matches = result.matches();if (matches.isEmpty()) {System.out.println(" (无相关结果)");} else {for (int i = 0; i < matches.size(); i++) {EmbeddingMatch<TextSegment> match = matches.get(i);String text = match.embedded().text();String preview = text.length() > 80 ? text.substring(0, 80) + "..." : text;System.out.printf(" %d. [%.4f] %s%n", i + 1, match.score(), preview);}}}// ========== 7. 分割粒度对比 ==========System.out.println("\n" + "=".repeat(60));System.out.println("📏 分割粒度对比实验");System.out.println("=".repeat(60));int[] chunkSizes = {50, 200, 1000};String testQuery = "怎么处理异常";for (int chunkSize : chunkSizes) {EmbeddingStore<TextSegment> tempStore = new InMemoryEmbeddingStore<>();DocumentSplitter tempSplitter = DocumentSplitters.recursive(chunkSize, chunkSize / 10, new OpenAiTokenCountEstimator("gpt-4o"));EmbeddingStoreIngestor tempIngestor = EmbeddingStoreIngestor.builder().documentSplitter(tempSplitter).embeddingModel(embeddingModel).embeddingStore(tempStore).build();tempIngestor.ingest(doc);Embedding qe = embeddingModel.embed(testQuery).content();EmbeddingSearchResult<TextSegment> res = tempStore.search(EmbeddingSearchRequest.builder().queryEmbedding(qe).maxResults(1).minScore(0.0).build());double bestScore = res.matches().isEmpty() ? 0.0 : res.matches().get(0).score();System.out.printf(" chunkSize=%4d → 片段数=%2d, 最佳相似度=%.4f%n",chunkSize, splitter.split(doc).size(), bestScore);}System.out.println("\n💡 粒度太小会丢失上下文,太大会稀释语义,300-500是甜蜜点!");}}
6.4 运行效果
📄 文档加载完成原文长度:856 字符🔪 分割结果(共 8 个片段):------------------------------------------------------------片段01(82字):# Spring Boot 快速入门指南\n\n## 项目创建\n\nSpring Boot...片段02(120字):## 配置文件\n\nSpring Boot 使用 application.yml 或...片段03(135字):## 数据库访问\n\nSpring Data JPA 是最常用的数据库...片段04(128字):## REST API开发\n\n使用 @RestController 和 @GetMappi...片段05(110字):## 异常处理\n\n全局异常处理用 @ControllerAdvice...片段06(130字):## 安全认证\n\nSpring Security 是 Spring 生态的...片段07(95字):## 部署\n\n打包命令:mvn clean package -DskipTes...📥 开始向量化并存储...✅ 入库完成============================================================🔍 检索效果测试============================================================📝 查询:「怎么连接数据库」----------------------------------------1. [0.9215] ## 数据库访问\n\nSpring Data JPA 是最常用的数据库访问方案...📝 查询:「如何处理全局异常」----------------------------------------1. [0.9378] ## 异常处理\n\n全局异常处理用 @ControllerAdvice + @ExceptionHandler...📝 查询:「怎么打包部署到生产环境」----------------------------------------1. [0.8934] ## 部署\n\n打包命令:mvn clean package -DskipTests...📏 分割粒度对比实验------------------------------------------------------------chunkSize= 50 → 片段数=20+, 最佳相似度=0.7123chunkSize= 200 → 片段数= 8, 最佳相似度=0.9215chunkSize=1000 → 片段数= 2, 最佳相似度=0.8456💡 粒度太小会丢失上下文,太大会稀释语义,300-500是甜蜜点!
七、DocumentTransformer:文本清洗与预处理(了解)
加载后的文档可能包含噪声——多余空格、HTML标签、特殊字符。DocumentTransformer 负责清洗:
import dev.langchain4j.data.document.Document;import dev.langchain4j.data.document.DocumentTransformer;// 自定义清洗器DocumentTransformer cleaner = document -> {String cleaned = document.text().replaceAll("\\s+", " ") // 多个空白合为一个空格.replaceAll("<[^>]+>", "") // 去HTML标签.replaceAll("[\\x00-\\x1F]", "") // 去控制字符.trim();return Document.from(cleaned, document.metadata());};// 使用Document dirty = Document.from("<p>Hello World</p>\n\n\n <br/>test");Document clean = cleaner.transform(dirty);System.out.println(clean.text()); // → "Hello World test"
也可以在 Ingestor 中集成:
EmbeddingStoreIngestor ingestor = EmbeddingStoreIngestor.builder().documentTransformer(cleaner) // 先清洗.documentSplitter(splitter) // 再分割.embeddingModel(embeddingModel).embeddingStore(store).build();
💡 简单场景不需要自定义 Transformer,默认的分割器已经够用。只有遇到解析质量差的文档(比如 HTML 提取后残留标签)才需要。
八、总结
核心概念
| 概念 |
一句话解释 |
|
DocumentLoader |
从文件系统读取文件,配合 DocumentParser 解析成 Document |
|
DocumentParser |
按格式解析文档:PDFBox(PDF)、Tika(万能)、Text(纯文本) |
|
DocumentSplitter |
把长文档切成适合 Embedding 的小片段 |
|
EmbeddingStoreIngestor |
一键管道:清洗→分割→向量化→存储 |
分割器选择
| 场景 |
推荐分割器 |
chunk_size |
|
通用文档 |
recursive |
300 Token |
|
FAQ/短文本 |
byParagraph |
200 Token |
|
技术文档 |
recursive |
500 Token |
|
日志/CSV |
byLine |
按行 |
关键经验
1.chunk_size 是 RAG 效果的第一大参数——调好了事半功倍,调坏了检索全废
2.recursive 分割器是默认首选——它自动从段落→句子→字符逐级递归
3.overlap 不能太大——10%~15% 即可,太大会导致冗余片段
4.Tika 是万能但重量级的选择——只解析 PDF 就用 PDFBox
5.Ingestor 简化代码——不用手动拼四步,一个 builder 搞定
🧠 课后思考
在进入下一篇之前,试着回答这几个问题。别慌,每题后面都有提示方向。
1. 你有一份10万字的技术手册(纯文本),需要做成 RAG 知识库。你会选择哪种分割器?chunk_size 设多大?
提示:技术手册通常有章节结构,先想想什么分割器能利用这个结构。
2. 检索「如何重启服务」时,返回的片段却是「如何部署」的内容,相似度还很高。这正常吗?
提示:两个话题都在同一章节里,想想是不是 chunk_size 太大了。
3. 知识库里混了 PDF、Word、Markdown 三种格式。Tika 统一解析 vs 按格式分别解析,你怎么选?
提示:想想 Tika 的 80MB 代价,以及 Markdown 本质是纯文本这个事实。
4. (挑战题)分割粒度从 300 Token 改成 1000 Token,存储成本和检索速度怎么变?
提示:每个片段都要做一次 Embedding,1000 Token 的片段数量少了但每个向量更”模糊”。从存储和精度两个角度想。
💡 不用急着找标准答案——带着这些问题读下一篇,你会发现 RAG 的每个环节都在回答这些问题。
与前几篇的关系
第8篇:向量数据库 → RAG架构 + Chroma/Milvus选型第9篇:Embedding → 文字变向量的原理 + 语义搜索第10篇:文档加载与分割 → 从文件到TextSegment ⭐ 本篇第11篇:RAG核心原理 → 检索增强生成完整链路
下一篇我们把这些全部串起来——从文档加载到最终生成回答,走完 RAG 的全流程。
下期预告
第11篇:RAG核心原理——检索增强生成完整链路
剧透:
·Indexing:文档→分割→Embedding→存储 的完整流水线
·Retrieval:检索质量如何评估和优化
·Generation:检索结果如何注入Prompt
·实战:一个真正的知识库问答系统(不是手写字符串,而是从文件加载)

往期链接
| 篇目 |
标题 |
状态 |
|
第1篇 |
开篇:AI应用时代,Java还能打吗? |
✅ 已发布 |
|
第2篇 |
开发环境搭建:5分钟跑通第一个AI对话 |
✅ 已发布 |
|
第3篇 |
Prompt工程:不只是写提示词 |
✅ 已发布 |
|
第4篇 |
Memory机制:让AI拥有记忆 |
✅ 已发布 |
|
第5篇 |
LLM模型调用:ChatModel核心机制 |
✅ 已发布 |
|
第6篇 |
Chain:让AI工作流串联起来 |
✅ 已发布 |
|
第7篇 |
实战:构建一个智能客服对话系统 |
✅ 已发布 |
|
第8篇 |
向量数据库:RAG的根基 |
✅ 已发布 |
|
第9篇 |
Embedding与向量检索:让AI理解语义 |
✅ 已发布 |
|
第10篇 |
文档加载与分割:让AI读到你的资料 |
🔜 本文 |

📱 小貔貅Agent日记
一个专注Java AI应用开发的技术号
关注我,带你用Java玩转AI!国产模型,不需要翻墙!
夜雨聆风
