登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  科技周边 >  人工智能

Embeddings 怎样分块长文本

来源:17golang原创

时间:2026-10-25 11:57:26 185浏览 收藏

长文档直接做 Embeddings,问题通常不在“向量不够多”,而在一个块里混入了多个主题,或者把关键定义截在了两个块之间。我的做法是先按标题和段落保留语义边界,再用 token 预算兜底,最后用少量重叠覆盖跨段关系。Embeddings API 的输入按 token 计费,当前文档列出的第三代模型最大输入为 8192 token,所以分块预算不能只看字符数。

官方地址:https://developers.openai.com/api/docs/guides/embeddings

要点速览
  • 先按标题、段落、句子切分,token 上限只负责兜底。
  • overlap 用来保留跨块语义,不是越大越好;可先从 10%~15% 的块长度试起。
  • 每个向量必须保存来源、位置、版本和分块参数,方便回溯与重建。
更稳的起点不是固定“每 500 字一块”,而是先保证一个块只回答一个相近问题,再在模型上限内调节长度。检索问答效果应由真实问题集决定,不能只凭块数或向量维度判断。

先把分块目标定成可检索的证据单元

一个好块应当能独立表达一个小主题:例如“退款条件”和“退款操作步骤”可以相邻,但不要把整章产品介绍、异常处理和价格说明塞进同一个向量。建议给块保留 doc_id、chunk_id、标题路径、字符区间、版本号和正文原文。这样召回后可以展示上下文,也能在文档更新时只重建变动部分。

长度要用 tokenizer 估算,而不是把中文字符数当成 token 数。以当前文档为例,8192 token 是模型输入的上限,不代表检索块应该贴着上限;还要给查询、元数据拼装和后续生成留出空间。工程上可以先把单块预算放在 400~900 token,再用评测集观察是否出现“命中标题、缺少定义”或“召回内容过宽”。

按语义边界切分,超长时再递归降级

切分顺序可以是标题路径 → 段落 → 句子 → 词或字符。先尝试把完整段落放入当前块;放不下时再拆句,单句仍超长才做硬切。下面的示例省略具体 tokenizer 实现,但把分块的关键边界写清楚,实际项目中应替换为与所选模型匹配的计数器。

def split_text(paragraphs, max_tokens=700, overlap_tokens=80):
    # paragraphs 已按标题路径整理;tokenize 应使用项目实际模型的 tokenizer
    chunks, current = [], []
    current_tokens = 0
    for paragraph in paragraphs:
        paragraph_tokens = tokenize(paragraph)
        if len(paragraph_tokens) > max_tokens:
            # 超长段落继续按句子拆分,避免直接截断定义
            sentences = split_sentences(paragraph)
        else:
            sentences = [paragraph]
        for sentence in sentences:
            sentence_tokens = tokenize(sentence)
            if current and current_tokens + len(sentence_tokens) > max_tokens:
                chunks.append("\n".join(current))
                # 只保留尾部少量句子,形成可控的语义重叠
                current = keep_tail_by_tokens(current, overlap_tokens)
                current_tokens = count_tokens(current)
            current.append(sentence)
            current_tokens += len(sentence_tokens)
    if current:
        chunks.append("\n".join(current))
    return [chunk for chunk in chunks if count_tokens(chunk) >= 40]

这里的 700 和 80 只是可调参数,不是通用答案。法律条款、接口参数表和故障排查步骤的最佳边界往往不同;如果一个块经常被召回却缺少前置定义,应优先调整语义切分,而不是盲目增加 overlap。

Embeddings 长文本分块的标题路径、段落、句子和 token 预算边界说明图
图1:Embeddings 分块结构说明图,展示语义层级与 token 预算之间的静态关系,不是运行截图。

overlap 只补跨块关系,别把重复文本扩大成噪声

重叠适合处理“上一段定义、下一段条件”这类跨边界信息。可以从目标块长度的 10%~15% 开始,观察相邻块是否重复太多、向量库体积是否明显上涨。短文档不需要强行重叠;表格、代码和编号清单也不宜按普通句子切开,最好把表头或前置说明随块保存。

分块完成后先做三项清理:去掉空白和纯导航文本;合并过短且主题相同的尾块;对同一版本的重复内容做指纹去重。每个块写入向量库时,同时保存 source_start、source_end 和 chunker_version,否则以后很难解释“为什么这次命中了另一段”。

批量生成向量,再用检索反馈调参数

解释文本块、批量 Embeddings 返回、向量库元数据和检索上下文的静态关系。
图2:向量化与检索上下文结构说明图,展示结果如何回到原文位置。

OpenAI 文档说明 Embeddings 返回的是浮点数向量,可保存到向量数据库用于检索;输入也可以按数组批量提交。批量处理时要保持输入块与返回结果的索引一一对应,并把模型名、分块版本和生成时间写进记录。

from openai import OpenAI

client = OpenAI()
chunks = [{"chunk_id": "doc-7-003", "text": "退款条件与例外说明"}]

# 一次提交同一批文本,按返回顺序绑定 chunk_id;生产环境还要记录模型与版本
response = client.embeddings.create(
    model="text-embedding-3-small",
    input=[item["text"].replace("\n", " ") for item in chunks],
)
records = []
for item, embedding in zip(chunks, response.data):
    # metadata 与向量一起保存,召回后才能回到原文位置
    records.append({"id": item["chunk_id"], "vector": embedding.embedding,
                    "metadata": {"chunker_version": "v1"}})

评测时不要只看向量数量。准备一组真实查询,记录正确块是否在前几名、定义是否与条件同时出现、回答是否需要跨三个以上块才能完成。若命中内容过碎,增大块长度或合并相邻主题;若命中内容总是混杂,缩短块并加强标题路径元数据。这样调参才有明确方向。

现象优先调整不要先做
命中标题但缺少定义语义边界、少量 overlap直接把块扩大到模型上限
每次召回都带大量重复降低 overlap、做指纹去重继续增加召回数量
旧文档内容被召回版本过滤与增量重建只改相似度阈值

相关问题

中文长文本一定要按字数切吗?

不一定。字数只能做粗略限制,最终应以目标模型 tokenizer 的 token 数为准,并优先保留标题、段落和句子边界。

overlap 越大,检索效果越好吗?

不是。过大的 overlap 会制造重复向量和重复上下文;从较小比例开始,用真实查询集验证更稳妥。

分块后还要保存原文吗?

要保存。向量只适合相似度检索,回答和审计仍需要原文、来源位置、版本及分块参数来回溯。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>