Skip to content

RAG

RAG原理

image-20251117154819954

矢量距离

通过计算两个向量之间的距离,可以判断向量相似度。

欧式距离越小,相似度越高;余弦距离越大,相似度越高。

image-20251117155016859

java
package com.itheima.ai.utils;

/**
 * 矢量距离工具类
 */
public class VectorDistanceUtils {

    // 防止实例化
    private VectorDistanceUtils() {
    }

    // 浮点数计算精度阈值
    private static final double EPSILON = 1e-12;

    /**
     * 计算欧氏距离
     *
     * @param vectorA 向量A(非空且与B等长)
     * @param vectorB 向量B(非空且与A等长)
     * @return 欧氏距离
     * @throws IllegalArgumentException 参数不合法时抛出
     */
    public static double euclideanDistance(float[] vectorA, float[] vectorB) {
        validateVectors(vectorA, vectorB);

        double sum = 0.0;
        for (int i = 0; i < vectorA.length; i++) {
            double diff = vectorA[i] - vectorB[i];
            sum += diff * diff;
        }
        return Math.sqrt(sum);
    }

    /**
     * 计算余弦距离
     *
     * @param vectorA 向量A(非空且与B等长)
     * @param vectorB 向量B(非空且与A等长)
     * @return 余弦距离,范围[0, 2]
     * @throws IllegalArgumentException 参数不合法或零向量时抛出
     */
    public static double cosineDistance(float[] vectorA, float[] vectorB) {
        validateVectors(vectorA, vectorB);

        double dotProduct = 0.0;
        double normA = 0.0;
        double normB = 0.0;

        for (int i = 0; i < vectorA.length; i++) {
            dotProduct += vectorA[i] * vectorB[i];
            normA += vectorA[i] * vectorA[i];
            normB += vectorB[i] * vectorB[i];
        }

        normA = Math.sqrt(normA);
        normB = Math.sqrt(normB);

        // 处理零向量情况
        if (normA < EPSILON || normB < EPSILON) {
            throw new IllegalArgumentException("Vectors cannot be zero vectors");
        }

        // 处理浮点误差,确保结果在[-1,1]范围内
        double similarity = dotProduct / (normA * normB);
        similarity = Math.max(Math.min(similarity, 1.0), -1.0);

        return similarity;
    }

    // 参数校验统一方法
    private static void validateVectors(float[] a, float[] b) {
        if (a == null || b == null) {
            throw new IllegalArgumentException("Vectors cannot be null");
        }
        if (a.length != b.length) {
            throw new IllegalArgumentException("Vectors must have same dimension");
        }
        if (a.length == 0) {
            throw new IllegalArgumentException("Vectors cannot be empty");
        }
    }
}

单元测试:

java
    @Test
    void contextLoads() {
        // 1.测试数据
        // 1.1.用来查询的文本,国际冲突
        String query = "global conflicts";

        // 1.2.用来做比较的文本
        String[] texts = new String[]{
                "哈马斯称加沙下阶段停火谈判仍在进行 以方尚未做出承诺",
                "土耳其、芬兰、瑞典与北约代表将继续就瑞典“入约”问题进行谈判",
                "日本航空基地水井中检测出有机氟化物超标",
                "小明今天很开心,因为他喝到了自己想喝的牛奶",
                "我国首次在空间站开展舱外辐射生物学暴露实验",
        };
        // 2.向量化
        // 2.1.先将查询文本向量化
        float[] queryVector = embeddingModel.embed(query);

        // 2.2.再将比较文本向量化,放到一个数组
        List<float[]> textVectors = embeddingModel.embed(Arrays.asList(texts));

        // 3.比较欧氏距离
        // 3.1.把查询文本自己与自己比较,肯定是相似度最高的
        System.out.println("自己比较自己的欧式距离:" + VectorDistanceUtils.euclideanDistance(queryVector, queryVector));
        // 3.2.把查询文本与其它文本比较
        for (int i = 0; i < textVectors.size(); i++) {
            System.out.println(String.format("相似度:%s -> %s = ", query, texts[i]) + VectorDistanceUtils.euclideanDistance(queryVector, textVectors.get(i)));
        }
        System.out.println("------------------");

        // 4.比较余弦距离
        // 4.1.把查询文本自己与自己比较,肯定是相似度最高的
        System.out.println("自己比较自己的余弦距离:" + VectorDistanceUtils.cosineDistance(queryVector, queryVector));
        // 4.2.把查询文本与其它文本比较
        for (int i = 0; i < textVectors.size(); i++) {
            System.out.println(String.format("余弦距离:%s -> %s = ", query, texts[i]) + VectorDistanceUtils.cosineDistance(queryVector, textVectors.get(i)));
        }
    }

