Spring AI 从入门到精通-Embedding

7. Embedding:AI 的"理解"从数字开始

7.1 一个比喻:气味地图

想象你走进一家咖啡店,你闻到咖啡的香气、烤面包的焦味、还有一点点奶香。你的大脑不需要看到"咖啡豆"三个字,就能判断这是咖啡店。

Embedding(嵌入)就是这个过程的数字版。 它把一段文字转换成一串浮点数(向量),这串数字"代表"了文字的含义。语义相近的文字,向量在空间中距离也近。
#mermaid-svg-mEKBUjLxaLchOEKP{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-mEKBUjLxaLchOEKP .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-mEKBUjLxaLchOEKP .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-mEKBUjLxaLchOEKP .error-icon{fill:#552222;}#mermaid-svg-mEKBUjLxaLchOEKP .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-mEKBUjLxaLchOEKP .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-mEKBUjLxaLchOEKP .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-mEKBUjLxaLchOEKP .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-mEKBUjLxaLchOEKP .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-mEKBUjLxaLchOEKP .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-mEKBUjLxaLchOEKP .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-mEKBUjLxaLchOEKP .marker{fill:#333333;stroke:#333333;}#mermaid-svg-mEKBUjLxaLchOEKP .marker.cross{stroke:#333333;}#mermaid-svg-mEKBUjLxaLchOEKP svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-mEKBUjLxaLchOEKP p{margin:0;}#mermaid-svg-mEKBUjLxaLchOEKP .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-mEKBUjLxaLchOEKP .cluster-label text{fill:#333;}#mermaid-svg-mEKBUjLxaLchOEKP .cluster-label span{color:#333;}#mermaid-svg-mEKBUjLxaLchOEKP .cluster-label span p{background-color:transparent;}#mermaid-svg-mEKBUjLxaLchOEKP .label text,#mermaid-svg-mEKBUjLxaLchOEKP span{fill:#333;color:#333;}#mermaid-svg-mEKBUjLxaLchOEKP .node rect,#mermaid-svg-mEKBUjLxaLchOEKP .node circle,#mermaid-svg-mEKBUjLxaLchOEKP .node ellipse,#mermaid-svg-mEKBUjLxaLchOEKP .node polygon,#mermaid-svg-mEKBUjLxaLchOEKP .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-mEKBUjLxaLchOEKP .rough-node .label text,#mermaid-svg-mEKBUjLxaLchOEKP .node .label text,#mermaid-svg-mEKBUjLxaLchOEKP .image-shape .label,#mermaid-svg-mEKBUjLxaLchOEKP .icon-shape .label{text-anchor:middle;}#mermaid-svg-mEKBUjLxaLchOEKP .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-mEKBUjLxaLchOEKP .rough-node .label,#mermaid-svg-mEKBUjLxaLchOEKP .node .label,#mermaid-svg-mEKBUjLxaLchOEKP .image-shape .label,#mermaid-svg-mEKBUjLxaLchOEKP .icon-shape .label{text-align:center;}#mermaid-svg-mEKBUjLxaLchOEKP .node.clickable{cursor:pointer;}#mermaid-svg-mEKBUjLxaLchOEKP .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-mEKBUjLxaLchOEKP .arrowheadPath{fill:#333333;}#mermaid-svg-mEKBUjLxaLchOEKP .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-mEKBUjLxaLchOEKP .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-mEKBUjLxaLchOEKP .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-mEKBUjLxaLchOEKP .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-mEKBUjLxaLchOEKP .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-mEKBUjLxaLchOEKP .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-mEKBUjLxaLchOEKP .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-mEKBUjLxaLchOEKP .cluster text{fill:#333;}#mermaid-svg-mEKBUjLxaLchOEKP .cluster span{color:#333;}#mermaid-svg-mEKBUjLxaLchOEKP div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-mEKBUjLxaLchOEKP .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-mEKBUjLxaLchOEKP rect.text{fill:none;stroke-width:0;}#mermaid-svg-mEKBUjLxaLchOEKP .icon-shape,#mermaid-svg-mEKBUjLxaLchOEKP .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-mEKBUjLxaLchOEKP .icon-shape p,#mermaid-svg-mEKBUjLxaLchOEKP .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-mEKBUjLxaLchOEKP .icon-shape .label rect,#mermaid-svg-mEKBUjLxaLchOEKP .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-mEKBUjLxaLchOEKP .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-mEKBUjLxaLchOEKP .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-mEKBUjLxaLchOEKP :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 距离近
距离远
咖啡

0.23, -0.45, 0.78, ...

拿铁

0.21, -0.42, 0.80, ...

汽车

-0.67, 0.12, -0.33, ...

7.2 EmbeddingModel 接口

Spring AI 的 EmbeddingModel 接口极其简洁:

java 复制代码
public interface EmbeddingModel extends Model<EmbeddingRequest, EmbeddingResponse> {
    
    // 嵌入一段文本
    float[] embed(String text);
    
    // 嵌入一个 Document 对象
    float[] embed(Document document);
    
    // 批量嵌入
    List<float[]> embed(List<String> texts);
    
    // 嵌入并返回完整响应(含元数据)
    EmbeddingResponse embedForResponse(List<String> texts);
    
    // 获取向量维度
    int dimensions();
}

7.3 第一个 Embedding 示例

java 复制代码
@Autowired
private EmbeddingModel embeddingModel;

