什么时候值得用 Embeddings
Embeddings 适合“找相似的”这类问题:用户搜一句话,你要从几万条文档里找出语义相关的几条;或者你要把一批工单自动归类、把重复问题合并。它不适合需要精确匹配编号、型号、报错码的场景——那些仍然要靠关键词或结构化字段。
- 语义搜索:查询和文档用词不同但意思相近时仍能命中
- RAG 检索:先把相关片段捞出来,再交给模型回答,减少幻觉
- 聚类与分类:把文本向量聚类做主题发现,或接一个简单分类器
- 去重:相似度超过阈值就认为是同一类问题
两个模型怎么选:small 还是 large
官方提供 text-embedding-3-small 和 text-embedding-3-large 两个尺寸。默认维度分别是 1536 和 3072;large 更贵、更大,通常在检索精度上更好,但差距在中文短文本、领域词汇密集的数据上未必明显。
| 对比项 | text-embedding-3-small | text-embedding-3-large |
|---|---|---|
| 默认维度 | 1536 | 3072 |
| 单价 | 低 | 约为 small 的数倍(以官方定价为准) |
| 检索精度 | 一般场景够用 | 更难的数据、更细的语义区分更稳 |
| 存储与内存 | 每条向量约 6 KB(float32) | 每条向量约 12 KB(float32) |
| 建议 | 先用它做基线,便宜且快 | 基线不够用时再换,或只对难样本换 |
选择顺序建议:先用 small 跑一版检索评测,记录 recall@k;如果错例集中在“语义确实相近但没检索到”,再换 large 对比。直接上 large 往往只是多花钱,因为你没有基线判断它到底提升了多少。
最小可用示例
调用方式很简单:把一批文本传进去,拿回同样数量的向量。注意 input 可以是字符串或字符串数组,一次传多条比循环单条快得多。
import OpenAI from "openai";
const client = new OpenAI();
export async function embedDocuments(chunks: string[]) {
const response = await client.embeddings.create({
model: "text-embedding-3-small",
input: chunks,
});
return response.data.map((item) => item.embedding);
}
const vectors = await embedDocuments([
"会员代充多久到账?",
"支付失败会退款吗?",
]);查询和文档必须用同一个模型、同一套预处理。换了模型就要整库重算,否则两边的向量不在同一个空间里,相似度没有意义。
维度截断:用更少的维度换成本
text-embedding-3 系列支持用 dimensions 参数把向量截短(large 可以从 3072 降到 1024、512 甚至更低)。截断不是随机丢弃,模型训练时就保证了前面维度的信息密度更高,所以精度下降通常比“按比例丢信息”要温和。
const response = await client.embeddings.create({
model: "text-embedding-3-large",
input: chunks,
dimensions: 1024,
});| 维度 | 存储(每条,float32) | 适用 |
|---|---|---|
| 3072 | 约 12 KB | 数据量不大、精度优先 |
| 1024 | 约 4 KB | 大多数检索场景的平衡点 |
| 512 及以下 | 约 2 KB | 百万级以上、或端侧/内存受限 |
检索质量:分块、归一化与混合检索
实际效果差,多半不是模型问题,而是分块和检索策略问题。文档切得太长,向量会被平均掉;切得太碎,又丢上下文。常见做法是按语义段落切,每块 200–500 字,带一点重叠。
- 分块:按标题和段落切,保留标题作为块的前缀,能显著提升召回
- 归一化:把向量做 L2 归一化后,余弦相似度等价于点积,方便用向量库的索引
- 混合检索:向量检索 + 关键词(BM25)各取前 N 条再融合,专有名词和编号类查询更稳
- 重排:召回 20 条后用交叉编码器或对话模型重排,取前 3–5 条交给生成模型
export function cosineSimilarity(a: number[], b: number[]) {
let dot = 0;
let normA = 0;
let normB = 0;
for (let i = 0; i < a.length; i += 1) {
dot += a[i] * b[i];
normA += a[i] * a[i];
normB += b[i] * b[i];
}
return dot / (Math.sqrt(normA) * Math.sqrt(normB));
}成本与工程细节
| 环节 | 注意点 |
|---|---|
| 首次全量向量化 | 几十万条以上优先走 Batch 接口,成本减半;注意 batch 输出顺序不保证,要用自定义 ID 对齐 |
| 增量更新 | 文档改动才重算,用内容哈希做去重,别每次全量重建 |
| 缓存 | 相同文本的向量可以直接落库复用,查询侧也可以缓存热门查询的向量 |
| 存储 | pgvector、sqlite-vec、向量数据库都可以;关键是同时存原文、元数据和模型版本 |
| 监控 | 记录模型名、维度、分块策略版本,换模型时才能定位是哪一批数据不一致 |
常见坑
- 查询和文档用了不同模型或不同 dimensions,相似度完全不可比。
- 把整篇长文塞进一条向量,关键信息被稀释,检索几乎命中不了。
- 只做向量检索,用户搜“订单号 12345”这种精确串时召回很差。
- 没有评测集,改完分块策略不知道是变好还是变坏。
- 把 embeddings 当成生成模型,指望它直接回答问题和处理多轮对话。
上线前至少准备 30–50 条真实查询作为评测集,记录 recall@5 和首条命中率。每次调整分块、维度或模型都跑一遍,这比任何“最佳实践”都可靠。
常见问题
中文用 Embeddings 效果好吗?
text-embedding-3 系列对中文支持不错,短文本和客服问答类场景通常够用。专有名词、缩写多的领域,建议配合关键词检索做混合召回。
向量必须存数据库吗?
数据量小的时候存 JSON 或 SQLite 也能用,几万条以内暴力计算余弦相似度完全够快。超过十万条再考虑 pgvector 或专门的向量库。
换模型后旧向量还能用吗?
不能混用。不同模型、不同 dimensions 的向量不在同一空间,必须整库重新生成。所以要在数据表里记录模型名和维度,方便迁移。
官方文档与参考
本文依据上述官方文档整理;价格、额度与功能会随官方更新变化,第三方中转站的行为以其自身说明为准。