尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
RAG二 文档切分实战:从语义分层到长度优化
1. 背景与目标在构建知识库或 RAG 检索链路时Markdown 文档的切分质量直接决定了后续向量召回的效果。本文围绕一个基于 LangGraph 的导入流程节点讲解如何先按标题做语义切分再对超长块做递归二次切割最终产出结构清晰、长度可控的文本块。整体目标可以概括为三点语义完整优先以 Markdown 标题为边界保证每个块的主题相对独立。长度可控单块内容不超过模型上下文限制超长块需要二次切割。可追溯每个块保留所属标题与文件标题便于后续检索与引用。2. 文档切分节点概览文档切分节点node_document_split位于app/import_process/agent/nodes/node_document_split.py核心职责是把长篇、无结构或半结构化的 Markdown 文档转化为可直接向量化入库、可精准检索的微观语义单元Chunk。它是 RAG 知识库的核心基础切分质量直接决定后续检索召回率与答案准确率没有合理切分再好的向量模型也无法发挥作用。2.1 切割核心原则语义完整不切断句子、代码块、表格、条款保证单个 Chunk 是完整的知识点或逻辑单元。长度可控Chunk 大小适配大模型上下文窗口和 Embedding 模型输入限制不超上限、不碎片化。边界清晰相邻 Chunk 保留合理重叠避免关键信息落在两块交界处。可追溯每个 Chunk 携带完整元数据标题、父标题、文件名便于后续溯源和引用。3. RAG 文档切分核心思路文档切分的核心逻辑是「先保语义再控大小最后补细节」全程遵循三步核心流程所有操作都围绕「不破坏语义、适配检索」展开。3.1 第一步先做语义分层无论什么类型的文档先不考虑长度优先按文档本身的语义边界拆分这是 RAG 切分的核心原则——语义完整永远比「凑长度」更重要。核心逻辑找到文档的天然语义边界标题、章节、段落、条款、对话轮次按边界拆分确保每个拆分后的单元能独立表达一个完整的意思比如一个章节、一个步骤、一个条款、一轮对话。关键动作先识别文档结构是否有标题、条款、表格、代码块再按结构拆分不盲目按字符数切割。3.2 第二步再做长度优化语义拆分后会出现两种情况单个语义单元过长超过模型输入限制或者单个语义单元过短碎片化导致检索无意义。此时进行针对性优化超长处理对超过模型适配长度的语义单元进行递归切分按更小的语义边界段落 → 句子 → 标点切割不硬断句子、不破坏逻辑。过短处理对过短的语义单元如单个句子、零散短语在同语义层级内合并比如同一章节下的短段落合并避免碎片化。具体切割大小会根据不同文档类型有差异后文会单独说明。3.3 第三步最后补细节保障长度优化后补充两个关键细节确保切分结果可落地、可调试重叠设置相邻 Chunk 保留一定重叠5%~10%避免关键信息如步骤、参数、条款落在切口上导致检索漏招。元数据补齐给每个 Chunk 添加元数据标题、父标题、文件名、块序号便于后续检索溯源、问题定位。4. 不同类型文档的切分形式不同文档的结构、用途不同切分形式和侧重点也会不同。下面按类型介绍切分形式、核心要求与操作细节。4.1 技术文档 / 手册 / API 文档这类文档有清晰的标题层级#一级标题、##二级标题……包含步骤、代码块、参数说明核心是「步骤完整、代码可读、参数不遗漏」。语义拆分按标题层级切分一级标题拆分为大模块二级标题拆分为子模块以此类推确保每个模块对应一个完整的知识点。代码块保护识别代码块标记/~~~代码块整体保留不拆分、不切割哪怕代码块过长也单独作为一个 Chunk避免代码不可读。长度优化若单个标题下的内容过长如超过 2000 字符可自行设定按段落拆分优先在段落空行处切割不切断步骤、不切断代码若内容过短如小于 300 字符且与相邻子标题属于同一知识点合并为一个 Chunk。细节补充重叠率 5%~8%如 2000 字符的 Chunk 重叠 100~160 字符元数据保留父标题、当前标题、文件名便于追溯知识点所属模块。核心要求不切断步骤、不切断代码块、不切断参数说明确保每个 Chunk 能独立体现一个操作或知识点。4.2 论文 / 研究报告 / 长叙述文本这类文档有摘要、引言、章节、结论以文字叙述为主核心是「论点与论据连贯、逻辑完整」无代码块多为段落式结构。语义拆分按章节标题加段落切分先按章节标题拆分为大模块再按段落拆分为子模块确保每个子模块对应一个完整的论点或论据如「2.1 实验方法」下的某一段实验描述。长度优化若单个段落过长如超过 3000 字符按句子拆分优先在句号、感叹号处切割不硬断句子若单个段落过短如小于 400 字符且与相邻段落属于同一论点合并为一个 Chunk。细节补充重叠率 8%~12%如 2500 字符的 Chunk 重叠 200~300 字符避免论点边界丢失元数据保留章节标题、段落序号、文件名标注论点所属章节。核心要求论点与论据不分离不切断句子确保每个 Chunk 能完整表达一个观点或一段论述。4.3 对话记录 / 日志 / 工单这类文档无标题、无章节多为「角色 内容」的轮次结构如客服对话、系统日志核心是「上下文承接、轮次完整」。语义拆分按轮次或时间戳切分优先按完整轮次拆分如「用户提问 → 客服回复」为一个完整轮次日志按时间戳分段拆分。长度优化若轮次过长如多轮对话累计超过 1500 字符按单轮对话拆分确保每轮对话独立成块若轮次过短如单句对话小于 200 字符合并相邻轮次如连续 3 轮短对话合并为一个 Chunk避免碎片化。细节补充重叠率 15%~20%如 1000 字符的 Chunk 重叠 150~200 字符确保上下文承接不丢失元数据保留角色如用户、客服、时间戳、文件名标注对话或日志的时间顺序。核心要求轮次完整、上下文承接不切断单轮对话确保每个 Chunk 能体现一段完整的交互或日志片段。4.4 法律 / 合同 / 制度文档这类文档有清晰的条款编号如「第一条、第二条」语言严谨核心是「条款完整、不破坏引用关系」不可拆分单条条款。语义拆分按条款编号切分单条条款如「第一条 定义」作为一个基础语义单元若条款过长如包含多个子条款按子条款拆分如「第一条 1.1 定义」。长度优化若单条条款过长如超过 2000 字符按条款内的分句拆分优先在分号、句号处切割不破坏条款逻辑不合并任何条款哪怕条款过短避免条款边界模糊、引用错误。细节补充重叠率 5%~10%如 1500 字符的 Chunk 重叠 75~150 字符元数据保留条款编号、父条款若有、文件名标注条款所属章节。核心要求不拆分单条条款、不破坏引用关系确保每个 Chunk 是完整的一条或一段条款可直接用于法律检索、条款引用。5. 切分的核心原则补充语义优先原则无论长度如何先保证语义完整不硬断句子、代码、条款这是 RAG 切分的底线。模型适配原则Chunk 大小不超过 Embedding 模型输入限制总和不超过大模型上下文窗口的 70%。不碎片化原则单个 Chunk 不小于 300 字符纯中文条款除外避免零散碎片导致检索噪声。边界保护原则代码块、表格、条款整体保留不拆分、不切割。6. 代码实现步骤本节点负责将 Markdown 文本切分为适合向量检索的语义块Chunk采用「先按标题语义切分 → 再按长度精细化切割」的稳定策略。获取与清洗内容Step 1从 state 中提取 Markdown 内容与文件标题统一换行符格式\r\n/\r→\n保证跨平台兼容。按标题语义初切Step 2基于 Markdown 标题语法#~######进行语义级切分自动跳过代码块内的标题匹配避免误切注释保证每个块语义完整。无标题文档兜底处理Step 2 内置若文档无任何标题自动生成默认标题「无主题」确保内容不丢失、流程不中断。超长块递归精细化切割Step 3使用RecursiveCharacterTextSplitter对超过指定长度的语义块进行二次切割按「段落 → 换行 → 句子 → 空格」优先级切割不产生碎片、不硬断句子、无需手动合并。构建标准 Chunk 结构为每个切片补充完整元数据title、content、file_title、parent_title、part序号保证可检索、可溯源。本地备份与状态更新Step 4将切分结果备份到本地chunks.json文件同时将最终 chunks 存入 state供后续向量入库使用。7. 全局配置先定义切分相关的常量方便后续统一调整# 全局配置可根据模型调整 CHUNK_MAX_SIZE 500 # 500 字符触发二次切割 # 单个文本块最大长度控制不超过模型上下文 CHUNK_SIZE 200 # 小值方便测试切割 # 块之间重叠长度保证语义不丢失 CHUNK_OVERLAP 20其中CHUNK_MAX_SIZE是触发二次切割的阈值CHUNK_SIZE与CHUNK_OVERLAP则交给 LangChain 的递归切割器使用。8. 依赖导入import json import os import re from pathlib import Path from typing import Tuple, List, Dict, Any from langchain_text_splitters import RecursiveCharacterTextSplitter from app.core.logger import logger, node_log, step_log from app.import_process.agent.state import ImportGraphState from app.utils.task_utils import add_running_task, add_done_task9. 第一步数据校验与清洗该步骤负责从 state 中取出md_content、file_title和md_path完成非空校验、兜底读取以及换行符统一。step_log(step_1_validate_clean) def step_1_validate_clean(state) - Tuple[str, str]: 1. 获取数据 md_content, file_title, md_path 2. md_content 进行非空校验空 - 异常 3. md_content 不为空 - 数据清洗 - 字符串统一替换 4. file_title 进行非空判断 - 空 - 通过 md_path 获取 file_title 5. 返回数据即可 md_content state[md_content] file_title state[file_title] md_path state[md_path] # 进行必要非空校验 if not md_content: logger.warning(没有从 state 读取到 md_content 内容我们使用 md_path 尝试再次读取) if md_path: md_content Path(md_path).read_text(encodingutf-8) state[md_content] md_content if not md_content: logger.error(md_content 没数据并且尝试读取 md_path 依然没有数据终止执行) raise ValueError(md_content 没数据并且尝试读取 md_path 依然没有数据终止执行) if not file_title: logger.warning(没有在 state 读取到 file_title计划给与默认值) if md_path: file_title Path(md_path).stem if not file_title: file_title default state[file_title] file_title md_content 内容进行清洗\r\n、\r -gt; \n md_content md_content.replace(\r\n, \n).replace(\r, \n) return md_content, file_title/code/pre 这里有两个细节值得注意一是 md_content 缺失时会尝试从 md_path 兜底读取避免上游传参不完整导致流程中断二是统一换行符保证后续按 \n 切行时行为一致。 10. 第二步按标题做语义切分 核心思路是逐行扫描 Markdown识别 # 到 ###### 的标题行并以标题为边界聚合内容。同时需要跳过代码块内部的 #避免把注释误判为标题。 step_log(step_2_data_chunk_title) def step_2_data_chunk_title(md_content, file_title) - List[Dict[str, str]]: 1. 定义正则 2. 根据 \n 进行行的切割 3. 准备数据容器 4. 循环处理每行数据 5. 检查是否进入和退出代码块 6. 判断是不是标题且不在代码块 - 标题处理 7. 不是代码块也不是标题 - 普通行处理 8. 跳出循环后处理最后一个标题的内容 9. 检查没有标题的文档 - title default, md_content 10. 返回结果 # 1. 定义正则 reg re.compile(r^\s*#{1,6}\s.) # 2. 根据 \n 进行行的切割 lines md_content.split(\n) 3. 准备数据容器 chunks [] # 最终结果 current_title None # 记录当前标题 current_title_lines [] # 记录当前标题的行内容 is_code_block False # 是否进入代码块默认没有 chunk_size 0 # 切的块数 4. 循环处理每行数据 for line in lines: # 5. 检查是否进入和退出代码块 if line.startswith() or line.startswith(~~~): is_code_block not is_code_block current_title_lines.append(line) continue # 6. 判断是不是标题且不在代码块 -amp;gt; 标题处理 if reg.match(line) and not is_code_block: # 结算上一个标题的内容 if current_title: chunks.append({ content: \n.join(current_title_lines), title: current_title, file_title: file_title }) # 清空旧数据 current_title line current_title_lines [current_title] chunk_size 1 else: # 7. 不是代码块也不是标题 -amp;gt; 普通行处理 current_title_lines.append(line) 8. 跳出循环后处理最后一个标题的内容 if current_title: chunks.append({ content: \n.join(current_title_lines), title: current_title, file_title: file_title }) 9. 检查没有标题的文档 if chunk_size 0: chunks.append({ content: md_content, title: default, file_title: file_title }) chunk_size 1 10. 返回结果 logger.info(f完成语义切割切块数量{chunk_size}内容{chunks[:3]}) return chunks/code/pre 几个关键点 代码块状态机用 is_code_block 布尔值在 与 ~~~ 之间翻转保证代码里的 # 注释不会被当成标题。 标题即内容首行current_title_lines 初始化为 [current_title]让块内容自带标题便于后续展示与检索。 无标题兜底当 chunk_size 为 0 时整篇文档作为一个 default 块返回。 11. 第三步超长块二次切割 按标题切分后单个块仍可能超过 CHUNK_MAX_SIZE。此时使用 LangChain 的 RecursiveCharacterTextSplitter 做二次切割并为每个子块补充 parent_title 与 part 信息。 step_log(step_3_data_refine_chunk) def step_3_data_refine_chunk(chunks) - List[Dict[str, str]]: 作用将超过指定 size 的标题内容如 1000 字符进行二次切分 二次切分产生 parent_title 与 part。 入参chunks 出参chunks含 part、parent_title 步骤 1. 先定义 LangChain 提供的递归切割器块大小、重叠部分、切割符号 2. 循环原来的 chunks 3. 获取每块的 content 长度 4. 没有超过 - 不需要二次切割 - 补充 part、parent_title 5. 超过 - 递归切割器二次切割 6. 最终记录结果 final_chunks # 1. 定义递归切割器 splitter RecursiveCharacterTextSplitter( chunk_sizeCHUNK_SIZE, chunk_overlapCHUNK_OVERLAP, separators[\n\n, \n, 。, , , , , , ] ) final_chunks [] 2. 循环原来的 chunks for chunk in chunks: content chunk.get(content, ) title chunk.get(title, default) file_title chunk.get(file_title, default) # 3. 获取每块的 content 长度 if len(content) amp;lt; CHUNK_MAX_SIZE: # 4. 没有超过 -amp;gt; 不需要二次切割 final_chunks.append({ content: content, title: title, file_title: file_title, parent_title: title, part: 0 }) continue 5. 超过 -amp;gt; 递归切割器二次切割 sub_texts splitter.split_text(content) for idx, sub_text in enumerate(sub_texts): final_chunks.append({ content: sub_text, title: title, file_title: file_title, parent_title: title, part: idx }) 6. 最终记录结果 logger.info(f完成二次切割最终块数量{len(final_chunks)}) return final_chunks/code/pre 切割符号的顺序体现了「先粗后细」的策略优先按段落切其次按行、句号、感叹号、问号、分号、逗号最后退化到空格与空字符串尽可能保证语义边界完整。 12. 流程总览 flowchart TD A[读取 state: md_content / file_title / md_path] -- B{md_content 是否为空} B -- 是 -- C[尝试从 md_path 读取] C -- D{是否读取成功} D -- 否 -- E[抛出异常终止流程] D -- 是 -- F[统一换行符] B -- 否 -- F F -- G[按标题语义切分] G -- H{单块长度是否超过 CHUNK_MAX_SIZE} H -- 否 -- I[直接保留补充 parent_title 与 part] H -- 是 -- J[递归切割器二次切割] J -- K[补充 parent_title 与 part] I -- L[输出 final_chunks] K -- L 13. 小结 这套切分方案的核心是「两级切割」第一级用标题做语义边界保证主题聚合第二级用递归切割器控制长度保证不超上下文。配合 parent_title 与 part 字段后续无论是做向量检索还是原文回溯都能快速定位到内容所属章节。 实践中还可以继续优化例如把 CHUNK_SIZE 与 CHUNK_OVERLAP 做成配置项或针对代码块单独设置不切割策略避免代码被截断影响可读性。
RELATED

