Java 转 AI 应用开发之·向量篇|Spring AI + 智谱 Embedding

八年 Java 干下来,我对「接入大模型」的分工是这样的:Chat 负责嘴碎,Embedding 负责记性好——当然这是玩笑话。认真讲:Embedding(嵌入) 就是把文字(以后还有图、视频)压成一串浮点数组(向量),让机器在语义空间里比「谁和谁更像」。
这篇不写玄学公式吓你,目标就三件:搞懂 EmbeddingModel 在 Spring AI 里怎么用、用 智谱(智谱 AI)embedding-2 跑通、再用余弦相似度做一个最简版「相似句」。中间我塞几个当时我也愣过的问题,你对照团队里评审时也好开口。

基础篇(DeepSeek 对话上手):Spring_AI_快速上手_Deepseek实战。概念补课:Spring_AI_核心概念_Javaer入门.md


这篇你能收获什么?

  • Embedding 在业务里常干什么(检索、RAG、聚类、推荐、风控语义——不是替代 Chat)。
  • Spring Boot 3.5 + spring-ai-starter-model-zhipuai,把智谱 Embedding接进来。
  • 一行 embed(String) / 一批 embed(List) 的调用姿势。
  • 余弦相似度是啥、代码咋写、为啥企业里最终往往还是向量库而不是内存里 for 循环。

安全提醒: API Key 只放环境变量,别贴进仓库;下文示例一律 ${ZHIPU_API_KEY}


1. Embedding 是啥?Spring AI 为啥要搞一层接口?

Embedding(嵌入/向量):把文本(也可以扩展到图像、视频等)映射成固定维度的数字向量。语义相近的内容,在空间里距离更近——所以可以做相似度近邻检索聚类

Spring AIEmbeddingModel 统一各家实现(OpenAI、Titan、Azure、Ollama、智谱等)。好处和我们对接支付通道一样:换供应商尽量改配置业务调用别跟着大改

常见落地场景(对着需求聊不慌)

场景 人话
相似度 / 语义搜索 查询和文档都向量化,近邻搜最像的段落。
聚类 / 分类 向量当特征,后面接传统 ML 或规则。
RAG 先向量检索知识块,再交给 Chat 带着上下文生成。
推荐 内容、提问、用户行为语义近的凑一堆。
异常检测 离群向量=语义「不像正常话/正常工单」的可能异味。

探究:有了 Chat 还要 Embedding 干嘛?

Chat 是「生成一段话」;Embedding 是「把话变成坐标,用来比、搜、聚」。
例如 RAG:Embedding 负责找相关段落,Chat 负责组织语言回答——两件事都省事。

再直白点:关键词搜会漏掉类似「车厘子 vs 樱桃」这种同义变形;向量搜往往更扛说法不同但意思近——代价是多一套向量模型、存储和运维。

探究:DeepSeek 怎么不接 Embedding?

不少团队是 Chat 用 A 厂商、Embedding 用 B 厂商,很正常:谁有哪项能力就用谁。下文用智谱 embedding-3 演示;你项目里换成别的 EmbeddingModel 实现,Java 侧调用形状类似


2. 智谱 AI 侧准备(注册、Key、文档)

智谱 AI(北京智谱华章科技有限公司,清华 KEG 相关成果转化)产品线包括 ChatGLM / GLM-4、多模态 GLM-4V、视频生成 CogVideoX 等——我们这篇只取 文本 Embedding-3

常用入口(完整 URL,自行复制):

说明 地址
开放平台 https://open.bigmodel.cn/
API Key 管理 https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys
充值 https://open.bigmodel.cn/finance/pay
模型计价 https://www.bigmodel.cn/pricing
开发文档 https://open.bigmodel.cn/dev/howuse/introduction

上面文档链接以官网为准;若路径微调,从开放平台首页进 开发文档 即可。


3. 动手:Spring Boot 项目 SpringAIEmbedding

3.1 约定目录

