news 2026/4/28 7:48:04

告别乱切片!Java + LangChain4j 实现高质量 RAG 文档拆分

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
告别乱切片!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 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 @RequiredArgsConstructor publicclass 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 系统优化,是一项极其细致的脏活累活,考验的全是对非结构化数据治理的细致把控。

学习资源推荐

如果你想更深入地学习大模型,以下是一些非常有价值的学习资源,这些资源将帮助你从不同角度学习大模型,提升你的实践能力。

一、全套AGI大模型学习路线

AI大模型时代的学习之旅:从基础到前沿,掌握人工智能的核心技能!​

因篇幅有限,仅展示部分资料,需要点击文章最下方名片即可前往获取

二、640套AI大模型报告合集

这套包含640份报告的合集,涵盖了AI大模型的理论研究、技术实现、行业应用等多个方面。无论您是科研人员、工程师,还是对AI大模型感兴趣的爱好者,这套报告合集都将为您提供宝贵的信息和启示

​因篇幅有限,仅展示部分资料,需要点击文章最下方名片即可前往获取

三、AI大模型经典PDF籍

随着人工智能技术的飞速发展,AI大模型已经成为了当今科技领域的一大热点。这些大型预训练模型,如GPT-3、BERT、XLNet等,以其强大的语言理解和生成能力,正在改变我们对人工智能的认识。 那以下这些PDF籍就是非常不错的学习资源。

因篇幅有限,仅展示部分资料,需要点击文章最下方名片即可前往获取

四、AI大模型商业化落地方案

作为普通人,入局大模型时代需要持续学习和实践,不断提高自己的技能和认知水平,同时也需要有责任感和伦理意识,为人工智能的健康发展贡献力量。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/28 7:39:21

收藏!Java程序员如何通过学习AI智能体实现年薪翻倍,职业华丽转身!

收藏&#xff01;Java程序员如何通过学习AI智能体实现年薪翻倍&#xff0c;职业华丽转身&#xff01; 本文讲述了Java开发工程师李阳通过学习AI智能体技术&#xff0c;在一年内实现年薪从20万到60万的跃升。文章深入分析了AI智能体领域的发展趋势、高薪岗位、技能要求及就业版图…

作者头像 李华
网站建设 2026/4/28 7:34:32

2026年AI配图神器GPT-Image-2震撼发布

作为一名长期在CSDN等技术社区活跃的内容创作者&#xff0c;你一定深有体会&#xff1a;一篇技术干货文章&#xff0c;如果配上几张直观、精美的配图&#xff0c;阅读体验和传播效果会提升好几个档次。但找图、制图往往比写作本身更耗时——直到AI图像生成技术迎来爆发。进入20…

作者头像 李华
网站建设 2026/4/28 7:30:31

一次大规模 PDF 导出系统的工程复盘

——从“能跑”到“稳定可控”的完整决策过程 背景 业务中存在一类历史记录数据(若干字段 + 图片),需要支持批量导出为 PDF,用于归档和离线查看。 单页约包含 3 条记录,每条记录包含图片资源。 在极端情况下,导出任务可能涉及: 数千页内容 上万张图片 国内 / 海外多云…

作者头像 李华
网站建设 2026/4/28 7:27:49

Qwen3.5-9B-GGUF与STM32CubeMX结合:嵌入式项目代码注释生成

Qwen3.5-9B-GGUF与STM32CubeMX结合&#xff1a;嵌入式项目代码注释生成 1. 嵌入式开发的痛点与解决方案 在嵌入式开发领域&#xff0c;STM32CubeMX已经成为工程师们快速生成初始化代码的标配工具。但随之而来的一个普遍问题是&#xff1a;自动生成的代码往往缺乏详细注释&…

作者头像 李华