Spring AI Embedding Models
本文介绍了Embedding技术及其应用实践。Embedding是一种将文本转换为数字向量的技术,通过SpringAI的EmbeddingModel接口可统一访问多种模型。文章详细展示了智普Embedding的实现步骤,包括POM配置、属性文件和控制器代码。在相似文本查找部分,演示了如何通过余弦相似度计算文本匹配度。针对RAG(检索增强生成)技术,文章阐述了其核心思想和工作流程,并提供了本地知识库
目录
1. 语义割裂(Semantic Fragmentation)
一、Embedding 介绍
Embedding 是一种将文本(也可扩展至图像、视频)转换成数字向量的技术,这些向量表示了输入内容在语义空间中的位置,能够反映它们之间的相似度——向量距离越近,内容越相似。
Spring AI 通过其 EmbeddingModel 接口提供一套统一、简单、可替换的访问方式,支持多种底层模型(如 OpenAI、Titan、Azure、Ollama、智谱等),这样可以统一接口,切换模型仅需要改配置,不改调用逻辑。
Spring AI 中的Embedding 使用场景如下:
- 相似度计算/语义搜索:将查询和文档全部转换为向量,构建向量数据库进行近邻检索;
- 聚类与分类:将文本转换为向量后,使用传统算法进行聚类或分类;
- 检索增强生成(RAG):先用向量搜索获取相关知识,再结合生成模型回答;
- 推荐系统:如问答推荐、内容推荐等;
- 异常检测:语义异常内容检测。
二、Embedding 使用示例
1、智普Embedding
pom.xml
<?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/> <!-- lookup parent from repository -->
</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 依赖的版本,
引用每个 Spring AI 模块时不用再写 <version>,只要依赖什么模块 Mavens 自动使用 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>
<!-- spring-ai-client-chat 中包括 TokenTextSplitter、TextReader、Document 等工具 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-client-chat</artifactId>
<version>1.0.0</version>
</dependency>
</dependencies>
<!-- 声明仓库, 用于获取 Spring AI 以及相关预发布版本-->
<repositories>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<releases>
<enabled>false</enabled>
</releases>
</repository>
<repository>
<name>Central Portal Snapshots</name>
<id>central-portal-snapshots</id>
<url>https://central.sonatype.com/repository/maven-snapshots/</url>
<releases>
<enabled>false</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>
</project>
application.properties
spring.application.name=SpringAIEmbedding
server.port=8080
#使用 智普AI Embedding 模型,需要在pom.xml中引入对应依赖
spring.ai.zhipuai.api-key=${zhipuai_api_key}
spring.ai.zhipuai.base-url=https://open.bigmodel.cn/api/paas
spring.ai.zhipuai.embedding.options.model=embedding-2
#使用 智普AI Chat 模型,需要在pom.xml中引入对应依赖
spring.ai.zhipuai.chat.options.model=GLM-4-Flash
EmbeddingController
@RestController
@RequestMapping("/ai")
public class EmbeddingController {
@Autowired
private EmbeddingModel embeddingModel;
@Autowired
private EmbeddingService service;
//测试 embedding
@GetMapping("/embedding")
public Map embed(@RequestParam(value = "message", defaultValue = "测试") String message) {
// 调用 embed 方法,将文本转换成向量,默认向量维度为 1024
float[] vector = embeddingModel.embed(message);
return Map.of(
"message", message,
"vector", vector
);
}
}
2、查找相似文本

EmbeddingService
import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class EmbeddingService {
private EmbeddingModel embeddingModel;
private final List<float[]> docVectors;
// 1. 准备知识库文本内容
private final List<String> docs = List.of(
"美食非常美味,服务员也很友好。",
"这部电影既刺激又令人兴奋。",
"阅读书籍是扩展知识的好方法。"
);
public EmbeddingService(EmbeddingModel embeddingModel) {
this.embeddingModel = embeddingModel;
// 2. 启动时,将所有文档向量化
this.docVectors = this.embeddingModel.embed(docs);
}
/**
* 输入用户查询,返回最相似文档的索引 (使用余弦相似度计算)
* @param query 用户输入的查询文本
* @return 最相似的知识库文本
*/
public String queryBestMatch(String query) {
//将用户的输入通过EmbeddingModel转换成向量
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] 之间
// 相似度越高,越接近1,越接近0,越不相似
private double cosineSimilarity(float[] a, float[] b) {
double dot = 0, na = 0, 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];
}
return dot / (Math.sqrt(na) * Math.sqrt(nb));
}
}
//文本相似检测
@GetMapping("/similarity")
public Map<String, String> similarity(@RequestParam("query") String query) {
String ans = service.queryBestMatch(query);
return Map.of("query", query, "answer", ans);
}
3、RAG本地知识库检索
RAG(Retrieval-Augmented Generation,检索增强生成)是一种将信息检索与语言生成相结合的 AI 架构,旨在让大语言模型(LLM)在回答问题时能够基于外部知识库提供准确、可靠、最新且可溯源的答案,从而克服传统 LLM 的以下局限:
- 知识截止(无法知道训练后的新事件)
- 幻觉(编造看似合理但错误的信息)
- 缺乏领域专业知识(如企业内部文档、医疗指南等)
RAG 的核心思想
“先查资料,再作答”
不像普通 LLM 仅靠内部参数“凭记忆”回答,RAG 在生成答案前,会:
- 从外部知识库中检索与用户问题最相关的文档片段;
- 将这些片段作为上下文,连同问题一起输入给 LLM;
- 让 LLM 基于真实证据生成答案。
这样既保留了 LLM 强大的语言理解与生成能力,又引入了可验证的事实依据。
RAG 的工作流程

