告别乱切片!Java + LangChain4j 实现高质量 RAG 文档拆分
业务中 RAG 召回率高不高其实数据源头就占了很大原因数据切片 Chunking 的质量决定了整个系统召回率的上限而用的各种昂贵大模型和神级 Prompt仅仅是在无限逼近这个上限而已。如果面试一个 AI 相关的后端研发被问到文档怎么切分要是敢回答“按 500 个字符截取一下”面试官基本会认为你只做过玩具 Demo。不要暴力定长切分新手刚搭 RAG 的时候最喜欢用 Fixed-size Chunking 定长切分比如代码里写死每 500 个字切一块。这种切法的痛点极其明显语义极其容易被物理腰斩。设想你正在处理一份复杂的法考案例题或者业务合同一段极其关键的因果逻辑刚好横跨了第 499 到 505 个字符。切分器无情地一刀劈下去前半句留在了 Chunk A后半句分到了 Chunk B。这两块残缺的文本分别扔给 Embedding 模型去算向量原本完整的语义裂开了。用户提问无论是匹配前半句的特征还是后半句的特征召回引擎都大概率找不到这块被破坏的文本召回率肯定是不高的。三阶语义切片落地方案在实际业务中做语义切片 Semantic Chunking 是一套层层递进的我们直接上干货和代码。方案一基于标点符号的递归切分这是目前最常用也是性价比最高的基础方案。核心逻辑是绝不直接按死板的字数切而是顺应自然语言的“呼吸节奏”来切。我们会设定一个降级递归的规则先尝试按双换行符\n\n通常是段落切分如果切出来的段落依然超长退而求其次按单换行符\n切如果还超长按句号。切实在不行最后才按逗号切。这种做法能最大程度保全最基础的业务语义。方案二引入重叠窗口即便用了递归切分也难免会在长文本边界出现上下文割裂。这时候就需要设置一个 10% 到 20% 的重叠区比如 Chunk 2 的开头实际上是 Chunk 1 的末尾用冗余的方式强行维持语境连贯。新手喜欢自己写substring截取字符串这绝对是个坑。大模型的限制是 Token中文的 500 个字符可能对应 300 个 Token也可能对应 600 个 Token。必须注入与模型一致的分词器 Tokenizer 进行精准切分。用 LangChain4j 实现非常简单复制import dev.langchain4j.data.document.Document; import dev.langchain4j.data.document.DocumentSplitter; import dev.langchain4j.data.document.splitter.DocumentSplitters; import dev.langchain4j.model.openai.OpenAiTokenizer; publicclass DocumentProcessService { public ListTextSegment processWithOverlap(Document document) { // 1. 定义分词器 (这里以 OpenAI 为例私有化部署可以用 HuggingFace 的分词器) Tokenizer tokenizer new OpenAiTokenizer(gpt-4); // 2. 创建带有重叠的递归切分器 int maxTokens 500; // 每个 Chunk 最大 500 Token int overlapTokens 50; // 相邻 Chunk 之间重叠 50 Token (约 10%) DocumentSplitter splitter DocumentSplitters.recursive( maxTokens, overlapTokens, tokenizer ); // 3. 执行切分框架会自动处理递归降级和重叠部分的计算逻辑 return splitter.split(document); } }方案三父子文档语义映射我们做检索经常会陷入一个两难的困境切得太长向量特征失焦查不准切得太短查得确实准但喂给大模型时缺乏上下文模型开始瞎编。解决办法小切片负责召回大段落负责喂给大模型。写入时入库大段落 Parent 存入 Redis小段落 Child 进行 Embedding 存入 Qdrant 向量库并在 Qdrant 的 Payload元数据里记录 Redis 的 Keyparent_id。读取时检索查 Qdrant 拿到小段落的parent_id去 Redis 里把大段落捞出来拼装好再喂给大模型。1. 数据入库阶段 (Ingestion) 的核心代码public void ingestParentChild(String largeText) { // 1. 先切出大段落 (父文档) - 比如按双换行符切分段落 ListString parentChunks splitIntoParagraphs(largeText); for (String parentText : parentChunks) { // 生成该大段落唯一的 parent_id String parentId UUID.randomUUID().toString(); // 2. 将完整的父文档存入 KV 存储 (Redis) redisTemplate.opsForValue().set(doc:parent: parentId, parentText); // 3. 将父文档进一步切成极短的小句子 (子文档) ListString childChunks splitIntoSentences(parentText); ListTextSegment childSegments new ArrayList(); for (String childText : childChunks) { // 4. 【灵魂操作】将 parent_id 塞入子文档的 Metadata (元数据) Metadata metadata new Metadata(); metadata.put(parent_id, parentId); childSegments.add(TextSegment.from(childText, metadata)); } // 5. 对子文档进行 Embedding 并存入 Qdrant 向量库 embeddingStore.addAll(embeddingModel.embedAll(childSegments).content(), childSegments); } }2. 自定义检索阶段 (Custom Retriever) 的核心代码要想让业务主链路用上这套机制必须重写 LangChain4j 的ContentRetriever接口。Component RequiredArgsConstructor publicclass ParentChildRetriever implements ContentRetriever { privatefinal EmbeddingStoreTextSegment qdrantStore; privatefinal EmbeddingModel embeddingModel; privatefinal StringRedisTemplate redisTemplate; Override public ListContent retrieve(Query query) { // 1. 将用户问题转为向量 Embedding queryEmbedding embeddingModel.embed(query.text()).content(); // 2. 去 Qdrant 中精准检索最相似的“小句子 (Child Chunks)” (比如取 Top 5) ListEmbeddingMatchTextSegment matches qdrantStore.findRelevant(queryEmbedding, 5); // 3. 提取命中句子的 parent_id并进行【去重】 (因为有可能命中同一个父段落里的两句话) SetString parentIds matches.stream() .map(match - match.embedded().metadata().getString(parent_id)) .collect(Collectors.toSet()); // 4. 拿着 ID 去 Redis 中批量捞出完整的大段落 (Parent Chunks) ListContent finalContents new ArrayList(); for (String parentId : parentIds) { String parentText redisTemplate.opsForValue().get(doc:parent: parentId); if (parentText ! null) { // 组装成最终的 Content 返回 finalContents.add(Content.from(parentText)); } } // 5. 此时大模型拿到的是极其精准且拥有完整上下文的大段落 return finalContents; } }这套代码逻辑弄下来RAG召回率还是可以提升不少的。元数据注入做完了上面的切分和召回数据流水线上还有极其重要的一步防止切片变成失去全局语境的垃圾数据。举个例子经过切分后有这么一个切片“张三被判处有期徒刑三年”。大模型拿到这句话根本不知道这是几几年的案子、什么犯罪类型。正确的做法是在数据抽取清洗环节比如用 Apache NiFi 处理 PDF 时顺手提取出当前文档的标题、章节名甚至页码。然后把这些全局上下文强行拼在切片的前面或者存进 Metadata 里。最终存入向量引擎的文本变成了[《2023年刑法经典案例》 - 抢劫罪章节 - 第12页] 张三被判处有期徒刑三年。经过这一步处理这个 Chunk 就在物理层面拥有了绝对完整的全局语义。谁在最后真正的 RAG 系统优化是一项极其细致的脏活累活考验的全是对非结构化数据治理的细致把控。学习资源推荐如果你想更深入地学习大模型以下是一些非常有价值的学习资源这些资源将帮助你从不同角度学习大模型提升你的实践能力。一、全套AGI大模型学习路线AI大模型时代的学习之旅从基础到前沿掌握人工智能的核心技能因篇幅有限仅展示部分资料需要点击文章最下方名片即可前往获取二、640套AI大模型报告合集这套包含640份报告的合集涵盖了AI大模型的理论研究、技术实现、行业应用等多个方面。无论您是科研人员、工程师还是对AI大模型感兴趣的爱好者这套报告合集都将为您提供宝贵的信息和启示因篇幅有限仅展示部分资料需要点击文章最下方名片即可前往获取三、AI大模型经典PDF籍随着人工智能技术的飞速发展AI大模型已经成为了当今科技领域的一大热点。这些大型预训练模型如GPT-3、BERT、XLNet等以其强大的语言理解和生成能力正在改变我们对人工智能的认识。 那以下这些PDF籍就是非常不错的学习资源。因篇幅有限仅展示部分资料需要点击文章最下方名片即可前往获取四、AI大模型商业化落地方案作为普通人入局大模型时代需要持续学习和实践不断提高自己的技能和认知水平同时也需要有责任感和伦理意识为人工智能的健康发展贡献力量。