业务中 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%免费

在这里插入图片描述

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