RagService
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
@Service
public class RagService {
private final EmbeddingModel em; //嵌入模型,用于生成文本的向量
private final ChatClient chatClient; // 聊天客户端,用于与 AI 进行交互
private final List<String> docs = new ArrayList<>();//存储本地文档内容
private final List<float[]> vectors = new ArrayList<>();//存储本地文档向量
public RagService(EmbeddingModel embeddingModel, ChatClient.Builder chatBuilder) throws IOException {
this.em = embeddingModel;
this.chatClient = chatBuilder.build(); // 创建 智普AI Chat 聊天客户端
// 加载本地文档
Resource res = new ClassPathResource("古代诗歌常用意向.txt");
String content = new String(res.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
// 分割文档内容并生成向量
for (String part : content.split("----")) {
System.out.println("part: " + part);
if (part.isBlank()) continue;
docs.add(part);// 存储切分的文档内容
vectors.add(em.embed(part)); // 将文档生成 embedding 并存储
}
}
// 对用户输入的问题进行回答
public String answer(String q) {
// 生成用户问题的向量
float[] qv = em.embed(q);
// 最相似两个文档的索引
int index1 = -1, index2 = -1;
// 最相似两个文档的相似度
double v1 = -1, v2 = -1;
// 找到最相似的两个文档
for (int i = 0; i < vectors.size(); i++) {
// 计算用户问题向量与切分的每个文档向量的余弦相似度
double sim = cosineSimilarity(qv, vectors.get(i));
if (sim > v1) {
index2 = index1;
v2 = v1;
index1 = i;
v1 = sim;
} else if (sim > v2) {
index2 = i;
v2 = sim;
}
}
//获取两个最相似文档的内容,拼接在一起作为上下文
String ctx = docs.get(index1) + (index2 >= 0 ? "\n---\n" + docs.get(index2) : "");
//构建 AI 模型的提示信息,包含上下文和用户问题
String prompt = "以下是知识内容:\n" + ctx + "\n请基于上述知识回答用户问题:“" + q + "”";
// 使用 ChatClient 流式构建
var response = chatClient
.prompt()
.system("你是知识助手,结合上下文回答问题") // 添加系统提示
.user(prompt) // 用户 + 上下文
.call(); // 发起同步调用
// 获取内容文本
return response.content();
}
// 余弦相似度实现
// 计算两个向量之间的余弦相似度,数值范围在 [-1, 1] 之间
// 相似度越高,越接近1,越接近0,越不相似
private double cosineSimilarity(float[] a, float[] b) {
double dot = 0, na = 0, 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];
}
return dot / (Math.sqrt(na) * Math.sqrt(nb));
}
}
RagController
import com.example.springaiembedding.service.RagService;
import org.springframework.beans.factory.annotation.Autowired;
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("/rag")
public class RagController {
@Autowired
private RagService ragService;
@GetMapping("/ask")
public Map<String, String> ask(@RequestParam("question") String question) {
String answer = ragService.answer(question);
return Map.of("question", question, "answer", answer);
}
}
4、RAG本地知识库检索升级
手动切分文档的常见问题
1. 语义割裂(Semantic Fragmentation)
- 问题:强行按固定长度(如每 512 字符)切分,可能把一个完整句子或段落从中切断。
- 后果:
- 检索时,关键信息分散在多个 chunk 中,单个 chunk 无法提供完整上下文;
- LLM 无法理解残缺语义,导致答非所问或遗漏重点。
📌 示例:
原文:“根据2025年新政策,员工每周可远程办公3天,但需提前在HR系统中申请。”
若在“3天,”处切开 →
Chunk A: “根据2025年新政策,员工每周可远程办公3天,”
Chunk B: “但需提前在HR系统中申请。”
单独检索任一块都无法回答“远程办公需要什么流程?”
2. 信息冗余或遗漏
- 问题:人工设定的切分规则(如按段落、标题)可能忽略逻辑边界。
- 后果:
- 重要上下文(如前提条件、例外说明)被遗漏;
- 相同内容在多个 chunk 重复,浪费存储和计算资源。
3. 缺乏一致性
- 问题:不同人、不同文档结构(PDF/Word/Web)导致切分标准不统一。
- 后果:向量数据库中 chunk 质量参差不齐,影响整体检索效果。
4. 难以适应多语言/复杂格式
- 问题:中文无空格、表格、代码块、图文混排等场景下,简单按字符或换行切分会出错。
- 后果:嵌入向量无法准确反映真实语义。
RagService2
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.document.Document;
import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.ai.reader.TextReader;
import org.springframework.ai.transformer.splitter.TokenTextSplitter;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.stream.Collectors;
@Service
public class RagService2 {
private final EmbeddingModel em;//嵌入模型,用于生成文本的向量
private final ChatClient chatClient;// 聊天客户端,用于与 AI 进行交互
private final List<String> docs = new ArrayList<>(); //存储本地文档内容
private final List<float[]> vectors = new ArrayList<>(); //存储本地文档向量
public RagService2(EmbeddingModel embeddingModel, ChatClient.Builder chatBuilder) throws Exception {
this.em = embeddingModel;
this.chatClient = chatBuilder.build();// 创建 智普AI Chat 聊天客户端
// 1. 从 resources 读取长文本文档
var resource = new ClassPathResource("古代诗歌常用意象.txt");
// 创建 TextReader 并读取文档内容
TextReader reader = new TextReader(resource);
List<Document> rawDocs = reader.read();
// 2. 使用 TokenTextSplitter 工具将长文本拆分为合理大小片段
TokenTextSplitter splitter = TokenTextSplitter.builder()
.withChunkSize(800) // 拆分每段最多 800 token
.withMinChunkSizeChars(400) // 每段最小允许 400 字符
.withKeepSeparator(true) // 保留分隔符,提高上下文连贯
.build();
//按照设置的参数拆分长文本为多个Chunk
List<Document> chunks = splitter.apply(rawDocs);
// 3. 遍历每个 chunk,生成 embedding 并存储
for (Document d : chunks) {
// 获取文本内容,并去除首尾空白
String text = d.getText().strip();
System.out.println("text: " + text);
if (text.isBlank()) continue;
docs.add(text);// 存储切分的文档内容
vectors.add(em.embed(text));// 将文档生成 embedding 并存储
}
}
public String answer(String q) {
// 4. 将用户提问 embedding
float[] qVec = em.embed(q);
// 5. 计算每个 chunk 与提问的相似度,并找出前 K 高
int K = 5;
double threshold = 0.05; // 相似度阈值
// 该列表用于存储 大于阈值的 chunk 信息(chunk索引和相似度)
List<IndexSim> sims = new ArrayList<>();
for (int i = 0; i < vectors.size(); i++) {
// 计算用户问题向量与切分的每个文档向量的余弦相似度
double sim = cosineSimilarity(qVec, vectors.get(i));
if (sim >= threshold) {
sims.add(new IndexSim(i, sim));
}
}
// 按相似度降序排序
sims.sort((a, b) -> Double.compare(b.sim, a.sim));
// 取相似度最高的前 K 个片段
List<IndexSim> topKs = sims.stream().limit(K).collect(Collectors.toList());
// 6. 扩展上下文范围:在每个匹配片段基础上加入其前后相邻片段
// 为了避免重复,使用 TreeSet 来存储所有需要的片段索引,TreeSet 会自动去重并排序,保持从小到大顺序
Set<Integer> idxSet = new TreeSet<>();
for (IndexSim is : topKs) {
idxSet.add(is.index);
if (is.index - 1 >= 0) idxSet.add(is.index - 1);
if (is.index + 1 < docs.size()) idxSet.add(is.index + 1);
}
// 7. 将这些片段拼接成最终上下文内容,用于生成回答
String context = idxSet.stream()
.map(docs::get)
.collect(Collectors.joining("\n---\n"));
String prompt = "以下是相关知识片段:\n" + context + "\n请基于这些内容回答问题:“" + q + "”";
// 8. 调用 ChatClient 生成回答
var response = chatClient
.prompt()
.system("你是一个知识助手,请结合上下文进行回答")
.user(prompt)
.call();
// 返回大模型的回答结果
return response.content();
}
// 辅助类:记录 index 与 相似度
static class IndexSim {
int index;// 文档索引
double sim;// 相似度
IndexSim(int index, double sim) {
this.index = index;
this.sim = sim;
}
}
// 余弦相似度实现
// 计算两个向量之间的余弦相似度,数值范围在 [-1, 1] 之间
// 相似度越高,越接近1,越接近0,越不相似
private double cosineSimilarity(float[] a, float[] b) {
double dot = 0, na = 0, 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];
}
return dot / (Math.sqrt(na) * Math.sqrt(nb));
}
}
@Autowired
private RagService2 ragService2;
@GetMapping("/ask2")
public Map<String, String> ask2(@RequestParam("question") String question) {
String answer = ragService2.answer(question);
return Map.of("question", question, "answer", answer);
}
更多推荐

所有评论(0)