前言

Github:https://github.com/HealerJean

博客:http://blog.healerjean.com

一、嵌入模型

1、概念解释

1)Embedding 到底是什么

Embedding = 把文字变成高维向量

  • 句子含义越接近 → 向量距离越近
  • 向量可存入向量库(Chroma/Milvus/PGvector
  • 用于:语义搜索、RAG、相似推荐、文本分类

Spring AI 的价值:一套 API 通吃所有嵌入模型(OpenAI/Ollama/ 智谱 / 通义 / 文心),切换只改配置,不改代码。

在 RAG 应用中,流程通常是:

  1. 存储时:文档通过 EmbeddingModel 转为向量存入向量数据库。
  2. 查询时:用户的问题也通过同一个 EmbeddingModel 转为向量。
  3. 匹配:计算问题向量与数据库中文档向量的相似度(如余弦相似度),找出最相关的片段。

2、核心 API 结构

1)顶层接口:EmbeddingModel

所有嵌入模型都实现这个接口:

interface EmbeddingModel {
    EmbeddingResponse call(EmbeddingRequest request);
}

2)请求 / 响应

  • EmbeddingRequest:批量文本输入

    • public class EmbeddingRequest implements ModelRequest<List<String>> {
      	private final List<String> inputs;
      	private final EmbeddingOptions options;
      	// other methods omitted
      }
      
  • EmbeddingResponse:批量向量输出

    • public class EmbeddingResponse implements ModelResponse<Embedding> {
          
      	private List<Embedding> embeddings;
      	private EmbeddingResponseMetadata metadata = new EmbeddingResponseMetadata();
      	// other methods omitted
      }
      
  • Embedding:单个文本对应的向量

    • public class Embedding implements ModelResult<float[]> {
      	private float[] embedding;
      	private Integer index;
      	private EmbeddingResultMetadata metadata;
      	// other methods omitted
      }
      

3)常用实现类

  • OpenAiEmbeddingModel
  • AzureOpenAiEmbeddingModel
  • OllamaEmbeddingModel
  • BgeSmallEmbeddingModel(本地轻量)
  • VertexAiEmbeddingModel
  • DashscopeEmbeddingModel(通义千问)
  • ZhipuAiEmbeddingModel(智谱)

4)关键方法

  • float[] embed(String text):最常用的快捷方法,直接输入文本,返回向量。
  • List<float[]> embed(List<String> texts):批量处理,效率更高。
  • EmbeddingResponse call(EmbeddingRequest request):底层通用方法,用于处理复杂的请求参数。

5)维度说明(非常重要)

不同模型向量维度不同,向量库必须同维度

  • text-embedding-ada-002 → 1536 维
  • text-embedding-3-small → 1536 维
  • text-embedding-3-large → 3072 维
  • bge-small-zh → 512/768/1024 维
  • m3e/bce-embedding → 768 维

3、实战案例

spring:
  application:
    name: hlj-strata
  profiles:
    include: project-config
  ai:
    ollama:
      base-url: http://127.0.0.1:11434  # Ollama 服务地址(默认 11434)
      chat:
        model: qwen3:14b # 默认使用的模型(与 ollama pull 一致)
        options:
          temperature: 0.7  # 温度参数(0-1,值越高越随机,越低越精准)
          max-tokens: 1000  # 最大生成 token 数
      embedding:
        model: nomic-embed-text  # 嵌入模型 ollama  pull nomic-embed-text

1)案例1:获取文本向量

  /**
   * 案例 1:获取文本向量
   * 场景:将一段文本转换为高维向量数组
   * URL: GET /ai/embedding/vector?text=Spring AI is awesome
   */
  @GetMapping("/vector")
  public Map<String, Object> getVector(@RequestParam String text) {
      // 1. 调用模型生成向量
      float[] vector = embeddingModel.embed(text);

      // 2. 返回结果(通常前端只需要向量用于计算或存储)
      return Map.of(
              "text", text,
              "dimensions", vector.length, // 维度,例如 768
              "vector", Arrays.toString(vector) // 实际向量数据
      );
  }



GET http://localhost:8080/ai/embedding/vector?text=HealerJean
{
  "text": "HealerJean",
  "vector": "[-0.0013825513, 0.0020674136, ……………………]",
  "dimensions": 768
}

2)案例2:语义相似度计算

/**
 * 案例 2:语义相似度计算
 * 场景:判断两段文本在语义上是否相似
 * URL: GET /ai/embedding/similarity?text1=如何做西红柿炒鸡蛋&text2=西红柿炒鸡蛋的食谱
 */
@GetMapping("/similarity")
public Map<String, Object> calculateSimilarity(
        @RequestParam String text1,
        @RequestParam String text2) {

    // 1. 将两段文本转换为向量
    float[] vector1 = embeddingModel.embed(text1);
    float[] vector2 = embeddingModel.embed(text2);

    // 2. 计算余弦相似度
    double similarity = cosineSimilarity(vector1, vector2);

    // 3. 结果解读
    String interpretation;
    if (similarity > 0.8) {
        interpretation = "语义高度相似";
    } else if (similarity > 0.5) {
        interpretation = "语义有一定相关性";
    } else {
        interpretation = "语义不相关";
    }

    return Map.of(
            "text1", text1,
            "text2", text2,
            "score", similarity,
            "interpretation", interpretation
    );
}


