AI 入门拆解 03:RAG 召回效果差?问题不在模型,而在 Chunk 切分

0 阅读6分钟

大家好,我是小编~

上一节我们讲了RAG:

AI不是“知道答案”,而是“去资料里找答案”

于是很多小伙伴兴冲冲做了第一版RAG。

然后马上遇到一个诡异的问题:

明明资料里有答案,AI却说不知道

或者答得断断续续,像拼接失败

有时候甚至答非所问

你们的第一反应是不是:

embedding不行?

prompt写错了?

模型太弱?

Maybe, But我想说

你可能冤枉模型了

还有一种可能,也是概率最大的一种:

chunk(文本切分)


一、什么是chunk?

不妨这样想象一下:

你让一个实习生帮你在文档里找答案。

你有三种给法:

方式1:整本说明书丢给他

100页文档

没有分段

结果:他找不到重点

方式2:把文档剪成碎片

一句话一张纸

完全打散

结果:他看不懂上下文

给你2秒钟思考一下

你也许想到了,正确的做法就是:

按“有意义的段落”切好,再给他

这个“切段落”的动作:

就是 chunk


二、技术定义(够用就行)

chunk,本质就是:

把长文本切成多个“有语义的小段”,每一段单独做向量化(embedding)

但这里很容易忽略的关键点:

AI并不是在“理解整篇文档”,而是在“理解这些被切出来的小段”

这样说你能明白吗:

在RAG系统里,真正参与检索的,不是文档本身,而是这些chunk。

而chunk的流程其实就是这样的:

原始文档 → 切分成多个chunk → 每个chunk生成向量 → 存入向量数据库

当用户提问时:

问题 → 转成向量 → 去匹配最相似的chunk → 再把这些chunk交给AI生成答案

“后面我将用代码完整的演示这个流程”

所以其实你可以这样理解:

chunk = AI可搜索的最小知识单元

再进一步说:

每一个chunk,都会变成一条“可被检索的记录”,类似这样:

{
 content"ServiceA接口使用POST,请求路径为/api/v1/serviceA",
 embedding: [0.12, -0.98, ...],
 metadata: {...}
}

这里的embedding向量,本质上是:

把“这段话的语义”,转换成一串数字

前面我已经说过,AI并不是在做关键词匹配,而是在做 “语义相似度计算”

当用户提问

“serviceA怎么调用?”

系统不会去找“有没有完全一样的句子”,

而是去找:

“语义上最接近‘怎么调用’的那几段chunk”

这也是为什么:

chunk切得好不好,直接决定了“能不能被找到”

按我理解

我们不是在“存文档”,而是在“构建一堆可以被AI检索的语义块”

所以chunk这一步,其实做了三件事:

降低理解难度(把长文本拆小)

提高匹配精度(每段更聚焦)

决定检索边界(AI最多只能看到这些块)


【三、代码展示】

假如你的代码是这样写的

List<TextSegment> segments = 
DocumentSplitters.recursive(1000, 200).split(document);

有的小伙伴可能看不懂参数,解释一下

SegmentSize: 每个chunk最大长度1000

Overlap: chunk之间重叠200

意思就是每一段不会超过1000字符,并且相邻两段之间,会有200字符是重复的。

注意哦,不是硬切哦

它是“尽量按语义切”,实在不行才按长度切

按长度切的策略大致是:

优先按“段落”切(比如换行)

如果一段本身 < 1000 → 直接作为一个chunk

如果段落太大(超过1000)

再继续往下拆:

按句子

按标点(句号、逗号等)

如果还太大

最后才按字符数硬切

可能有的小伙伴会问,为什么需要overlap,为什么要重复100个字符?这不是浪费吗?

如果你能想到这个问题,恭喜你

你已经超越了小编啦

那么,overlap的作用是什么?

可以这样说,overlap是整个chunk策略里,最容易被忽略、但最关键的一环

咱们先看一个没有 overlap 的情况

原文:

ServiceA接口使用POST,请求路径为/api/v1/serviceA,请求头需要Authorization token

如果没有 overlap,切出来可能是:

chunk1:ServiceA接口使用POST

chunk2:请求路径为/api/v1/serviceA

chunk3:请求头需要Authorization token

问题来了:

👉 用户问:serviceA怎么调用?

系统可能只检索到:

chunk1(因为包含“serviceA”)

但 chunk1 只有:“使用POST”

路径呢?token呢?全没了

有 overlap 的情况(完全不一样)

假设 overlap = 50

chunk1:ServiceA接口使用POST,请求路径为/api/v1/serviceA

chunk2:请求路径为/api/v1/serviceA,请求头需要Authorization token

这时候:

无论命中 chunk1 还是 chunk2

AI都能看到“完整信息的一部分”,它就可以拼出完整答案

所以overlap 其实就是在解决防止信息被“切断”的这个问题,换句话说overlap 是在保证“上下文连续性”,所以overlap 不是“冗余”,而是“容错机制”

即使只命中一个chunk,也尽量保证信息完整


【四、如何自定义切法】

很多小伙伴看到这里估计就会问了,我自己怎么自定义chunk的切法呢?

方案一:调参数

DocumentSplitters.recursive(500, 100)

但是你只能控制chunk和overlap的大小,无法改变它的“切法”

方案二:自己先切,再喂给它

比如按接口切

String[] parts = document.split("【接口说明】");
List<TextSegment> segments = new ArrayList<>();
for (String part : parts) {
    segments.add(TextSegment.from(part));
}

按业务切,再用 recursive 微调

List<TextSegment> finalSegments = new ArrayList<>();
for (String part : parts) {
    List<TextSegment> subSegments =
        DocumentSplitters.recursive(500, 100)
                         .split(TextSegment.from(part));
    finalSegments.addAll(subSegments);
}

方案三:完全自定义切分器

public List<TextSegmentsplitBySentence(String text) {
    List<TextSegment> list = new ArrayList<>();
    String[] sentences = text.split("。|!|?");
    for (String s : sentences) {
        list.add(TextSegment.from(s));
    }
    return list;
}

无论哪种方法,请记住:

  • 一个接口 = 一个chunk(不能拆)

  • 一问一答必须在一起

  • 表格 / 配置说明不能被拆散

  • 有明显结构的文档,比如:标题,小节,列表,recursive 很容易切坏

没有最好的,只有适合自己的。


【五、chunk在RAG中的地位】

chunk 在 RAG 三要素中的位置

RAG效果 = chunk × embedding × 检索

但在工程实践中:

chunk 的权重远高于你想象,原因很简单:

embedding 再强,也无法弥补“信息被切断”

检索再准,也找不到“不存在的chunk”

换句话说

embedding 决定“找得准不准”,

chunk 决定“有没有得找”


好了,讲到这里,相信你已经对chunk有了深刻的理解,赶紧去看看你的chunk合理不合理吧!

`📦 配套完整源码为方便大家直接落地,我把完整工程整理好了:完整 Spring Boot 项目、pgvector 建表脚本、chunk 切分优化、批量导入、Postman 示例一应俱全。

关注公众号「进阶技术客栈」,回复 RAG 源码 即可领取。`