Semantic Chunk 为什么需要 Embedding API

固定长度分块不需要任何外部服务,语义分块却必须调用 Embedding API------这背后的原因是什么?

先说结论

Semantic chunking 的核心思想是在语义边界处切分文本。判断"两段文字是否属于同一个话题"需要将文本转换为向量后计算相似度------这就是 Embedding API 不可或缺的原因。

传统分块 vs 语义分块

维度 固定长度 / 递归分块 语义分块
切分依据 字符数、token 数、分隔符 相邻句子的语义相似度
是否需要 Embedding ❌ 不需要 ✅ 必须
切分质量 可能在话题中间断开 在话题转换处切分,保持语义完整

固定长度分块就像用尺子量纸------不管内容写了什么,到了 500 字就剪一刀。语义分块则像一个读者,读完一段后判断"下一段是不是在说同一件事",如果不是,就在这里切开。

两种主流语义分块策略

策略一:相邻相似度法(Kamradt 方法)

核心思路:计算相邻句子之间的语义距离,在距离突变处切分。

复制代码
流程:
1. 将文本切成小句子
2. 为每个句子拼接前后 buffer_size 个句子作为上下文
3. 调用 Embedding API 获取每个组合句子的向量
4. 计算相邻句子向量的余弦距离
5. 通过二分搜索找到阈值,在距离超过阈值的位置切分

伪代码:

python 复制代码
# Step 1: 拼接上下文窗口
for i, sentence in enumerate(sentences):
    combined = ""
    for j in range(max(0, i - buffer_size), i):
        combined += sentences[j] + " "
    combined += sentence
    for j in range(i + 1, min(n, i + 1 + buffer_size)):
        combined += " " + sentences[j]
    combined_texts.append(combined)

# Step 2: 获取所有组合句子的 embedding(一次批量调用)
embeddings = embedding_client.embed_texts(combined_texts)
embedding_matrix = np.array(embeddings)

# Step 3: 只计算相邻句子间的余弦距离
distances = []
for i in range(len(sentences) - 1):
    similarity = dot(embedding_matrix[i], embedding_matrix[i + 1])
    distances.append(1 - similarity)  # 距离越大 = 话题差异越大

# Step 4: 二分搜索找阈值,使切分数量接近 total_size / avg_chunk_size
threshold = binary_search_threshold(distances, target_cuts)

# Step 5: 在距离超过阈值的位置切分
breakpoints = [i for i, d in enumerate(distances) if d > threshold]

直觉理解:想象你在读一篇文章,每读完一句就问自己"这句和下一句是不是在说同一件事?"。如果突然觉得话题跳了,就在这里切一刀。

关键特征:只看相邻关系。 它只计算 sentence[i] 和 sentence[i+1] 之间的距离,是一种局部贪心策略。

策略二:聚类最优分割法(动态规划方法)

核心思路:构建所有句子对之间的相似度矩阵,用动态规划找到使簇内相似度总和最大的最优分割。

复制代码
流程:
1. 将文本切成小句子
2. 调用 Embedding API 获取所有句子的向量
3. 构建 N×N 相似度矩阵
4. 对矩阵做均值归一化(防止退化为单一大簇)
5. 用动态规划找到最优分割方案

伪代码:

python 复制代码
# Step 1: 获取所有句子的 embedding(注意:没有 buffer 拼接)
embeddings = embedding_client.embed_texts(sentences)
embedding_matrix = np.array(embeddings)

# Step 2: 构建 N×N 相似度矩阵
similarity_matrix = dot(embedding_matrix, embedding_matrix.T)

# Step 3: 均值归一化,防止 DP 退化为"全部放一个簇"
mean_sim = mean(upper_triangle(similarity_matrix))
similarity_matrix -= mean_sim
fill_diagonal(similarity_matrix, 0)

# Step 4: 动态规划寻找最优切分
# dp[i] = 前 i+1 个句子的最大簇内相似度总和
for i in range(n):
    for size in range(1, i + 2):
        start = i - size + 1
        if cluster_size(start, i) > max_chunk_size and size > 1:
            break
        reward = sum(similarity_matrix[start:i+1, start:i+1])
        if start > 0:
            reward += dp[start - 1]
        dp[i] = max(dp[i], reward)