结果:

自己比较自己的欧式距离:0.0
相似度:global conflicts -> 哈马斯称加沙下阶段停火谈判仍在进行 以方尚未做出承诺 = 1.0722205301828829
相似度:global conflicts -> 土耳其、芬兰、瑞典与北约代表将继续就瑞典“入约”问题进行谈判 = 1.0844350869313875
相似度:global conflicts -> 日本航空基地水井中检测出有机氟化物超标 = 1.1185223356097924
相似度:global conflicts -> 小明今天很开心,因为他喝到了自己想喝的牛奶 = 1.1930812071817303
相似度:global conflicts -> 我国首次在空间站开展舱外辐射生物学暴露实验 = 1.1499045763089124
------------------
自己比较自己的余弦距离:0.9999999999999998
余弦距离:global conflicts -> 哈马斯称加沙下阶段停火谈判仍在进行 以方尚未做出承诺 = 0.4251716163869882
余弦距离:global conflicts -> 土耳其、芬兰、瑞典与北约代表将继续就瑞典“入约”问题进行谈判 = 0.41200032867283726
余弦距离:global conflicts -> 日本航空基地水井中检测出有机氟化物超标 = 0.37445397231274447
余弦距离:global conflicts -> 小明今天很开心,因为他喝到了自己想喝的牛奶 = 0.2882787076676059
余弦距离:global conflicts -> 我国首次在空间站开展舱外辐射生物学暴露实验 = 0.3388597327534832

配置向量模型

yaml
spring:
  ai:
      # 向量模型
      embedding:
        options:
          model: text-embedding-v3
          # 维度
          dimensions: 1024

阿里云模型广场:https://bailian.console.aliyun.com/?tab=model#/model-market/all

详细:https://bailian.console.aliyun.com/?tab=model#/model-market/detail/text-embedding-v3

向量数据库

image-20251117155641754

主流的向量数据库:

image-20251117155754683

Spring官方文档:Vector Databases :: Spring AI Reference

引入依赖:

pom.xml

xml
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-redis-store-spring-boot-starter</artifactId>
</dependency>

配置向量数据库:

yaml
spring:
  ai:
    vectorstore:
      redis:
        index: spring_ai_index  # 向量库索引名
        initialize-schema: true  # 是否初始化向量库索引结构
        prefix: "doc:"  # 向量库key前缀
  data:
    redis:
      host: 127.0.0.1
      port: 6379  # Redis端口,默认6379

注入bean(使用Spring-ai自带的简易向量库):

java
    @Bean
    public VectorStore vectorStore(OpenAiEmbeddingModel embeddingModel) {
        return SimpleVectorStore.builder(embeddingModel).build();
    }

数据读写接口:

java
public interface VectorStore extends DocumentWriter {

    // 添加文档列表到向量存储
    void add(List<Document> documents);

    // 根据文档ID列表删除文档
    void delete(List<String> idList);

    // 根据过滤表达式删除文档
    void delete(Filter.Expression filterExpression);

    // 相似性搜索(可能返回null)
    @Nullable
    List<Document> similaritySearch(String query);
}

Redis向量数据库

docker官网:https://hub.docker.com/r/redis/redis-stack

  • redis/redis-stack:包含Redis Stack服务器和RedisInsight。此容器最适合本地开发,可以使用RedisInsight来可视化数据
  • redis/redis-stack-server:提供Redis Stack,但不包括RedisInsight。此容器最适合生产部署

拉取镜像:

redis/redis-stack-server

docker pull redis/redis-stack:7.4.0-v8

redis/redis-stack-server

docker pull redis/redis-stack-server:7.4.0-v8

运行命令:

docker run -d --name redis-stack -p 6379:6379 -p 8001:8001 -e REDIS_ARGS="--requirepass mypassword" redis/redis-stack:latest

Web界面可以通过访问:http://localhost:8001/即可