SpringAIEmbedding/
├── pom.xml
└── src/main/
    ├── java/com/example/springaiembedding/
    │   ├── controller/EmbeddingController.java
    │   └── service/EmbeddingService.java
    └── resources/
        └── application.properties

包名按你 Initializr 生成即可,下文用 com.example.springaiembedding 示意。

3.2 pom.xml

文件: pom.xml

要点:Spring Boot 3.5Spring AI BOM智谱 starter;需要文档切块工具时加 spring-ai-client-chat(内含 DocumentTokenTextSplitter 等)。BOM 已管理版本时,优先不写死子依赖 version,避免和快照 BOM 打架——若你本地解析失败,再以官方示例为准锁版本。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.5.0</version>
        <relativePath/>
    </parent>

    <groupId>com.example</groupId>
    <artifactId>SpringAIEmbedding</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>SpringAIEmbedding</name>
    <description>SpringAIEmbedding</description>

    <properties>
        <java.version>17</java.version>
    </properties>

    <!-- Spring AI BOM:统一版本;各 spring-ai-* 模块由 BOM 对齐 -->
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.ai</groupId>
                <artifactId>spring-ai-bom</artifactId>
                <version>1.0.0-SNAPSHOT</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-starter-model-zhipuai</artifactId>
        </dependency>
        <!-- 文档切分、Document 等;版本建议交给 BOM,必要时再显式指定 -->
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-client-chat</artifactId>
        </dependency>
    </dependencies>

    <repositories>
        <repository>
            <id>spring-snapshots</id>
            <name>Spring Snapshots</name>
            <url>https://repo.spring.io/snapshot</url>
            <releases>
                <enabled>false</enabled>
            </releases>
        </repository>
        <repository>
            <id>central-portal-snapshots</id>
            <name>Central Portal Snapshots</name>
            <url>https://central.sonatype.com/repository/maven-snapshots/</url>
            <releases>
                <enabled>false</enabled>
            </releases>
            <snapshots>
                <enabled>true</enabled>
            </snapshots>
        </repository>
    </repositories>
</project>

3.3 application.properties

文件: src/main/resources/application.properties

# ---------- 应用 ----------
spring.application.name=SpringAIEmbedding
server.port=8080

# ---------- 智谱:Embedding(示例模型 embedding-2)----------
spring.ai.zhipuai.api-key=${ZHIPU_API_KEY}
spring.ai.zhipuai.base-url=https://open.bigmodel.cn/api/paas
spring.ai.zhipuai.embedding.options.model=embedding-3

# ---------- 智谱:Chat(可选,与本篇 Embedding 演示无强依赖)----------
spring.ai.zhipuai.chat.options.model=GLM-4-Flash

环境变量:

export ZHIPU_API_KEY="你的密钥"

4. 最小接口:把一句话打成向量

文件: src/main/java/com/example/springaiembedding/controller/EmbeddingController.java

package com.example.springaiembedding.controller;

import com.example.springaiembedding.service.EmbeddingService;
import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.Map;

@RestController
@RequestMapping("/ai")
public class EmbeddingController {

    private final EmbeddingModel embeddingModel;
    private final EmbeddingService embeddingService;

    public EmbeddingController(EmbeddingModel embeddingModel, EmbeddingService embeddingService) {
        this.embeddingModel = embeddingModel;
        this.embeddingService = embeddingService;
    }

    /**
     * 单条文本 → 向量(embedding-3 默认维度以官方为准,常见为 1024 维量级)。
     */
    @GetMapping("/embedding")
    public Map<String, Object> embed(
            @RequestParam(value = "message", defaultValue = "给我讲个笑话") String message) {
        float[] vector = embeddingModel.embed(message);
        return Map.of(
                "message", message,
                "vector", vector
        );
    }

    @GetMapping("/similarity")
    public Map<String, String> similarity(@RequestParam("query") String query) {
        String answer = embeddingService.queryBestMatch(query);
        return Map.of("query", query, "answer", answer);
    }
}