# Step 5: 回溯得到最优分割
clusters = backtrack(segmentation)

关键特征:全局最优。 它考虑所有句子对之间的关系,通过 DP 找到整体最优的分割方案。

两种策略的深度对比

算法本质差异

维度 Kamradt(相邻相似度) Cluster(动态规划)
视野 局部------只看相邻句子 全局------看所有句子对
决策方式 贪心:距离超阈值就切 最优化:最大化簇内总相似度
阈值确定 二分搜索目标切分数 无需阈值,DP 自动决定
上下文增强 ✅ 有 buffer_size 拼接 ❌ 直接用原始句子
大小约束 avg_chunk_size + max_chunk_size 双重约束 max_chunk_size 硬约束

核心区别用一句话概括:

  • Kamradt 问的是:"这两个相邻句子之间是否存在话题跳转?"
  • Cluster 问的是:"哪种分组方式能让每组内部的句子最相似?"

一个直观的例子

假设有 6 个句子,话题分布如下:

复制代码
句子1: 讨论苹果公司的财报
句子2: 讨论苹果公司的新产品
句子3: 讨论天气预报
句子4: 讨论明天的气温
句子5: 讨论苹果公司的股价
句子6: 讨论苹果公司的竞争对手

Kamradt 的切法: 逐对比较相邻距离 - 句子2→3:话题跳转(苹果→天气),切! - 句子4→5:话题跳转(天气→苹果),切! - 结果:[1,2] [3,4] [5,6]

Cluster 的切法: 全局相似度矩阵显示 1,2,5,6 彼此高度相似 - 但由于 DP 要求连续分割(不能跳着分组),它仍然只能切连续片段 - 结果可能也是 [1,2] [3,4] [5,6],但决策依据不同

关键差异出现在边界模糊的情况:

考虑一篇从"电动车技术"渐变到"能源政策"的文章:

复制代码
句子1: 特斯拉发布了新一代电池技术
句子2: 新电池的能量密度提升了 50%
句子3: 更高的能量密度意味着更长的续航里程
句子4: 续航焦虑一直是消费者购买电动车的障碍
句子5: 政府为缓解这一问题推出了充电桩补贴政策
句子6: 补贴政策同时覆盖了家用和商用充电设施
句子7: 商用充电设施的电价采用峰谷分时定价
句子8: 分时电价机制是电力市场化改革的重要组成部分

Kamradt 看到的(相邻距离):

复制代码
1→2: 0.08  (都在说电池)
2→3: 0.10  (电池→续航,很近)
3→4: 0.12  (续航→续航焦虑,很近)
4→5: 0.15  (消费者→政府政策,稍远但不突出)
5→6: 0.09  (都在说补贴)
6→7: 0.13  (补贴→电价,有点远)
7→8: 0.11  (都在说电价)

没有任何一个距离明显"跳起来"------话题是一步步滑过去的。Kamradt 的二分搜索很难找到一个合理的阈值,可能切出 [1-4][5-8] 或 [1-3][4-6][7-8] 这样不太理想的结果。

Cluster 看到的(全局相似度矩阵摘要):

复制代码
        句1   句2   句3   句4   句5   句6   句7   句8
句1     --   0.9   0.7   0.4   0.2   0.1   0.1   0.05
句2          --    0.8   0.5   0.2   0.15  0.1   0.05
句3                --    0.6   0.3   0.2   0.15  0.1
句4                      --    0.5   0.4   0.3   0.2
句5                            --    0.8   0.6   0.4
句6                                  --    0.7   0.5
句7                                        --    0.8
句8                                              --

全局视角清晰地显示:句子 1-3 彼此高度相似(电池/续航技术),句子 5-8 彼此高度相似(政策/电价),句子 4 是过渡句。DP 优化会发现 [1-3][4-8] 或 [1-4][5-8] 的簇内总相似度最大,从而做出更合理的切分。

