文章编号: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 将检索与生成结合。
—
标题上下文: ## 文档切分
内容: 切分质量决定召回上限。
—
标题上下文: ### 边界策略
内容: 在标题、段落、句子边界切分。
—
每个片段都携带所属标题前缀,即使正文只有一行,索引后也能通过标题还原语境。生产环境可进一步把多级标题拼接成完整路径,例如“检索增强生成 > 文档切分”,并把路径作为独立字段写入向量库的元数据,用于后续过滤与重排。
常见陷阱
落地清单
- 明确所用 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,我免费发你。
如果你正好在做类似系统、或有工程化难题想找人做,也欢迎邮件聊一句——我按实际情况评估,能落地的就接单或出方案。评论和邮件都能直接找到我,不用跳别的平台。


