Embedding向量化原理与模型选型

假设你是个媒人,手里有1000个单身男女的资料,要给他们配对。

每个人的资料是一段文字描述:

小王:28岁程序员,喜欢打游戏看动漫,宅男一枚,偶尔爬山
小李:26岁设计师,热爱旅行摄影,周末喜欢逛展览
小张:27岁产品经理,喜欢打篮球健身,性格外向

现在有个新人小美:"25岁,喜欢户外运动和摄影,希望找一个有共同爱好的人"

你怎么快速找出最适合小美的人选?

方法一:关键词匹配

搜索"户外运动"和"摄影",找到包含这些词的人。

问题是:小王的资料里写的是"爬山",不是"户外运动";小李的资料里写的是"旅行摄影",不是单独的"摄影"。关键词匹配可能漏掉他们。

方法二:理解语义

"爬山"和"户外运动"意思相近,"旅行摄影"也包含"摄影"的意思。如果能理解这层语义关系,就能找到真正匹配的人。

这就是Embedding要解决的问题——让机器理解文字的"意思",而不只是匹配字面。

Embedding的本质

Embedding(向量化)是将文字转换为高维数值向量的过程。其核心价值在于:语义相近的文字,在向量空间中的距离也近。这使得机器能够通过数学运算来理解和比较文字的"意思",而不仅仅是匹配字面字符。

什么是向量

在讲Embedding之前,先回顾一下高数里学过的向量。

向量就是一串数字

向量可以理解为一个带方向的箭头,用一组数字来表示:

  • 一维向量[3],数轴上的一个点
  • 二维向量[3, 4],平面上的一个点
  • 三维向量[3, 4, 5],空间中的一个点

三维以上就不好画图了,但数学上没问题——可以是384维、768维、1536维,这些就叫高维向量

向量能表示什么

在RAG场景里,向量用来表示文字的语义特征

每个维度代表一个"语义方向",比如(这是打比方,实际不是这么简单):

  • 第1维:是否涉及"人物"
  • 第2维:是否涉及"动作"
  • 第3维:是否涉及"时间"
  • 第4维:是否涉及"技术"
  • ...

一段文字被转成向量后,每个维度上的数值就表示这段文字在这个"语义方向"上的权重。

向量之间的距离

这是关键点——语义相近的文字,向量在空间中的距离也近。

"打印机怎么用" → [0.23, -0.45, 0.67, ...]
"产品使用方法" → [0.25, -0.42, 0.65, ...] ← 很接近
"今天天气不错" → [-0.89, 0.12, 0.03, ...] ← 差很远

这就是为什么向量检索能做语义匹配——不是比字面,是比"意思"。

向量空间的可视化

在这种表示下,语义相近的内容会在向量空间中聚集(距离更近)。例如,"猫""狗"等词的向量在空间中会聚在一起,因为它们都是"动物"相关词;而与车辆相关的词则聚成另一簇。

2-Learning/05-项目/08-企业级项目深读/01-SuperAgent/06-系统讲解AI相关技术/03-RAG详细解析/assets/FkQ3lLvgzcDRWrn1TiK8i4JZdJEH-6f6ca760

下图展示了更多词汇在向量空间中的分布,可以看到同类概念自然聚集:

2-Learning/05-项目/08-企业级项目深读/01-SuperAgent/06-系统讲解AI相关技术/03-RAG详细解析/assets/FoupCi4e9k8WIU2hCy9IWiWuWdxj-289a9ad4

通过向量表示,模型可以量化语义相似度——两个内容的向量越接近,它们的含义就越相近。

下图展示了"Animals"(动物类:Wolf、Dog、Cat)和"Fruits"(水果类:Banana、Apple)两个语义簇,当查询"Kitten"时,它会自动落在动物类聚簇附近:

2-Learning/05-项目/08-企业级项目深读/01-SuperAgent/06-系统讲解AI相关技术/03-RAG详细解析/assets/FuCf7xnG_4mZRy53m2-p536kFCQ5-7f6a0a6f

Embedding:把文字变成向量

Embedding是个翻译官