自测 URL:

http://localhost:8080/ai/embedding?message=今天要好好学习

返回里 vector 是一大串 float,别被长度吓到——那就是模型眼中的「这句话在 space 里的坐标」。

探究:embedembedForResponse 一类 API 区别?
实践里先满足产品要的是「float[]」还是「带 metadata 的响应」;需要用量、模型名排查问题时,再走 EmbeddingResponse 更香。


5. 案例:本地三条知识,谁和用户问得最像?

思路很土但很清晰:

  1. 启动时把几条「知识」embed(List) 成向量缓存起来。
  2. 用户来问,把 query 向量化。
  3. 和每条知识向量算 余弦相似度,取最大的那条。

生产环境会换成 向量数据库(Vector Store)+ 索引不会在百万文档上 Java for;教学阶段足够讲清链路。


5.1 余弦相似度(Cosine Similarity)是啥?

它解决什么问题?

把两段话都变成向量之后,你要回答:有多「像」?
余弦相似度是其中最常用的一种度量:看两个向量在空间里「指向」是否接近,而不太在意它们有多长(向量模长多大)。语义相近的句子,经同一套 Embedding 模型编码后,方向往往更接近,余弦值就偏大

几何直觉(从二维想到高维)

余弦相似度是一种衡量两个向量方向相似程度的度量方法,通过计算它们夹角的余弦值来评估相似性,广泛应用于文本分析、数据挖掘等领域。

余弦相似度的数学本质是向量空间模型中夹角的余弦值,计算公式为两个向量的点积除以它们的模长乘积。对于n维向量A和B,公式可表示为:


image.png

其值范围在-1到1之间,1表示完全相同,-1表示完全相反,0表示无关。

余弦相似度典型应用场景如下:

文本相似度计算:将文档转化为词频向量后,通过余弦相似度比较内容相似性。
聚类分析:衡量数据点在高维空间中的分布方向是否相近,常用于推荐系统或异常检测。

取值怎么读?

余弦值(约) 含义(口语)
接近 1 方向几乎一致,语义上通常更相似(还要看模型质量,不是 100% 真理)。
接近 0 近似正交,经常理解为「不怎么搭边」、关联弱。
接近 -1 反方向;文本语义里相对少见,但若出现说明向量在空间里「对着干」。

理论上取值区间是 ([-1, 1])。实际文本 Embedding 里分量有正有负,同类语义往往落在 ((0, 1]) 的偏多——所以 Demo 里用「谁最大谁最像」是合理策略;若你要严格阈值(例如 sim > 0.75 才返回),需要在业务语料上标一批样本再定,别拍脑袋。

为啥检索 / RAG 里爱用余弦?

  • 文档长短不一时,向量模长可能差很多;余弦用夹角说话,减少「长文档天然占优势」的偏差。
  • 很多向量库(Milvus、pgvector 等)/API 默认就提供 cosine / inner product归一化后内积与余弦等价)一类距离,工程上成套

当然:欧氏距离、点积(配合归一化) 也能做检索,团队里对齐一种度量 + 一套评测,比争论「哪个绝对最科学」更重要。

探究:为啥不用欧氏距离?

都可以。余弦对「向量长短」不敏感、文本场景常见;欧氏绝对位置敏感。团队里先统一一种,RAG 评测时再对比。


5.2 EmbeddingService

文件: src/main/java/com/example/springaiembedding/service/EmbeddingService.java

package com.example.springaiembedding.service;

