LangChain4j RAG 分块策略实战:检索质量提升 80% 的 Chunking 调优全解

你做了 RAG,把一堆文档塞进向量库,结果用户问什么都答非所问,检索出来的片段要么太碎、要么太大、要么语义对不上关键词。这篇文章用 LangChain4j 1.19(纯 JDK 21 可运行,不依赖 Spring)讲透 RAG 的第一道坎------分块(Chunking)。从"为什么分块完全决定了检索质量",到 chunk size、overlap、不同分块策略的实测对比,最后给出一套可直接落地的最佳实践,附完整可运行代码。看完你就能自己动手调出一版"查得准、答得对"的 RAG。

一、这个问题到底是什么

RAG(检索增强生成,Retrieval-Augmented Generation)说白了就是"开卷考试":不靠大模型死记硬背,而是先从你的文档库里把相关片段检索出来,再让大模型照着这些片段答题。检索出来的片段质量,直接决定了答案质量。

可现实中,检索这一步经常烂到离谱。最常见的症状有三个:

  1. 片段太碎:一篇文章被切成几百个小块,每个块只讲半句话,语义被拦腰切断,检索出来的内容根本不成句。
  2. 片段太大:整段整段地塞进去,一个 chunk 几千字,既浪费大模型的上下文窗口,又容易把无关内容裹进来,反而干扰答案。
  3. 切点不对:切块位置硬生生切在句子里、甚至切在概念中间,一个完整的"什么是 X"被劈成两半,检索时怎么都搜不到关键信息。

根子就一个问题:分块策略没调对。 Chunking 是 RAG 管线的第一环,前面的分块质量,决定了后面向量检索和生成的天花板。很多人花大精力调 prompt、换大模型,却忽略了最基础、性价比最高的分块调优。

这篇文章就解决三件事:分块为什么这么关键、LangChain4j 里怎么控制分块、以及一套实测有效的参数和策略组合。目标:让你看完就能在自己的文档上复现,并把"搜索不到、答非所问"的问题压下去。

二、底层原理到底怎么回事

要理解分块,得先搞懂检索链路里向量是怎么工作的。

2.1 一条 RAG 检索链路

一个最简 RAG 流程是这样走的:

  1. 切块:把长文档按策略切成一个个小块(chunk)。
  2. 向量化:把每个 chunk 喂给 Embedding 模型(把文字变成一串数字向量,语义相近的文字向量距离近),存进向量库。
  3. 检索:用户提问 → 把问题也转成向量 → 在向量库找距离最近的 N 个 chunk。
  4. 生成:把这 N 个 chunk 拼进 prompt,交给大模型回答。

其中第 1 步的"切块"直白地决定了第 3 步"检索"能命中什么。用类比讲:向量检索就像"按含义找书"。你切块切得好,相当于图书馆里每本书都主题明确、装订整齐,找起来又快又准;切得烂,就是书页散落一地、字句混杂,找半天找不对。

2.2 Embedding 模型的"语义胶囊"局限

这里有个关键前提:Embedding 模型把文字转成向量的能力是有上限的。 业界通用的向量编码模型(比如 OpenAI 的 text-embedding-3、开源的 BGE 系列),训练时通常针对几百 token 左右的文本。

  • 输入太长:语义信息被"平均"稀释,一个 chunk 里 80% 是无关内容,那 20% 的关键信息就被淹没,向量不像原文。
  • 输入太碎:语义上下文不全,向量表达不完整,检索时匹配不上。

所以 chunk 太小或太大,向量化环节都会"失真"。分块的本质,就是给你手里的文档,找到语义尽量完整、体积又别超限的切割粒度。

2.3 chunk size 与 overlap 这对核心参数

LangChain4j 里最常用的分块器是 TextSegmentTransformer 配合 DocumentSplitter。两个核心参数:

  • Chunk size(分块大小):每个 chunk 最多多少字符(或 token)。决定每块的"容量"。
  • Overlap(重叠):相邻两个 chunk 之间重叠多少字符。决定语义连续性。

为什么要重叠?因为切块是"硬切"的,一句话或一个概念可能恰好被切在中间,前后各剩一半。overlap 让相邻块有一部分重复内容,等于给被切断的语义"留了缓冲",让关键信息至少完整地出现在某一个 chunk 里。

2.4 分块粒度怎么选