相关推荐

单链表从入门到实战:概念、结构与 C 语言接口实现全解析

单链表从入门到实战:概念、结构与 C 语言接口实现全解析

目录 1. 链表的定义 1.1 什么是链表1.2 什么是节点1.3 节点的结构体代码1.4 节点和链表的关系 2. 链表的性质3. 链表的优势4. 链表的打印5. 单链表 5.1 申请节点5.2 链表尾插5.3 链表头插5.4 链表头删5.5 链表尾删5.6 在指定位置前插入5.7 在指定位置后插入5.8 指定位置删除5.…

📅 2026/11/1 1:55:44
NSG与VNet Peering深度指南:DevOps Insiders Batch 18控制跨虚拟网络访问的完整方法

NSG与VNet Peering深度指南:DevOps Insiders Batch 18控制跨虚拟网络访问的完整方法

【免费下载链接】azure-aws-gcp-devsecops-mlops-batch-18 Welcome to the official repository for Batch18! 📚👩‍💻 Here, youll find a organized collection of class notes, assignments, and code snippets generated during our ses…

📅 2026/11/1 1:50:44
BrowserSkill bsk update 升级指南:daemon 重启的 3 条路径、Windows 分阶段替换与升级后 30 秒验证

BrowserSkill bsk update 升级指南:daemon 重启的 3 条路径、Windows 分阶段替换与升级后 30 秒验证

