Spring AI RAG 体验项目

Spring AI RAG 体验项目

一个rag应用包含如下几个阶段。为了方便搭建上线,很多阶段可以用云厂商的服务,不用自己准备资源、找开源实现啥的。本文会调用阿里云的百炼大模型。EmbaddingsRerankLLM阶段调用阿里云的服务,会产生费用。

原图: java2ai.com/docs/dev/pr...

依赖

java 21, spring boot 3.3.3, milvus 2.6.4.

github.com/zhouruibest...

spring ai alibaba 的仓库 github.com/spring-ai-a... , 包含许多 Example 模块项目来介绍 Spring AI 和 Spring AI Alibaba 从基础到高级的各种用法和 AI 项目的最佳实践。

主要依赖如下: pom.xml

xml 复制代码
<properties>
    <spring-ai.version>1.0.0-M5</spring-ai.version>
    <spring-ai-alibaba.version>1.0.0-M5.1</spring-ai-alibaba.version> 
</properties>

<dependencies>
    <dependency>
        <groupId>com.alibaba.cloud.ai</groupId>
        <artifactId>spring-ai-alibaba-starter</artifactId>
        <version>${spring-ai-alibaba.version}</version>
    </dependency>

    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-pdf-document-reader</artifactId>
        <version>${spring-ai.version}</version>
    </dependency>

    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-milvus-store</artifactId>
        <version>${spring-ai.version}</version>
    </dependency>
</dependencies>

项目配置

application.yaml

yaml 复制代码
spring:
  debug: true
  application:
    name: handbook
  ai:
    dashscope:
      api-key: ${MY_APP_API_KEY}

milvus:
  host: mydevcvm # 域名,配置了hosts
  port: 19530
  token: "root:Milvus"
  database: "ragtestdatebase"
  collection: "ragtestcollection"

MY_APP_API_KEY是配置在环境变量中的密钥,在百炼大模型的控制台取得,如下

向量数据库

单机部署的,参考 milvus基本概念和使用

java 复制代码
@Configuration
public class VectorStoreConfig {

    /**
     * 定义一个名为 milvusServiceClient 的Bean,用于创建并返回一个 MilvusServiceClient 实例。
     */
    @Bean
    public MilvusServiceClient milvusServiceClient() {
        try {
            // 创建 MilvusServiceClient 实例
            MilvusServiceClient client = new MilvusServiceClient(ConnectParam.newBuilder().withHost(host).withPort(port).build());

            // 测试连接状态 - 尝试列出所有数据库
            R<ListDatabasesResponse> response = client.listDatabases();

            if (response.getStatus() == R.Status.Success.getCode()) {
                logger.info("Milvus 连接成功! 服务器上的数据库数量: {}", response.getData().getDbNamesCount());
            } else {
                logger.error("Milvus 连接失败! 错误信息: {}", response.getMessage());
                throw new RuntimeException("Failed to connect to Milvus server: " + response.getMessage());
            }

            return client;
        } catch (Exception e) {
            logger.error("创建 MilvusServiceClient 失败! 主机: {}, 端口: {}", host, port, e);
            throw new RuntimeException("Failed to create MilvusServiceClient", e);
        }
    }

    /**
     * 定义一个名为 vectorStore2 的Bean,用于创建并返回一个 VectorStore 实例。
     */
    @Bean(name = "vectorStore2")
    public VectorStore vectorStore(MilvusServiceClient milvusClient, EmbeddingModel embeddingModel) {
        return MilvusVectorStore.builder(milvusClient, embeddingModel) //  嵌入模型实例,用于将文本转换为向量表示
                .collectionName(collection)
                .databaseName(database)
                .embeddingDimension(1536) //  向量嵌入维度,设置为1536,这通常与使用的嵌入模型(如OpenAI的text-embedding-ada-002)匹配
                .indexType(IndexType.IVF_FLAT) // 设置为IVF_FLAT(倒排文件Flat索引),是一种常用的近似最近邻搜索索引
                .metricType(MetricType.COSINE)
                .batchingStrategy(new TokenCountBatchingStrategy())
                .initializeSchema(false).build();
    }
}
  • @Bean(name = "vectorStore2") 起一个名字,否则会跟默认的冲突
  • 启动器spring-ai-alibaba-starter包含了阿里百炼(DashScope)相关模型的自动配置类,会根据配置自动创建 EmbeddingModel 的实现类实例, 是一个文本嵌入模型
  • initializeSchema参数控制在构建MilvusVectorStore时是否自动初始化数据库架构。启动项目时要事先在Milvus创建database, 不用创建collection。第一次运行项目时,initializeSchema参数为True,之后为False。

