【AI】Agent 专项进阶|RAG 三大切片策略

简介: 本文系统对比了RAG中三种文档切片方法:窗口切分、递归切分与语义切分。窗口切分简单高效但易断句且冗余高;递归切分基于分隔符按语义边界切割,适合结构化文档;语义切分通过向量相似度识别话题切换,语义完整性最佳,但计算成本较高。结合实际案例与性能分析,提出应根据文档类型与场景需求选择合适策略,以提升检索召回精度与模型表现。

200x200

  ◆ 博主名称: QuZhengRong

  AI俘虏,样式苦手

⭐️ LuckReport专栏LuckReport

⭐️ SpringBoot专栏SpringBoot

⭐️ SpringCloud专栏SpringCloud


目录

image

文档切片是 RAG 的前置处理步骤,它直接决定了后续检索可获取的内容,切得不好,召回的片段要么臃肿,要么缺少关键信息,无法为 大模型 提供强有力的数据支持。

常见的切片方法有窗口、递归、语义这三种,下面分别介绍下这些方法的优缺点,以及如何在实际场景中选择合适的切片方法。

一、为什么做文档切片

RAG 的流程是先把文档转化为向量然后存储到 向量数据库 中,用户提问时把问题也向量化,找出最相似的片段再返回给模型。

文档切片要做的是把文档切成 chunk 块,之后再分块存入向量数据库。那你可能好奇,为什么不直接把整篇文档做成一个向量?原因有以下三点:

  1. 模型上下文有限。长的文档有几十万字,全部放进对话可能会超出模型的上下文限制。

  2. 长文档转化为一个向量后,语义会被平均掉。比如文档里有选址、成本、外卖、加盟好几个话题,压成一个向量之后,每个话题的特征都不突出,检索评分变低。

  3. 长文档里混杂大量无关内容,会稀释核心语义,向量表征无法聚焦局部关键信息,导致召回精度大幅下降

基于以上理由,长文档要做切片。切片的目标也有三个:

  1. 块内的语义尽量完整
  2. 块的大小适配 embedding 模型
  3. 块与块之间不要重复太多

用一份 餐饮经营避坑手册.md 作为切片素材,点击链接可下载文件:

餐饮经营避坑手册.md

二、准备工作

用 uv 安装切片依赖。

uv add langchain-text-splitters langchain-openai python-dotenv

嵌入模型用的是 百炼 的 text-embedding-v4,在 .env 里配 API_KEY 和 BASE_URL

from dotenv import load_dotenv
from langchain_openai import OpenAIEmbeddings
import os

load_dotenv()

embedder = OpenAIEmbeddings(
    model="text-embedding-v4",
    api_key=os.getenv("API_KEY"),
    base_url=os.getenv("BASE_URL"),
    # 百炼只收原始字符串,必须关掉,否则 langchain 会先按 token 截断再发,接口报 400
    check_embedding_ctx_length=False,
    # 百炼单次最多 10 条,langchain 默认按 1000 条一批发,也会报 400
    chunk_size=10,
)

三、窗口切分

窗口切分是按固定长度来切,每块之间留一段重叠。重叠的部分是被切断的那句话,上一块的结尾在下一块的开头再出现一次,检索时至少有一块是完整的,可以作为标记拼接文档。

# 块大小按字符数算,中文 1 个字大约 0.6~1 个 token
CHUNK_SIZE = 300
# 相邻两块重叠的字符数,一般取 CHUNK_SIZE 的 10%~20%
OVERLAP = 60
# 末尾碎片小于这个长度就并入上一块
MIN_CHUNK_SIZE = 80

def window_split(text: str, chunk_size: int = CHUNK_SIZE, overlap: int = OVERLAP) -> list[str]:
    step = chunk_size - overlap  # 每次向前滑动的步长
    chunks = [text[i:i + chunk_size] for i in range(0, len(text), step)]

    # 最后一块太短时并入上一块:overlap 部分已经在上块里了,只补新增内容
    if len(chunks) > 1 and len(chunks[-1]) < MIN_CHUNK_SIZE:
        tail = chunks.pop()
        chunks[-1] = chunks[-1] + tail[overlap:]

    return chunks

运行结果:

原文总长度:3690 字

块数:16
平均长度:287 字
最长 / 最短:300 / 90 字
冗余率(重叠部分占比):19.6%

第 2 块结尾:...扣点普遍在营业额的百分之十到百分之二十之间,另外还有物业费、推广费、保证
第 3 块开头:证金。商场店适合标准化的连锁品牌,独立小店进场容易被各种规则耗死。社区店租金...

第 2 块结尾那句保证金被切成了两半,这就是窗口切分的典型问题:切分的地点可能不是句子、段落的边界,分割了语义。

再看冗余率,16 块里有 15 处重叠,重复内容占了 19.6%,也就是要多存五分之一的文本。overlap 调大能缓解断句,但冗余也跟着涨,一般取 10% 到 20%。

优缺点:

✅ 实现最简单,不挑文档类型,速度快,无需额外调模型

