告别乱切片!Java + LangChain4j 实现高质量 RAG 文档拆分
业务中 RAG 召回率高不高,其实数据源头就占了很大原因,数据切片 Chunking 的质量,决定了整个系统召回率的上限,而用的各种昂贵大模型和神级 Prompt,仅仅是在无限逼近这个上限而已。
业务中 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 List<TextSegment> 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 的 Key(
parent_id)。 - 读取时(检索): 查 Qdrant 拿到小段落的
parent_id,去 Redis 里把大段落捞出来,拼装好再喂给大模型。
1. 数据入库阶段 (Ingestion) 的核心代码:
public void ingestParentChild(String largeText) { // 1. 先切出大段落 (父文档) - 比如按双换行符切分段落 List<String> 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. 将父文档进一步切成极短的小句子 (子文档) List<String> childChunks = splitIntoSentences(parentText); List<TextSegment> 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@RequiredArgsConstructorpublicclass ParentChildRetriever implements ContentRetriever { privatefinal EmbeddingStore<TextSegment> qdrantStore; privatefinal EmbeddingModel embeddingModel; privatefinal StringRedisTemplate redisTemplate; @Override public List<Content> retrieve(Query query) { // 1. 将用户问题转为向量 Embedding queryEmbedding = embeddingModel.embed(query.text()).content(); // 2. 去 Qdrant 中精准检索最相似的“小句子 (Child Chunks)” (比如取 Top 5) List<EmbeddingMatch<TextSegment>> matches = qdrantStore.findRelevant(queryEmbedding, 5); // 3. 提取命中句子的 parent_id,并进行【去重】 (因为有可能命中同一个父段落里的两句话) Set<String> parentIds = matches.stream() .map(match -> match.embedded().metadata().getString("parent_id")) .collect(Collectors.toSet()); // 4. 拿着 ID 去 Redis 中批量捞出完整的大段落 (Parent Chunks) List<Content> 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 系统优化,是一项极其细致的脏活累活,考验的全是对非结构化数据治理的细致把控。
学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%免费】

更多推荐

所有评论(0)