本文中,我们将探讨如何借助Spring AI为项目添加AI相关功能,并对RAG(检索增强生成)进行初步了解,并且给出了使用到以上技术的具有摘要生成、对话功能的项目示例。
引言
RAG的作用
Spring AI支持聊天对话、记忆和检索增强生成(RAG),此功能允许我们为项目添加AI相关功能,如和文章对话、生成摘要等。
何谓RAG(检索增强生成)?
矢量数据库(Vector Store)用于将数据与 AI 模型集成。 使用它们的第一步是将您的数据加载到矢量数据库中。 然后,当要将用户查询发送到 AI 模型时,首先检索一组类似的文档。 然后,这些文档用作用户问题的上下文,并与用户的查询一起发送到 AI 模型。 这种技术被称为检索增强生成 (RAG)。。
Vector Store 是 RAG 系统中负责“检索”这一步的核心基础设施。
通俗来说,大型语言模型(LLMs)在训练后会被冻结,导致知识陈旧,无法访问或修改外部数据。所以问它没有被训练到的内容时,就会容易胡说八道。RAG的使用,可以避免得到不准确的信息。
RAG的流程
RAG 的核心思想是:不只靠模型“记”下来的知识,而是让模型在回答的当下“查”到最新/最相关的知识。
其典型流程为:
- 用户问题 → 转成向量(Embedding)
- 向量检索 → 在海量文档块(chunk)的向量库里找最相似的 Top K 段文字
- 召回 → 得到 K 段最相关的文本(retrieved chunks)
- 增强 → 把这 K 段文本塞到 Prompt 里(通常跟系统提示、用户问题一起组成很长的上下文)
- 生成 → 大语言模型拿着这个“加强版Prompt”进行生成
- 输出 → 得到最终答案(理论上更准确、更新的信息)
使用
引入依赖
引入管理依赖
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>1.0.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
引入模型依赖
Spring AI为我们提供了许多AI模型的starter,以DeepSeek为例:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-deepseek</artifactId>
</dependency>
使用其他AI将spring-ai-starter-model-xxx替换为要使用的AI即可,详见聊天模型
配置模型
以OpenAI为例:
spring:
ai:
openai:
base-url:
api-key:
chat:
options:
model:
temperature:
配置客户端
配置Client
使用ChatClient工厂构建Client
@Bean
public ChatClient chatClient(OllamaChatModel model) {
return ChatClient.builder(model)
.defaultSystem("你是一个AI助手")
.build();
}
- 阻塞式返回结果(等待完全生成后返回)
String content = chatClient.prompt()
.user("你是谁?")
.call()
.content();
- 流式返回结果(边生成边返回)
Flux<String> content = chatClient.prompt()
.user()
.stream()
.content();
什么是Flux?
是 Spring5 添加新的模块,用于 web 开发的,功能和 SpringMVC 类似的,Webflux 使用当前一种比较流行响应式编程出现的框架。
示例
以知文摘要生成、对话功能为例,项目地址
以下内容主要展示AI相关功能,故部分代码省略
知文摘要生成
大模型配置类LlmConfig.java
@Configuration
public class LlmConfig {
@Bean
public ChatClient chatClient(@Qualifier("deepSeekChatModel") ChatModel chatModel) {
return ChatClient.builder(chatModel).build();
}
}
@Qualifier("deepSeekChatModel")是什么?
@Qualifier 是 Spring 框架中用于解决 bean 名称冲突的一种注解写法,在 Spring AI 中,当你引入 spring-ai-deepseek-spring-boot-starter(或相关依赖)后,框架会自动创建一个 DeepSeekChatModel 类型的 bean,在本例中即为"deepSeekChatModel",用于在代码中明确注入这个特定的 DeepSeek 模型。
知文摘要生成接口 KnowPostDescriptionService.java
public interface KnowPostDescriptionService {
String generateDescription(String content);
}
AI 生成知文摘要实现 KnowPostDescriptionServiceImpl.java
@Service
@RequiredArgsConstructor
public class KnowPostDescriptionServiceImpl implements KnowPostDescriptionService {
private final ChatClient chatClient;
public String generateDescription(String content){
// 正文内容合法判断,略
String system = "你是中文文案编辑,请基于用户提供的知文正文,生成一个中文描述,简洁有吸引力,"
String user = "正文如下" + content + "\n\n 请直接给出不超过50字的中文描述";
try {
String result = chatClient
.prompt()
.system(system)
.user(user)
.options(DeepSeekChatOptions.builder()
.model("deepseek-chat")
.temperature(0.8)
.maxTokens(120)
.build())
.call()
.content();
return result;
} catch (Exception e) {
// 抛出大模型调用失败异常,略
}
}
}
prompt()是 Spring AI 中 ChatClient 接口的核心入口方法,作用为:开启一次新的对话 Prompt(提示)的构建过程,返回一个建造者(Builder),让你可以一步步“拼装”这次要发给大模型的完整 Prompt。.system():系统提示词.user():用户消息
DeepSeekChatOptions属性详见:DeepSeek聊天属性
@RequiredArgsConstructor 是 Lombok 提供的一个非常常用的注解,主要作用是:自动生成一个包含「所有 final 字段 + @NonNull 字段」的构造方法**
接口控制层 KnowPostAiController.java
@RestController
@RequestMapping(path = "/api/v1/knowposts", produces = "MediaType.APPLICATION_JSON_VALUE")
@RequiredArgsConstructor
public class KnowPostAiController {
private final KnowPostDescriptionService descriptionService;
/**
* 生成不超过50字的知文描述
*/
@PostMapping(path = "/description/suggest", consumes = MediaType._APPLICATION_JSON_VALUE_)
public DescriptionSuggestResponse suggest(@Valid @RequestBody DescriptionSuggestRequest req) {
String desc = descriptionService.generateDescription(req.content());
return new DescriptionSuggestResponse(desc);
}
}
对话功能(使用RAG)
RAG 索引构建服务 RagIndexService.java
@Service
@RequiredArgsConstructor
public class RagIndexService {
// 创建一个专门属于当前类(RagIndexService)的日志工具对象 log
private static final Logger log = LoggerFactory.getLogger(RagIndexService.class);
// 向量库封装,负责写入/检索向量
private final VectorStore vectorStore;
// 数据访问:根据postId查询知文详情
private final KnowPostMapper knowPostMapper;
// 拉取Markdown正文内容
private final RestTemplate http = new RestTemplate();
// 使用ES客户端操作切片
private final ElatsticsearchClient es;
// ES相关配置
private final EsProperties esProps;
public int reindexSinglePost(long postId) {
KnowPostDetailRow row = knowPostMapper.findDetailById(postId);
// 判断row是否为空
// 仅索引公开的已发布的知文
// 判断内容地址是否缺失
// 抓取Markdown正文
String text = fetchContent(row.getContentUrl());
// 判断正文是否为空
List<String> chunks = chunkMarkdown(text);
deleteExistingChunks(postId);
// 组装 Document(文本 + 业务元数据),用于向量写入与检索过滤
List<Document> docs = new ArrayList<>(chunks.size());
for(int i = 0; i < chunks.size(); i++){
String cid = postId + "#" + i;
Map<String, Object> meta = new HashMap<>();
meta.put("postId", String._valueOf_(postId));
meta.put("chunkId", cid); meta.put("position", i);
meta.put("contentEtag", currentEtag);
meta.put("contentSha256", currentSha);
meta.put("contentUrl", row.getContentUrl());
meta.put("title", row.getTitle());
docs.add(new Document(chunks.get(i), meta));
}
try {
vectorStore.add(docs);
} catch (Exception e) {
log.error("VectorStore add failed");
return 0;
}
// 返回本次写入的切片数量
return docs.size();
}
/**
*获取正文内容
*/
private String fetchContent(String url) {}
/**
*按Markdown 标题切段
*/
private List<String> chunkMarkdown(String text) {}
RAG 问答查询服务 RagQueryService.java
@Service
@RequiredArgsConstructor
public class RagQueryService {
// 向量检索接口(Elasticsearch 向量库封装)
private final VectorStore vectorStore;
// 大模型对话客户端(在 LlmConfig 中通过 @Qualifier 绑定 deepSeekChatModel)
private final ChatClient chatClient;
// 索引服务:确保帖子在问答前已建立/更新索引
private final RagIndexService indexService;
public Flux<String> streamAnswerFlux(long postId, String question, int topK, int maxTokens) {
// 检索上下文:先宽召回,再按 postId 做服务端过滤
List<String> contexts = searchContexts(String._valueOf_(postId), question, Math._max_(1, topK));
// 组装上下文文本,分隔符用于提示词中分块标识
String context = String._join_("\n\n---\n\n", contexts);
// 系统提示:限定只依据提供的上下文作答,无法确定需明确说明
String system = "你是中文知识助手。只能依据提供的知文上下文回答;无法确定的请说明不确定。";
// 用户消息:包含问题和召回到的上下文
String user = "问题:" + question + "\n\n上下文如下(可能不完整):\n" + context + "\n\n请基于以上上下文作答。";
return chatClient.prompt() // 构建对话
.system(system)
.user(user)
.options(DeepSeekChatOptions.builder()
.model("deepseek-chat") // 指定 DeepSeek 模型
.temperature(0.2) // 低温度:更稳健、少发散
.maxTokens(maxTokens) // 控制最大输出长度
.build())
.stream() // 以流式(SSE)返回模型输出
.content(); // 转换为 Flux<String>
}
接口控制层 KnowPostRagController.java
@RestController
@RequestMapping("/api/v1/knowposts")
@Validated
@RequiredArgsConstructor
public class KnowPostRagController {
private final RagIndexService indexService;
private final RagQueryService ragQueryService;
@GetMapping(value = "/{id}/qa/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> qaStream(@PathVariable("id") long id,
@RequestParam("question") String question,
@RequestParam(value = "topK", defaultValue = "5") int topK,
@RequestParam(value = "maxTokens", defaultValue = "1024") int maxTokens) {
return ragQueryService.streamAnswerFlux(id, question, topK, maxTokens);
}
Comments NOTHING