欢迎光临
我们一直在努力

文档切分的边界策略

文章编号:article_414

问题背景

上一篇我们解决了生成侧的问题:让模型稳定输出 JSON 并完成结构校验。但检索增强生成(RAG)的质量由输入侧与生成侧共同决定,其中输入侧的文档处理往往被低估。真实语料通常是数百 KB 的 Markdown、PDF 或网页,远超过模型上下文窗口,必须切分为片段(chunk)后建立向量索引。切分策略直接决定召回上限:同样一句话,若被拦腰切断,向量化后语义失真,最终检索出的片段可能只包含半句噪声。本文聚焦一个可落地的问题——如何选择切分边界,让每个片段既不超过长度限制,又能独立承载完整语义。

核心原理

切分的核心矛盾是长度约束与语义完整性的平衡。一个实用的做法是定义边界优先级,从高到低依次为:标题层级、段落空行、句子结束符(。!?)、分号与逗号。切分器先在目标长度附近向前回溯,寻找最近的高优先级边界作为切分点;只有确实找不到合适边界时才退化为硬截断。这样做的好处是,片段在语法和语义上尽量自洽,模型检索时无需借助原文上下文即可理解片段含义。此外,标题作为结构元数据可以随片段一起索引,形成“路径 + 内容”的复合表示。下面先通过最小示例,直观对比固定长度切分与边界感知切分的差异。

第一次代码实验及输出

import re

text = (
"向量数据库可以高效检索相似内容。"
"但检索质量高度依赖切分方式。"
"如果按固定长度截断,"
"一句话可能被拦腰斩断。"
"边界感知策略会优先在句号、换行处切分。"
"这样每个片段都保持语义完整。"
)

def fixed_chunks(text, size=12):
return [text[i:i + size] for i in range(0, len(text), size)]

def sentence_chunks(text):
sentences = re.split(r"(?<=[。!?])\\s*", text.strip())
chunks = []
current = ""
for sentence in sentences:
if len(current) + len(sentence) <= 20:
current += sentence
else:
if current:
chunks.append(current)
current = sentence
if current:
chunks.append(current)
return chunks

print("固定长度切分:")
for chunk in fixed_chunks(text):
print(" ", chunk)
print("边界感知切分:")
for chunk in sentence_chunks(text):
print(" ", chunk)

运行输出:

固定长度切分:
向量数据库可以高效检索相似内
似内容。但检索质量高度
依赖切分方式。如果按固
定长度截断,一句话可能
被拦腰斩断。边界感知策
略会优先在句号、换行处
切分。这样每个片段都保持语
义完整。
边界感知切分:
向量数据库可以高效检索相似内容。
但检索质量高度依赖切分方式。
如果按固定长度截断,
一句话可能被拦腰斩断。
边界感知策略会优先在句号、换行处切分。
这样每个片段都保持语义完整。

可以看到,固定长度在第 12 个字符处截断,第一句末尾“内容”被拆成“相似内”和“似内容”,两个片段都出现残缺语义;边界感知版本则把六个完整句子各自独立成片,检索时任何一个片段都能完整回答相关子问题。句子级边界是最基础的策略,但它没有利用文档结构,接下来补上标题层级信息。

工程化改进

纯文本之外,真实文档通常带有标题、列表和代码块。工程上常用的改进有三类。第一,结构化切分:按 Markdown 标题把文档切为带层级的块,并将所属标题写入片段头部,保证片段脱离原文仍可定位语境。第二,长度治理:为片段设置最小长度,把过短的相邻块合并,避免产生大量碎片稀释相关度。第三,重叠窗口:在相邻片段之间保留 10%–20% 的重叠内容,防止关键信息恰好落在边界两侧。下面实现一个标题感知切分器,演示如何把标题上下文注入片段。

第二次代码实验及输出

from dataclasses import dataclass

@dataclass
class Chunk:
heading: str
body: str

def split_by_headings(markdown):
lines = markdown.splitlines()
chunks = []
current_heading = ""
current_body = []
for line in lines:
if line.startswith("#"):
if current_body:
chunks.append(Chunk(current_heading, "\\n".join(current_body)))
current_heading = line
current_body = []
else:
current_body.append(line)
if current_body:
chunks.append(Chunk(current_heading, "\\n".join(current_body)))
return chunks

doc = """# 检索增强生成
RAG 将检索与生成结合。

## 文档切分
切分质量决定召回上限。

### 边界策略
在标题、段落、句子边界切分。
"""

for chunk in split_by_headings(doc):
print("标题上下文:", chunk.heading)
print("内容:", chunk.body)
print("—")

运行输出:

标题上下文: # 检索增强生成
内容: RAG 将检索与生成结合。


标题上下文: ## 文档切分
内容: 切分质量决定召回上限。


标题上下文: ### 边界策略
内容: 在标题、段落、句子边界切分。

每个片段都携带所属标题前缀,即使正文只有一行,索引后也能通过标题还原语境。生产环境可进一步把多级标题拼接成完整路径,例如“检索增强生成 > 文档切分”,并把路径作为独立字段写入向量库的元数据,用于后续过滤与重排。

常见陷阱

  • 只按字符数切分:中英文、代码、数字混排时,字符数与 token 数偏差很大,应基于模型的 tokenizer 计算实际长度。
  • 标题与正文分离存储:片段丢失标题上下文后,检索阶段无法判断内容归属,召回的相关性下降。
  • 过度切分:粒度太细会产生大量低信息量片段,噪声稀释真实相关结果,应设置最小长度并做合并。
  • 忽略重叠:恰好落在边界两侧的关键句会被切断,通常保留 10%–20% 的重叠可缓解。
  • 硬编码长度上限:不同 embedding 模型最大输入长度不同,切分上限应作为配置项动态调整。
  • 落地清单

    • 明确所用 embedding 模型的最大 token 限制,并以此设定片段长度上限。
    • 按标题、段落、句子优先级实现边界规则,并为片段注入标题或路径元数据。
    • 设置最小片段长度与相邻重叠比例,避免碎片化与边界丢失。
    • 用真实业务查询做召回抽样评估,对比固定长度、句子级、结构化三种切分的效果。
    • 将切分逻辑封装为独立模块,便于在语料变化时单独调参。

    参考来源

    • Python 官方文档:re 模块,https://docs.python.org/zh-cn/3/library/re.html
    • Python 官方文档:dataclasses 模块,https://docs.python.org/zh-cn/3/library/dataclasses.html
    • LangChain 官方文档:Text Splitters 概念,https://python.langchain.com/docs/concepts/text_splitters/

    👍 觉得有用就点个 赞 + 收藏,方便回头查阅;有疑问直接在评论区留言,我看到都会回。

    🚀 本文属于 《可靠LLM应用工程》 系列,持续更新,关注不迷路。

    📌 文章里的代码都能直接跑。想要可直接 clone 的完整工程 + 配套部署脚本 / 踩坑清单?评论一声或发邮件到 cj2664@qq.com,我免费发你。
    如果你正好在做类似系统、或有工程化难题想找人做,也欢迎邮件聊一句——我按实际情况评估,能落地的就接单或出方案。评论和邮件都能直接找到我,不用跳别的平台。

    赞(0)
    未经允许不得转载:171主机测评 » 文档切分的边界策略
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址