GET http://localhost:8080/ai/embedding/similarity?text1=如何做西红柿炒鸡蛋&text2=西红柿炒鸡蛋的食谱

{
  "score": 0.8514120004471687,
  "text1": "如何做西红柿炒鸡蛋",
  "text2": "西红柿炒鸡蛋的食谱",
  "interpretation": "语义高度相似"
}

3)案例3:底层 API 调用,获取元数据(如 Token 消耗)

/**
 * 案例 3:底层 API 调用
 * 场景:获取更详细的元数据(如 Token 消耗)
 * URL: GET /ai/embedding/raw
 */
@GetMapping("/raw")
public Map<String, Object> rawEmbedding() {
    String text = "Testing raw API call";

    // 1. 构建请求对象
    EmbeddingRequest request = new EmbeddingRequest(
            List.of(text),
            EmbeddingOptions.builder().build()
    );

    // 2. 调用底层接口
    EmbeddingResponse response = embeddingModel.call(request);

    // 3. 提取元数据
    // 注意:不同模型的元数据字段可能不同
    return Map.of(
            "input_text", text,
            "vector_length", response.getResult().getOutput().length,
            "usage", response.getMetadata().getUsage() // 查看消耗了多少 Token
    );
}



GET http://localhost:8080/ai/embedding/raw

{
  "vector_length": 768,
  "input_text": "Testing raw API call",
  "usage": {
    "promptTokens": 6,
    "completionTokens": 0,
    "totalTokens": 6
  }
}

4、FQA

1)嵌入模型和 大模型之前的调用关系

简单来说,大模型(LLM)负责“思考和表达”,而嵌入模型(Embedding)负责“记忆和检索”。

特性 大模型 嵌入模型
核心任务 生成 理解/分类
输入 文本(Prompt) 文本(任何长度)
输出 文本(回答、代码、文章) 向量(一串数字数组,如 [0.1, 0.9, ...]
能力 逻辑推理、语言组织、数学计算 语义捕捉、相似度计算
比喻 作家/教授:能写出优美的文章 图书管理员:能快速给书分类、找书
消耗 较贵(计算量大) 便宜(计算量相对小)

2)向量和 token 的关系

a、Token:离散的“货物”

Token 是文本的最小单位。:当你输入一句话(比如“我爱吃苹果”)时,大模型看不懂汉字,它先要把这句话切碎。

  • 切碎后["我", "爱", "吃", "苹果"]
  • 这四个词就是 4 个 Token
  • 特点:它们是离散的、独立的符号。对计算机来说,Token 只是一个个没有意义的编号(比如 ID: 101, ID: 205…)。

b、向量:标准化的“快递盒”

向量是 Token 的数学表示。

计算机(显卡)看不懂“苹果”这个词,但它擅长算数学题。所以,模型必须把每个 Token 塞进一个标准的“数学盒子”里,这个盒子就是向量

  • 转换后
    • “我” -> [0.1, 0.5, -0.9...]
    • “苹果” -> [0.2, 0.8, -0.1...]
  • 这个由几百到几千个数字组成的数组,就是向量
  • 特点:它是连续的数值。向量里的每一个数字,都代表了“苹果”的某种特征(比如:是不是水果、是不是红色的、是不是圆的)。

c、如何转化的

这个过程在 AI 内部是一瞬间完成的,叫作 Embedding(嵌入)

  • 第一步(Token化)
    • 你把文字发给模型,模型先把它拆成 Token
    • 比如:把 “Cat” 拆成一个 Token。
  • 第二步(查表/映射)
    • 模型内部有一张巨大的“字典”(Embedding Table)。它拿着 Token 的编号去查表,找到对应的 向量
    • 比如:查到 “Cat” 对应的向量是 [0.9, 0.2, ...]
  • 第三步(计算)
    • 模型拿着这个 向量 去进行复杂的数学运算(矩阵乘法)。
    • 注意:模型全程都在和向量打交道,它从来不看 Token 本身。
  • 第四步(还原)
    • 算完之后,模型得到一个结果向量,再通过“反向查表”,
    • 把这个向量变回 Token(比如预测下一个词是 “Food”),最后拼成文字给你看。

3)为什么调用 Embedding 也会消耗 Token

很多初学者认为只有 ChatGPT 那种“聊天”才费 Token,其实所有大模型操作(包括生成向量)都是按 Token 收费的。

消耗点:模型需要“阅读”并“理解”你的文字才能生成向量。“阅读”的过程就是消耗算力和资源的过程,所以 API 提供商(如 OpenAI)会按照你发送的文字长度(即 Token 数量)来计费。

当你发送 "Testing raw API call" 这段文字给 OpenAI 的嵌入模型时,流程如下:

  1. 分词OpenAI 的服务器收到文字,先把它切分成 Token
    • 例如:Testing -> Test + ing (2个 Token)
  2. 计算:模型通过复杂的神经网络算法,分析这几个 Token 之间的语义关系。
  3. 生成向量:最后输出一个由数字组成的向量(比如 [0.1, 0.05, ...])。