PDF处理

image-20251118103040333

引入依赖

pom.xml

xml
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-pdf-document-reader</artifactId>
</dependency>

文档处理

官方文档:ETL Pipeline :: Spring AI Reference

java
private void writeFileToVectorStore(Resource resource) {

    // 1. 创建PagePdfDocumentReader
    PagePdfDocumentReader reader = new PagePdfDocumentReader(
        resource, // 文件源
        PdfDocumentReaderConfig.builder()
                .withPageExtractedTextFormatter(ExtractedTextFormatter.defaults())
                .withPagesPerDocument(1) // 每1页PDF作为一个Document
                .build()
    );

    // 2. 读取PDF文件为Document格式
    List<Document> documents = reader.read();

    // 3. 将文档写入向量数据库
    vectorStore.add(documents);
}

测试用例

java
    public void testVectorStore() {
        Resource resource = new FileSystemResource("详细需求说明.pdf");
        // 1.创建PDF的读取器
        PagePdfDocumentReader reader = new PagePdfDocumentReader(
                resource, // 文件源
                PdfDocumentReaderConfig.builder()
                        .withPageExtractedTextFormatter(ExtractedTextFormatter.defaults())
                        .withPagesPerDocument(1) // 每1页PDF作为一个Document
                        .build()
        );
        // 2.读取PDF文档,拆分为Document
        List<Document> documents = reader.read();
        // 3.写入向量库
        vectorStore.add(documents);
        // 4.搜索
        SearchRequest request = SearchRequest.builder()
                .query("创作者中心是什么?")
                .topK(3) // 获取相似度最高的前3个
                .similarityThreshold(0.6) // 相似度阈值
                .filterExpression("file_name == '详细需求说明.pdf'") // 过滤无关的文档
                .build();
        List<Document> docs = vectorStore.similaritySearch(request);
        if (docs == null) {
            System.out.println("没有搜索到任何内容");
            return;
        }
        for (Document doc : docs) {
            System.out.println(doc.getId());
            System.out.println(doc.getScore());
            System.out.println(doc.getText());
        }
    }

文档过滤表达式的写法:Vector Databases :: Spring AI Reference