import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class EmbeddingService {

    private final EmbeddingModel embeddingModel;
    private final List<float[]> docVectors;

    private final List<String> docs = List.of(
            "美食非常美味,服务员也很友好。",
            "这部电影既刺激又令人兴奋。",
            "阅读书籍是扩展知识的好方法。"
    );

    public EmbeddingService(EmbeddingModel embeddingModel) {
        this.embeddingModel = embeddingModel;
        this.docVectors = this.embeddingModel.embed(docs);
    }

    /**
     * 输入查询,返回余弦相似度最高的那条知识。
     */
    public String queryBestMatch(String query) {
        float[] queryVec = embeddingModel.embed(query);

        int bestIdx = -1;
        double bestSim = -1;

        for (int i = 0; i < docVectors.size(); i++) {
            double sim = cosineSimilarity(queryVec, docVectors.get(i));
            if (sim > bestSim) {
                bestSim = sim;
                bestIdx = i;
            }
        }
        return docs.get(bestIdx);
    }

    /**
     * 余弦相似度,范围约 [-1, 1],越大越相似。
     */
    private double cosineSimilarity(float[] a, float[] b) {
        double dot = 0;
        double na = 0;
        double nb = 0;
        for (int i = 0; i < a.length; i++) {
            dot += a[i] * b[i];
            na += a[i] * a[i];
            nb += b[i] * b[i];
        }
        double denom = Math.sqrt(na) * Math.sqrt(nb);
        return denom == 0 ? 0 : dot / denom;
    }
}

这段 cosineSimilarity 在算什么?(对着代码读一遍)

数学上就是上一节的公式:点积 /((|\mathbf{a}|\times|\mathbf{b}|))。实现里没单独写开方后再相乘以外的花样,就是朴素循环版,CPU 上对短向量够用。

变量 含义
dot 点积(内积) (\sum_i a_i b_i)。同一维上两个分量相乘再累加:同号且绝对值都大的维度,对 dot 贡献大——两条向量在同一特征方向上同时「做强」时,点积更容易变大。
na (\sum_i a_i^2),向量 a 各分量平方和(还没开方)。
nb 同理,向量 b 的平方和。
Math.sqrt(na) (|\mathbf{a}|),即向量 a欧氏长度(模长)。
denom (|\mathbf{a}||\mathbf{b}|),公式里的分母
dot / denom 即 (\dfrac{\mathbf{a}\cdot\mathbf{b}}{|\mathbf{a}||\mathbf{b}|}),也就是余弦相似度

denom == 0 ? 0 : … 是干啥的?
若某一向量全 0(或数值下溢成 0),模长为 0,除法无意义;这里直接返回 0,避免 NaN。正规 Embedding 输出一般不会是全零向量,但防御式写法在工程里不丢人。

前提:a.length == b.length
同一模型、同一维度下,两条文本的向量长度一定相同;若你混了两个模型或手工拼向量,要先 assert 或短路,否则循环会越界或语义全错。

为啥用 double 累加?
float 分量相乘再累加到 double,比全程 float 稍抗一点舍入误差;结果和向量库里的 cosine 在浮点误差内应一致,线上要以评测集 + 供应商说明为准。

Controller 里注入 EmbeddingService/similarity 已在 §3 合并写出;若你更喜欢两个 Controller 类,拆出去也行,别重复映射同一路径


5.3 自测

http://localhost:8080/ai/similarity?query=美食
http://localhost:8080/ai/similarity?query=电影
http://localhost:8080/ai/similarity?query=书籍

预期(大意):

query answer(三条知识里最相近的一条)
美食 美食非常美味,服务员也很友好。
电影 这部电影既刺激又令人兴奋。
书籍 阅读书籍是扩展知识的好方法。

6. 收尾:从 Demo 到上线还差啥?

八年经验里,Embedding 最容易翻车的不是公式,而是:

  • 向量维度 / 模型版本换了,旧索引全废
  • 网关超时、限流没在压测里摸过;
  • 成本:按 token 或按条计费,日志别打印整段向量
  • 真正规模必须上 Vector Store(Milvus、pgvector、Redis 向量模块等),内存 List<float[]> 只适合讲清楚原理

如果你下一篇想写 Spring AI + VectorStore + RAG 一条龙,可以按同一风格续上。


附录:参考链接

说明 地址
Spring AI Reference https://docs.spring.io/spring-ai/reference/
智谱开放平台 https://open.bigmodel.cn/

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容