下午三点,老李端着保温杯在工位间溜达,路过小王时停住了。
“小王,你那个 RAG 系统不太行啊。”老李的语气带着“我就知道”的味道,“我昨天把产品手册丢进去了,问‘Postgres 连接池怎么配置’,你猜它回我什么?它说‘max_connections 参数在生产环境建议设置为’——然后就没了。话说到一半,断那儿了。”
小王把嘴里的咖啡咽下去:“老李,文档你是怎么切的?”
“切?就按你说的,把文档拆成一块一块的嘛。我写了段代码,每 512 个字符切一刀,整整齐齐。”老李语气里满是自信,“我们以前处理日志就是这么干的,固定大小,简单可靠。”
“老李你想啊——”
“我想什么想?”
“你烤羊肉串的时候,是按厘米切的吗?”
老李举着保温杯的手悬在半空,表情像被人问住了。
“烤串你得顺着肉的纹理切,肥瘦相间,一块太厚不入味,一块太薄烤糊。切文档也是这个道理——你按 512 字符硬切,正好把‘建议设置为 200’和下一句‘同时配合 pgBouncer 使用’切成了两块。RAG 检索到了上半块,没检索到下半块,大模型只能照着半句话回答。”
老李若有所思:“那你的意思是,我这刀法不对?”
第一刀:固定大小分块——简单但粗暴
“固定大小切块不是不能用,”小王把椅子滑到老李旁边,“但它是所有策略里最粗暴的一种。我看看你切的——”
小王打开老李的代码,几行简洁的 Python 躺在那里:
# 老李的刀法:一刀切,512 字符一块
def old_li_chunker(text, chunk_size=512):
chunks = []
for i in range(0, len(text), chunk_size):
chunks.append(text[i:i + chunk_size])
return chunks
doc = "Postgres 连接池在生产环境建议设置 max_connections 为 200," \
"同时务必配合 pgBouncer 使用以避免连接数暴涨。"
chunks = old_li_chunker(doc, chunk_size=30) # 故意用小尺寸演示问题
for i, c in enumerate(chunks):
print(f"块{i}: {c}")
# 输出:
# 块0: Postgres 连接池在生产环境建议设置 max_connect
# 块1: ions 为 200,同时务必配合 pgBouncer 使用以避免连
# 块2: 接数暴涨。老李看着输出,脸有点黑:“第一块把 max_connections 给腰斩了,max_connect 这谁看得懂?”
“对。一句话被拦腰截断,向量化之后,max_connect 跟 max_connections 的语义相似度差了一大截。用户搜‘连接池配置’,这块大概率匹配不上。”
“那你说怎么切?”
“看肉的纹理。”小王在屏幕上画了几个框图。

