目录

一、Embedding 介绍

二、Embedding 使用示例

1、智普Embedding

pom.xml

application.properties

EmbeddingController

2、查找相似文本

EmbeddingService

3、RAG本地知识库检索

RAG 的核心思想

RAG 的工作流程

RagService

RagController

4、RAG本地知识库检索升级

手动切分文档的常见问题

1. 语义割裂(Semantic Fragmentation)

2. 信息冗余或遗漏

3. 缺乏一致性

4. 难以适应多语言/复杂格式

RagService2


一、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 在生成答案前,会:

  1. 从外部知识库中检索与用户问题最相关的文档片段;
  2. 将这些片段作为上下文,连同问题一起输入给 LLM;
  3. 让 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);
}

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