Embedding的工作就是把文字"翻译"成向量。

输入:一段文字
输出:一串数字(向量)

这个翻译过程由Embedding模型(也叫向量模型)完成。

2-Learning/05-项目/08-企业级项目深读/01-SuperAgent/06-系统讲解AI相关技术/03-RAG详细解析/assets/Fk24tDLOUERZioGLILbKzVo6Md8M-e6a1c849

Embedding模型是怎么训练出来的

简单说,是让模型学习大量的文本,学会"什么样的文字意思相近"。

训练过程大致是:

  • 准备大量的文本对,标注哪些是相似的,哪些是不相似的
  • 让模型把这些文本转成向量
  • 调整模型参数,让相似文本的向量距离近,不相似的距离远
  • 反复训练,直到模型学会

训练好的模型就能把任意文字转成合适的向量了。

常见的Embedding模型

模型 维度 特点
text-embedding-ada-002 (OpenAI) 1536 效果好,但要调API
text-embedding-v2 (阿里) 1536 国产,对中文友好
bge-large-zh (BAAI) 1024 开源,中文效果好
m3e-base 768 开源,轻量级
nomic-embed-text 768 开源,支持离线

选择建议:

  • 要效果好:text-embedding-v2 或 OpenAI的模型
  • 要私有部署:bge系列或m3e
  • 要轻量级:m3e-base

向量相似度计算

把文字变成向量后,怎么判断两个向量有多"像"?

余弦相似度(最常用)

想象两个向量是两根箭头,从原点出发指向不同方向。

余弦相似度看的是这两根箭头的夹角

  • 夹角越小(指向差不多),越相似
  • 夹角90度(垂直),完全不相关
  • 夹角180度(方向相反),语义相反

下图直观展示了四种常见的向量距离度量方式,其中余弦相似度关注的是向量夹角:

2-Learning/05-项目/08-企业级项目深读/01-SuperAgent/06-系统讲解AI相关技术/03-RAG详细解析/assets/FsLqOnJhL3JNUFnxfJswelmtIWEM-25682374

计算公式:

 余弦相似度 = (A · B) / (|A| × |B|)

结果范围是[-1, 1]:

  • 1:完全相同
  • 0:不相关
  • -1:完全相反

优点:只关心方向,不关心长度。即使两段文字长短差很多,只要意思相近,相似度就高。

这也是为什么RAG系统普遍用余弦相似度。

欧几里得距离

就是空间中两点的直线距离。距离越小,越相似。

适合有"量"的概念的场景,比如图像相似度。

点积

A · B = a1×b1 + a2×b2 + ... + an×bn

计算简单,速度快。Transformer的注意力机制用的就是点积。

实际应用中怎么选

绝大部分RAG场景,用余弦相似度就对了

所有主流向量数据库(Milvus、PGVector、Qdrant)都默认支持余弦相似度。

相似度计算的选择

余弦相似度是RAG场景的标准选择,原因是它只关注向量的方向(语义倾向),不受向量长度(文本长短)的影响。这对于长短不一的文档块特别重要——一段100字的摘要和一段1000字的详述,只要讲的是同一件事,余弦相似度就能正确识别它们的相关性。

Spring AI中的向量化

EmbeddingModel接口

Spring AI通过EmbeddingModel接口统一封装了不同的向量模型:

public interface EmbeddingModel {
    // 单条文本向量化
    float[] embed(String text);

    // 批量向量化
    List<float[]> embed(List<String> texts);

    // 获取向量维度
    int dimensions();
}

不管你用的是OpenAI、阿里云还是本地模型,代码都是一样的。

使用阿里云的向量模型

先加依赖:

<dependency>
    <groupId>com.alibaba.cloud.ai</groupId>
    <artifactId>spring-ai-alibaba-starter</artifactId>
    <version>1.0.0</version>
</dependency>

配置API Key:

spring:
  ai:
    dashscope:
      api-key: sk-xxxxx
      embedding:
        model: text-embedding-v2

然后就可以注入使用了:

@Service
public class EmbeddingService {

