Appearance
Embedding 模型选型与批处理:别用对话模型做向量化
1. 本节产出
一个可切换的 Embedding 封装:国内主流 Embedding 模型可配置切换、支持批处理与失败重试、维度与向量库一致,并且有一份自己实测的选型对比表。
2. 前置依赖
- 02-04 切分策略:已有待向量化的片段
- 00-03 版本矩阵
3. 为什么 Embedding 不能用对话模型将就
有个项目为了省事,用对话模型的隐藏层输出当向量。三个后果:
| 问题 | 表现 |
|---|---|
| 语义相似度不靠谱 | 「如何请假」和「请假流程」的相似度低于「如何请假」和「如何报销」 |
| 维度过大 | 4096 维,存储和检索都慢 |
| 成本极高 | 对话模型调用费远高于专用 Embedding 模型 |
根因:对话模型是为「生成下一个 Token」训练的,它的表示空间关注的是「接下来该说什么」,而不是「这两句话意思是否相近」。专用 Embedding 模型是用对比学习训练的,目标就是让语义相近的文本在向量空间里靠近。
结论:Embedding 一定要用专用模型。 这不是优化建议,是必要条件。
4. 核心原理
4.1 选型要看的四件事
| 维度 | 关注点 | 影响 |
|---|---|---|
| 中文能力 | 是否在中文语料上训练过 | 决定相似度是否靠谱(最关键) |
| 维度 | 768 / 1024 / 1536 / 2048 | 存储成本、检索速度、精度 |
| 最大输入长度 | 通常 512~8192 Token | 超过会被截断,长片段要再切 |
| 单价与吞吐 | 元/百万 Token、单次批量上限 | 灌库成本与时间 |
维度不是越高越好。1536 维到 3072 维的精度提升通常很小,但存储翻倍、检索变慢。项目初期选 768~1536 即可。
4.2 国内主流选择对比(以实测为准)
| 模型 | 维度 | 中文能力 | 特点 |
|---|---|---|---|
| 通义 text-embedding-v3 | 1024 | 强 | 支持自定义维度,性价比高 |
| BGE-M3(开源可私有化) | 1024 | 强 | 支持多语言与稀疏向量,可本地部署 |
| BCE / bge-large-zh | 768~1024 | 强 | 中文语义检索表现好 |
| OpenAI text-embedding-3 | 1536 | 中 | 中文表现一般,需实测 |
提示:这张表只能作为起点。Embedding 选型必须用你自己的文档和评测集实测,因为不同领域的语义分布差异很大。通用榜单第一的模型在你的业务上未必最好。
4.3 批处理:灌库时间的分水岭
单条调用:10000 片段 × 0.3s = 50 分钟
批量 32: 10000 片段 ÷ 32 × 0.6s ≈ 3 分钟批处理能把灌库时间压缩一个数量级,因为网络往返是主要开销。但要注意:
| 注意点 | 说明 |
|---|---|
| 批量上限 | 各厂商不同(常见 16/25/32/64),超了报错 |
| 失败重试 | 整批失败要能拆分重试,不能整批丢弃 |
| 限流 | 批量不减少 Token 消耗,仍受配额限制 |
| 长文本 | 超过模型最大长度会被截断,先切再批 |
4.4 一个容易忽略的对称性要求
查询向量和文档向量必须用同一个模型、同一个版本生成。
正确:文档用 v3 向量化 → 查询也用 v3 向量化 → 可比
错误:文档用 v3 向量化 → 查询用 v2 向量化 → 完全不可比,检索全乱模型升级(v2 → v3)意味着全库重灌。这是一个真实的运维成本,做选型时要考虑进去。
5. 代码走查
5.1 配置
yaml
# application.yml
ai:
embedding:
provider: qwen # 可切换
model: text-embedding-v3
dimensions: 1024
batch-size: 32
max-input-tokens: 8000提示:
dimensions必须和向量库建表时的维度一致。不一致时通常在第一次查询时才报「维度不匹配」,而不是在启动时——所以灌库前先确认一次。
5.2 统一封装
java
// src/main/java/com/example/rag/config/EmbeddingConfig.java
@Configuration
public class EmbeddingConfig {
@Bean
public EmbeddingModel embeddingModel(EmbeddingProperties props) {
OpenAiApi api = OpenAiApi.builder()
.apiKey(props.apiKey())
.baseUrl(props.baseUrl()) // 国内厂商的兼容端点
.build();
return new OpenAiEmbeddingModel(
OpenAiEmbeddingOptions.builder()
.model(props.model())
.dimensions(props.dimensions()) // 部分厂商支持自定义维度
.build(),
api);
}
}5.3 批处理与失败重试
java
// src/main/java/com/example/rag/ingest/BatchEmbedder.java
@Service
public class BatchEmbedder {
private final EmbeddingModel model;
private final int batchSize;
/** 批量向量化;单批失败时拆半重试,避免整批丢失 */
public List<float[]> embedAll(List<String> texts) {
List<float[]> result = new ArrayList<>(texts.size());
for (int i = 0; i < texts.size(); i += batchSize) {
List<String> batch = texts.subList(i, Math.min(i + batchSize, texts.size()));
result.addAll(embedWithRetry(batch));
// 限速:避免触发厂商配额
if (i + batchSize < texts.size()) sleepQuietly(120);
}
return result;
}
private List<float[]> embedWithRetry(List<String> batch) {
try {
return model.embed(batch).stream()
.map(float[]::clone)
.toList();
} catch (Exception first) {
log.warn("批量向量化失败 size={},拆半重试", batch.size(), first);
if (batch.size() == 1) throw first; // 单条都失败,抛出去
int mid = batch.size() / 2;
List<float[]> left = embedWithRetry(batch.subList(0, mid));
List<float[]> right = embedWithRetry(batch.subList(mid, batch.size()));
return Stream.concat(left.stream(), right.stream()).toList();
}
}
}拆半重试这个设计很实用:批量调用时只要有一条文本有问题(比如含特殊字符、超长),整批都会失败。拆半重试能把问题文本隔离出来,而不是丢掉整批。
5.4 超长文本保护
java
private String truncate(String text, int maxTokens) {
int tokens = TokenEstimator.estimate(text);
if (tokens <= maxTokens) return text;
// 截断而不是让厂商静默截断——静默截断会导致向量代表不了全文
log.warn("文本超长,已截断:{} tokens → {}", tokens, maxTokens);
return text.substring(0, (int) (text.length() * (maxTokens / (double) tokens)));
}6. 跑起来
bash
git checkout ch02-05-embedding
mvn -q test -Dtest=EmbeddingSelectionTest选型实测(本节核心产出):用一组已知相似度的中文句子对,跑不同模型看相似度排序。
bash
# 输出示例
模型:qwen-v3 维度 1024
「如何申请年假」 vs 「年假申请流程是什么」 → 0.91 ✅ 应为高
「如何申请年假」 vs 「差旅报销标准是多少」 → 0.23 ✅ 应为低
「服务器宕机了」 vs 「服务不可用」 → 0.87 ✅ 应为高
「服务器宕机了」 vs 「服务器采购流程」 → 0.31 ✅ 应为低
模型:openai-3-small 维度 1536
「如何申请年假」 vs 「年假申请流程是什么」 → 0.78 ⚠️ 偏低
...| 检查项 | 通过标准 |
|---|---|
| 相似对得分 | > 0.8 |
| 不相似对得分 | < 0.4 |
| 区分度 | 相似对与不相似对分差 > 0.4 |
| 批处理 | 1000 条片段在 1 分钟内完成 |
| 失败重试 | 注入一条异常文本,验证拆半重试能隔离它 |
这张实测表要保存下来,它是你交付给客户的选型依据,比任何公开榜单都有说服力。
7. 生产避坑
- 查询和文档必须用同一个 Embedding 模型与版本。这是最容易在模型升级时翻车的地方:只升级了查询侧,忘记重灌文档库,结果检索质量断崖式下跌且不报错。做法:把模型名和版本写进每个片段的元数据,查询时校验一致性。
- 批量调用失败要有拆半重试。整批丢弃会导致灌库静默丢数据——文档看起来灌进去了,实际少了几十个片段,排查极其困难。
- 不要相信「维度越高越好」。实测中 1024 维和 3072 维在多数业务上差距很小,但存储和检索开销差 3 倍。先用中等维度跑评测集,确认有收益再考虑升维。
8. 延伸与锚点
- 思考题:文档库有 200 万片段,现在要换 Embedding 模型,全量重灌要多久、多少钱?有没有不停机的办法?(提示:双写 + 灰度切换——答案在 02-14 增量更新)
- 代码锚点:
git checkout ch02-05-embedding - 下一课时:02-06 向量库选型
- 对应课件:L02-05 Embedding 模型选型