图:同样的文档,固定大小切出断句残词,语义切分保持话题完整
第二刀:语义分块——顺着话题的纹理
“好的切分,要让每一个块都是一个相对完整的‘话题单元’。”小王打开一个新文件,“最简单的语义切分就是按段落来。写文档的人不是乱写的,一个自然段通常讲一个具体的小问题。”
# 小王的第一版改进:按自然段落切
def paragraph_chunker(text):
# 按双换行分段,保护每个段落的完整性
paragraphs = text.split("\n\n")
return [p.strip() for p in paragraphs if p.strip()]
# 但长段落还是太长了——需要第二刀
def semantic_chunker(text, max_chars=1000):
"""按段落切,超长的再按句号分"""
paragraphs = text.split("\n\n")
chunks = []
for para in paragraphs:
if len(para) <= max_chars:
chunks.append(para.strip())
else:
# 长段落按句子进一步切
sentences = para.replace("。", "。||").split("||")
current = ""
for sent in sentences:
if len(current) + len(sent) <= max_chars:
current += sent
else:
chunks.append(current.strip())
current = sent
if current:
chunks.append(current.strip())
return chunks“这个思路好理解,”老李点点头,“但小王你想过没有,有些文档根本没有段落结构——聊天记录、会议纪要、客服对话,全是一行一行的,你怎么找‘话题边界’?”
“问到点了。这时候就要上更聪明的切分方式。”
第三刀:递归分块——像剥洋葱一样
“LangChain 里有一个 RecursiveCharacterTextSplitter,它的逻辑特别像剥洋葱。”
老李又皱眉了:“怎么你们年轻人都爱用厨房打比方?”
“因为它确实像。你拿到一个洋葱,先试着按整个切,太大了;那就按层剥,还是大;按瓣剥,刚刚好。递归分块器的逻辑就是:先用双换行符切,如果块还是太大,换单换行符切,再大换句号,再大换空格,实在不行才按字符硬切。”
小王调出了 LangChain 的实现:
from langchain.text_splitter import RecursiveCharacterTextSplitter
# 这哥们儿自带"剥洋葱"逻辑
splitter = RecursiveCharacterTextSplitter(
chunk_size=500, # 目标块大小(字符数)
chunk_overlap=50, # 块与块之间重叠 50 字符
separators=["\n\n", "\n", "。", " ", ""] # 洋葱的层
)
doc = "## 连接池配置\n\nmax_connections = 200\n建议配合 pgBouncer。\n\n## 注意事项\n..."
chunks = splitter.split_text(doc)
for i, c in enumerate(chunks):
print(f"--- 块 {i} (长度 {len(c)}) ---\n{c}\n")
# 输出:每块在 500 字符内,且在句子/段落边界处断开老李看着代码,保温杯在手里慢慢转:“这个 separators 列表有意思,优先级从高到低,尽量在语义边界切。”
“对!而且你注意到那个 chunk_overlap=50 了吗?这就是烤串里的‘肥瘦相间’。”
为什么需要重叠?——像串肉的签子
“重叠又是啥讲究?这不就重复了吗?”老李指着那个参数。
“老李,你想想烤串为什么肥瘦要相间?纯瘦的太柴,纯肥的太腻。一块纯瘦的羊肉旁边配一点肥的,串在一起才好吃。但更重要的是——你得有一根签子把相邻的肉连在一起。”
小王在玻璃上画了一排方块:
“假设一句话是‘max_connections 建议设为 200,同时配合 pgBouncer’。这句话的意思,分布在两个相邻的块里。如果两块完全不重叠,RAG 检索时只命中了一块,大模型看到的是残缺的上下文。但如果两块之间重叠了半句话,RAG 无论命中哪一块,大模型都能看到完整的因果逻辑。”
# 重叠的作用:避免语义在边界处丢失
chunk_a = "在生产环境中,max_connections 建议设置为 200,"
chunk_b = "max_connections 建议设置为 200,同时务必配合 pgBouncer 使用。"
# ^^^^^^^^^^^^^^^^^^^^^^^^ 重叠部分保证了语义完整
# 检索"连接池怎么配",可能命中 chunk_b
# 如果 chunk_b 只是 "同时务必配合 pgBouncer 使用",大模型会一头雾水“所以重叠不是浪费,是给语义边界的断点上保险?”老李问。
“精准。通常 overlap 设为 chunk_size 的 10%~20%。太小了不管用,太大了浪费存储。”
分块策略的选择框架
“那你说说,我到底该用多大的块?什么时候用哪种策略?”老李掏出了他的小本本,这个动作说明他认真了。
“三个问题就能决定。”小王伸出手指。
“第一,文档是什么类型?技术文档段落整齐,语义分块就很好。聊天记录没有段落结构,递归分块更稳。代码文件?那得用 AST 分块,按函数/类来切。”
“第二,你的嵌入模型能处理多长的文本?text-embedding-3-small 上限是 8192 token,但实际最优效果在 512 token 左右。超出最优长度,语义信号会被稀释,就像一锅汤里放了太多水。”
“第三,下游任务需要多大的上下文?如果是 FAQ 式的短问答,小块就够了。如果是‘总结这篇文档的核心观点’,块太小了就看不到全貌。”
老李在本子上刷刷写着,嘴里念叨:“文档类型、模型上限、任务需求——我记下了。”
“有个经验值:短问答用 256~512 token,技术文档用 512~1024 token,需要完整推理的长文档用 1024~2048 token。但不要超过嵌入模型的最优长度,否则效果反而下降。”

图:分块策略选择流程——从文档特征出发,选择最合适的刀法
老李合上小本本,沉默了一会儿。
“所以我不是 RAG 不好用,是我切肉的方式不对。”
“可以这么说。”小王没客气。
“那我把产品手册重新切一遍,你给我看看那个递归分块的参数。”老李站起来,走了两步又回头,“话说回来,烤羊肉串这个比喻你哪学的?你平时加班都吃这个?”
“楼下那个新疆馆子,老板每次都给我多撒一把孜然。老李你要不要下次一起去?顺便聊聊 token 化的部分——那又是另一门切肉的手艺了。”
老李摆摆手,保温杯里的枸杞晃了晃:“下不为例啊。一个分块搞出这么多花样,你们年轻人就是爱折腾。”
但他走到工位门口时,又探头回来:“那什么,你说的 token 化,是跟字数还不一样对吧?你再给我讲讲这个……”
小王笑着拉开椅子,下午的日光正好打在键盘上。
夜雨聆风