本质区别: Kamradt 只看"相邻两句之间的落差",渐变过渡中每一步落差都很小,就像温水煮青蛙;Cluster 看"整组句子之间的整体相似度",即使过渡平滑,它也能发现句子 1 和句子 8 之间其实已经毫无关系了。

Embedding 开销对比

这是两种策略最重要的实际差异之一:

维度 Kamradt Cluster
Embedding 输入 combined_sentence(含 buffer 上下文) 原始句子(无 buffer)
Embedding 调用次数 N 个文本,1 次批量调用 N 个文本,1 次批量调用
每个文本的平均长度 较长(~7 句,buffer_size=3) 较短(1 句)
总 token 消耗 较高(buffer 导致输入膨胀) 较低(无冗余)
后续计算开销 O(N)------只算相邻距离 O(N²)------构建完整相似度矩阵
DP 计算开销 O(N × max_cluster_size)
具体数字对比(假设 1000 个句子,平均每句 30 tokens)

Kamradt: - Embedding 输入:1000 个 combined_sentence,每个约 7×30 = 210 tokens - 总 token 消耗:1000 × 210 = 210,000 tokens - 距离计算:999 次点积 → 可忽略 - 内存:1000 × embedding_dim 的矩阵

Cluster: - Embedding 输入:1000 个原始句子,每个约 30 tokens - 总 token 消耗:1000 × 30 = 30,000 tokens - 相似度矩阵:1000 × 1000 = 100 万个浮点数(约 8MB) - DP 计算:O(1000 × max_cluster_size) 次循环

结论: - Embedding API 费用 :Kamradt 消耗约 7 倍 token(因为 buffer 拼接),API 成本更高 - 计算资源 :Cluster 的 O(N²) 矩阵和 DP 在本地 CPU/内存上开销更大 - 网络延迟:两者相同(都是 1 次批量调用,或按 batch_size 分多次)

大规模场景(10 万句子)
指标 Kamradt Cluster
Embedding token 总量 ~2100 万 tokens ~300 万 tokens
API 调用次数(batch_size=500) 200 次 200 次
相似度计算 99,999 次点积 100 亿次点积(N²矩阵)
内存占用 ~400MB(embedding 矩阵) ~40GB(N²相似度矩阵)⚠️

10 万句子时 Cluster 策略的 N² 矩阵会爆内存,这是它的硬伤。实际使用中,Cluster 策略更适合中等长度文档(几百到几千句子),而 Kamradt 可以处理任意长度。

切分质量对比

场景 Kamradt 表现 Cluster 表现
话题边界清晰 ✅ 优秀,距离突变明显 ✅ 优秀
话题渐变过渡 ⚠️ 可能找不到切点 ✅ 全局优化仍能找到最佳分割
短文档(<50 句) ✅ 快速 ✅ 质量更高
长文档(>1 万句) ✅ 线性扩展 ❌ 内存爆炸
句子很短 ⚠️ 需要 buffer 补充上下文 ⚠️ 短句 embedding 质量差

如何选择?

你的场景 推荐策略 原因
文档长度不确定,需要通用方案 Kamradt 线性复杂度,不会爆内存
文档较短(<2000 句),追求最优切分 Cluster 全局最优,质量更高
Embedding API 按 token 计费 Cluster 无 buffer 膨胀,token 消耗低 7 倍
本地计算资源有限 Kamradt O(N) 计算,内存友好
话题边界模糊,需要精确切分 Cluster DP 全局优化更鲁棒

为什么不能用其他方法替代 Embedding?

替代方案 问题
关键词重叠 / TF-IDF 无法捕捉同义词和上下文语义("汽车"和"车辆"会被认为不相关)
规则分隔符(段落、句号) 同一段落可能包含多个话题,不同段落可能讨论同一话题
LLM 直接判断 成本过高,延迟大,不适合批量处理数万句子

Embedding 将文本映射到高维语义空间,语义相近的文本向量距离小,语义不同的文本向量距离大。这是目前在成本、速度、质量之间最优的语义相似度度量方式。