    @Autowired
    private EmbeddingModel embeddingModel;

    public float[] getEmbedding(String text) {
        return embeddingModel.embed(text);
    }

    public List<float[]> getEmbeddings(List<String> texts) {
        return embeddingModel.embed(texts);
    }
}

计算两段文字的相似度

public double calculateSimilarity(String text1, String text2) {
    float[] vec1 = embeddingModel.embed(text1);
    float[] vec2 = embeddingModel.embed(text2);

    return cosineSimilarity(vec1, vec2);
}

private double cosineSimilarity(float[] a, float[] b) {
    double dotProduct = 0;
    double normA = 0;
    double normB = 0;

    for (int i = 0; i < a.length; i++) {
        dotProduct += a[i] * b[i];
        normA += a[i] * a[i];
        normB += b[i] * b[i];
    }

    return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB));
}

试一试:

double sim1 = calculateSimilarity("打印机怎么用", "产品使用方法");
// 结果约 0.85,相似度高

double sim2 = calculateSimilarity("打印机怎么用", "今天天气真好");
// 结果约 0.12,相似度低

实战:电商商品推荐

来个具体的业务场景——电商平台的商品推荐。

场景描述

用户在搜索框输入"适合夏天穿的裙子",我们要从商品库里找出最相关的商品。

商品库里的商品描述是这样的:

商品1:夏季新款碎花连衣裙,轻薄透气,适合度假穿着
商品2:冬季加厚羽绒服,保暖防寒,适合北方冬天
商品3:春季百搭针织衫,柔软舒适,可搭配各种下装
商品4:夏日清凉吊带裙,纯棉面料,凉爽舒适

传统关键词匹配

搜索"夏天"+"裙子",只能匹配到商品4(包含"裙")。

商品1虽然是"夏季"+"连衣裙",但关键词不完全匹配,可能被漏掉。

向量语义匹配

把所有商品描述转成向量,存起来。

用户搜索时,把搜索词也转成向量,找最相似的。

@Service
public class ProductSearchService {

    @Autowired
    private EmbeddingModel embeddingModel;

    @Autowired
    private VectorStore vectorStore;

    // 商品入库时,生成向量并存储
    public void indexProduct(Product product) {
        String description = product.getDescription();

        Document doc = new Document(description);
        doc.getMetadata().put("productId", product.getId());
        doc.getMetadata().put("productName", product.getName());
        doc.getMetadata().put("price", product.getPrice());

        vectorStore.add(List.of(doc));
    }

    // 用户搜索时,做语义匹配
    public List<Product> search(String query, int topK) {
        List<Document> results = vectorStore.similaritySearch(
            SearchRequest.builder()
                .query(query)
                .topK(topK)
                .similarityThreshold(0.5)
                .build()
        );

        return results.stream()
            .map(doc -> {
                Long productId = (Long) doc.getMetadata().get("productId");
                return productRepository.findById(productId).orElse(null);
            })
            .filter(Objects::nonNull)
            .collect(Collectors.toList());
    }
}

搜索"适合夏天穿的裙子",结果:

1. 商品1:夏季新款碎花连衣裙(相似度0.89)
2. 商品4:夏日清凉吊带裙(相似度0.87)
3. 商品3:春季百搭针织衫(相似度0.45)
4. 商品2:冬季加厚羽绒服(相似度0.21)

语义匹配能理解"夏天≈夏季≈夏日"、"裙子≈连衣裙≈吊带裙",比关键词匹配智能多了。

完整Demo:给学习诉求匹配课程路线

上面的商品推荐更偏业务理解,下面补一个真正可以独立跑起来的完整示例。

这次我们不再用“商品搜索”或“退货规则”做例子,而是换成一个更贴近本站内容的场景:

  • 候选数据不是商品,而是几条不同的学习路线
  • 查询语句不是搜索词,而是一段学员的学习诉求
  • 先把每条路线的介绍文字转成向量,再把学习诉求转成向量
  • 最后用余弦相似度排序,找出最匹配的路线

示例中项目地址

  • 项目模块:ai-example-demo-rag

