本文代码:
https://github.com/JunTeamCom/ai-demo/tree/release-5.0
https://github.com/JunTeamCom/ai-demo-tools/tree/release-5.0/data-loader
本章讲解查询向量数据库、RAG应用。
Spring with AI系列,只关注上层AI的应用程序(基于JAVA搭建),不关注底层的LLM原理、搭建等技术。
RAG能通过实时搜索数据库的方式,扩展已经训练固化的大模型的知识能力。
RAG(R etrieval-A ugmented G eneration)检索增强生成
retrieval /rɪˈtriːvl/ n.检索
augmented /ɔ:g'mentɪd/ adj.增强的
generation /ˌdʒenəˈreɪʃn/ n.生成
通过检索、增强大模型生成的内容。
在前文中,我们通过文本模板,补充了"规则",进而增强了大模型生成的内容。
然而这只是简单的"文本匹配+配置文件"的方式,面对复杂问题和庞大的知识库,完全不具备现实意义。
RAG整体的流程和组成部分如下:
flowchart LR subgraph VectorDB[向量数据库模块] VectorStore[(向量数据库)]:::db end subgraph DocumentLoader[文档加载模块] OriginalDocs[① 原始文档] --> Splitter[文档分割器]:::process Splitter --> DocChunks[② 文档分块] DocChunks --> CalcEmb[③ 计算向量嵌入]:::process CalcEmb -->|存入| VectorStore end subgraph RAGApp[支持RAG的应用] Question[问题] --> App[④ 应用]:::app App -->|搜索相似文档| VectorStore VectorStore -->|返回相似文档| App App --> Prompt[⑤ 组装提示词] Prompt --> LLM[大语言模型] LLM --> Answer[答案] Answer --> App App --> Output[输出答案] end classDef process fill:#a5d6a7,stroke:#388e3c,stroke-width:2px classDef app fill:#ffab91,stroke:#e64a19,stroke-width:2px classDef db fill:#bbdefb,stroke:#1976d2,stroke-width:2px classDef llm fill:#ffcc80,stroke:#f57c00,stroke-width:2px
本Demo的具体场景化,上图变为:
具体场景的流程如下:
flowchart LR subgraph VectorDB[向量数据库模块] VectorStore[(🗄️ 向量数据库<br>支持按国家/语义检索)]:::db end subgraph DataLoader[数据加载与预处理模块] direction TB OriginalDocs[📚 原始文档<br>国家历史地理风俗] --> EntityExtract[🏷️ 大模型自动抽取<br>国家名称 & 描述信息] EntityExtract --> Splitter[✂️ 文档分割器] Splitter --> DocChunks[📄 文档分块<br>含元数据: 国家/地区] DocChunks --> CalcEmb[🧮 计算向量嵌入] CalcEmb -->|存入向量与元数据| VectorStore end subgraph RAGApp[智能问答应用] direction TB Question[❓ 用户问题<br>例: 日本有哪些传统节日?] --> App[④ 应用入口]:::app App -->|1. 问题分析与意图识别| IntentCheck{是否涉及<br>已知国家/地区?} IntentCheck -->|是| SearchByCountry[🔍 按国家元数据过滤] IntentCheck -->|否| SemanticSearch[🔍 纯语义相似搜索] SearchByCountry -->|组合查询| VectorStore SemanticSearch --> VectorStore VectorStore -->|返回相似文档块<br>+ 所属国家信息| App App --> Prompt[⑤ 组装增强提示词<br>包含: 检索到的风俗/地理描述<br>+ 原始问题] Prompt --> LLM[🤖 大语言模型]:::llm LLM --> Answer[✨ 增强答案<br>例: 依据日本文化资料,主要节日有...] Answer --> App App --> Output[📢 输出答案] end classDef process fill:#a5d6a7,stroke:#388e3c,stroke-width:2px classDef app fill:#ffab91,stroke:#e64a19,stroke-width:2px classDef db fill:#bbdefb,stroke:#1976d2,stroke-width:2px classDef llm fill:#ffcc80,stroke:#f57c00,stroke-width:2px
可以看到:
RAG,通过搜索能力、增强大模型生成内容的质量。
核心的一个误解,就是搜索能力不是在大模型生成完之后补充结果、也不是把搜索内容导入到大模型内部。
而是组装到发给大模型的提示词里。
1 依赖与配置
1.1 Qdrant配置
回到AI-Demo项目,添加向量数据库和RAG依赖:
spring-ai-starter-vector-store-qdrant(通过Qdrant搜索Starter)
spring-ai-advisors-vector-store(通过Advisors Vector搜索Dependency)
spring-ai-rag(通过Spring RAG搜索Dependency)
配置数据源(注意在Qdrant中配置好API Key、并配置到环境变量):
yaml
spring:
application:
name: ai-demo
ai:
openai:
base-url: https://dashscope.aliyuncs.com/compatible-mode # Qwen的OpenAI式Endpoint
api-key: ${DASHSCOPE_API_KEY}
chat:
options:
model: qwen3.5-plus
embedding: # 新增嵌入模型配置
options:
model: text-embedding-v2 # 阿里云支持的嵌入模型
vectorstore:
qdrant:
host: 127.0.0.1
port: 6334
api-key: ${QDRANT_API_KEY}
initialize-schema: true
collection-name: ai_demo
然后启动Qdrant服务(参考上一节)。
1.2 Lombok配置
为了方便打日志、构建实体等,添加Lombok依赖与插件(初始化创建项目时,可以直接添加Starter实现):
xml
<!-- pom.xml配置-->
<!-- 1. 依赖配置 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- 2. 插件配置 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
1.3 Prompt模板配置
修改Prompt模板,完全基于搜索内容进行推理:
plaintext
你是一个有用的助手,负责回答有关{countryTitle}的历史地理风俗问题。
根据"文档"部分中的上下文且无先验知识,回答用户关于历史地理风俗的问题。
如果答案不在"文档"部分,请回复"我不知道"。
文档:
----------
{rules}
----------
而填入的{rules},就是上一节导入向量数据库的内容。
2 代码修改
2.1 规则定义
搜索向量数据库,根据"问题+国家"搜索信息。
java
package com.junteam.ai.demo.service;
import java.util.stream.Collectors;
import org.springframework.ai.document.Document;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.ai.vectorstore.filter.FilterExpressionBuilder;
import org.springframework.stereotype.Service;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Service
public class ChatRulesService {
private VectorStore vectorStore;
public ChatRulesService(VectorStore vectorStore) {
this.vectorStore = vectorStore;
}
public String getRulesFor(String countryTitle, String question) {
var searchRequest = SearchRequest
.builder()
.query(question)
.filterExpression(new FilterExpressionBuilder()
.eq("countryTitle", countryTitle)
.build())
.build();
log.info("Search Request:" + searchRequest);
var similarDocuments = vectorStore.similaritySearch(searchRequest);
if (similarDocuments.isEmpty()) {
return countryTitle + "的信息不可用。";
}
return similarDocuments.stream()
.map(Document::getText)
.collect(Collectors.joining(System.lineSeparator()));
}
}
2.2 规则使用
将上一步从向量数据库搜索到的信息(system方法调用、文本嵌入)、以及问题(user方法调用、聊天的问题),拼装到大模型。
java
package com.junteam.ai.demo.service.impl;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Service;
import com.junteam.ai.demo.model.ChatAnswer;
import com.junteam.ai.demo.model.ChatQuestion;
import com.junteam.ai.demo.service.ChatRulesService;
import com.junteam.ai.demo.service.ChatService;
@Service
public class OpenAIChatServiceImpl implements ChatService {
private final ChatClient chatClient;
private final ChatRulesService chatRulesService;
public OpenAIChatServiceImpl(ChatClient.Builder chatClientBuilder, ChatRulesService chatRulesService) {
this.chatClient = chatClientBuilder.build();
this.chatRulesService = chatRulesService;
}
@Value("classpath:/promptTemplates/questionPromptTemplate.st")
Resource questionPromptTemplate;
@Override
public ChatAnswer ask(ChatQuestion chatQuestion) {
var chatRules = chatRulesService.getRulesFor(chatQuestion.title(), chatQuestion.question());
var answer = chatClient
.prompt()
.system(systemSpec -> systemSpec
.text(questionPromptTemplate)
.param("countryTitle", chatQuestion.title())
.param("rules", chatRules))
.user(chatQuestion.question())
.call();
var answerText = answer.content();
return new ChatAnswer(chatQuestion.title(), answerText);
}
}
3 测试验证
bash
curl http://localhost:8080/web/ask \
-X POST \
-H "Content-Type: application/json" \
-d '{"title": "德国", "question": "购物一般有哪些渠道?"}'
返回结果形如:
json
{
"title":"德国",
"answer":"根据文档,德国购物一般有以下渠道:\n\n1. **网购**:一般使用易贝、亚马逊平台。\n2. **超市**:买东西一般去超市。\n3. **生活用品店**:购买常常去 IDEL。\n4. **化妆品护肤品店**:一般去 ROSSMAN、DM 购买。"
}
整理出内容如下:
根据文档,德国购物一般有以下渠道:
- 网购:一般使用易贝、亚马逊平台。
- 超市:买东西一般去超市。
- 生活用品店:购买常常去 IDEL。
- 化妆品护肤品店:一般去 ROSSMAN、DM 购买。
可见是"向量数据库的知识"+"大模型的推理能力",组合实现了答案输出。
RAG的大部分工作都在实际提示词发送给大模型之前完成。
4 顾问选项
RAG在Spring AI中的这种工作模式,称为"顾问"(Advisor)。
4.1 问答顾问
默认的顾问选项,称为"问答顾问"(QuestionAnswerAdvisor);
可以通过.advisors()方法修改控制:
java
var answer = chatClient
.prompt()
.system(systemSpec -> systemSpec
.text(questionPromptTemplate)
.param("countryTitle", chatQuestion.title())
.param("rules", chatRules))
.user(chatQuestion.question())
.advisors(
QuestionAnswerAdvisor.builder(vectorStore).build())
.call();
var answerText = answer.content();
或者可以定义Config类,在ChatClient的构建过程中定义:
java
@Configuration
public class AiConfig {
@Bean
ChatClient chatClient(
ChatClient.Builder chatClientBuilder, VectorStore vectorStore) {
return chatClientBuilder
.defaultAdvisors(
QuestionAnswerAdvisor.builder(vectorStore).build())
.build();
}
}
4.2 增强搜索顾问
增强搜索顾问(RetrievalAugmentationAdvisor)提供更加灵活强大的顾问选项:
java
package com.junteam.ai.demo.config;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.rag.advisor.RetrievalAugmentationAdvisor;
import org.springframework.ai.rag.retrieval.search.VectorStoreDocumentRetriever;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AiConfig {
@Bean
ChatClient chatClient(ChatClient.Builder chatClientBuilder, VectorStore vectorStore) {
// 构建RAG顾问
var advisor = RetrievalAugmentationAdvisor.builder()
.documentRetriever(VectorStoreDocumentRetriever.builder()
.vectorStore(vectorStore)
.build())
.build();
// 设置顾问配置项
return chatClientBuilder
.defaultAdvisors(advisor)
.build();
}
}
可以修改.documentRetriever()方法的构造入参,实现多种顾问方式。
5 补充优化
基于RetrievalAugmentationAdvisor,一些对聊天问题的重要扩充项:
翻译、重写(简化)、扩展相关话题
java
package com.junteam.ai.demo.config;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.rag.advisor.RetrievalAugmentationAdvisor;
import org.springframework.ai.rag.preretrieval.query.expansion.MultiQueryExpander;
import org.springframework.ai.rag.preretrieval.query.transformation.RewriteQueryTransformer;
import org.springframework.ai.rag.preretrieval.query.transformation.TranslationQueryTransformer;
import org.springframework.ai.rag.retrieval.search.VectorStoreDocumentRetriever;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AiConfig {
@Bean
ChatClient chatClient(ChatClient.Builder chatClientBuilder, VectorStore vectorStore) {
// 构建RAG顾问
var advisor = RetrievalAugmentationAdvisor.builder()
.documentRetriever(VectorStoreDocumentRetriever.builder()
.vectorStore(vectorStore)
.build())
// 对聊天的问题进行加工处理
.queryTransformers(
// 翻译问题
TranslationQueryTransformer.builder()
.chatClientBuilder(chatClientBuilder)
.targetLanguage("Chinese")
.build(),
// 重写(简化)问题
RewriteQueryTransformer.builder()
.chatClientBuilder(chatClientBuilder)
.build())
// 扩展相关话题(扩展5个)
.queryExpander(MultiQueryExpander.builder()
.chatClientBuilder(chatClientBuilder)
.numberOfQueries(5)
.includeOriginal(false)
.build())
.build();
// 设置顾问配置项
return chatClientBuilder
.defaultAdvisors(advisor)
.build();
}
}