DEBUG.WIKI
京东云;让 AI 进入真实业务;零售 · 物流 · 客服 · 硬件;查看方案 →
ZgoCloud:多地区部署,按需选节点;日本、美国、香港、德国
Evoxt;最高 6.0GHz CPU;高频云服务器 · 免费每周备份;查看配置 →

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 返回了固定维度的数组,无法证明长文已被完整处理。