❌ 无法识别句子边界,一句话常被截断;重叠区域会产生冗余,调大 overlap 虽然能改善断句问题,但存储成本也会随之增加

窗口切片适合没有明显结构的长文本,比如日志、访谈记录、纯文字的会议纪要。

四、递归切分

递归切分的思路是给一堆分隔符排优先级,先用最粗的分隔符切,切完后如果文本还是太大了,再换细一级的分隔符继续切,直到每块都小于阈值。和窗口切分的区别是它优先在段落和句子的边界上断开。

from langchain_text_splitters import RecursiveCharacterTextSplitter

# 默认分隔符是针对英文设计的(\n\n、\n、空格),中文文档要自己排优先级
CN_SEPARATORS = [
    "\n## ",
    "\n### ",
    "\n\n",
    "\n",
    "。",
    "!",
    "?",
    ";",
    ",",
    " ",
    "",
]

splitter = RecursiveCharacterTextSplitter(
    separators=CN_SEPARATORS,
    chunk_size=300,
    chunk_overlap=50,
    length_function=len,  # 中文按字符数统计,不用 tiktoken 估 token
    is_separator_regex=False,
)
chunks = splitter.split_text(text)

分隔符这一项是递归切割场景里最值得关注的地方。 langchain 的默认分隔符是 \n\n、\n、空格这些,对英文好用,但在中文场景下就不太适用了。这次要切的是手册,我把 \n## 、\n### 放在最前面,更符合中文文档的结构。

【默认分隔符】
块数:16
平均长度:238 字
最长 / 最短:296 / 116 字
以句号/问号结尾的块:7/16

【中文分隔符】
块数:18
平均长度:203 字
最长 / 最短:296 / 79 字
以句号/问号结尾的块:18/18

以句号结尾的块从 7 个变成 18 个,也就是基本每块都停在完整句子上了。

优缺点:

✅ 通用性强,切出来的块基本停在句子边界上,速度快,不需要调 embedding,改改分隔符就能适配不同类型的文档

❌ 只认文本分隔符,不认文档结构,表格和代码块这类需要整体保留的内容会被切坏;块的语义完整靠运气,跨句子的语义切换它感知不到

递归切分是 RAG 里最常用的切片方法,markdown、技术文档都能用。

五、语义切分

语义切分不按长度切,按语义切。做法是先把文档拆成句子,算相邻两句的向量相似度,相似度掉下去说明话题换了,就在这里断开。

def semantic_split(sentences: list[tuple[str, str]], vectors: list[list[float]]) -> list[dict]:
    chunks: list[dict] = []
    current_title, current_text = sentences[0]
    current_body: list[str] = [current_text]
    break_points: list[tuple[int, float]] = []

    for i in range(1, len(sentences)):
        title, sentence = sentences[i]
        similarity = cosine_similarity(vectors[i - 1], vectors[i])
        too_long = len("".join(current_body)) + len(sentence) > MAX_CHUNK_SIZE

        # 相似度掉到阈值以下,说明换话题了;块太长也要强制切
        if similarity < SIMILARITY_THRESHOLD or too_long:
            chunks.append({
   
                "title": current_title,
                "text": "".join(current_body),
                "sentence_count": len(current_body),
            })
            break_points.append((i, similarity))
            current_title, current_text = title, sentence
            current_body = [sentence]
        else:
            current_body.append(sentence)

    chunks.append({
   
        "title": current_title,
        "text": "".join(current_body),
        "sentence_count": len(current_body),
    })

    # 打印最明显的几个断点,方便判断阈值合不合适
    break_points.sort(key=lambda x: x[1])
    print("\n相似度最低的 5 个断点(切得最狠的位置):")
    for index, similarity in break_points[:5]:
        print(f"  第 {index} 句 相似度 {similarity:.3f} -> {sentences[index][1][:30]}")

    return merge_small_chunks(chunks)

句子拆分那一步我把 markdown 的标题行单独拎出来,作为这一句所属的章节记下来,不参与相似度计算。这样切完之后每块都知道自己属于哪一章,检索时能顺便带上章节信息。

def load_sentences(text: str) -> list[tuple[str, str]]:
    sentences: list[tuple[str, str]] = []
    current_title = ""
    for line in text.splitlines():
        line = line.strip()
        if not line:
            continue
        if line.startswith("#"):
            current_title = line.lstrip("#").strip()
            continue
        # 中文句末标点切句,保留标点本身
        for piece in re.split(r"(?<=[。!?;])", line):
            piece = piece.strip()
            if piece:
                sentences.append((current_title, piece))
    return sentences

阈值是这个策略唯一要调的参数,但它没有通用值,换一份文档、换一个 embedding 模型,相似度分布都会变,所以先跑一遍看均值再定数字更可靠些。

优缺点:

✅ 切出来的块语义最完整,基本都在话题切换处断开,检索和生成体验最好,无需手动设定块大小

❌ 需要额外多一轮 embedding 调用;阈值没有通用值,更换文档或模型就需要重新调参;效果依赖分句质量,遇到标点不规范、无标点的文本,表现不如递归切分