2025-11-17 16:46:11.999  INFO [] 45200 [                main] org.springframework.ai.reader.pdf.PagePdfDocumentReader      - (PagePdfDocumentReader.java:115) [get,115]                   : Processing PDF page: 1
2025-11-17 16:46:13.348  INFO [] 45200 [                main] org.springframework.ai.reader.pdf.PagePdfDocumentReader      - (PagePdfDocumentReader.java:115) [get,115]                   : Processing PDF page: 2
2025-11-17 16:46:13.702  INFO [] 45200 [                main] org.springframework.ai.reader.pdf.PagePdfDocumentReader      - (PagePdfDocumentReader.java:115) [get,115]                   : Processing PDF page: 3
2025-11-17 16:46:13.996  INFO [] 45200 [                main] org.springframework.ai.reader.pdf.PagePdfDocumentReader      - (PagePdfDocumentReader.java:115) [get,115]                   : Processing PDF page: 4
2025-11-17 16:46:14.334  INFO [] 45200 [                main] org.springframework.ai.reader.pdf.PagePdfDocumentReader      - (PagePdfDocumentReader.java:115) [get,115]                   : Processing PDF page: 5
2025-11-17 16:46:14.673  INFO [] 45200 [                main] org.springframework.ai.reader.pdf.PagePdfDocumentReader      - (PagePdfDocumentReader.java:115) [get,115]                   : Processing PDF page: 6
2025-11-17 16:46:15.005  INFO [] 45200 [                main] org.springframework.ai.reader.pdf.PagePdfDocumentReader      - (PagePdfDocumentReader.java:115) [get,115]                   : Processing PDF page: 7
2025-11-17 16:46:15.287  INFO [] 45200 [                main] org.springframework.ai.reader.pdf.PagePdfDocumentReader      - (PagePdfDocumentReader.java:115) [get,115]                   : Processing PDF page: 8
2025-11-17 16:46:15.584  INFO [] 45200 [                main] org.springframework.ai.reader.pdf.PagePdfDocumentReader      - (PagePdfDocumentReader.java:115) [get,115]                   : Processing PDF page: 9
2025-11-17 16:46:15.948  INFO [] 45200 [                main] org.springframework.ai.reader.pdf.PagePdfDocumentReader      - (PagePdfDocumentReader.java:115) [get,115]                   : Processing PDF page: 10
2025-11-17 16:46:17.960  INFO [] 45200 [                main] org.springframework.ai.reader.pdf.PagePdfDocumentReader      - (PagePdfDocumentReader.java:157) [get,157]                   : Processing 16 pages
2025-11-17 16:46:22.585  INFO [] 45200 [                main] org.springframework.ai.vectorstore.SimpleVectorStore         - (SimpleVectorStore.java:113) [doAdd,113]                     : Calling EmbeddingModel for document id = 741fb7f5-d9dc-43d9-a39b-9448494b5a86
2025-11-17 16:46:22.937  INFO [] 45200 [                main] org.springframework.ai.vectorstore.SimpleVectorStore         - (SimpleVectorStore.java:113) [doAdd,113]                     : Calling EmbeddingModel for document id = 62710064-37fb-4d95-8788-6cdd270394ab
2025-11-17 16:46:23.283  INFO [] 45200 [                main] org.springframework.ai.vectorstore.SimpleVectorStore         - (SimpleVectorStore.java:113) [doAdd,113]                     : Calling EmbeddingModel for document id = 85f324f7-2e92-4d34-818e-d838b016d605
2025-11-17 16:46:23.730  INFO [] 45200 [                main] org.springframework.ai.vectorstore.SimpleVectorStore         - (SimpleVectorStore.java:113) [doAdd,113]                     : Calling EmbeddingModel for document id = 6dcf703a-8318-475f-ab0f-29238e01b6c0
2025-11-17 16:46:24.120  INFO [] 45200 [                main] org.springframework.ai.vectorstore.SimpleVectorStore         - (SimpleVectorStore.java:113) [doAdd,113]                     : Calling EmbeddingModel for document id = cb4439b1-7dd5-429f-9772-d05db260bb52
2025-11-17 16:46:24.492  INFO [] 45200 [                main] org.springframework.ai.vectorstore.SimpleVectorStore         - (SimpleVectorStore.java:113) [doAdd,113]                     : Calling EmbeddingModel for document id = 07fba5d3-6575-427c-a7d9-23eafff79fc1
2025-11-17 16:46:24.817  INFO [] 45200 [                main] org.springframework.ai.vectorstore.SimpleVectorStore         - (SimpleVectorStore.java:113) [doAdd,113]                     : Calling EmbeddingModel for document id = 064e25f3-deb9-41f4-b1d7-3ac250575faf
2025-11-17 16:46:25.332  INFO [] 45200 [                main] org.springframework.ai.vectorstore.SimpleVectorStore         - (SimpleVectorStore.java:113) [doAdd,113]                     : Calling EmbeddingModel for document id = 6c627cee-4593-45e2-91f2-e34e28fd116a
2025-11-17 16:46:25.695  INFO [] 45200 [                main] org.springframework.ai.vectorstore.SimpleVectorStore         - (SimpleVectorStore.java:113) [doAdd,113]                     : Calling EmbeddingModel for document id = 4bb4df76-f4c8-4f32-b900-86cb92bd4ad1
2025-11-17 16:46:26.038  INFO [] 45200 [                main] org.springframework.ai.vectorstore.SimpleVectorStore         - (SimpleVectorStore.java:113) [doAdd,113]                     : Calling EmbeddingModel for document id = fb9db470-0f83-4708-82d2-88ca3ea4e1e5
2025-11-17 16:46:26.438  INFO [] 45200 [                main] org.springframework.ai.vectorstore.SimpleVectorStore         - (SimpleVectorStore.java:113) [doAdd,113]                     : Calling EmbeddingModel for document id = aba8c9ad-1e21-4bc6-a321-f741ad5104cf
2025-11-17 16:46:26.826  INFO [] 45200 [                main] org.springframework.ai.vectorstore.SimpleVectorStore         - (SimpleVectorStore.java:113) [doAdd,113]                     : Calling EmbeddingModel for document id = cccebd58-9f1f-484b-9c07-69645b6c1a44
2025-11-17 16:46:27.332  INFO [] 45200 [                main] org.springframework.ai.vectorstore.SimpleVectorStore         - (SimpleVectorStore.java:113) [doAdd,113]                     : Calling EmbeddingModel for document id = 80a93331-0cce-42ee-9225-3945f25c06a8
2025-11-17 16:46:27.869  INFO [] 45200 [                main] org.springframework.ai.vectorstore.SimpleVectorStore         - (SimpleVectorStore.java:113) [doAdd,113]                     : Calling EmbeddingModel for document id = 2a9aac4f-652f-44df-8677-0a8cfbadabcb
2025-11-17 16:46:28.239  INFO [] 45200 [                main] org.springframework.ai.vectorstore.SimpleVectorStore         - (SimpleVectorStore.java:113) [doAdd,113]                     : Calling EmbeddingModel for document id = d860a29b-f3f5-47b5-a84c-518fba67e139
cb4439b1-7dd5-429f-9772-d05db260bb52
0.617596227455027

                            统计:分享→回流→购买归因。数据点                                                                                                                                                                   

                      5、创作者中心(小程序&Web 创作者后台)                                                                                                                                                                    

                            (1)资质与结算                                                                                                                                                                            

                            实名/资质上传,协议签署(电子签)                               ;打款账户(对公/对私)                     ,税务信                                                                                               

                      息。                                                                                                                                                                                        

                            收益:按结算周期(T+7/T+15)出账单;比率=平台抽成+分润;对账导                                                                                                                                                

                      出。                                                                                                                                                                                        

                            (2)内容管理                                                                                                                                                                             

                            新建剧/分集:批量分片上传、断点续传、MD5 秒传;自动转码(多码率                                                                                                                                                  

                      HLS)  、生成封面。                                                                                                                                                                              

                            元数据:名称、简介、标签、适龄提示、题材。                                                                                                                                                               

                            定价策略:单集价、整部价、限时折扣、券码(张数/生效时间/适用范                                                                                                                                                    

                      围)   。                                                                                                                                                                                    

                            上下架/版本化:草稿→待审→已上架→下架;内容变更走二次审核。                                                                                                                                                     

                            违规处理:告知单、可申诉通道。                                                                                                                                                                     

                            (3)数据看板                                                                                                                                                                             

                            播放:VV(视频播放次数)                      、UV(独立访客数)               、完播率、播放时长、30s                                                                                                           

                      留存。                                                                                                                                                                                       

                            {                                                                                                                                                                                   

                               VV(Video       View)   :视频播放次数,也叫播放量。一次用户点击播放算一                                                                                                                                  

                      次 VV(部分业务会定义至少播放几秒才算有效 VV)                                        。                                                                                                                       

                               UV(Unique        Visitor)    :独立访客数,也叫独立观看人数。一天内同一个                                                                                                                              

                      用户无论播放多少次,只算一个 UV。                                                                                                                                                                        

                               完播率(Completion               Rate)  :用户把视频完整看完的比例=完播人数÷播                                                                                                                         

                      放人数。

引入项目

配置RAG Advisor

java
    @Bean
    public ChatClient pdfChatClient(OpenAiChatModel model, ChatMemory chatMemory, VectorStore vectorStore) {
        return ChatClient
                .builder(model)
                .defaultSystem("请根据上下文回答问题,遇到上下文没有的问题,不要随意编造。")
                .defaultAdvisors(
                        new SimpleLoggerAdvisor(),
                        new MessageChatMemoryAdvisor(chatMemory), // 会话历史记录
                        new QuestionAnswerAdvisor(
                                vectorStore, // 向量库
                                SearchRequest.builder() // 向量检索的请求参数
                                        .similarityThreshold(0.6) // 相似度阈值
                                        .topK(2) // 返回满足条件的前两个
                                        .build()
                        )
                )
                .build();
    }

对话检索:

java
    public Flux<String> chat(String prompt, String chatId) {
        return pdfChatClient.prompt()
                .user(prompt)
                // 传递会话历史给MessageChatMemoryAdvisor
                .advisors(a -> a.param(CHAT_MEMORY_CONVERSATION_ID_KEY, chatId))
                // 传递文件过滤表达式给QuestionAnswerAdvisor
                .advisors(a -> a.param(FILTER_EXPRESSION, "file_name == '" + file.getFilename() + "'"))
                .stream()
                .content();
    }