运行前,直接改 main 方法开头这三个参数即可:

String apiKey = "请替换成你的 SiliconFlow API Key";
String apiUrl = "https://api.siliconflow.cn/v1/embeddings";
String modelName = "Qwen/Qwen3-Embedding-8B";

完整代码如下:

package org.javaup.ai.test;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.HashMap;
import java.util.Map;

/**
 * 演示如何使用 Embedding 给“学习诉求”和“课程路线”做语义匹配。
 * 这里直接调用 SiliconFlow 的 Embedding API,不依赖项目里的 Spring AI 配置。
 */
public class EmbeddingLearningPathDemo {

    public static void main(String[] args) throws Exception {
        String apiKey = "请替换成你的 SiliconFlow API Key";
        String apiUrl = "https://api.siliconflow.cn/v1/embeddings";
        String modelName = "Qwen/Qwen3-Embedding-8B";

        HttpClient httpClient = HttpClient.newBuilder()
            .connectTimeout(Duration.ofSeconds(20))
            .build();
        ObjectMapper objectMapper = new ObjectMapper();

        List<LearningPath> learningPaths = List.of(
            new LearningPath(
                "P001",
                "Java 高并发与分布式实战",
                "已经掌握 Spring Boot,希望补齐缓存、消息队列、限流熔断和分布式事务的同学",
                "Spring Boot、Redis、Kafka、分布式锁、幂等设计、秒杀系统",
                "能独立设计高并发下单链路,并定位常见性能瓶颈"
            ),
            new LearningPath(
                "P002",
                "MySQL 与 Redis 性能优化路线",
                "经常写 SQL,但对索引设计、慢查询治理和缓存一致性还不够熟的同学",
                "索引优化、执行计划、锁机制、缓存击穿、缓存雪崩、热点 Key",
                "能系统分析数据库慢查询问题,并设计稳定的缓存策略"
            ),
            new LearningPath(
                "P003",
                "Spring AI 与 RAG 项目实战",
                "想用 Java 做 AI 应用,希望掌握文档切块、向量化、检索增强和答案生成流程的同学",
                "Spring AI、Embedding、向量数据库、Chunk、Prompt、RAG、知识库问答",
                "能搭建一个完整的智能问答系统,并理解从文本到召回的核心链路"
            ),
            new LearningPath(
                "P004",
                "前端工程化与交互设计",
                "需要独立完成管理后台或业务门户,希望系统掌握组件化和工程化实践的同学",
                "Vue、React、TypeScript、状态管理、构建优化、组件设计",
                "能设计清晰可维护的前端工程结构,并完成复杂页面开发"
            ),
            new LearningPath(
                "P005",
                "云原生部署与 DevOps 路线",
                "负责服务上线和日常运维,希望掌握容器化、CI/CD 和服务可观测性的同学",
                "Docker、Kubernetes、GitHub Actions、灰度发布、监控告警、日志采集",
                "能把 Java 服务稳定部署到容器平台,并建立基础运维体系"
            ),
            new LearningPath(
                "P006",
                "数据分析与可视化入门",
                "想做报表分析、经营复盘和指标看板,希望提升数据建模与图表表达能力的同学",
                "数据清洗、指标体系、BI 报表、可视化图表、业务分析、经营复盘",
                "能围绕业务问题搭建基础分析模型,并输出清晰的数据结论"
            )
        );

        String learnerNeed = """
            我已经会一点 Spring Boot,最近想做一个能读取文档、切分文本块、
            计算相似度并回答问题的 Java AI 项目,最好还能顺手学会向量检索和 RAG 的完整链路。
            """;

        runSemanticMatch(httpClient, objectMapper, apiKey, apiUrl, modelName, learningPaths, learnerNeed, 3);
    }

