Sentence Transformers 编码长文时,后半段为什么没起作用?
长文正常返回向量也可能已经截断。检查 max_seq_length、完整 token 数与实际编码长度;切分后核对尾部覆盖,不把调大上限当成长文能力。
文档前面加了一段很长的固定内容,后面的故障描述不同,得到的 embedding 却没有明显变化。先检查后半段是否真的进入编码器:Sentence Transformers 可以在分词时截断超长输入,encode 正常返回一个向量,并不等于整篇文字都被模型读取。
这里以 sentence-transformers 5.1.1 的普通文本编码、sentence-transformers/all-MiniLM-L6-v2 模型为例。该模型配置中的 max_seq_length 为 256,长度按分词后的 token 计算;不是 256 个汉字、字符或空格分隔的单词。其他模型和修订可能使用不同上限。
先看模型实际使用的长度
读取当前加载的对象,而不是只看模型名称:
from sentence_transformers import SentenceTransformer
model = SentenceTransformer("sentence-transformers/all-MiniLM-L6-v2")
print("encoding limit:", model.max_seq_length)
print("tokenizer limit:", model.tokenizer.model_max_length)
这段示例首次加载可能下载模型文件;离线环境应使用已经审核并保留的本地模型。固定生产行为时,还需记录模型修订和依赖版本。
两个属性不能混用。max_seq_length 是这条 Sentence Transformers 编码路径使用的最大输入长度。普通 Transformer 模块在分词时传入这个值,并开启 truncation。tokenizer.model_max_length 则属于底层 tokenizer 的配置,两者不一定相等,也不能单凭后者判断模型架构能承受多长的输入。
如果 tokenizer 配置允许更长的序列,但 max_seq_length 较小,encode 仍可能先按较小值截断。将一篇文章读进 Python 字符串,没有改变这一步的输入上限。
不截断地计数,再比较真正送入模型的 token
对于上面这个无额外提示前缀的模型,可以用同一个 tokenizer 观察一条普通文本。示例只分词,不运行长序列的模型推理:
text = "设备启动后出现连接错误。" * 200
prepared_text = text.strip()
full_ids = model.tokenizer(
prepared_text,
add_special_tokens=True,
truncation=False,
)["input_ids"]
features = model.tokenize([prepared_text])
encoded_length = int(features["attention_mask"][0].sum().item())
print("before truncation:", len(full_ids))
print("sent to encoder:", encoded_length)
print("limit:", model.max_seq_length)
特殊 token 也占长度,所以按字符切到 256,或切出 256 个不带特殊 token 的内容 token,都未必能完整进入 256 长度的模型输入。这里用 attention_mask 统计有效输入,避免把批量 padding 的位置算成正文。
若完整分词长度超过上限,而实际编码长度止于上限,就确认了这次输入被截断。若长度没有超限,应继续检查预处理、文本读取、重复数据和模型行为,不能把任何相似向量都归因于截断。不同文本也可能产生相近的语义表示。
这个计数写法针对所选模型的普通单文本编码。使用 encode_query、encode_document、显式 prompt 或文本对时,应把实际附加的提示与预处理也纳入核对,不能用裸文本计数冒充最终输入长度。
检索长文时,让每个片段都有自己的向量
如果目标是搜索整篇文档中的具体问题,可以先按章节或段落拆分,再用相同 tokenizer 检查每段是否超限。过长的段落继续拆分,保留文档 ID、片段顺序和原文位置,查询命中后才知道该返回哪一段。
下面的编码入口只接受已经切好的普通文本片段。它会拒绝仍然过长的片段,避免建库时静默丢失尾部;它不是自动切分器:
def encode_checked_chunks(chunks):
prepared = [chunk.strip() for chunk in chunks]
if not prepared or any(not chunk for chunk in prepared):
raise ValueError("expected nonempty text chunks")
for position, chunk in enumerate(prepared):
token_count = len(model.tokenizer(
chunk,
add_special_tokens=True,
truncation=False,
)["input_ids"])
if token_count > model.max_seq_length:
raise ValueError(f"chunk {position} exceeds encoding limit")
return model.encode(prepared)
vectors = encode_checked_chunks([
"设备启动时提示连接超时。",
"重启后,服务能够建立连接。",
])
实际切分还应给提示和特殊 token 留出空间。片段间保留多少重叠,应根据跨段信息与存储成本决定;重复内容会增加索引条数,也可能让同一文档占据多条检索结果。不要把所有片段向量直接平均,就认定已经恢复了长文中的每个细节。
重新切分后需要重新建立对应索引及片段映射。保留旧版本,核对代表性查询能否命中文档末尾,再切换读取入口。若使用 Chroma,还要检查模型是否一并更换,避免迁移时出现 向量维度不匹配。
把 max_seq_length 改大之前,先核对模型
max_seq_length 可以被修改,但调大它不会改变模型架构,也不会替模型补上长文本训练。某些架构会因位置编码限制报错,能运行的模型也可能在长输入上表现变差,显存和计算需求则随输入变化。
因此先确认所用模型支持的输入范围和任务适配,再决定提高上限或更换长文本模型。不能看到 tokenizer 的某个较大数值,就把它当成所有模型都可安全采用的设置。
验收应同时核对片段 token 长度、索引中的片段数量和原文覆盖范围,再用明确依赖文档末尾信息的查询检查结果。只看 encode 返回了固定维度的数组,无法证明长文已被完整处理。


