引言

上一篇我们完成了本地知识库的文档解析、文本向量化和向量库入库,正式搭建好了 RAG 问答助手的底层知识库。

但很多人搭完向量库后会发现一个痛点:文档切块太生硬、关键语义被强行割裂,上下文断层,进而导致检索不准、问答答案残缺或逻辑断裂。

想要从根源提升 RAG 问答的准确率,Chunk 切块大小 + Overlap 上下文重叠 是最基础也最关键的优化点。合适的块大小能保留完整语义,合理的重叠度能避免知识点被切分割裂,从源头减少检索漏配、答非所问的问题。

本篇就结合 AI 问答助手实战,讲解 Chunk 大小选型Overlap 重叠原理不同文档类型的适配方案,以及实操中的优化经验,给后续检索和问答打好底层基础。

版本目标

让本地向量库的chunk块按合适的大小进行切分,提升RAG的检索准确率,让AI问答助手更“智能”。

现在的“知识库切分方式”是:一整段 or 一整行,导致检索不精准、上下文不完整、回答容易“偏”

什么是Chunk

大模型一次能处理的信息长度有限(即上下文窗口,例如 128K tokens)。当处理长文章时,系统会先把原文切成许多个小片段,每个小片段就是一个 Chunk。

Chunk = 把大文本切成“小块”

为什么需要Chunk

1. 上下文窗口的硬性限制

大模型一次能处理的 token 数量是有上限的(比如 4k、128k 甚至 1M token)。如果原始文本(例如一本小说、一份财报)超过了这个长度,模型根本无法一次性读完。

分块就是把超长文档切成若干个模型能一口“吞下”的小片段。

2. 降低计算成本与延迟

即便模型拥有很长的上下文窗口(比如 1M token),一次性输入整本《三体》也会导致:

  • 计算量爆炸:注意力机制的计算复杂度随长度呈平方级增长(O(n²)),处理 1M token 的成本极高。
  • 响应缓慢:用户等不了几十秒才出一个字。

分块后,只把最相关的几个块送给模型,大幅节省算力和时间

3. RAG(检索增强生成)的必然要求

这是目前最普遍的应用场景。当需要让模型回答基于海量私有知识(如公司全部合同)的问题时,不可能把所有知识都塞进 prompt(太长、太贵)

可行的做法: 预先将所有文档切成 Chunk,建立索引。用户提问时,系统先快速检索出最相关的 3~5 个 Chunk,再把“问题 + 这些 Chunk”一起给模型。

没有 Chunk,就无法实现高效、精准的检索。

4. 提升长文本处理的注意力质量

研究与实践表明,当文本极长时,模型对位于中间位置的信息容易“遗忘”或关注不足(称为“中间迷失”现象)。将长文本分块后:
可以对每个块单独做摘要或提取关键信息,再汇总这些结果,得到更可靠的整体输出。

分块相当于把一个大任务分解成多个互不干扰的小任务,降低模型的认知负担。

现在的本地知识库Chunk是这样的,例如:

Java是一种面向对象编程语言,具有平台无关性,由Sun公司开发...
(很长一段)

优化后变成:

[1] Java是一种面向对象编程语言  
[2] Java具有平台无关性  
[3] Java由Sun公司开发  

这样优化的好处是:检索更精准、更容易命中关键内容

什么是Overlap

在文本分块(Chunking)的上下文中,Overlap(重叠) 指的是相邻两个 Chunk 之间共享的那一部分文本。

Overlap = 块之间“重叠一部分”

简单来说就是块内容的冗余,比如:

1:Java是一种面向对象编程语言
块2:面向对象编程语言具有封装继承多态

为什么需要Overlap?

Overlap是为了避免“信息被切断”,如果完全没有冗余,那么切分后根本分不清楚哪个块跟哪个块原本是连接在一起的。

比如:一个完整的句子“张三在2025年成为了CEO”,如果切分点恰好落在“张三在”和“2025年成为了CEO”之间,那么:

前一个 Chunk 只包含“张三在”
后一个 Chunk 只包含“2025年成为了CEO”

当用户问“谁成为了CEO?”时,检索系统可能只命中后一个 Chunk,缺少主语“张三”,导致模型无法给出完整答案。

怎么设计Chunk大小

最核心原则:

Chunk 太大,检索不准
Chunk 太小,上下文不完整
最优:“适中 + 有重叠”

代码实现

第一步:新增文本切分函数

def split_text(text, chunk_size=100, overlap=20):
    chunks = []
    start = 0

    while start < len(text):
        end = start + chunk_size
        chunk = text[start:end]
        chunks.append(chunk)

        start += chunk_size - overlap

    return chunks

上面这段文本切分函数的效果,举个例子:

原文:1000字
chunk_size=100
overlap=20

调用该文本切分函数后会被切分成:

chunk1: 0-100
chunk2: 80-180
chunk3: 160-260
...

第二步,接入本地知识库,修改 load_knowledge()

def load_knowledge():
    with open("data/knowledge.txt", "r", encoding="utf-8") as f:
        text = f.read()

    chunks = split_text(text)

    return chunks

第三步,效果对比

原来

问题:Java特点?
命中:整段大文本
→ 噪声很多

现在命中率大幅提升:

问题:Java特点?
命中:
✔ 面向对象
✔ 平台无关性

注意

改了 chunk 后,必须删除旧的向量库,让程序重新构建本地向量库。其实就是删除项目data目录下的docs.pkl和faiss.index这两个文件,直接在项目里删除不行,就右键进入资源管理器目录下删除。
在这里插入图片描述

进阶优化

按“语义切分”(比长度更高级)

比如按句号、段落、标题

不同类型用不同 chunk_size

内容 chunk_size
FAQ
文档
论文

踩过的坑

分不清答案到底是直接把原文一整段拿出来还是多个 chunk 命中后拼接的

我测试的命中结果是:Java的特点包括:是一种面向对象的编程语言,具有平台无关性,由Sun公司开发。

我的知识库原文:Java是一种面向对象的编程语言,具有平台无关性,由Sun公司开发。

我如何能判定它不是取的知识库原文一大段,而是命中了多个切片后组装的呢?

排查分析方案

加日志,是的,把目前的整个RAG过程从输入到输出都打印出来,尤其是要把检索结果打印出来。
找到vector_db.py中的search方法

  # ===== 4. 搜索 =====
    def search(self, query, top_k=3):
        query_vec = model.encode([query])

        distances, indices = self.index.search(
            np.array(query_vec).astype("float32"),
            top_k
        )

        print("命中的 chunks:")
        for i in indices[0]:
            print(self.docs[i])
        return [self.docs[i] for i in indices[0]]

更进一步验证,在 prompt 前打印上下文,这是LLM实际看到的东西。

def ask(question: str) -> str:
    # 用向量检索代替关键词匹配
    related_docs = vector_db.search(question)

    context = "\n".join(related_docs)

    print("\n最终拼接的上下文:")
    print(context)

解决方案

根据前面加了日志打印运行后控制台打印出来的内容,可进行以下修改:

如果本地知识库很小

比如我们测试用的本地知识库,只有几行,文本量很少。
解决方案就是扩充知识库,把测试用的知识库扩充到100条甚至几百条文本。

chunk_size太大

比如chunk_size = 500,文本只有100字,那么切了也只有一个chunk
解决方案就是减小 chunk_size

split_text(text, chunk_size=20, overlap=5)

结论

判断RAG是否真的生效,不是看“答案”,而是看检索命中的chunks

因为答案是LLM生成的(可能“猜对”),只有chunk命中才是真正“基于知识库”。

Logo

更多推荐