示例代码:
def chunk_by_sentences(input_text: str, tokenizer: callable):
"""
Split the input text into sentences using the tokenizer
:param input_text: The text snippet to split into sentences
:param tokenizer: The tokenizer to use
:return: A tuple containing the list of text chunks and their corresponding token spans
"""
inputs = tokenizer(input_text, return_tensors='pt', return_offsets_mapping=True)
punctuation_mark_id = tokenizer.convert_tokens_to_ids('.')
sep_id = tokenizer.convert_tokens_to_ids('[SEP]')
token_offsets = inputs['offset_mapping'][0]
token_ids = inputs['input_ids'][0]
chunk_positions = [
(i, int(start + 1))
for i, (token_id, (start, end)) in enumerate(zip(token_ids, token_offsets))
if token_id == punctuation_mark_id
and (
token_offsets[i + 1][0] - token_offsets[i][1] > 0
or token_ids[i + 1] == sep_id
)
]
chunks = [
input_text[x[1] : y[1]]
for x, y in zip([(1, 0)] + chunk_positions[:-1], chunk_positions)
]
span_annotations = [
(x[0], y[0]) for (x, y) in zip([(1, 0)] + chunk_positions[:-1], chunk_positions)
]
return chunks, span_annotations
首先我们要明白为什么这段代码要这么处理句子,背后的设计动机和实际应用场景是什么。
🎯 总目标:做“句子级”chunk(分块)
这段函数 chunk_by_sentences
的核心目标是:
把长文本切分成一句一句的小段(chunk),每一段尽量是一个完整的句子,而不是强行按字数截断。
❓为什么要按“句号”来切分?
我们处理长文本(比如新闻、文档)时,经常要:
- 分块送进 embedding 模型(BERT、LLaMA)
- 做检索(RAG)、摘要、问答
- 限制最大 token 数量(模型处理能力有限)
如果直接按照长度硬切(比如100个字一截)会出现:
- 句子断裂:句子被切一半,语义丢失
- 对齐错乱:无法用 chunk 做回溯,原始句子找不到了
- embedding 不准确:被截断的句子语义被破坏,向量不好用
所以更好的做法是:
✅ “遇到句号再切”,让每个 chunk 是一整句,语义完整!
📘 中文例子:为什么必须精确切句?
示例文本:
今天北京下雪。气温零下五度。适合打雪仗。
我们希望分成:
- 今天北京下雪。
- 气温零下五度。
- 适合打雪仗。
如果我们只是按固定长度切,比如每15个字符一段:
- 第一个chunk可能是:
今天北京下雪。气温零
(句子被切开) - 第二个chunk:
下五度。适合打雪仗。
(不连贯)
这对以下任务都不好:
任务 | 为什么需要整句切 |
---|---|
Embedding | 每个句子要独立编码成向量,不能被切坏 |
RAG 检索 | 检索时要找出“哪句话最相关” |
可视化高亮 | 前端要高亮整句,而不是一半句 |
错误定位 | 如果模型输出有误,需要精确知道是哪个句子错了 |
🔍 那为什么还要用 tokenizer 的 offset_mapping 来切?
你可能会想:
直接用
str.split("。")
不就行了吗?为啥这么复杂?
这是因为:
❗️直接字符串切割的问题:
- 会丢失原始 token 的起止位置(比如第几个 token)
- 无法和 BERT 模型的 token 位置对应(embedding 用不上)
- 遇到标点、空格、英语等语言混合情况,会切不准
所以这段代码精细处理每个 token 的 offset,确保:
✅ chunk 是按完整句子切的
✅ chunk 的 token 索引位置也能对应(用在 embedding 或模型中)
✅ 多语言、标点复杂的情况都能处理好
✅ 总结:这段代码解决的问题
你要做的任务 | 遇到的痛点 | 这段代码怎么帮你解决 |
---|---|---|
拆分长文本成句子 | 字符切不准、token对不上 | 用 tokenizer 精准对齐 token 和字符 |
BERT/Embedding | 模型只能吃 512 个 token,需要提前分块 | 按句号切分,每句都是一小段 |
多语言混合(中英) | 标点不同、空格不同,直接切会错 | 用 offset_mapping 处理 token 对应位置 |
检索问答 / RAG | 需要句子粒度的上下文 | 每个 chunk 都是语义完整的句子 |
回溯文本原文 | 需要定位哪个句子被用了 | 提供了 span_annotations 标记每句对应的 token 起止范围 |