语义切分适合知识库、FAQ、客服记录、产品说明书这类对语义完整要求高的内容。

六、总结

三种策略在同一份文档上的比较结果:

策略 块数 平均长度 需要调模型 适合什么文档
窗口切分 16 287 字 不需要 日志、纪要、结构不明显的长文本
递归切分 18 203 字 不需要 通用文档、博客、技术手册
语义切分 19 173 字 需要 知识库、FAQ、话题切换频繁的内容

七、LuckReport 项目推荐

image

导航:LuckReport专栏

1、项目简介

Luck-Report 是一款基于开源项目 UReport2 重构的 Java 高性能报表引擎,通过迭代单元格可以实现任意复杂的中国式报表。相较于 UReport2,Luck-Report 在技术架构上进行了全新升级,后端基于 SpringBoot 框架开发、前端采用 Vue 框架构建,技术选型贴合当下主流项目开发标准,可精准适配各类实际开发需求。

Luck-Report 提供了全新的基于网页的报表设计器,可以在 Chrome、Firefox、Edge 等各种主流浏览器运行(IE 浏览器除外)。使用 Luck-Report,打开浏览器即可完成各种复杂报表的设计制作。

Luck-Report 基于 Apache-2.0 开源协议 开源

2、在线体验

相关文章
|
3天前
|
人工智能 API 内存技术
刚刚 DeepSeek V4.1 Flash 开启内测,1 分钟教你用上!
刚刚 DeepSeek 内测群发布了 DeepSeek V4.1 Flash 中间版本内测的消息,这次的模型采用了新的结构,原生支持多模态、能力更强、速度更快、且成本更低。
1618 4
|
7天前
|
人工智能 运维 BI
阿里云千问办公QwenWork深度解析:基于Qwen3.8,六大核心能力重构企业全自动化工作流与计费选型指南
传统AI办公工具大多停留在对话问答、文档摘要、简单文案生成层面,只能完成单点碎片化任务,无法自主拆解复杂业务流程,很难串联多工具、多文档、外部业务系统完成端到端完整工作交付。很多企业在落地AI办公的时候,需要组合多款不同工具,来回切换界面,手动复制粘贴中间结果,智能化改造落地门槛居高不下。千问办公QwenWork是整合多款智能体产品能力打造的一体化企业办公智能体平台,底层基座依托Qwen3.8大模型,打通桌面端Agent、云端Agent、企业协同Agent三种运行形态,不再局限简单问答,接收业务目标之后自主拆解任务步骤,调用各类工具,处理文档、表格、浏览器自动化、数据查询,直接输出可交付的办公
1598 0
|
4天前
|
SQL 人工智能 前端开发
QoderWake 1.0 正式发布:从桌面里的 Agent,到工作现场的数字员工
QoderWake v1.0正式发布:企业级数字员工团队平台。支持“一句话建岗”,预置10类特训岗位;Waker常驻钉钉/飞书群,@即响应、自动协作、跨任务记忆;具备定时/事件/API多触发方式与统一任务看板;已沉淀27.6万条记忆、12.3万项技能,助力组织实现人机协同增效。
698 0
|
16天前
|
人工智能 自然语言处理 安全
阿里云千问办公、Qoder Teams、Qoder CN区别与选择指南:模型能力、适用场景与最新活动参考
本文聚焦阿里云2026年推出的三款自研AI办公产品,清晰拆解千问办公、Qoder Teams、Qoder CN的差异化定位与能力边界:千问办公主打职场全场景提效,支持自然语言指令一键完成PPT生成、数据分析等高频办公任务;Qoder Teams面向程序员团队,深度整合AI代码生成、团队协同与企业知识库能力;Qoder CN则专为金融、政务等强合规场景打造,实现数据不出境与VPC私有化部署。文章同步给出分场景选型指南与最新活动定价,帮助不同类型的企业按需组合产品,实现业务岗、研发岗与强合规场景的AI能力全覆盖。
3843 5
阿里云千问办公、Qoder Teams、Qoder CN区别与选择指南:模型能力、适用场景与最新活动参考
|
7天前
|
人工智能 自然语言处理 安全
阿里云AI数智鉴密:AI 生成内容如何拿到一张"防篡改的身份证"
隐形水印 + C2PA签名:让AI生成内容“持证上岗”。
1141 0
|
8天前
|
网络协议 Linux iOS开发
【2026实测】Wireshark下载+安装+汉化+使用教程(图文版,巨详细)
Wireshark 是一款免费开源的网络协议分析工具,可实时捕获、解析并可视化数据包,助你诊断网络故障、分析通信协议(如HTTP、DNS、TCP等)。支持Windows/macOS/Linux,含中文界面,新手入门便捷。(239字)
|
2天前
|
缓存 测试技术 API
DeepSeek V4.1 Flash 内测接入:改个模型名即可调用(附代码)
DeepSeek V4.1 Flash 内测不用申请,base_url 不变、改个模型名就能调,9/10 到期。本文讲清接入、计费限流与多模态注意点。
643 0
DeepSeek V4.1 Flash 内测接入:改个模型名即可调用(附代码)

热门文章

最新文章