解析文档并存到向量数据库

向量数据库中的数据,可以在应用上线之前生成(可能数据量很大),也可以项目上线之后,不断调用接口投喂。

本例项目的 resources/docs/ 目录下放置了一个名为 handbook.pdf 文件,是22年的一份关于疫情奥密克戎的问答。该文件将被解析并进行向量化处理,以便后续的检索增强生成(RAG)流程能够基于此文档内容进行检索和验证。通过这一过程,可以评估RAG在本地知识库检索中的准确性和有效性。

java 复制代码
@GetMapping("/insertDocuments")
public String insertDocuments() throws IOException {
    // 1. parse document
    DocumentReader reader = new PagePdfDocumentReader(springAiResource);
    List<Document> documents = reader.get();
    logger.info("{} documents loaded", documents.size());

    // 2. split trunks
    List<Document> splitDocuments = new TokenTextSplitter().apply(documents);
    logger.info("{} documents split", splitDocuments.size());

    // 3. create embedding and store to vector store
    logger.info("create embedding and save to vector store");
    vectorStore.add(splitDocuments);

    return "success";
}

当调用 vectorStore.add(splitDocuments) 时,内部会执行以下操作:

  1. 遍历文档集合:对传入的 splitDocuments 列表中的每个 Document 对象进行处理
  2. 调用 EmbeddingModel:使用初始化时注入的 EmbeddingModel 对文档内容生成向量表示
  3. 向量存储:将生成的向量与文档元数据一起存储到 Milvus 向量数据库中

/resouces/docs/目录下面还包含了system-sa.st文件,内容如下。

复制代码
你是一个专业的智能问答助手。请根据以下提供的上下文信息来回答用户的问题。

如果上下文信息不足以回答用户的问题,请直接告知用户"根据我掌握的知识,暂时无法回答这个问题",不要编造答案。

上下文信息如下:
{{question_answer_context}}
  • 第二句: 如果上下文信息不足以回答用户的问题,请直接告知用户"根据我掌握的知识,暂时无法回答这个问题",不要编造答案。存在与否对结果影响很大,因为handbook.pdf文档内容很少,非常容易匹配不到。
  • question_answer_context是默认的一个占位符

ChatModel

chatModel是基于Spring AI框架的标准聊天模型接口,通过Spring AI Alibaba自动配置机制初始化,具体实现来自阿里通义千问(DashScope)大模型服务。

打印了一下具体调用的那个LLM, 是qwen-plus

java 复制代码
// Bean构建完成后执行

@PostConstruct
public void printInfo() {
    logger.info("Bean构建完成!打印信息:{}", chatModel.getDefaultOptions().toString());
}
bash 复制代码
Bean构建完成!打印信息:DashScopeChatOptions: {"proxyToolCalls":false,"model":"qwen-plus","temperature":0.8,"enable_search":false,"incremental_output":true,"multi_model":false}

文档问答

java 复制代码
/**
 * 根据用户输入的消息生成JSON格式的聊天响应。
 * 创建一个 SearchRequest 对象,设置返回最相关的前2个结果。
 * 从 systemResource 中读取提示模板。
 * 使用 ChatClient 构建聊天客户端,调用 RetrievalRerankAdvisor 进行检索和重排序,并生成最终的聊天响应内容。
 */