    private static void runSemanticMatch(HttpClient httpClient,
                                         ObjectMapper objectMapper,
                                         String apiKey,
                                         String apiUrl,
                                         String modelName,
                                         List<LearningPath> learningPaths,
                                         String learnerNeed,
                                         int topK) throws Exception {
        List<String> pathPortraits = learningPaths.stream()
            .map(EmbeddingLearningPathDemo::buildPortrait)
            .toList();

        System.out.println("=== Embedding 学习路线匹配 Demo ===");
        System.out.println("Embedding 模型: " + modelName);
        System.out.println("候选路线数量: " + learningPaths.size());
        System.out.println();
        System.out.println("学习诉求:");
        System.out.println(learnerNeed);

        List<float[]> pathVectors = embedTexts(httpClient, objectMapper, apiKey, apiUrl, modelName, pathPortraits);
        float[] needVector = embedText(httpClient, objectMapper, apiKey, apiUrl, modelName, learnerNeed);

        System.out.println("向量生成完成,向量维度: " + pathVectors.get(0).length);
        System.out.println();
        System.out.println("--- Top 匹配结果 ---");

        List<MatchResult> results = new ArrayList<>();
        for (int i = 0; i < learningPaths.size(); i++) {
            LearningPath learningPath = learningPaths.get(i);
            double similarity = cosineSimilarity(needVector, pathVectors.get(i));
            results.add(new MatchResult(learningPath, similarity));
        }

        results.stream()
            .sorted(Comparator.comparingDouble(MatchResult::similarity).reversed())
            .limit(topK)
            .forEachOrdered(result -> printResult(result, buildPortrait(result.learningPath())));
    }

    private static float[] embedText(HttpClient httpClient,
                                     ObjectMapper objectMapper,
                                     String apiKey,
                                     String apiUrl,
                                     String modelName,
                                     String text) throws Exception {
        return embedTexts(httpClient, objectMapper, apiKey, apiUrl, modelName, List.of(text)).get(0);
    }

    private static List<float[]> embedTexts(HttpClient httpClient,
                                            ObjectMapper objectMapper,
                                            String apiKey,
                                            String apiUrl,
                                            String modelName,
                                            List<String> texts) throws Exception {
        Map<String, Object> requestBody = new HashMap<>();
        requestBody.put("model", modelName);
        requestBody.put("input", texts);
        requestBody.put("encoding_format", "float");

        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create(apiUrl))
            .timeout(Duration.ofSeconds(60))
            .header("Authorization", "Bearer " + apiKey)
            .header("Content-Type", "application/json")
            .POST(HttpRequest.BodyPublishers.ofString(objectMapper.writeValueAsString(requestBody)))
            .build();

        HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
        if (response.statusCode() != 200) {
            throw new IllegalStateException("Embedding API 调用失败,HTTP " + response.statusCode() + ",响应体: " + response.body());
        }

        return parseEmbeddings(objectMapper, response.body());
    }

    private static List<float[]> parseEmbeddings(ObjectMapper objectMapper, String responseBody) throws IOException {
        JsonNode root = objectMapper.readTree(responseBody);
        JsonNode data = root.get("data");
        if (data == null || !data.isArray() || data.isEmpty()) {
            throw new IllegalStateException("Embedding API 响应缺少 data 字段,响应体: " + responseBody);
        }

        List<float[]> vectors = new ArrayList<>();
        for (JsonNode item : data) {
            JsonNode embeddingNode = item.get("embedding");
            if (embeddingNode == null || !embeddingNode.isArray()) {
                throw new IllegalStateException("Embedding API 响应缺少 embedding 字段,响应体: " + responseBody);
            }

            float[] vector = new float[embeddingNode.size()];
            for (int i = 0; i < embeddingNode.size(); i++) {
                vector[i] = embeddingNode.get(i).floatValue();
            }
            vectors.add(vector);
        }

        return vectors;
    }

    private static String buildPortrait(LearningPath learningPath) {
        return """
            路线编号:%s
            路线名称:%s
            适合人群:%s
            核心关键词:%s
            学完之后:%s
            """.formatted(
            learningPath.code(),
            learningPath.title(),
            learningPath.audience(),
            learningPath.keywords(),
            learningPath.outcome()
        );
    }

    private static void printResult(MatchResult result, String portrait) {
        LearningPath path = result.learningPath();
        System.out.printf("[%s] %s -> 相似度 %.4f%n", path.code(), path.title(), result.similarity());
        System.out.println("画像内容:");
        System.out.println(portrait);
        System.out.println();
    }

    private static double cosineSimilarity(float[] left, float[] right) {
        if (left.length != right.length) {
            throw new IllegalArgumentException("向量维度不一致,无法计算相似度");
        }

        double dotProduct = 0.0;
        double leftNorm = 0.0;
        double rightNorm = 0.0;

        for (int i = 0; i < left.length; i++) {
            dotProduct += left[i] * right[i];
            leftNorm += left[i] * left[i];
            rightNorm += right[i] * right[i];
        }

        if (leftNorm == 0.0 || rightNorm == 0.0) {
            return 0.0;
        }

        return dotProduct / (Math.sqrt(leftNorm) * Math.sqrt(rightNorm));
    }

    private record LearningPath(String code, String title, String audience, String keywords, String outcome) {
    }

    private record MatchResult(LearningPath learningPath, double similarity) {
    }

}