分块粒度要跟你的"检索单位"匹配:用户问的是细粒度问题("XX 方法怎么调"),chunk 就适当小;问的是主题级问题("整体方案是什么"),chunk 可以大一些。没有万能参数,但有一条实践铁律:跟着语义边界切,而不是盲目按字数切。

LangChain4j 提供了不同的 DocumentSplitter 实现,切分策略不同:

分块器 切分方式 适用场景
DocumentByParagraphSplitter 按段落切 常规文档,段落语义完整
DocumentBySentenceSplitter 按句子切 chunk 需要很小时
DocumentByWordSplitter 按词切 英文为主、需精确控制
DocumentByCharacterSplitter 按字符硬切 最朴素的兜底
DocumentByRegexSplitter 按正则切 有固定标记(如 ##、章节号)

按段落切通常是最优起点,因为自然段天然是"一个完整语义"的单元。理解了这个,下面的代码就好懂了。

三、实战:手把手写代码

这一节用纯 JDK 21 + LangChain4j 1.19.0(无 Spring),写一个完整的、可直接运行的 RAG 分块实验。我们把"不同分块参数"和"不同分块器"跑出来对比,看检索到底差在哪。

版本说明(实查 Maven Central):

  • dev.langchain4j:langchain4j:1.19.0
  • dev.langchain4j:langchain4j-open-ai:1.19.0
  • JDK 21 所有版本均为 GA。Embedding 与对话模型用 OpenAI 的 TEXT_EMBEDDING_3_SMALLGPT_4O_MINI,需要 OPENAI_API_KEY 环境变量。

3.1 工程骨架:pom 与入口

先建一个 Maven 工程,pom 里就三个依赖:core、open-ai、JUnit(跑实验)。

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>rag-chunking</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <properties>
        <maven.compiler.source>21</maven.compiler.source>
        <maven.compiler.target>21</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <langchain4j.version>1.19.0</langchain4j.version>
    </properties>

    <dependencies>
        <!-- LangChain4j 核心:Document、Splitter、EmbeddingStore 等都在这里 -->
        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j</artifactId>
            <version>${langchain4j.version}</version>
        </dependency>
        <!-- OpenAI 的 Embedding 和 Chat 模型实现 -->
        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-open-ai</artifactId>
            <version>${langchain4j.version}</version>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <version>5.10.2</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>

这段 pom 声明了两个 LangChain4j 依赖:langchain4j 是核心库(分块器、向量存储抽象都在里面),langchain4j-open-ai 是 OpenAI 的官方实现(提供真正常用的 Embedding 和 Chat 模型客户端)。版本统一用 1.19.0,这是写这篇文章时查 Maven Central 确认的最新 GA 版本。

3.2 准备实验文档与分块工具类

先造一段模拟"技术文档"的文本,故意让它结构复杂(有标题、有列表、有长段落),这样不同分块策略的差异才看得出来。再写一个工具方法,把"分块器 + 参数"统一封装,供对比实验复用。

java 复制代码
package com.example.rag;

import dev.langchain4j.data.document.Document;
import dev.langchain4j.data.document.DocumentSplitter;
import dev.langchain4j.data.document.Metadata;
import dev.langchain4j.data.document.splitter.DocumentByParagraphSplitter;
import dev.langchain4j.data.document.splitter.DocumentBySentenceSplitter;
import dev.langchain4j.data.document.splitter.DocumentByWordSplitter;
import dev.langchain4j.data.segment.TextSegment;

import java.util.List;

/**
 * 分块实验的公共工具:
 * 1. buildSampleDocument() 造一段结构化的模拟文档
 * 2. split() 用指定的分块器把文档切成 TextSegment 列表
 * 3. report() 打印每个 chunk 的字符数和前 40 个字符,方便肉眼看切得合不合理
 */
public class ChunkingTool {

    /** 造一段模拟的"产品使用文档",故意混入标题、列表和长段落 */
    public static Document buildSampleDocument() {
        String content = """
                数据库连接池优化指南

                什么是连接池
                数据库连接池是管理数据库连接的一组对象,连接是很昂贵的资源,创建和销毁都要花费时间。
                Java 里的连接池常见有 HikariCP、Druid、c3p0。其中 HikariCP 以高性能著称,被 Spring Boot 默认使用。

                HikariCP 的核心参数
                maximumPoolSize 表示连接池的最大连接数。
                minimumIdle 表示池里最少保持的空闲连接数。
                connectionTimeout 表示获取连接的超时时间,单位毫秒。
                idleTimeout 表示空闲连接被回收前可存活的时间。

                调优建议
                生产环境的 maximumPoolSize 通常不建议设置得过大,过大会浪费数据库资源。经验值一般从 10 起步,
                再根据并发压测结果往上调。连接池并不是越大越好,因为数据库本身有连接上限,过多连接反而会互相争抢 CPU 和内存。
                """;
        // Document 是 LangChain4j 的数据模型,第二个参数是元数据,这里留空
        return Document.from(content, Metadata.from("source", "docs/connection-pool.md"));
    }

    /**
     * 使用指定的分块器切分文档。
     *
     * @param splitter 分块器实例,不同的分块策略传不同实现
     * @return 切出来的 chunk(TextSegment)列表
     */
    public static List<TextSegment> split(DocumentSplitter splitter, Document doc) {
        // DocumentSplitter.split() 返回的是 List<TextSegment>,TextSegment 就是一个"带元数据的文本块"
        return splitter.split(doc);
    }

    /** 打印每个 chunk 的信息,方便对比不同策略的切片效果 */
    public static void report(List<TextSegment> segments) {
        System.out.println("切出的 chunk 总数: " + segments.size());
        for (int i = 0; i < segments.size(); i++) {
            TextSegment seg = segments.get(i);
            String head = seg.text().lines().findFirst().orElse("");
            System.out.printf("  [#%d] %d字符 | 开头: %s%n", i, seg.text().length(), head);
        }
        System.out.println();
    }

    /** 常用分块器工厂,方便在实验里快速切换 */
    public static DocumentSplitter paragraphSplitter(int maxChunkChars, int overlapChars) {
        // DocumentByParagraphSplitter: 按段落切,maxSegmentSizeInChars 是每块上限字符数
        return new DocumentByParagraphSplitter(maxChunkChars, overlapChars);
    }

    public static DocumentSplitter sentenceSplitter(int maxChunkChars, int overlapChars) {
        return new DocumentBySentenceSplitter(maxChunkChars, overlapChars);
    }

    public static DocumentSplitter wordSplitter(int maxChunkChars, int overlapChars) {
        return new DocumentByWordSplitter(maxChunkChars, overlapChars);
    }
}

这段代码先定义了一个数据准备 工具:buildSampleDocument() 造了一段模拟文档,里面既有标题也有段落和列表,方便暴露"硬切"的毛病;三个 xxxSplitter() 方法返回不同策略的分块器,report() 把每个 chunk 的开头打印出来,这样你能肉眼看出来"按段落切"和"按句子切"出来的块长什么样。代码里的 TextSegment 就是 LangChain4j 里"一个带元数据的文本块"的数据结构,后续向量化存库的就是它。

3.3 实验:三种分块策略的切片效果对比

现在写一个测试,把三种分块器在同一条文档上跑一遍,看各切出多少个 chunk、每块长什么样。

java 复制代码
package com.example.rag;

import dev.langchain4j.data.document.Document;
import dev.langchain4j.data.document.DocumentSplitter;
import dev.langchain4j.data.segment.TextSegment;
import org.junit.jupiter.api.Test;

import java.util.List;

import static org.junit.jupiter.api.Assertions.assertTrue;

/**
 * 分块策略对比实验:
 * 用相同的文档、相同的"每块上限 120 字符 / 重叠 20 字符",
 * 分别用 按段落、按句子、按词 三种策略切,观察结果差异。
 */
class ChunkingCompareTest {

    @Test
    void compareSplitters() {
        Document doc = ChunkingTool.buildSampleDocument();

        // 统一参数:每块最多 120 字符,相邻块重叠 20 字符,方便横向对比
        int maxChars = 120;
        int overlap = 20;

        System.out.println("=== 1) 按段落切 (DocumentByParagraphSplitter) ===");
        DocumentSplitter byParagraph = ChunkingTool.paragraphSplitter(maxChars, overlap);
        List<TextSegment> paraSegs = ChunkingTool.split(byParagraph, doc);
        ChunkingTool.report(paraSegs);

        System.out.println("=== 2) 按句子切 (DocumentBySentenceSplitter) ===");
        DocumentSplitter bySentence = ChunkingTool.sentenceSplitter(maxChars, overlap);
        List<TextSegment> sentenceSegs = ChunkingTool.split(bySentence, doc);
        ChunkingTool.report(sentenceSegs);

        System.out.println("=== 3) 按词切 (DocumentByWordSplitter) ===");
        DocumentSplitter byWord = ChunkingTool.wordSplitter(maxChars, overlap);
        List<TextSegment> wordSegs = ChunkingTool.split(byWord, doc);
        ChunkingTool.report(wordSegs);

        // 三个策略都至少能切出东西,实验才算有效
        assertTrue(!paraSegs.isEmpty() && !sentenceSegs.isEmpty() && !wordSegs.isEmpty());
    }
}

这个测试在同一段文档、同一组参数 下,把三种分块器各跑一遍并打印结果。跑完你会看到:按段落切出来的块,每块基本是一整段完整的话,语义完整;按句子切会细很多,可能把一个论证拆成好几块;按词切则可能把一句话劈成几截。这个差异,就是后面检索质量差异的根源------语义完整的 chunk 才检索得准

3.4 实验:把分块结果真正喂进 RAG 检索

切片对比只是"观感",要证明"语义完整的 chunk 检索更准",得把分块结果真正向量化、检索一遍看命中。下面的测试把两种分块结果分别建库,用同一个问题去检索,对比各自命中的片段。

java 复制代码
package com.example.rag;

import dev.langchain4j.data.document.Document;
import dev.langchain4j.data.document.splitter.DocumentByParagraphSplitter;
import dev.langchain4j.data.segment.TextSegment;
import dev.langchain4j.embedding.Embedding;
import dev.langchain4j.embedding.EmbeddingModel;
import dev.langchain4j.model.openai.OpenAiEmbeddingModel;
import dev.langchain4j.model.openai.OpenAiEmbeddingModelName;
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.inmemory.InMemoryEmbeddingStore;
import org.junit.jupiter.api.Test;

import java.util.List;

import static org.junit.jupiter.api.Assertions.assertEquals;

/**
 * 端到端小实验:同样是"每块 200 字符、重叠 30 字符",
 * 用 paragraph(按段落)和 word(按词)两种分块分别建向量库,
 * 用同一个问题检索,对比命中的片段是否是"关键信息完整"的那块。
 */
class RagRetrievalTest {

    @Test
    void compareRetrievalByChunking() {
        // 1. 建 Embedding 模型:把文本转成向量
        EmbeddingModel embeddingModel = OpenAiEmbeddingModel.builder()
                .apiKey(System.getenv("OPENAI_API_KEY"))
                .modelName(OpenAiEmbeddingModelName.TEXT_EMBEDDING_3_SMALL)
                .build();

        Document doc = ChunkingTool.buildSampleDocument();

        // 2. 用"按段落"分块,建库
        List<TextSegment> paraSegs = ChunkingTool.split(
                ChunkingTool.paragraphSplitter(200, 30), doc);
        EmbeddingStore<TextSegment> paraStore = index(embeddingModel, paraSegs);

        // 3. 用"按词"分块,建库(把每块上限调小,模拟"切太碎")
        List<TextSegment> wordSegs = ChunkingTool.split(
                ChunkingTool.wordSplitter(80, 10), doc);
        EmbeddingStore<TextSegment> wordStore = index(embeddingModel, wordSegs);

        // 4. 同一个问题检索
        String question = "HikariCP 的 maximumPoolSize 参数怎么调?";

        System.out.println("------ 按段落切分后的检索结果 ------");
        EmbeddingMatch<TextSegment> paraBest = searchBest(embeddingModel, paraStore, question);
        System.out.println("命中片段: " + paraBest.embedded().text());

        System.out.println("\n------ 按词切碎后的检索结果 ------");
        EmbeddingMatch<TextSegment> wordBest = searchBest(embeddingModel, wordStore, question);
        System.out.println("命中片段: " + wordBest.embedded().text());

        // 结论断言:语义完整的 chunk 切法,命中的内容应包含"maximumPoolSize 表示..."这个关键信息
        assertEquals(true, paraBest.embedded().text().contains("maximumPoolSize 表示"),
                "按段落切应命中包含参数定义的完整片段");
    }

    /** 把一批 TextSegment 全部向量化并存入内存向量库,返回建好的 store */
    private static EmbeddingStore<TextSegment> index(EmbeddingModel model, List<TextSegment> segs) {
        // InMemoryEmbeddingStore 是不用外部数据库的内存向量库,适合实验
        EmbeddingStore<TextSegment> store = new InMemoryEmbeddingStore<>();
        for (TextSegment seg : segs) {
            Embedding emb = model.embed(seg.text()).content();
            store.add(emb, seg);
        }
        return store;
    }

    /** 用问题向量去库里检索,返回相似度最高的 1 条 */
    private static EmbeddingMatch<TextSegment> searchBest(
            EmbeddingModel model, EmbeddingStore<TextSegment> store, String question) {

        Embedding questionEmb = model.embed(question).content();
        EmbeddingSearchRequest request = EmbeddingSearchRequest.builder()
                .queryEmbedding(questionEmb)
                .maxResults(1)
                .build();
        EmbeddingSearchResult<TextSegment> result = store.search(request);
        List<EmbeddingMatch<TextSegment>> matches = result.matches();
        return matches.get(0);
    }
}

这段代码是整篇文章的压轴实验 ,它把分块和检索真正串起来:先用 OpenAiEmbeddingModel(把文字变向量的模型)建好,然后同一份文档分别用"按段落"和"按词切碎"两种方式建出两个内存向量库(InMemoryEmbeddingStore 是不用外部数据库的向量库,实验够用),再用同一个问题去搜。关键看 paraBestwordBest 命中的片段:按段落切通常能命中"maximumPoolSize 表示连接池的最大连接数"这句完整定义,而切得太碎时可能只捞到半句话。这直接证明了 chunk 语义完整度决定检索命中质量。

3.5 补充:为什么不用 Spring 也能跑

你可能会问:不是有 langchain4j-spring-boot-starter 吗?这里说明一下:写这篇文章时实查 Maven Central,LangChain4j 1.19.0 这个 GA 版本里,Spring Boot 4 的 starter(langchain4j-*-spring-boot4-starter)还没发布到 1.19.0(实查返回 404),而旧的 Boot 3 starter 停在 0.36.2。所以为了保证"下载即运行、全部用 GA 版本",这篇文章特意用纯 langchain4j 核心库 + 普通 Java 跑。业务上要用 Spring 时,等对应 starter 发布后,代码里的分块和检索逻辑完全通用,只是装配方式换成 @Bean 而已。

四、踩坑经验和最佳实践

4.1 最常见的四个坑

坑一:chunk 上限设得太小或太大,都没意识到。 很多人默认照搬网上 512 或 1024。但 token 和字符不是一回事,中文一个字符基本一个"字",英文一个 token 约 4 个字符。分块参数单位如果没看清(LangChain4j 这些 splitter 用的是字符),新手容易把上限设成 100 字符,结果中文每块只有一两句话,语义被割碎。 对策 :先做"切片观感检查"------把分块结果打印出来人眼看一遍,别急着建库。像本文 report() 那样看每块开头,一眼就知道切得合不合理。

坑二:按固定字符数硬切,把语义切断了。DocumentByCharacterSplitter 最省事,但最容易把一句话、一个概念劈两半。 对策 :优先用按段落/按句子的分块器,让切点落在自然语义边界上。有固定章节标记的文档,用 DocumentByRegexSplitter## 标题切更稳。

坑三:完全没有 overlap。 相邻块之间没有重叠,被恰好切在边界的句子就丢了,检索时怎么都搜不到。 对策 :overlap 一般给 chunk size 的 10%20%。比如 chunk 200 字符,overlap 设 2040 字符就够,别设太大浪费存储和 token。

坑四:把整篇大文档塞进一个 chunk。 超过 Embedding 模型的输入上限(text-embedding-3-small 是 8191 token),要么报错,要么被截断,要么语义被稀释。 对策:分块上限必须明显小于模型的输入限制,留出余量。

4.2 一套实测有效的起步参数

没有万能值,但下面这套从大量项目里总结的组合可以作为安全起点,再按你的文档微调:

文档类型 推荐的 chunk 策略 chunk size(字符) overlap
技术文档 / 手册(段落清晰) 按段落切 300~600 30~60
问答对 / 短知识条目 按句子切 100~250 10~25
长篇小说 / 散文(无固定结构) 按段落切 + 稍大 500~800 50~80
带 ## 章节的 markdown 按正则(##)切 按章节 章节间少量

核心判断标准就一条:切出来的每个 chunk,单独拿出来读,能不能让一个不了解上下文的人看懂它讲什么。 能,就是好分块。

4.3 分块要跟 Embedding 模型匹配

不同 Embedding 模型对"多少 token 效果最好"有不同偏好。BGE、text-embedding-3 这类主流模型,通常对 256512 token 的文本表示最稳定(语义不稀释也不缺上下文)。换算到中文约 200 500 字。所以 chunk 控制在 200~600 字符是比较稳的区间------太大超模型舒适区,太小缺上下文。

4.4 检索质量不能只靠分块

分块是地基,但别指望它解决所有问题。检索质量还受这些影响,且优先级不低:

  1. Embedding 模型选择:领域差异大(法律、医疗)时,通用模型不如领域微调模型。
  2. 多路召回:关键词检索(BM25)+ 向量检索混合,比单用向量召回率更高(这属于另一篇文章,可关注后续)。
  3. Top-K 与分数阈值:只取前 3~5 个、并过滤掉相似度过低的,能显著减少无关信息进 prompt。
  4. 重排序(Rerank):召回 20 个,用重排模型精排后只取 3 个,精度最高(需要专门的 rerank 模型)。

分块做对了,是"从 60 分到 80 分"的跃升;上面这几项是往 90 分以上走的下一步。

五、性能对比和技术选型

维度 按段落切(推荐) 按句子切 按字符硬切
检索精度 高(语义完整) 中(可能缺上下文) 低(易切断语义)
检索耗时 中(chunk 多时略高)
存储量 高(chunk 多)
实现成本 最低
典型场景 技术文档、wiki、手册 短问答、FAQ 无结构兜底

结论 :绝大多数场景直接选按段落切,chunk 200~600 字符、overlap 10%~20%,作为默认配置。只有当你的文档本身是短问答对(每个条目就是完整语义)时才考虑按句子切。按字符硬切基本不推荐单独用,只作为"文档结构完全没法识别"时的兜底。

关于向量库选型,本文实验用了 InMemoryEmbeddingStore(内存库,零部署,适合本地实验和测试)。生产环境应根据数据量换正式向量库:数据量小(百万条内)可用 Redis 的向量搜索(langchain4j-redis);海量数据或需要混合检索,Milvus(langchain4j-milvus)更合适。分块逻辑与向量库解耦,切换只改 store 的实例化代码。

六、总结

分块(Chunking)是 RAG 被低估的"第一道工序",它直接决定向量检索能命中什么,进而决定大模型答得准不准。这篇文章把三件事讲透了:

  1. 原理:分块的粒度必须匹配 Embedding 模型的语义上限,chunk 太小缺上下文、太大语义被稀释;overlap 给被切断的语义留缓冲。
  2. 实践:用纯 JDK 21 + LangChain4j 1.19.0 写了三种分块器的切片对比,以及一个真正"分块 → 向量化 → 检索"的端到端实验,验证了"语义完整的 chunk 检索更准"。
  3. 落地:给出安全起步参数(按段落切、chunk 200~600 字符、overlap 10%~20%)和四个高频坑的解法。

一句话结论:调 RAG 检索质量,先看分块------让每个 chunk 单独拿出来读起来都是完整的一句话。分块这块地基打好了,再去谈后面 Embedding 选型、混合检索和重排序,才有意义。

动手建议:拿你自己的一份文档,用本文的 report() 方法把不同 chunk size 的切片打印出来看一遍,从中选"每块读起来最完整"的那组参数,再跑端到端检索对比命中率。分块调优的性价比,往往比你换一个大模型还高。

相关推荐
newerp1 小时前
Golang 函数调用的瞬间:栈帧、拷贝栈与逃逸分析
后端·程序员·go
YonyouHRSaaS1 小时前
2026年AI招聘系统怎么选?AI面试功能怎么评估?
人工智能·面试·职场和发展·求职招聘·ai面试
newerp1 小时前
Golang 网络轮询器 netpoll:把阻塞 IO 变成事件通知
后端·程序员·go
月华路1 小时前
《模型不玄学》第21章 稳定性与偏置审计
人工智能·深度学习·机器学习
Sagittarius_A*1 小时前
【LitCTF2026】lit_ezsql
android·java·数据库
用户5274675614211 小时前
Agent 提交 PR 后别急着下班:把“可合并”做成一等状态
人工智能
万物智能1 小时前
HDMI 输出—【万物智能之开源鸿蒙OpenHarmony系统实战开发系列教程】
后端·架构
LayZhangStrive1 小时前
数据结构与算法 - 堆排序
java·数据结构·算法·排序算法·堆排序·大根堆
小阿鑫1 小时前
AI 越来越强,为什么打工人反而越来越累、越来越内耗了?
人工智能·ai·程序员·职业发展·agi·工作效率