小崔盯着屏幕:"Embedding 不就是把一句话变成一个浮点数数组吗,为什么能表示语义?"白歌说:"你不需要懂 Transformer 数学,你只需要知道:两段语义相似的文本,它们的向量在空间中角度接近。"
EmbeddingModel 与文本向量化
定义与作用
EmbeddingModel 是 Spring AI 的文本向量化接口——将一段自然语言文本转换为固定维度的浮点数数组(float[])。这个数组就是"语义指纹"。
public interface EmbeddingModel {
EmbeddingResponse call(EmbeddingRequest request);
default Embedding embed(String text) {
// 便捷方法:单文本向量化
}
List<Embedding> embed(List<String> texts); // 批量
float[] embed(String text, EmbeddingOptions options); // 带参数
}
核心价值:向量化是 AI 理解私有文档的前提。没有 Embedding,就没有语义搜索、没有 RAG。
核心原理:文本到向量的转换流程
图释:文本经过 Embedding 模型(通常调用 OpenAI API)转换为 1536 维向量。每个维度没有"可读"的物理含义,但整体向量在空间中编码了文本的语义。
不同模型的向量维度对比
| 模型 | 维度 | 最大输入 Token | 适用场景 |
|---|---|---|---|
| text-embedding-3-small | 1536 | 8191 | 通用,性价比高 |
| text-embedding-3-large | 3072 | 8191 | 高精度语义搜索 |
| text-embedding-ada-002 | 1536 | 8191 | 旧版本,兼容 |
| Ollama nomic-embed-text | 768 | 8192 | 本地部署,免费 |
维度越高,语义表达能力越强,但存储成本也越高。对于大多数企业场景,1536 维已经足够。
完整示例一:课程描述向量化
场景说明
飞翔科技学生管理系统需要将课程描述向量化,为后续课程语义搜索做准备。
关键代码
@RestController
public class EmbeddingDemoController {
private final EmbeddingModel embeddingModel;
public EmbeddingDemoController(EmbeddingModel embeddingModel) {
this.embeddingModel = embeddingModel;
}
@GetMapping("/embed")
public Map<String, Object> embedText(@RequestParam String text) {
// 单文本向量化
Embedding embedding = embeddingModel.embed(text);
Map<String, Object> result = new HashMap<>();
result.put("input", text);
result.put("dimensions", embedding.getOutput().length);
result.put("first_5_values", Arrays.copyOf(embedding.getOutput(), 5));
result.put("embedding_length", embedding.getOutput().length);
return result;
}
@GetMapping("/embed/batch")
public Map<String, Object> embedBatch() {
// 批量向量化——一次 API 调用处理多条文本
List<String> courses = List.of(
"高等数学:微积分、线性代数和概率论基础",
"数据结构:链表、树、图等数据组织方式",
"操作系统:进程管理、内存管理和文件系统"
);
List<Embedding> embeddings = embeddingModel.embed(courses);
// 计算结果数量
Map<String, Object> result = new HashMap<>();
result.put("count", embeddings.size());
result.put("dimensions_per_vector", embeddings.get(0).getOutput().length);
return result;
}
}
运行结果
GET /embed?text=人工智能导论:机器学习与深度学习基础
{
"input": "人工智能导论:机器学习与深度学习基础",
"dimensions": 1536,
"first_5_values": [-0.023, 0.145, -0.089, 0.056, 0.002],
"embedding_length": 1536
}
完整示例二:相似课程检测
场景说明
孙鹤提了个 "课程去重" 需求——同一门课可能被不同老师以不同标题提交,需要用向量相似度检测。
关键代码
@Service
public class CourseSimilarityService {
private final EmbeddingModel embeddingModel;
public CourseSimilarityService(EmbeddingModel embeddingModel) {
this.embeddingModel = embeddingModel;
}
/**
* 计算两段课程描述之间的余弦相似度
* 返回值范围 [-1, 1],接近 1 表示语义高度相似
*/
public double cosineSimilarity(String text1, String text2) {
float[] v1 = embeddingModel.embed(text1).getOutput();
float[] v2 = embeddingModel.embed(text2).getOutput();
return dotProduct(v1, v2) / (norm(v1) * norm(v2));
}
private double dotProduct(float[] a, float[] b) {
double sum = 0;
for (int i = 0; i < a.length; i++) {
sum += a[i] * b[i];
}
return sum;
}
private double norm(float[] v) {
double sum = 0;
for (float x : v) sum += x * x;
return Math.sqrt(sum);
}
}
运行结果
// 测试1:同一门课的不同描述
similarity = courseSimilarityService.cosineSimilarity(
"人工智能导论:涵盖机器学习、深度学习和强化学习",
"AI基础:神经网络的原理与实践"
);
// 结果:0.92 ← 高度相似(尽管措辞完全不同)
// 测试2:不同方向的课程
similarity = courseSimilarityService.cosineSimilarity(
"人工智能导论:机器学习基础",
"中国现代文学:鲁迅与五四新文化运动"
);
// 结果:0.12 ← 语义不相关
易错场景与面试考点
易错场景一:向量维度不匹配
// ❌ 错误:用 text-embedding-3-small (1536维) 生成的向量
// 存入配置为 dimensions=768 的向量库
// 结果:PostgreSQL 插入时报错 "expected 768 dimensions, got 1536"
问题分析
Embedding 模型的输出维度必须与 VectorStore 配置的维度一致。
# ✅ 正确:确保维度匹配
spring:
ai:
openai:
embedding:
model: text-embedding-3-small # 1536 维
vectorstore:
pgvector:
dimensions: 1536 # 与模型维度一致
面试高频题
Q1:Embedding 向量为什么能表示语义?
嵌入模型在训练时,通过让相似上下文的文本在向量空间中靠近、不相似的远离来优化参数。最终的结果是:语义相近的文本(如"北京天气"和"首都气温")在向量空间中的夹角很小(余弦相似度接近 1),而语义无关的文本夹角接近 90 度。
Q2:为什么不用 TF-IDF 而要用 Embedding?
TF-IDF 基于词频统计,无法处理同义词("汽车"和"轿车"被视为完全不同的词)。Embedding 模型能理解语义等价,精度显著更高。
本章小结
- EmbeddingModel.embed(text) 将文本转为 float[] 向量
- 不同模型的向量维度不同(1536 / 3072 / 768),需与 VectorStore 配置一致
- 余弦相似度是判断语义相似度的标准度量
- 批量向量化可减少 API 调用次数,降低成本