BrowserSkill bsk update 升级指南:daemon 重启的 3 条路径、Windows 分阶段替换与升级后 30 秒验证 【免费下载链接】BrowserSkill Let AI agents use your real, logged-in browser without interrupting your work. CLI extension for browser automation acros…

📅 2026/11/1 1:50:44
MORE NEWS

更多资讯

📰

启动报Port 8088 was already in use?先查是谁占的

启动报Port 8088 was already in use?先查是谁占的 项目启动报 Port 8088 was already in use,意思很直白:这个端口已经有人占了。 先说原理:端口是"门牌号",一间房只住一个进程 端口就是一台机器上的门牌号…

📰

担心文档上云?本地运行的办公AI才敢放心用

单位上周开了个会,主题是"AI 提效",散会时领导补了一句:涉及客户资料的文档,不许往云端传。这话一出,会议室里本来跃跃欲试的同事全蔫了——大家平时用的那些在线 AI,文档一上传就不知道存哪儿去…

📰

先能用,再好用:从烂尾复盘到两周上线,全栈项目的‘原型—快速迭代‘方法论

先能用,再好用:从烂尾复盘到两周上线,全栈项目的“原型—快速迭代”方法论 全栈学习路径的文章已经很多了:怎么选技术栈、怎么搭环境、怎么记文档,几乎每个方向都有人写。但一个更扎心的问题很少有人正面讨论——为什…

📰

深度思考模式把 ARC-AGI-2 从 17.6% 拉到 45.1%:对照 Gemini 3 Pro 的三份能力评测,看‘会思考‘改写了哪些边界

深度思考模式把 ARC-AGI-2 从 17.6% 拉到 45.1%:对照 Gemini 3 Pro 的三份能力评测,看“会思考”改写了哪些边界 一、先说结论:变强的不是“知识”,而是“解法” 每次新模型发布,评测榜单都会涨几个点,大…

📰

1.c++入门篇

1.c中的关键字asmdoifreturntrycontinueautodoubleinlineshorttypedefforbooldynamic_castintsignedtypeidpublicbreakelselongsizeoftypenamethrowcaseenummutablestaticunionwchar_tcatchexplicitnamespacestatic_castunsigneddefaultcharexportnewstructusingfriendclassext…

📰

《江灯合声》:有可行的分框版,也仍可以选择连续鸣谢

武汉江岸区栎坡合唱团在免费节目册《江灯合声》中,以表格集中鸣谢资助者。各家共同要求改为独立小框,团里仍想保留表格。现有全名与支持事项清楚可见,双方此前未承诺具体排版。我支持保留表格。补充的试排材料让选择更具体:分框能…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