@GetMapping(value = "/ragJsonText", produces = MediaType.APPLICATION_STREAM_JSON_VALUE + ";charset=UTF-8")
public String ragJsonText(@RequestParam(value = "message", defaultValue = "今夕是何年?") String message) throws IOException {
    SearchRequest searchRequest = SearchRequest.builder().topK(2).build();

    String promptTemplate = systemResource.getContentAsString(StandardCharsets.UTF_8);

    return ChatClient.builder(chatModel)
            .defaultAdvisors(new RetrievalRerankAdvisor(vectorStore, rerankModel, searchRequest, promptTemplate, 0.8))
            .build().prompt().user(message).call().content();
}

RetrievalRerankAdvisor(com.alibaba.cloud.ai.advisor.RetrievalRerankAdvisor)是ChatClient的默认增强顾问,这里在使用的时候只是调整了一下参数。它的两个核心方法是beforedoRerank:before方法在调用大模型前执行预处理操作:

  • 它接收用户原始查询和提示模板
  • 调用vectorStore检索与用户查询相关的文档
  • 调用doRerank方法对检索结果进行重排序
  • 构建包含重排序后文档的上下文信息
  • 将上下文信息注入到提示模板中,生成完整的提示内容
  • 返回增强后的提示,供大模型生成回答使用

从vectorStore检索到的数据,需要先进行重排序,目的是规避向量检索的一些局限性。重排序依赖的rerankModel要调用百炼的服务。

下面是不使用重排序的一个版本。基本上是对before方法的一个重新实现。期望它能够根据handbook中的Q/A回答。

yaml 复制代码
public String ragJsonText2(@RequestParam(value = "message", defaultValue = "今夕是何年?") String message) throws IOException {

    SearchRequest searchRequest = SearchRequest.builder().query(message).topK(2).build();

    String promptTemplate = systemResource.getContentAsString(StandardCharsets.UTF_8);
    logger.info("使用的Prompt模板: {}", promptTemplate);
    logger.info("用户输入: {}", message);

    // 检索相关文档用于日志
    List<Document> relevantDocs = vectorStore.similaritySearch(searchRequest);
    logger.info("检索到的文档数量: {}", relevantDocs.size());

    StringBuilder contextBuilder = new StringBuilder();
    for (Document doc : relevantDocs) {
        // 可以添加文档相似度分数的日志
        Double score = doc.getScore();
        if (score instanceof Double) {
            logger.info("文档相似度分数: {}", score);
            if (score > 0.8) {
                contextBuilder.append(doc.getContent()).append("\n\n");
            }
        }
    }
    String context = contextBuilder.toString();
    logger.info("构建的上下文内容: {}", context);

    // 构建最终提交给大模型的完整prompt
    String finalPrompt = promptTemplate.replace("{{question_answer_context}}", context);
    logger.info("最终提交给大模型的完整Prompt:\n{}", finalPrompt);

    return ChatClient.builder(chatModel).build().prompt().user("用户问题: " + message + "\n" + finalPrompt).call().content();
}

测试验证

不使用重排序

http://localhost:8080/milvus2/ragJsonText2?message="现在我们身边一些老年人感染后病情就很重,那是不是老年人感染后一定会走向重症和危重症?"

markdown 复制代码
不是所有老年人感染后都会发展为重症或危重症,但老年人确实是高风险人群。

根据第九版诊疗指南,60岁以上的老年人被列为新冠病毒感染的高危人群,尤其是年龄越大、合并有基础疾病(如高血压、糖尿病、心衰等)的人,发展为重症的风险更高。但这并不意味着一旦感染就一定会走向重症。

关键在于**及时评估和密切观察病情变化**。老年人不必等到症状非常严重才就医,建议适当放宽住院评估标准。例如:

- 体温超过39℃持续3天;
- 活动后出现明显心慌、气促、胸闷;
- 原有基础疾病(如高血压、高血糖)控制不佳;