运行后,你会得到类似下面这样的排序结果:

=== Embedding 学习路线匹配 Demo ===
Embedding 模型: Qwen/Qwen3-Embedding-8B
候选路线数量: 6

学习诉求:
我已经会一点 Spring Boot,最近想做一个能读取文档、切分文本块、
计算相似度并回答问题的 Java AI 项目,最好还能顺手学会向量检索和 RAG 的完整链路。

向量生成完成,向量维度: 4096

--- Top 匹配结果 ---
[P003] Spring AI  RAG 项目实战 -> 相似度 0.8073
画像内容:
路线编号:P003
路线名称:Spring AI  RAG 项目实战
适合人群:想用 Java  AI 应用,希望掌握文档切块、向量化、检索增强和答案生成流程的同学
核心关键词:Spring AI、Embedding、向量数据库、Chunk、Prompt、RAG、知识库问答
学完之后:能搭建一个完整的智能问答系统,并理解从文本到召回的核心链路

[P001] Java 高并发与分布式实战 -> 相似度 0.4896
画像内容:
路线编号:P001
路线名称:Java 高并发与分布式实战
适合人群:已经掌握 Spring Boot,希望补齐缓存、消息队列、限流熔断和分布式事务的同学
核心关键词:Spring Boot、Redis、Kafka、分布式锁、幂等设计、秒杀系统
学完之后:能独立设计高并发下单链路,并定位常见性能瓶颈

[P005] 云原生部署与 DevOps 路线 -> 相似度 0.3843
画像内容:
路线编号:P005
路线名称:云原生部署与 DevOps 路线
适合人群:负责服务上线和日常运维,希望掌握容器化、CI/CD 和服务可观测性的同学
核心关键词:Docker、Kubernetes、GitHub Actions、灰度发布、监控告警、日志采集
学完之后:能把 Java 服务稳定部署到容器平台,并建立基础运维体系

这个完整示例背后对应的就是 Embedding 检索的标准步骤:

  • 先把每条路线描述拼成一段“画像文本”
  • 调用 Embedding 模型,批量生成候选路线向量
  • 再把用户的学习诉求也转成查询向量
  • 对查询向量和候选向量逐个计算余弦相似度
  • 最后按得分排序,拿到 Top-K 结果

这就是一个最小可用版的语义匹配流程。真实项目里,把“学习路线”换成“知识库 chunk”“FAQ 文档”“商品描述”“岗位画像”,整体链路都是一样的。

离线向量模型

在线API调用有成本,而且涉及数据安全问题。有些场景需要用离线模型。

为什么要用离线模型

  • 数据安全:敏感数据不能传到外部API
  • 成本控制:大量调用API费用不低
  • 延迟要求:本地推理延迟更低
  • 离线场景:有些环境没有外网

离线模型推荐

涉及敏感数据或有离线要求时,优先考虑BGE系列(智源研究院出品,中文效果优秀)。bge-large-zh效果最好但资源消耗大,bge-small-zh轻量但效果稍弱,bge-base-zh是平衡选择。可以用Ollama一键部署,和在线API的使用方式完全兼容。