public void demo() {
    // 嵌入一段文本
    float[] vector = embeddingModel.embed("Spring AI 是一个强大的 AI 框架");
    
    System.out.println("向量维度: " + vector.length);  // 比如 1536(OpenAI)或 768(Ollama)
    System.out.println("前 5 个值: " + Arrays.toString(Arrays.copyOf(vector, 5)));
    // 输出示例: [0.0123, -0.0456, 0.0789, 0.0234, -0.0567]
    
    // 批量嵌入
    List<float[]> vectors = embeddingModel.embed(List.of(
        "Java 编程语言",
        "Python 编程语言",
        "今天天气很好"
    ));
    
    // 计算余弦相似度
    double similarity = cosineSimilarity(vectors.get(0), vectors.get(1));
    System.out.println("Java 和 Python 的相似度: " + similarity);  // 应该很高,比如 0.85
    
    double similarity2 = cosineSimilarity(vectors.get(0), vectors.get(2));
    System.out.println("Java 和天气的相似度: " + similarity2);      // 应该很低,比如 0.12
}

// 余弦相似度计算:衡量两个向量在方向上的相似程度
// 值越接近 1 表示语义越相似,越接近 0 表示越不相关
private double cosineSimilarity(float[] a, float[] b) {
    double dot = 0, normA = 0, normB = 0;
    for (int i = 0; i < a.length; i++) {
        dot += a[i] * b[i];
        normA += a[i] * a[i];
        normB += b[i] * b[i];
    }
    return dot / (Math.sqrt(normA) * Math.sqrt(normB));
}

7.4 支持的 Embedding 模型

提供商 依赖 维度 特点
OpenAI spring-ai-starter-model-openai 1536/3072 最常用
Azure OpenAI spring-ai-starter-model-azure-openai 1536 企业合规
Ollama spring-ai-starter-model-ollama 768/4096 免费本地
Transformers (ONNX) spring-ai-starter-model-transformers 384/768 纯本地,无网络
Vertex AI spring-ai-starter-model-vertex-ai 768 Google 生态
Bedrock spring-ai-starter-model-bedrock 1024/1536 AWS 生态
Mistral AI spring-ai-starter-model-mistral-ai 1024 欧洲厂商

7.5 配置 Embedding 模型

properties 复制代码
# OpenAI Embedding
spring.ai.openai.api-key=${OPENAI_API_KEY}
spring.ai.openai.embedding.options.model=text-embedding-3-small

# Ollama Embedding(免费本地)
spring.ai.ollama.embedding.options.model=nomic-embed-text

7.6 Document 对象:带元数据的文本

在 RAG 场景中,我们不只嵌入"纯文本",而是嵌入带元数据的 Document 对象:

java 复制代码
Document doc = new Document(
    "Spring AI 是 Spring 生态的 AI 框架,支持多种模型和向量数据库。",
    Map.of(
        "source", "官方文档",
        "page", 1,
        "author", "Spring 团队",
        "category", "AI"
    )
);

float[] embedding = embeddingModel.embed(doc);
// 元数据可用于后续过滤

7.7 一个完整的相似度搜索 Demo

java 复制代码
@Service
public class SemanticSearchService {

    private final EmbeddingModel embeddingModel;

    public SemanticSearchService(EmbeddingModel embeddingModel) {
        this.embeddingModel = embeddingModel;
    }

    /**
     * 在候选文档中搜索与查询最相似的文档
     */
    public List<ScoredDocument> search(String query, List<Document> candidates, int topK) {
        // 1. 嵌入查询
        float[] queryEmbedding = embeddingModel.embed(query);
        
        // 2. 嵌入所有候选文档(批量)
        List<float[]> candidateEmbeddings = embeddingModel.embed(
            candidates.stream().map(Document::getText).toList()
        );
        
        // 3. 计算相似度并排序
        return IntStream.range(0, candidates.size())
            .mapToObj(i -> new ScoredDocument(
                candidates.get(i),
                cosineSimilarity(queryEmbedding, candidateEmbeddings.get(i))
            ))
            .sorted((a, b) -> Double.compare(b.score, a.score))
            .limit(topK)
            .toList();
    }
}

record ScoredDocument(Document document, double score) {}

相关推荐
gregmankiw1 小时前
基于OpenClaw与DeepSeek V4 Flash的新闻因果时序自动化分析系统
人工智能
xian_wwq1 小时前
【学习笔记】「大模型安全:攻击面演化史」第 07 篇-安全左移
人工智能·笔记·学习
马拉AI2 小时前
我把科研人用AI的水平,分成了5个阶段
人工智能
武子康2 小时前
调查研究-164-NVIDIA DGX Station for Windows 解析:不是新显卡,而是企业本地 AI 超算
人工智能·openai
AndrewHZ2 小时前
【LLM技术全景】预训练与微调:大模型如何“学习“
人工智能·深度学习·大模型·llm·微调·预训练·rlhf
audyxiao0012 小时前
ICLR 2026论文分享 | WorldGym:用世界模型打造机器人策略评估新范式
大数据·人工智能·大模型·智能体·世界模型
泠不丁2 小时前
用 Obsidian 双链笔记管理智能家居技术知识体系
人工智能
泠不丁2 小时前
智能家居 Zigbee 协议在高并发传感数据时的丢包率实测
人工智能
螺丝钉code2 小时前
JAVA项目 Claude code CLAUDE.md 到底应该怎么写
java·人工智能·claude code