特别是糖尿病患者,由于高血糖环境容易导致继发感染且难以控制,感染后进展为重症的风险更高,更应加强监测。

因此,老年人感染后不必过度恐慌,但也不能掉以轻心,应密切观察身体状况,一旦出现预警信号,应及时就医,做到早干预、早治疗。

使用重排序

http://localhost:8080/milvus2/ragJsonText?message="现在我们身边一些老年人感染后病情就很重,那是不是老年人感染后一定会走向重症和危重症?"

复制代码
不是的,老年人感染后并不一定会走向重症或危重症。虽然老年人由于免疫力相对较弱、常伴有基础疾病(如高血压、糖尿病、心脑血管病等),确实是新冠病毒感染后发展为重症的高风险人群,但并不是所有老年感染者都会发展成重症。

通过及时接种疫苗、做好个人防护、早期监测和干预,很多老年人可以平稳度过感染期。临床数据显示,绝大多数老年感染者表现为轻症或普通型,只有少数未接种疫苗、有严重基础疾病或未能及时就医的人群才可能发展为重症或危重症。

因此,关键在于预防、早期识别症状和及时治疗。建议老年人尽早完成新冠疫苗全程接种和加强针,保持良好的生活习惯,感染后密切观察病情变化,一旦出现呼吸困难、持续高热、血氧饱和度下降等情况,应及时就医。

原文中的一页

css 复制代码
Q:现在我们身边一些老年人感染后病情就很重,那是不是老年人感染后一定会
走向重症和危重症?

A:文丹宁介绍,第九版指南说的高危人群,第一个就是 60 岁以上的老人。对
老人来说,年龄越大,有基础疾病的,风险就越大。但也并不是老年人一感染了,
就马上要去住院,这里需要做一个风险评估。老年人可以把评估指标放宽一点。
比如说,我们普通人说高烧 5 天才去医院,那么老年人如果体温超过了 39℃有
3 天,或者在活动之后有心慌、气促、胸闷,或者基础疾病如高血压、高血糖、
心衰等控制不好了,还是建议及时去医院。
糖尿病人因为糖尿病更容易继发感染,出现感染后特别不容易控制。人的血
糖升高,血液就像一个病原体的培养基。所以糖尿病人很容易继发感染。我们现
在碰到的重症,糖尿病人会多一点。所以,老年人或者患有糖尿病等基础疾病的
中青年感染了奥密克戎后,一定要密切观察病情变化,不能一味硬扛。

比较而言,不使用的好一些。

其他

  • 看了一些博客、仓库发现,在提示模板中加"如果上下文信息不足以回答用户的问题,请直接告知用户"根据我掌握的知识,暂时无法回答这个问题",不要编造答案。"是QA这一类rag的通用的做法
  • 文档内容不够丰富是回答质量差的主要原因
相关推荐
易协同低代码14 小时前
通达OA核心类库TD类深度解析
后端
Gopher_HBo14 小时前
Go语言学习笔记(十八)Gin处理Session
后端
谭光志15 小时前
工具塞满上下文窗口怎么办?深度拆解 AI Agent Tool Search 按需加载实现原理
前端·后端·ai编程
她说..15 小时前
Java 默认值设置方式
java·开发语言·后端·springboot
foggyprojects15 小时前
从0开始,一句话启动AI DataAgent
后端·数据分析·ai编程
郡杰15 小时前
一些基础和问题解决
后端
陈随易15 小时前
前端项目部署只要30秒
前端·后端·程序员
YIAN15 小时前
从零手写文件读取 MCP 服务:一文吃透 Model Context Protocol 全链路通信原理
前端·后端·mcp
Imchendiana15 小时前
《狂人日记NO.9》— 前后端一把梭,我的全栈实录
前端·后端
学渣超15 小时前
记一次分布式事务数据不一致的排查之旅:从超时到索引,层层剥茧
java·后端·架构