SpringAI系列|第四篇 4.1:Embedding基础

发布时间:2026/6/24 11:05:33
SpringAI系列|第四篇 4.1:Embedding基础 Spring AI 实战系列 | 第 4.1 篇Embedding 基础系列说明Embedding 是 RAG 的基石也是语义搜索的核心。这篇把 Embedding 的概念和 Spring AI 中的用法讲清楚。前置知识了解基本 AI 概念有 Spring Boot 基础。文章目录Spring AI 实战系列 | 第 4.1 篇Embedding 基础[toc]前言一、什么是 Embedding1.1 直观理解1.2 为什么需要 Embedding1.3 向量的维度二、EmbeddingModel 的使用2.1 依赖2.2 基础用法2.3 批量嵌入2.4 配置模型三、向量相似度计算3.1 余弦相似度3.2 欧氏距离3.3 实战文本相似度比较四、文本向量化实战4.1 文档向量化4.2 长文本分块4.3 带重叠的分块五、常见问题Q1Embedding 调用贵吗Q2中文 Embedding 哪个好Q3向量存在哪里Q4Embedding 结果不稳定写在最后前言我第一次接触 Embedding 的时候完全搞不懂这玩意儿是干嘛的。“把文本变成向量然后呢”直到做了第一个语义搜索功能我才明白 Embedding 的厉害之处。它让计算机能理解文字的意思而不是简单地匹配关键词。这篇从概念到代码把 Embedding 讲透。一、什么是 Embedding1.1 直观理解Embedding 就是把文字、图片、音频这些东西转换成一组数字向量。比如苹果这个词会被转成 1536 个数字组成的数组香蕉也是手机也是。这些数字不是随机的语义相近的词向量也相近。所以 “苹果” 和 “香蕉” 的向量距离很近都是水果而 “苹果” 和 “手机” 的距离就很远。1.2 为什么需要 Embedding传统搜索是这样的用户搜苹果手机系统匹配包含苹果和手机的文档。问题是如果文档里写的是 “iPhone”传统搜索就匹配不到了。Embedding 搜索不一样。用户搜苹果手机系统把查询转成向量然后找向量最接近的文档。“iPhone” 的向量和苹果手机很接近匹配成功。这就是语义搜索的威力。1.3 向量的维度不同的 Embedding 模型输出的向量维度不同。模型维度说明官方文档text-embedding-3-small1536OpenAI便宜OpenAI Embeddingtext-embedding-3-large3072OpenAI效果好OpenAI Embeddingtext-embedding-ada-0021536OpenAI旧版OpenAI Embeddingbge-large-zh1024智源中文效果好BGE GitHubm3e768开源中文 EmbeddingM3E GitHub选型建议预算充足用 text-embedding-3-large性价比选 text-embedding-3-small纯中文场景用 bge-large-zh。二、EmbeddingModel 的使用2.1 依赖Spring AI 对 OpenAI 的 Embedding 支持已经封装好了直接加 starter 就行。dependencygroupIdorg.springframework.ai/groupIdartifactIdspring-ai-openai-spring-boot-starter/artifactId/dependency如果你用的是其他模型提供商比如智谱、百度换成对应的 starter 即可。Spring AI 的抽象层让切换模型变得很简单。2.2 基础用法注入EmbeddingModel调用embed()方法一行代码搞定。importorg.springframework.ai.embedding.EmbeddingModel;importorg.springframework.ai.embedding.EmbeddingResponse;ServicepublicclassEmbeddingService{privatefinalEmbeddingModelembeddingModel;publicEmbeddingService(EmbeddingModelembeddingModel){this.embeddingModelembeddingModel;}publicListDoubleembed(Stringtext){EmbeddingResponseresponseembeddingModel.embed(text);returnresponse.getResult().getOutput();}}返回的EmbeddingResponse里包含了向量结果维度取决于你配置的模型。2.3 批量嵌入批量嵌入比逐个嵌入效率高很多建议尽量用批量。publicListListDoubleembedBatch(ListStringtexts){EmbeddingResponseresponseembeddingModel.embed(texts);returnresponse.getResults().stream().map(result-result.getOutput()).collect(Collectors.toList());}实测批量调用比单条循环快 5-10 倍因为减少了网络往返和连接开销。2.4 配置模型在application.yml里配置 API Key 和模型参数。spring:ai:openai:api-key:${OPENAI_API_KEY}embedding:options:model:text-embedding-3-small也可以把模型换成text-embedding-3-large效果会好一些但成本也更高。具体选哪个看你的场景对精度的要求。三、向量相似度计算拿到向量之后怎么判断两段文字是否相似3.1 余弦相似度最常用的是余弦相似度计算两个向量夹角的余弦值。publicclassVectorUtils{publicstaticdoublecosineSimilarity(ListDoublea,ListDoubleb){if(a.size()!b.size()){thrownewIllegalArgumentException(向量维度不一致);}doubledotProduct0.0;doublenormA0.0;doublenormB0.0;for(inti0;ia.size();i){dotProducta.get(i)*b.get(i);normAMath.pow(a.get(i),2);normBMath.pow(b.get(i),2);}returndotProduct/(Math.sqrt(normA)*Math.sqrt(normB));}}余弦相似度的值在 -1 到 1 之间。1 表示完全相同0 表示无关-1 表示完全相反Embedding 里基本不会出现负值。实际项目中0.7 以上通常认为语义相关0.85 以上认为高度相关。不过具体阈值得根据你的业务场景来定。3.2 欧氏距离除了余弦相似度欧氏距离也是一种常用度量。publicstaticdoubleeuclideanDistance(ListDoublea,ListDoubleb){doublesum0.0;for(inti0;ia.size();i){sumMath.pow(a.get(i)-b.get(i),2);}returnMath.sqrt(sum);}距离越小越相似。余弦相似度关注方向欧氏距离关注绝对距离。在文本 Embedding 场景里余弦相似度通常表现更好。3.3 实战文本相似度比较把前面的知识串起来写一个实际的相似度比较服务。ServicepublicclassSimilarityService{privatefinalEmbeddingModelembeddingModel;publicSimilarityService(EmbeddingModelembeddingModel){this.embeddingModelembeddingModel;}publicdoublecompare(Stringtext1,Stringtext2){ListDoubleembed1embeddingModel.embed(text1).getResult().getOutput();ListDoubleembed2embeddingModel.embed(text2).getResult().getOutput();returnVectorUtils.cosineSimilarity(embed1,embed2);}}// 使用doublesimilaritysimilarityService.compare(苹果手机,iPhone);// 输出0.85很相似doublesimilarity2similarityService.compare(苹果手机,香蕉);// 输出0.3不太相似这个例子很直观“苹果手机和iPhone的相似度远高于苹果手机和香蕉”。Embedding 模型确实理解了语义。四、文本向量化实战4.1 文档向量化在实际项目里我们通常需要把整篇文档转成向量存起来方便后续检索。ServicepublicclassDocumentEmbeddingService{privatefinalEmbeddingModelembeddingModel;publicDocumentEmbeddingService(EmbeddingModelembeddingModel){this.embeddingModelembeddingModel;}publicDocumentVectorvectorize(StringdocumentId,Stringcontent){ListDoublevectorembeddingModel.embed(content).getResult().getOutput();returnnewDocumentVector(documentId,content,vector);}publicrecordDocumentVector(Stringid,Stringcontent,ListDoublevector){}}DocumentVector是一个简单的记录类把文档 ID、原文内容和向量打包在一起。后续存入向量数据库时这三个字段都会用到。4.2 长文本分块Embedding 模型有输入长度限制通常是 8192 Token 左右。长文档不能直接丢进去需要分块。ServicepublicclassChunkingService{publicListStringsplit(Stringtext,intmaxChunkSize){ListStringchunksnewArrayList();// 简单按段落分块String[]paragraphstext.split(\n\n);StringBuildercurrentnewStringBuilder();for(Stringparagraph:paragraphs){if(current.length()paragraph.length()maxChunkSize){if(current.length()0){chunks.add(current.toString());currentnewStringBuilder();}}current.append(paragraph).append(\n\n);}if(current.length()0){chunks.add(current.toString());}returnchunks;}}按段落分块是最简单的策略但有时候一个段落本身就超过限制。更精细的做法是按句子分块或者按语义边界分块。Spring AI 提供了TokenTextSplitter可以按 Token 数精确切分后面讲文档处理时会详细介绍。4.3 带重叠的分块分块时加上重叠避免上下文断裂。比如一个段落被切到两块里没有重叠的话语义就断了。publicListStringsplitWithOverlap(Stringtext,intchunkSize,intoverlap){ListStringchunksnewArrayList();intstart0;while(starttext.length()){intendMath.min(startchunkSize,text.length());chunks.add(text.substring(start,end));startend-overlap;// 回退 overlap 个字符}returnchunks;}overlap 一般设为 chunkSize 的 10%-20%比如 1000 字的块重叠 100-200 字。这个参数需要调太小断语义太大浪费 Token。五、常见问题Q1Embedding 调用贵吗OpenAI 的 text-embedding-3-small 很便宜约 0.02 美元/百万 Token。一篇 1000 字的文章嵌入成本不到 0.001 美元。我算过一笔账一个中等规模的文档库10 万篇全部嵌入一次的成本也就几美元。嵌入是调用一次长期使用的不是每次查询都重新嵌入所以成本完全可以接受。Q2中文 Embedding 哪个好有预算OpenAI 的 text-embedding-3-large性价比text-embedding-3-small纯中文场景智源的 bge-large-zh开源免费m3e、BGE我自己的项目里中文内容用 bge-large-zh 效果确实比 OpenAI 的小模型好一些但差距没有想象中大。如果已经接入了 OpenAI没必要为了中文专门再搭一套。Q3向量存在哪里下一篇讲 VectorStore专门存向量。简单的场景也可以用内存存储。ComponentpublicclassInMemoryVectorStore{privatefinalListDocumentVectorvectorsnewArrayList();publicvoidadd(DocumentVectorvector){vectors.add(vector);}publicListDocumentVectorsearch(ListDoublequery,inttopK){returnvectors.stream().sorted((a,b)-{doublesimAcosineSimilarity(query,a.vector());doublesimBcosineSimilarity(query,b.vector());returnDouble.compare(simB,simA);// 降序}).limit(topK).collect(Collectors.toList());}}内存存储适合数据量小的场景比如几百条文档。数据量大了还是得用专门的向量数据库PGvector、Redis、Milvus 都是不错的选择。Q4Embedding 结果不稳定同一个文本多次嵌入结果会有微小差异这是浮点精度导致的。但相似度排序是稳定的不影响使用。如果你发现两次嵌入的结果差距很大检查一下是不是文本里有随机内容比如时间戳、UUID。写在最后Embedding 是连接人类语言和机器计算的桥梁。理解了 Embedding你就理解了语义搜索、RAG、推荐系统这些技术的底层原理。下一篇讲向量数据库集成包括 PGvector、Redis、Elasticsearch 等主流方案。系列目录第 1 篇Spring AI 概述与快速上手第 1.2 篇环境准备与第一个项目第 1.3 篇核心概念速览第 2.1 篇ChatClient 详解第 2.2 篇多模型提供商接入第 2.3 篇Prompt 工程化第 3.1 篇结构化输出第 3.2 篇多模态输入第 3.3 篇文生图与语音第 4.1 篇Embedding 基础 ✅本文第 4.2 篇向量数据库集成第 4.3 篇文档处理第 5 篇RAG 检索增强生成第 6 篇Tool Calling 工具调用第 7 篇Advisor 机制与对话管理第 8 篇MCP 模型上下文协议第 9 篇AI Agent 开发实战第 10 篇企业级应用与最佳实践如果这篇文章对你有帮助欢迎点赞收藏关注系列持续更新中参考文档Spring AI 官方文档、OpenAI Embedding 指南