4)RAG 消耗 token 的完整链路

a、 离线阶段(知识库构建)—— 一次性消耗

离线是一次性成本:建好库后不再重复消耗(除非更新知识库)

  • 文档分块(Chunking:将长文档切分为小片段(Spring AI 默认按 token 数切分)
  • 嵌入(Embedding)计算
    • 每个文档块 → 送入 Embedding 模型(如 text-embedding-ada-002
    • 每块文本都会算 token,按嵌入模型费率计费
    • Spring AI 自动做 TokenCountBatchingStrategy 批处理,防止超限制

b、在线阶段(用户问答)—— 每次查询都消耗(核心)

  1. 用户问题(~10–50 token)
  2. 向量检索(不消耗 LLM token,只算向量库查询)
  3. 召回 N 段相关文档(每段 100–500 token)
  4. 拼接 Prompt
系统提示 + 上下文文档1 + 文档2 + ... + 用户问题
  1. 送入 LLM 生成回答

    • 拼接后的整个 Prompt 全部算「输入 token」

    • 生成的回答算「输出 token」

二、图像模型

1、概念解释

1)核心接口

package org.springframework.ai.image;

public interface ImageClient {

    ImageResponse call(ImagePrompt prompt);

}

2)核心对象

a、**ImageModel **: 核心接口,定义了 call(ImagePrompt) 方法。

@FunctionalInterface
public interface ImageModel extends Model<ImagePrompt, ImageResponse> {

	ImageResponse call(ImagePrompt request);

}

b、ImagePrompt: 封装请求的类,包含 ImageMessage 列表和可选参数。

public class ImagePrompt implements ModelRequest<List<ImageMessage>> {

  private final List<ImageMessage> messages;

	private ImageOptions imageModelOptions;

    @Override
	public List<ImageMessage> getInstructions() {...}

	@Override
	public ImageOptions getOptions() {...}

    // constructors and utility methods omitted
}

c、ImageMessage:

ImageMessage类封装了要使用的文本以及该文本在影响生成的图像时应具有的权重。对于支持权重的模型,权重可以是正数或负数。

public class ImageMessage {

	private String text;

	private Float weight;

  public String getText() {...}

	public Float getWeight() {...}

   // constructors and utility methods omitted
}

d、ImageOptions: 模型特定的参数,如宽度、高度、质量、风格等。

表示可以传递给图像生成模型的选项。ImageOptions接口扩展了ModelOptions接口,用于定义可以传递给AI模型的少数可移植选项。

public interface ImageOptions extends ModelOptions {

	Integer getN();

	String getModel();

	Integer getWidth();

	Integer getHeight();

	String getResponseFormat(); // openai - url or base64 : stability ai byte[] or base64

}

e、ImageResponse: 包含生成的图像数据和元数据。

public class ImageResponse implements ModelResponse<ImageGeneration> {

	private final ImageResponseMetadata imageResponseMetadata;

	private final List<ImageGeneration> imageGenerations;

	@Override
	public ImageGeneration getResult() {
		// get the first result
	}

	@Override
	public List<ImageGeneration> getResults() {...}

	@Override
	public ImageResponseMetadata getMetadata() {...}

    // other methods omitted

}

f、ImageGeneration:表示输出响应和有关此结果的相关元数据

public class ImageGeneration implements ModelResult<Image> {

	private ImageGenerationMetadata imageGenerationMetadata;

	private Image image;

    @Override
	public Image getOutput() {...}

	@Override
	public ImageGenerationMetadata getMetadata() {...}

    // other methods omitted

}

2、实战案例

1)案例1:文本生成图片

/**
 * 基础:文本生成图片
 * @param prompt 提示词
 * @return 图片URL
 */
@GetMapping("/ai/image/generate")
public String generate(@RequestParam String prompt) {
    // 1. 构建请求
    ImagePrompt imagePrompt = new ImagePrompt(prompt);

    // 2. 调用生成
    ImageResponse response = imageClient.call(imagePrompt);

    // 3. 获取结果
    Image image = response.getResult().getOutput();
    return image.getUrl(); // 返回图片URL
}

2)自定义参数:大小、质量、模型

@GetMapping("/ai/image/generate/custom")
public String generateCustom(
        @RequestParam String prompt,
        @RequestParam(defaultValue = "1024x1024") String size,
        @RequestParam(defaultValue = "standard") String quality,
        @RequestParam(defaultValue = "dall-e-3") String model
) {
    // 构建 OpenAI 专用配置
    OpenAiImageOptions options = OpenAiImageOptions.builder()
            .model(model)
            .size(size)
            .quality(quality)
            .responseFormat("url") // url 或 b64_json
            .build();

    // 构建带配置的 Prompt
    ImagePrompt promptWithOptions = new ImagePrompt(prompt, options);

    ImageResponse response = imageClient.call(promptWithOptions);
    return response.getResult().getOutput().getUrl();
}

ContactAuthor