buffer_size:上下文窗口的作用

语义分块中有一个关键参数 buffer_size(默认值 3),它决定了为每个句子生成 embedding 时拼接多少上下文。

python 复制代码
# 拼接逻辑示意
for each sentence[i]:
    combined = sentence[i-3] + sentence[i-2] + sentence[i-1]  # 前 3 句
              + sentence[i]                                     # 当前句
              + sentence[i+1] + sentence[i+2] + sentence[i+3]  # 后 3 句

关键点:buffer_size 不影响 Embedding 调用次数,只影响每次输入的文本长度。

以 10 个句子为例,无论 buffer_size 是 1 还是 10,都是对 10 个 combined_sentence 做 embedding。区别在于每个文本包含的上下文多少:

buffer_size 每个文本平均包含句子数 效果
1 ~3 句 上下文少,可能误判
3(默认) ~7 句 平衡点
10 ~21 句 上下文丰富,但可能超出模型 token 限制

注意:Embedding 模型有输入长度上限(如 BGE-M3 最大 8192 tokens)。buffer_size 太大会导致文本被截断,反而丢失当前句子的信息。

大规模场景下的性能考量

假设一篇长文档被切成 10 万个句子:

  • 需要 embed 的文本数 = 100,000 个
  • 如果 batch_size 配置为 500,实际 API 调用次数 = 100,000 ÷ 500 = 200 次 HTTP 请求

性能瓶颈在 API 调用次数(由句子总数和 batch_size 决定),与 buffer_size 无关。

降级策略:Embedding 不可用时怎么办?

好的系统设计应该考虑 Embedding 服务不可用的情况。常见做法是:当 Embedding 调用失败时,自动降级为递归分块策略(纯规则分块,不需要 Embedding)。

这意味着语义分块是一种增强 而非依赖------没有 Embedding 服务时系统仍然可以工作,只是切分质量会下降。

总结

问题 答案
为什么需要 Embedding? 判断语义相似度需要向量表示
能否用规则替代? 不能,规则无法捕捉语义
能否用 LLM 替代? 理论上可以,但成本和延迟不可接受
Kamradt vs Cluster 核心区别? 局部相邻比较 vs 全局最优分割
哪个 Embedding 开销更大? Kamradt token 消耗高(buffer 膨胀),Cluster 计算开销高(N²矩阵)
大规模文档选哪个? Kamradt------线性复杂度,不会爆内存
追求最优切分选哪个? Cluster------全局 DP 优化,但限中等长度文档
服务不可用怎么办? 两者都降级为规则分块

Embedding API 是语义分块的"眼睛"------没有它,分块算法就是一个盲人在切蛋糕。两种策略用不同的方式"看"文本:Kamradt 像逐行扫描的阅读器,Cluster 像俯瞰全文的编辑。选择哪种,取决于你的文档规模和对切分质量的要求。

相关推荐
承渊政道4 小时前
【动态规划算法】(完全背包问题从状态定义到空间优化)
数据结构·c++·学习·算法·leetcode·动态规划·哈希算法
阿Y加油吧5 小时前
二刷 LeetCode:动态规划经典双题复盘
算法·leetcode·动态规划
莫等闲-6 小时前
代码随想录一刷记录Day44——leetcode1143.最长公共子序列 53. 最大子序和
数据结构·c++·算法·leetcode·动态规划
承渊政道6 小时前
【动态规划算法】(背包问题经典模型与解题套路)
数据结构·c++·学习·算法·leetcode·动态规划·哈希算法
庞轩px7 小时前
大模型为什么会有“幻觉”——从训练方式到推理局限
人工智能·prompt·rag·大模型幻觉·engineering·训练方式
new【一个】对象8 小时前
RAG详解
python·llm·agent·rag
Joseph Cooper1 天前
RAG 与 AI Agent:智能体真的需要检索增强生成吗?
数据库·人工智能·ai·agent·rag·上下文工程
xvhao20131 天前
单源、多源最短路
数据结构·c++·算法·深度优先·动态规划·图论·图搜索算法