常用的离线模型

BGE系列(智源研究院)

bge-small-zh: 512维,轻量级
bge-base-zh:  768维,平衡型
bge-large-zh: 1024维,效果最好

M3E系列

m3e-small: 512
m3e-base:  768

使用Ollama运行本地模型

Ollama可以方便地运行各种本地模型,包括Embedding模型。

安装Ollama后,拉取向量模型:

ollama pull nomic-embed-text

Spring AI配置:

spring:
  ai:
    ollama:
      base-url: http://localhost:11434
      embedding:
        model: nomic-embed-text

代码使用方式和在线API完全一样:

@Autowired
private EmbeddingModel embeddingModel;

public float[] embed(String text) {
    return embeddingModel.embed(text);
}

使用ONNX运行模型

如果不想装Ollama,也可以直接用ONNX Runtime加载模型:

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-transformers</artifactId>
    <version>1.1.0</version>
</dependency>
@Bean
public EmbeddingModel embeddingModel() {
    return new TransformersEmbeddingModel()
        .setModelPath("path/to/bge-small-zh");
}

向量模型选型建议

考虑因素

因素 说明
效果 对目标语言(中文/英文)的语义理解能力
维度 维度越高表达能力越强,但存储和计算成本也越高
速度 推理速度,影响实时性
部署方式 API调用还是本地部署
成本 API调用费用或服务器成本

我的建议

场景一:快速验证

直接用阿里云的text-embedding-v2,效果好,接入简单。

场景二:生产环境(无特殊安全要求)

继续用云服务的API,稳定可靠,省心。

场景三:生产环境(有数据安全要求)

部署bge-large-zh或m3e-base,效果和云服务差不多,数据不出内网。

场景四:资源有限

用bge-small-zh或m3e-small,牺牲一点效果换取更低的资源消耗。

不同模型效果对比

以中文语义相似度为例(数据来自网上的评测):

模型 MTEB中文平均分 维度
text-embedding-v2 (阿里) 65.2 1536
bge-large-zh 64.8 1024
bge-base-zh 62.4 768
m3e-base 61.5 768
bge-small-zh 58.3 512

可以看到,云服务和开源大模型效果差距不大,可以根据实际情况选择。

常见问题

Q1:向量维度越高越好吗?

不一定。

维度高,表达能力强,但:

  • 存储成本高
  • 计算成本高
  • 可能过拟合

一般768-1536维就够用了。

Q2:不同模型的向量能混用吗?

不能!

不同模型的向量空间是不兼容的。用模型A生成的向量,只能和模型A生成的向量比较。

如果中途换了向量模型,之前的向量都要重新生成。

切勿混用不同模型的向量

这是一个高频踩坑点:不同Embedding模型生成的向量绝对不能混用。如果在项目中途更换了向量模型(如从text-embedding-ada-002换到bge-large-zh),必须对知识库中所有已有向量进行重新计算,否则检索结果会完全错乱。在项目初期就要选定模型,后期更换代价很高。

Q3:中文和英文混合的内容怎么办?

选择对多语言支持好的模型,比如:

  • bge-m3(支持100+语言)
  • text-embedding-v2(中英文都不错)

Q4:向量模型需要微调吗?

大部分场景不需要。

除非你的领域非常垂直,通用模型效果不好,才考虑微调。

微调成本很高,需要准备大量的训练数据。

小结

这篇文章讲了Embedding的核心知识:

  • Embedding是什么:把文字翻译成向量,让机器理解语义
  • 向量相似度:余弦相似度是主流选择
  • 向量模型选型:云服务方便,离线部署安全
  • 实战应用:商品推荐与学习诉求匹配两个例子

Embedding是RAG的核心魔法,理解了它,后面的检索、重排就都好理解了。

下一篇讲向量检索的核心算法——为什么能做到毫秒级检索。


企业级项目导航:⬅️ 02-RAG三代进化史与架构选型 | 03-Embedding向量化原理与模型选型 | ➡️ 04-文档预处理读取清洗与标准化