九、构建企业级知识库:RAG 实现(下)
上一章我们完成了知识摄入,将私有文档处理成了向量并存入向量数据库。现在,我们要实现 RAG 的在线阶段:当用户提问时,从向量数据库中检索相关文档片段,并将其作为上下文提供给大模型,生成基于知识的答案。
本章将带你完成检索增强的问答接口,并探讨一些高级 RAG 策略,帮助你在生产环境中获得更好的效果。
9.1 检索增强的问答实现
Spring AI 提供了非常简洁的方式将向量检索与聊天模型集成。核心思路是:在构建 ChatClient 时,通过 .advisors() 方法添加一个 检索增强顾问(Retrieval Augmentation Advisor),它会自动在每次请求时执行向量检索,并将检索到的文档片段注入到提示词中。
9.1.1 添加检索顾问依赖
Spring AI 内置了 QuestionAnswerAdvisor,它实现了最基本的 RAG 流程:将用户问题作为查询,检索最相关的文档,然后将文档内容插入到用户消息之前。要使用它,需要引入相应的顾问模块(通常已在核心中)。
确保你的项目中已经包含了 spring-ai-core,它会自动包含基础顾问。
9.1.2 在 ChatClient 中集成 VectorStore
我们首先需要创建一个带有检索能力的 ChatClient Bean。可以在配置类中定义:
import org.springframework.ai.chat.ChatClient;
import org.springframework.ai.chat.ChatClientBuilder;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.ai.rag.preretrieval.query.transformation.QueryTransformer;
import org.springframework.ai.rag.retrieval.search.DocumentRetriever;
import org.springframework.ai.rag.retrieval.search.VectorStoreDocumentRetriever;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RagConfig {
@Bean
public ChatClient ragChatClient(ChatClient.Builder builder, VectorStore vectorStore) {
// 创建一个文档检索器,从 VectorStore 中检索
DocumentRetriever retriever = VectorStoreDocumentRetriever.builder()
.vectorStore(vectorStore)
.similarityThreshold(0.5) // 相似度阈值,低于此值的不返回
.topK(3) // 返回最相似的 3 个文档
.build();
// 构建 ChatClient,并添加检索顾问
return builder
.defaultAdvisors(new QuestionAnswerAdvisor(retriever))
.build();
}
}
QuestionAnswerAdvisor 会在每次调用时:
- 获取用户消息作为查询。
- 调用
DocumentRetriever检索相关文档。 - 将检索到的文档内容格式化后插入到用户消息前面,形成增强后的提示词。
默认的提示词模板类似于:
上下文信息:
---------------------
[文档1内容]
[文档2内容]
---------------------
根据以上上下文信息,请回答用户的问题:{用户问题}
9.1.3 创建 RAG 控制器
现在我们可以注入这个 ragChatClient,并提供一个 REST 接口用于问答。
package com.example.demo.controller;
import org.springframework.ai.chat.ChatClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class RagController {
private final ChatClient ragChatClient;
public RagController(ChatClient ragChatClient) {
this.ragChatClient = ragChatClient;
}
@GetMapping("/ask")
public String ask(@RequestParam String question) {
return ragChatClient.prompt()
.user(question)
.call()
.content();
}
}
注意:这里注入的 ragChatClient 是我们刚刚配置的带有检索顾问的 Bean,不是普通的 ChatClient。
9.1.4 测试 RAG 问答
启动应用,访问 /ask?question=什么是RAG。你应该会看到 AI 基于 knowledge.txt 中的内容进行回答,而不是凭空编造。
如果问题超出知识库范围(例如问“明天天气怎么样”),AI 可能会说不知道,或者根据自身知识回答(取决于你的设置)。你可以在系统消息中进一步约束 AI 的行为。
9.1.5 完整 RAG 流程示意图
sequenceDiagram
participant 用户
participant Controller
participant ChatClient
participant 检索顾问
participant VectorStore
participant 大模型
用户->>Controller: GET /ask?question=什么是RAG
Controller->>ChatClient: prompt().user(question).call()
ChatClient->>检索顾问: 执行检索增强
检索顾问->>VectorStore: 相似度搜索(question)
VectorStore-->>检索顾问: 返回最相关文档片段
检索顾问->>检索顾问: 构建增强提示词(上下文+问题)
检索顾问-->>ChatClient: 返回增强后的Prompt
ChatClient->>大模型: 发送增强后的Prompt
大模型-->>ChatClient: 返回AI回答
ChatClient-->>Controller: 返回content()
Controller-->>用户: 返回最终答案
9.2 高级 RAG 策略简介
基本的 QuestionAnswerAdvisor 已经能满足许多场景,但在生产环境中,你可能需要更精细的控制来提升检索质量和回答准确性。Spring AI 提供了一系列可插拔的组件,允许你定制 RAG 流程的每个环节。
9.2.1 查询转换(Query Transformation)
用户的问题可能不够精确,或者需要结合对话历史才能理解。查询转换可以在检索前对问题进行改写,以提高检索效果。
Spring AI 提供了 QueryTransformer 接口,常用实现有:
- CompressingQueryTransformer:结合对话历史压缩查询(例如将“它是什么意思”扩展为完整问题)。
- ExpandingQueryTransformer:生成多个查询变体,检索后合并结果。
- TranslationQueryTransformer:将查询翻译成其他语言后再检索(如果文档是多语言的)。
使用示例:
QueryTransformer transformer = new CompressingQueryTransformer(chatModel);
然后可以在构建检索器时传入:
DocumentRetriever retriever = VectorStoreDocumentRetriever.builder()
.vectorStore(vectorStore)
.queryTransformer(transformer) // 添加查询转换
.topK(3)
.build();
9.2.2 混合检索(Hybrid Search)
向量检索擅长语义匹配,但关键词检索在某些场景下更精确(如精确匹配产品型号)。混合检索结合了两者,通常能取得更好的效果。
Spring AI 目前没有内置的混合检索器,但你可以通过组合多个检索器来实现。例如,同时使用向量检索和 Elasticsearch 关键词检索,然后合并结果。
9.2.3 重排序(Reranking)
检索出的文档片段按相似度得分排序,但有时最相似的未必最有用。重排序可以使用专门的模型(如 Cross-encoder)对检索结果重新打分,提高相关性。
Spring AI 提供了 DocumentRanker 接口,你可以实现自己的重排序逻辑,或调用第三方服务。
9.2.4 自定义提示词模板
QuestionAnswerAdvisor 使用默认的提示词模板,但你可以通过覆盖其行为来自定义。例如,你希望 AI 在无法回答时明确说“根据现有知识库无法回答”。
可以创建自定义的 Advisor:
public class CustomQuestionAnswerAdvisor implements ChatMemoryAdvisor {
private final DocumentRetriever retriever;
private final String template;
public CustomQuestionAnswerAdvisor(DocumentRetriever retriever, String template) {
this.retriever = retriever;
this.template = template;
}
@Override
public Prompt advise(Prompt prompt, Map<String, Object> context) {
// 1. 从 prompt 中提取用户消息(可能需要处理多条消息)
String userMessage = ...;
// 2. 检索文档
List<Document> docs = retriever.retrieve(userMessage);
// 3. 格式化文档为上下文
String contextStr = docs.stream().map(Document::getContent).collect(Collectors.joining("\n---\n"));
// 4. 构建新提示词
String enhancedUserMessage = template.replace("{context}", contextStr)
.replace("{question}", userMessage);
// 5. 返回新的 Prompt(可能保留系统消息等)
return new Prompt(enhancedUserMessage, prompt.getOptions());
}
}
然后在构建 ChatClient 时使用这个自定义顾问。
9.3 实践:构建一个带高级功能的 RAG 助手
为了让你体验更完整的 RAG 实现,我们将构建一个具备以下功能的助手:
- 在应用启动时摄入知识文档
- 使用查询压缩(CompressingQueryTransformer)支持多轮对话
- 提供流式响应
- 返回检索到的文档来源(作为元数据)
9.3.1 添加依赖
确保有 WebFlux 依赖(用于流式)和对话记忆的支持(后续章节会用到)。
9.3.2 配置类
package com.example.demo.config;
import org.springframework.ai.chat.ChatClient;
import org.springframework.ai.chat.ChatClientBuilder;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.memory.InMemoryChatMemory;
import org.springframework.ai.rag.preretrieval.query.transformation.CompressingQueryTransformer;
import org.springframework.ai.rag.retrieval.search.DocumentRetriever;
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 AdvancedRagConfig {
@Bean
public ChatMemory chatMemory() {
return new InMemoryChatMemory();
}
@Bean
public ChatClient advancedRagChatClient(ChatClient.Builder builder,
VectorStore vectorStore,
ChatClient chatModel, // 用于查询压缩的模型
ChatMemory chatMemory) {
// 创建查询转换器(压缩)
CompressingQueryTransformer queryTransformer = new CompressingQueryTransformer(chatModel);
// 创建检索器
DocumentRetriever retriever = VectorStoreDocumentRetriever.builder()
.vectorStore(vectorStore)
.queryTransformer(queryTransformer)
.similarityThreshold(0.6)
.topK(5)
.build();
// 构建 ChatClient,添加多个顾问:记忆顾问 + 检索顾问
return builder
.defaultAdvisors(
new MessageChatMemoryAdvisor(chatMemory), // 对话记忆(需要引入相关包)
new QuestionAnswerAdvisor(retriever)
)
.build();
}
}
9.3.3 控制器支持流式
package com.example.demo.controller;
import org.springframework.ai.chat.ChatClient;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
@RestController
public class AdvancedRagController {
private final ChatClient advancedRagChatClient;
public AdvancedRagController(ChatClient advancedRagChatClient) {
this.advancedRagChatClient = advancedRagChatClient;
}
@GetMapping(value = "/ask-stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> askStream(@RequestParam String question) {
return advancedRagChatClient.prompt()
.user(question)
.stream()
.map(chatResponse -> chatResponse.getResult().getOutput().getContent());
}
}
9.3.4 返回文档来源
如果你希望在前端展示答案的来源(引用的文档片段),可以修改响应结构。Spring AI 的 ChatResponse 中包含了 metadata,其中可能包含检索到的文档列表。但需要顾问将文档信息放入 metadata 中。
QuestionAnswerAdvisor 默认会将检索到的文档放在 ChatResponse 的 metadata 中,键为 "retrievedDocuments"。我们可以通过返回 ChatResponse 而不是纯文本来获取这些信息。
@GetMapping(value = "/ask-with-sources")
public Map<String, Object> askWithSources(@RequestParam String question) {
ChatResponse response = advancedRagChatClient.prompt()
.user(question)
.call();
String answer = response.getResult().getOutput().getContent();
List<Document> sources = (List<Document>) response.getMetadata().get("retrievedDocuments");
return Map.of(
"answer", answer,
"sources", sources.stream().map(Document::getContent).collect(Collectors.toList())
);
}
9.4 本章小结
通过本章的学习,你完成了 RAG 的最后一个环节——检索增强的问答:
- 集成检索顾问:使用
QuestionAnswerAdvisor将向量检索自动融入 ChatClient。 - 自定义检索器:配置相似度阈值、返回数量等参数。
- 查询转换:引入
CompressingQueryTransformer提升多轮对话中的检索准确性。 - 流式响应:与 WebFlux 结合,提供打字机效果。
- 来源返回:获取并返回引用的文档片段,增强可信度。
十、注解式开发:声明式AI服务
在前面的章节中,我们已经熟悉了使用 ChatClient 进行 AI 交互的编程方式。这种方式非常灵活,但每次调用都需要编写类似的代码:构建 prompt、调用、获取结果。随着业务中 AI 功能点的增多,重复代码会变得臃肿,提示词也会散落在各处,不利于维护。
注解式开发提供了一种更优雅的解决方案:通过自定义注解,将提示词定义与业务逻辑分离,让开发者只需关注接口定义和注解配置,底层调用完全自动化。这类似于 Spring 的 @RequestMapping 注解将 HTTP 请求映射到方法,我们的 @AiPrompt 注解将自然语言交互映射到 Java 方法。
10.1 什么是注解式开发?为何需要?
注解式开发的核心思想是:让开发者通过注解声明 AI 的行为,框架自动实现调用细节。具体来说:
- 你定义一个 Java 接口,在接口方法上添加
@AiPrompt注解,注解值就是提示词模板。 - 框架(通过 AOP)自动为该接口生成代理实现,在调用方法时,根据注解模板和方法参数构造真正的提示词,调用大模型,并返回结果。
这样做的好处:
- 关注点分离:提示词与业务代码分离,便于维护和修改。
- 极简调用:业务代码只需注入接口并调用方法,就像调用普通方法一样。
- 类型安全:方法参数和返回值都是 Java 类型,无需手动解析 JSON。
- 复用性强:同一套接口可在多处复用,减少重复代码。
10.2 自定义 @AiPrompt 注解的设计
我们先来设计一个简单的注解 @AiPrompt,用于标记需要 AI 处理的方法。
package com.example.demo.annotation;
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AiPrompt {
/**
* 提示词模板,支持占位符 {name} 形式
*/
String value();
/**
* 系统消息模板(可选)
*/
String system() default "";
/**
* 模型参数,如温度等(可选,简化版暂不支持)
*/
// double temperature() default 0.7;
}
这个注解用于方法上,value 是用户消息模板,system 是可选的系统消息模板。我们将在切面中解析模板并填充方法参数。
10.3 使用 AOP 实现注解处理切面
Spring AOP 可以帮助我们拦截所有带有 @AiPrompt 注解的方法,并执行统一的 AI 调用逻辑。
10.3.1 引入 AOP 依赖
在 pom.xml 中添加 Spring AOP 起步依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
10.3.2 创建切面类
package com.example.demo.aspect;
import com.example.demo.annotation.AiPrompt;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.ai.chat.ChatClient;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.chat.prompt.PromptTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
@Aspect
@Component
public class AiPromptAspect {
@Autowired
private ChatClient chatClient;
@Around("@annotation(com.example.demo.annotation.AiPrompt)")
public Object handleAiPrompt(ProceedingJoinPoint joinPoint) throws Throwable {
// 1. 获取方法上的注解
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
AiPrompt aiPrompt = method.getAnnotation(AiPrompt.class);
// 2. 获取方法参数名和参数值
String[] paramNames = signature.getParameterNames();
Object[] paramValues = joinPoint.getArgs();
// 3. 构建变量映射
Map<String, Object> variables = new HashMap<>();
if (paramNames != null) {
for (int i = 0; i < paramNames.length; i++) {
variables.put(paramNames[i], paramValues[i]);
}
}
// 4. 渲染系统消息(如果有)
PromptTemplate systemTemplate = null;
String systemText = null;
if (!aiPrompt.system().isEmpty()) {
systemTemplate = new PromptTemplate(aiPrompt.system());
systemText = systemTemplate.render(variables);
}
// 5. 渲染用户消息
PromptTemplate userTemplate = new PromptTemplate(aiPrompt.value());
String userText = userTemplate.render(variables);
// 6. 构建 Prompt(支持系统消息)
Prompt prompt;
if (systemText != null) {
prompt = new Prompt(userText, systemText);
} else {
prompt = new Prompt(userText);
}
// 7. 调用 AI 客户端
String result = chatClient.prompt(prompt).call().content();
// 8. 返回结果(目前只支持 String 返回类型)
return result;
}
}
切面逻辑说明:
- 通过
@Around拦截所有带有@AiPrompt注解的方法。 - 获取方法参数名和参数值,构建变量 Map。
- 使用
PromptTemplate渲染注解中的模板(支持占位符)。 - 调用
ChatClient获取 AI 回复。 - 返回结果给调用方。
注意: 此切面仅处理返回类型为 String 的方法。如果需要返回复杂对象,可以进一步扩展(结合第五章的结构化输出),但本章先保持简单。
10.4 实践:用注解简化 AI 服务调用
现在我们来实际使用这个注解,构建一个简单的 AI 服务。
10.4.1 创建 AI Service 接口
package com.example.demo.service;
import com.example.demo.annotation.AiPrompt;
public interface AiAssistant {
@AiPrompt(value = "你好,我叫 {name},请用热情的语气向我问好。",
system = "你是一个热情的接待员,总是用感叹号结尾。")
String greet(String name);
@AiPrompt("请解释一下什么是 {concept},用通俗易懂的语言。")
String explain(String concept);
@AiPrompt("将以下文本翻译成 {targetLanguage}:{text}")
String translate(String text, String targetLanguage);
}
注意方法参数名与模板中的占位符名称一致。
10.4.2 实现接口(无需手动实现)
我们不需要编写实现类,因为切面会动态处理。但是 Spring 需要能够注入接口的实例,所以我们需要通过某种方式创建 Bean。
一种简单的方式是在配置类中通过 @Bean 返回代理对象,但更常见的是结合 Spring 的 @Service 和工厂方法。为了简化,我们可以使用 @Service 注解一个抽象类,但 AOP 只能作用于 Spring Bean 的方法,所以我们需要确保接口的实现类是一个 Spring Bean。
方案:使用动态代理手动创建 Bean
在配置类中,我们可以为每个接口创建代理 Bean,但那样太麻烦。更好的做法是让 Spring 自动扫描接口并使用工厂 Bean 生成代理。这里我们采用最简单的方式:创建一个实现类,但实现类中什么也不做,因为切面会拦截。
package com.example.demo.service;
import org.springframework.stereotype.Service;
@Service
public class AiAssistantImpl implements AiAssistant {
@Override
public String greet(String name) {
// 切面会拦截,实际不会执行到这里
return null;
}
@Override
public String explain(String concept) {
return null;
}
@Override
public String translate(String text, String targetLanguage) {
return null;
}
}
这个实现类只是为了让 Spring 创建一个 Bean,所有方法体为空。当这些方法被调用时,AOP 切面会拦截并执行 AI 逻辑,不会进入方法体。
10.4.3 在 Controller 中使用
package com.example.demo.controller;
import com.example.demo.service.AiAssistant;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class AiAssistantController {
private final AiAssistant aiAssistant;
public AiAssistantController(AiAssistant aiAssistant) {
this.aiAssistant = aiAssistant;
}
@GetMapping("/greet")
public String greet(@RequestParam String name) {
return aiAssistant.greet(name);
}
@GetMapping("/explain")
public String explain(@RequestParam String concept) {
return aiAssistant.explain(concept);
}
@GetMapping("/translate")
public String translate(@RequestParam String text,
@RequestParam String targetLanguage) {
return aiAssistant.translate(text, targetLanguage);
}
}
10.4.4 测试
启动应用,访问:
http://localhost:8080/greet?name=张三http://localhost:8080/explain?concept=多态http://localhost:8080/translate?text=Hello&targetLanguage=中文
你会看到 AI 根据注解中的提示词模板生成的回复。
注解式开发流程示意图:
graph TD
A[Controller调用aiAssistant.greet name] --> B[AOP切面拦截AiPrompt方法]
B --> C[获取注解模板和方法参数]
C --> D[用参数渲染模板]
D --> E[调用ChatClient]
E --> F[获取AI回复]
F --> G[返回结果给Controller]
10.5 注解式开发的优势
通过上面的实践,你已经感受到了注解式开发的魅力:
- 代码极简:业务代码中只需一行
aiAssistant.greet(name),所有 AI 交互细节都被封装在切面中。 - 提示词集中管理:所有提示词都定义在注解中,一目了然,修改方便。
- 类型安全:方法参数明确,编译期就能发现参数错误。
- 易于测试:可以轻松 Mock AI 服务,进行单元测试。
- 可扩展性:可以在注解中添加更多属性(如温度、模型名称等),切面中支持更多配置。
10.6 扩展:支持返回 Java 对象
如果你希望注解方法直接返回 Java 对象,可以结合 Spring AI 的 OutputParser 实现。大致思路是:在注解中增加 outputClass 属性,切面中根据该属性调用 chatClient.prompt().call().entity(outputClass)。
// 扩展注解
public @interface AiPrompt {
String value();
String system() default "";
Class<?> outputClass() default String.class; // 默认 String
}
然后在切面中判断:
if (aiPrompt.outputClass() != String.class) {
Object result = chatClient.prompt(prompt).call().entity(aiPrompt.outputClass());
return result;
} else {
return chatClient.prompt(prompt).call().content();
}
但要注意,返回类型必须与 outputClass 一致,否则会转换错误。
10.7 本章小结
通过本章的学习,你掌握了:
- 注解式开发的概念:将提示词通过注解声明,利用 AOP 自动化 AI 调用。
- 自定义注解:设计
@AiPrompt包含模板和系统消息。 - AOP 切面实现:拦截方法,渲染模板,调用
ChatClient。 - 实践:构建了基于注解的 AI 助手,实现了问候、解释、翻译功能。
- 优势总结:代码简洁、提示词集中、类型安全、易于维护。
注解式开发是构建企业级 AI 服务的高级模式,尤其适合团队中需要大量使用 AI 功能的场景,可以显著提升开发效率和代码质量。
十一、生产级特性:缓存与监控
当你的 AI 应用从原型走向生产,性能、成本和可观测性就成为必须考虑的因素。不加控制的 AI 调用可能导致响应缓慢、Token 费用飙升,而缺乏监控则让你对应用的实际运行情况一无所知。
本章将教你如何为 Spring AI 应用添加两大生产级特性:
- 缓存:减少重复请求,提升响应速度,节省 Token 消耗。
- 监控:通过指标收集,了解调用频率、耗时和 Token 用量,为容量规划和成本控制提供依据。
11.1 缓存优化减少重复调用
在实际业务中,用户可能会反复询问相同或相似的问题(例如常见的 FAQ)。每次都将同样的问题发送给大模型,不仅浪费 Token,还会增加响应延迟。通过引入缓存,我们可以将 AI 的回复缓存起来,当遇到相同问题时直接返回缓存结果。
11.1.1 Spring Cache 抽象
Spring 提供了强大的缓存抽象,通过注解就能轻松为方法添加缓存能力。核心注解:
@Cacheable:在方法执行前先检查缓存,如果缓存存在则直接返回,否则执行方法并将结果缓存。@CacheEvict:清除缓存。@CachePut:更新缓存。
我们需要选择一个缓存实现,比如 Caffeine(高性能本地缓存)或 Redis(分布式缓存)。本教程以 Caffeine 为例。
11.1.2 引入依赖
在 pom.xml 中添加 Spring Cache 和 Caffeine 依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
11.1.3 配置缓存管理器
在 application.yml 中配置 Caffeine 缓存:
spring:
cache:
cache-names: ai-responses
caffeine:
spec: maximumSize=1000, expireAfterWrite=1h # 最多缓存1000条,写入1小时后过期
或者在 Java 配置类中更精细地配置:
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.concurrent.TimeUnit;
@Configuration
@EnableCaching // 启用缓存注解
public class CacheConfig {
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager("ai-responses");
cacheManager.setCaffeine(Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(1, TimeUnit.HOURS));
return cacheManager;
}
}
11.1.4 在 AI 服务方法上添加 @Cacheable
假设我们有一个 AiAssistant 服务,其中的 chat 方法调用 AI。我们可以这样添加缓存:
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
@Service
public class AiAssistant {
private final ChatClient chatClient;
public AiAssistant(ChatClient.Builder chatClientBuilder) {
this.chatClient = chatClientBuilder.build();
}
@Cacheable(value = "ai-responses", key = "#userMessage")
public String chat(String userMessage) {
return chatClient.prompt()
.user(userMessage)
.call()
.content();
}
}
value = "ai-responses":指定缓存名称,对应配置中的缓存区域。key = "#userMessage":使用 SpEL 表达式,以userMessage参数作为缓存 key。这意味着完全相同的消息才会命中缓存。
如果你的方法包含多个参数(如系统消息、温度等),可以将它们组合成 key,例如 key = "#userMessage + #systemMessage"。
11.1.5 注意事项
- 缓存粒度:仅当用户消息完全相同时才会命中缓存。如果消息稍有不同(如标点符号),将视为不同请求。可以考虑对消息进行归一化处理(如去除多余空格、转为小写)来提升命中率,但需谨慎避免改变语义。
- 缓存过期策略:根据业务需求设置合理的过期时间。例如 FAQ 可以缓存较长时间,而实时性要求高的内容应设置较短的过期时间或不缓存。
- 缓存污染:避免缓存错误或异常的响应。可以在调用 AI 后对结果进行校验,只有成功结果才缓存。
- 分布式环境:如果应用多实例部署,本地缓存会导致每个实例有自己的缓存,可能不一致。此时应考虑使用 Redis 等分布式缓存。
11.2 监控与可观测性
为了了解 AI 服务的运行状况,我们需要收集以下指标:
- 调用次数:总调用量,成功/失败次数。
- 响应时间:每次调用的耗时分布。
- Token 用量:输入 Token、输出 Token、总 Token,用于成本核算。
- 缓存命中率:缓存的有效性。
Spring Boot Actuator 结合 Micrometer 可以轻松暴露这些指标。
11.2.1 引入 Actuator 依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId> <!-- 如果要用 Prometheus -->
</dependency>
11.2.2 配置 Actuator 端点
在 application.yml 中暴露需要的端点:
management:
endpoints:
web:
exposure:
include: health,info,prometheus,metrics
metrics:
tags:
application: ai-service # 为所有指标添加应用标签
11.2.3 自定义指标收集
我们可以通过 AOP 切面或拦截器来记录每次 AI 调用的耗时和 Token 用量,并使用 Micrometer 的 Counter 和 Timer 记录指标。
首先,注入 MeterRegistry:
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.ai.chat.ChatResponse;
import org.springframework.stereotype.Component;
import java.time.Duration;
import java.util.concurrent.TimeUnit;
@Aspect
@Component
public class AiMonitoringAspect {
private final MeterRegistry meterRegistry;
// 定义指标名称
private static final String AI_CALL_COUNT = "ai.call.count";
private static final String AI_CALL_DURATION = "ai.call.duration";
private static final String AI_TOKEN_TOTAL = "ai.token.total";
private static final String AI_TOKEN_INPUT = "ai.token.input";
private static final String AI_TOKEN_OUTPUT = "ai.token.output";
public AiMonitoringAspect(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
}
@Around("execution(* com.example.demo.service.AiAssistant.chat(..))")
public Object monitorAiCall(ProceedingJoinPoint joinPoint) throws Throwable {
// 记录开始时间
long start = System.nanoTime();
// 执行原始方法
Object result = joinPoint.proceed();
// 计算耗时
long durationNanos = System.nanoTime() - start;
Timer timer = meterRegistry.timer(AI_CALL_DURATION, "method", "chat");
timer.record(durationNanos, TimeUnit.NANOSECONDS);
// 增加调用计数
meterRegistry.counter(AI_CALL_COUNT, "method", "chat", "status", "success").increment();
// 尝试获取 Token 用量(需要从 ChatResponse 中提取)
if (result instanceof ChatResponse) {
ChatResponse response = (ChatResponse) result;
// 注意:Token 用量的获取方式取决于 Spring AI 版本,此处为示例
var usage = response.getMetadata().get("usage");
if (usage instanceof org.springframework.ai.openai.metadata.OpenAiUsage) {
var openAiUsage = (org.springframework.ai.openai.metadata.OpenAiUsage) usage;
meterRegistry.counter(AI_TOKEN_INPUT, "method", "chat").increment(openAiUsage.getPromptTokens());
meterRegistry.counter(AI_TOKEN_OUTPUT, "method", "chat").increment(openAiUsage.getCompletionTokens());
meterRegistry.counter(AI_TOKEN_TOTAL, "method", "chat").increment(openAiUsage.getTotalTokens());
}
}
return result;
}
}
如果我们的方法返回的是字符串(而不是 ChatResponse),则无法获取 Token 用量。为了收集 Token,可以让方法返回 ChatResponse,或者通过其他方式获取(如从请求上下文中提取)。更优雅的方式是使用 ChatClient 的 call() 方法返回 ChatResponse,然后从中提取 Token。
11.2.4 创建可返回 Token 用量的服务
我们可以修改 AiAssistant 服务,提供一个返回 ChatResponse 的方法:
@Cacheable(value = "ai-responses", key = "#userMessage")
public ChatResponse chatWithDetails(String userMessage) {
return chatClient.prompt()
.user(userMessage)
.call();
}
然后在 Controller 中调用此方法,并提取内容和 Token。
或者保持返回字符串,但通过监听器或拦截器在底层收集 Token(Spring AI 可能提供类似功能,但当前版本需手动实现)。
11.2.5 查看指标
启动应用,访问 http://localhost:8080/actuator/metrics/ai.call.count 可以看到调用计数。如果配置了 Prometheus,访问 /actuator/prometheus 可以看到所有指标。
11.3 实践:为 RAG 服务添加缓存和监控
结合上一章的 RAG 服务,我们来添加缓存和监控。
11.3.1 服务类添加缓存
@Service
public class RagService {
private final ChatClient ragChatClient;
public RagService(ChatClient ragChatClient) {
this.ragChatClient = ragChatClient;
}
@Cacheable(value = "ai-responses", key = "#question")
public String ask(String question) {
return ragChatClient.prompt()
.user(question)
.call()
.content();
}
}
11.3.2 监控切面
使用上面的 AiMonitoringAspect,但需要调整切入点,指向 RagService.ask 方法。也可以定义一个通用的注解,然后切面拦截该注解。
11.3.3 控制器
@RestController
public class RagController {
private final RagService ragService;
public RagController(RagService ragService) {
this.ragService = ragService;
}
@GetMapping("/ask")
public String ask(@RequestParam String question) {
return ragService.ask(question);
}
}
11.3.4 测试
- 第一次访问
/ask?question=什么是RAG,会调用 AI,耗时较长,缓存中存入结果。 - 第二次相同请求,直接从缓存返回,响应极快。
- 查看
/actuator/metrics/ai.call.duration可以看到两次调用的耗时分布(第二次可能因为缓存而不被切面记录?这取决于切面的位置。如果切面在服务层,第二次不会经过服务方法,所以不会记录。需要决定是否缓存命中也要记录为一次“调用”。通常我们只记录实际调用 AI 的次数,缓存命中不算。因此切面应在实际调用 AI 的方法上,即ChatClient的调用处,而不是服务方法。可以进一步细化。)
为了准确监控实际 AI 调用,我们应该将切面放在更底层,例如自定义一个 ChatClient 的包装器,或者在调用 chatClient.prompt().call() 的地方拦截。但为了简化,我们可以接受服务方法被缓存命中时不记录。
11.4 流程图
缓存命中流程
sequenceDiagram
participant 用户
participant Controller
participant Service
participant 缓存
用户->>Controller: 请求 /ask?question=...
Controller->>Service: ask(question)
Service->>缓存: 根据 key 查询缓存
缓存-->>Service: 返回缓存结果
Service-->>Controller: 返回结果
Controller-->>用户: 响应
缓存未命中流程
sequenceDiagram
participant 用户
participant Controller
participant Service
participant 缓存
participant AI
用户->>Controller: 请求 /ask?question=...
Controller->>Service: ask(question)
Service->>缓存: 根据 key 查询缓存
缓存-->>Service: 未找到
Service->>AI: 调用大模型
AI-->>Service: 返回回答
Service->>缓存: 存入结果
Service-->>Controller: 返回结果
Controller-->>用户: 响应
监控数据收集流程
graph TD
A[AI调用] --> B[监控切面拦截]
B --> C[记录开始时间]
B --> D[执行原始方法]
D --> E[记录耗时]
E --> F[更新Timer指标]
D --> G[获取Token用量]
G --> H[更新Counter指标]
H --> I[Micrometer Registry]
I --> J[Actuator端点暴露]
J --> K[Prometheus拉取]
J --> L[管理员查看]
11.5 本章小结
通过本章的学习,你掌握了为 Spring AI 应用添加生产级特性的方法:
- 缓存:使用 Spring Cache 和 Caffeine 减少重复 AI 调用,提升性能,节省成本。
- 监控:利用 Micrometer 和 Actuator 收集调用次数、耗时和 Token 用量,为运维和成本控制提供数据支持。
- 实践:为 RAG 服务添加缓存和监控,并了解了注意事项(缓存粒度、分布式缓存、Token 获取方式)。
十二、与 Spring Cloud 生态集成
在前面的章节中,我们已经构建了一个功能完备的 AI 服务,并为其添加了缓存和监控。但在真实的微服务架构中,一个服务往往不是孤立存在的——它需要配置管理、服务发现、负载均衡、灰度发布等能力。Spring Cloud 生态提供了这些基础设施,而 Spring AI 可以无缝融入其中,让你的 AI 服务成为整个微服务体系的一部分。
本章将带你探索如何将 Spring AI 与 Spring Cloud 组件集成,实现:
- 将提示词模板(Prompts)存储在配置中心,实现动态更新,无需重启服务。
- 将 AI 服务注册到服务发现中心,供其他服务调用。
- 通过灰度发布机制,平滑升级模型或切换不同版本。
12.1 Spring AI 在微服务架构中的定位
在微服务架构中,AI 能力通常以独立服务的形式提供,称为 AI 服务 或 智能服务。它对外提供统一的 API 接口(如聊天、问答、图像生成),内部封装了与大模型交互的复杂逻辑。其他业务服务(如订单服务、客服服务)通过 HTTP 或 RPC 调用 AI 服务,获取 AI 能力。
典型架构示意图:
graph TD
subgraph 业务服务层
A[订单服务]
B[客服服务]
C[商品服务]
end
subgraph AI服务层
D[AI服务 - 聊天]
E[AI服务 - 图像]
end
subgraph 基础设施层
F[配置中心<br>Nacos/Config]
G[服务注册中心<br>Eureka/Nacos]
end
A -->|调用| D
B -->|调用| D
C -->|调用| E
D -->|获取配置| F
E -->|获取配置| F
D -->|注册| G
E -->|注册| G
这种架构的优势:
- 职责分离:业务服务无需关心 AI 调用的细节,只需调用 AI 服务的接口。
- 独立演进:AI 服务可以独立升级模型、调整提示词,不影响业务服务。
- 统一管理:通过配置中心统一管理提示词模板,通过注册中心实现服务发现和负载均衡。
- 弹性伸缩:AI 服务可以根据负载水平伸缩,业务服务通过客户端负载均衡调用。
12.2 配置中心管理 Prompt 模板
提示词模板(Prompts)是 AI 服务的核心资产,它们经常需要调整(例如优化措辞、增加约束)。如果模板硬编码在代码中,每次修改都需要重新编译、部署,非常不便。通过配置中心,我们可以将模板存储在外部,并支持动态刷新,无需重启服务即可生效。
12.2.1 使用 Spring Cloud Config 管理模板
Spring Cloud Config 是 Spring 官方提供的配置中心,可以基于 Git 仓库管理配置文件。我们也可以使用 Nacos、Apollo 等。这里以 Nacos 为例,因为它功能强大且在国内广泛使用。
引入依赖:
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
<version>2022.0.0.0</version> <!-- 版本需与 Spring Cloud Alibaba 对应 -->
</dependency>
配置文件 bootstrap.yml:
spring:
application:
name: ai-service
cloud:
nacos:
config:
server-addr: 127.0.0.1:8848 # Nacos 服务器地址
file-extension: yaml # 配置文件格式
profiles:
active: dev
在 Nacos 中创建配置文件 ai-service-dev.yaml,内容可以包含提示词模板:
ai:
prompts:
greeting: "你好,我叫 {name},请用热情的语气向我问好。"
explain: "请解释一下什么是 {concept},用通俗易懂的语言。"
translate: "将以下文本翻译成 {targetLanguage}:{text}"
12.2.2 在 Java 代码中动态加载模板
我们可以使用 @ConfigurationProperties 将配置映射为 Java 对象,并利用 @RefreshScope 实现动态刷新。
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.stereotype.Component;
import java.util.Map;
@Component
@RefreshScope
@ConfigurationProperties(prefix = "ai.prompts")
public class PromptProperties {
private Map<String, String> prompts;
public Map<String, String> getPrompts() {
return prompts;
}
public void setPrompts(Map<String, String> prompts) {
this.prompts = prompts;
}
// 根据 key 获取模板
public String getPrompt(String key) {
return prompts.get(key);
}
}
12.2.3 在 AI 服务中使用动态模板
修改之前的注解式 AI 服务,让注解值支持占位符,并从配置中心获取实际模板。
首先,我们可能需要调整 @AiPrompt 注解,让它支持一个 key 而不是直接写死模板。或者保持原样,但模板内容动态获取。为了演示,我们创建一个新的注解 @DynamicPrompt,其值为配置 key。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DynamicPrompt {
String value(); // 配置 key,如 "greeting"
String system() default "";
}
修改切面,从 PromptProperties 中获取模板:
@Aspect
@Component
public class DynamicPromptAspect {
@Autowired
private ChatClient chatClient;
@Autowired
private PromptProperties promptProperties;
@Around("@annotation(dynamicPrompt)")
public Object handleDynamicPrompt(ProceedingJoinPoint joinPoint, DynamicPrompt dynamicPrompt) throws Throwable {
// 获取模板 key
String key = dynamicPrompt.value();
String template = promptProperties.getPrompt(key);
if (template == null) {
throw new IllegalArgumentException("未找到 prompt key: " + key);
}
// 后续渲染与之前相同...
// ...
}
}
这样,当我们修改 Nacos 中的模板内容后,通过调用 Spring Cloud Config 的刷新端点(或 Nacos 自动推送),PromptProperties 会更新,后续调用将使用新模板。
12.2.4 测试动态刷新
- 启动 Nacos 服务(本地可下载并启动)。
- 在 Nacos 配置列表中创建
ai-service-dev.yaml,填入上述内容。 - 启动 AI 服务。
- 调用接口,验证使用配置中的模板。
- 在 Nacos 中修改模板内容并发布。
- 调用
curl -X POST http://localhost:8080/actuator/refresh(需引入 actuator 并暴露 refresh 端点),触发配置刷新。 - 再次调用接口,观察是否使用新模板。
12.3 服务注册与发现
为了让其他业务服务能够发现并调用 AI 服务,我们需要将 AI 服务注册到服务注册中心。这里以 Nacos 作为注册中心。
12.3.1 引入依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<version>2022.0.0.0</version>
</dependency>
12.3.2 配置文件
在 application.yml 中添加:
spring:
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
service: ai-service # 注册的服务名
12.3.3 启用服务发现
在主类上添加 @EnableDiscoveryClient:
@SpringBootApplication
@EnableDiscoveryClient
public class AiServiceApplication {
public static void main(String[] args) {
SpringApplication.run(AiServiceApplication.class, args);
}
}
启动服务后,可以在 Nacos 控制台的服务列表看到 ai-service 实例。
12.3.4 业务服务调用 AI 服务
现在假设有一个订单服务需要调用 AI 服务的聊天接口。它可以使用 @LoadBalanced 的 RestTemplate 或 WebClient 来调用。
订单服务配置:
spring:
application:
name: order-service
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
配置 RestTemplate:
@Configuration
public class OrderServiceConfig {
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
调用 AI 服务:
@Service
public class OrderService {
@Autowired
private RestTemplate restTemplate;
public String askAI(String question) {
String url = "http://ai-service/ask?question=" + question;
return restTemplate.getForObject(url, String.class);
}
}
http://ai-service 中的 ai-service 是注册的服务名,Ribbon 或 Spring Cloud LoadBalancer 会负责将请求负载均衡到多个 AI 服务实例。
12.4 灰度发布与 A/B 测试
当我们需要升级 AI 模型(例如从 GPT-3.5 切换到 GPT-4)或修改提示词时,直接全量发布存在风险。通过灰度发布(金丝雀发布),我们可以让一小部分流量先使用新版本,验证无误后再逐步扩大范围。
12.4.1 基于元数据的版本标识
在服务注册时,可以为实例添加元数据,标识其版本。例如在 application.yml 中:
spring:
cloud:
nacos:
discovery:
metadata:
version: v1 # 或 v2
12.4.2 在 AI 服务中支持多模型
我们可以让一个 AI 服务实例内部支持多个模型,但更常见的做法是部署两个不同版本的服务实例(如 v1 使用 GPT-3.5,v2 使用 GPT-4),通过网关或客户端负载均衡根据策略选择调用哪个版本。
12.4.3 使用 Spring Cloud Gateway 进行灰度路由
Spring Cloud Gateway 可以根据请求头、参数等条件将请求路由到不同版本的服务。
引入 Gateway 依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
Gateway 配置文件示例:
spring:
cloud:
gateway:
routes:
- id: ai-service-v1
uri: lb://ai-service
predicates:
- Path=/ask/**
- Header=version, v1 # 请求头 version=v1 时路由到 v1 版本
filters:
- SetPath=/ask
- id: ai-service-v2
uri: lb://ai-service
predicates:
- Path=/ask/**
- Header=version, v2 # 请求头 version=v2 时路由到 v2 版本
filters:
- SetPath=/ask
- id: ai-service-default
uri: lb://ai-service
predicates:
- Path=/ask/**
filters:
- SetPath=/ask
但上述配置是基于服务级别的,无法直接区分同一服务的不同版本实例。要实现版本感知的路由,需要结合 服务发现元数据 和 负载均衡策略。
12.4.4 自定义负载均衡规则
我们可以编写自定义的负载均衡规则,根据请求的版本标识选择对应元数据的实例。
public class VersionLoadBalancer implements ReactorServiceInstanceLoadBalancer {
private final String serviceId;
private final ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider;
public VersionLoadBalancer(String serviceId,
ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider) {
this.serviceId = serviceId;
this.serviceInstanceListSupplierProvider = serviceInstanceListSupplierProvider;
}
@Override
public Mono<Response<ServiceInstance>> choose(Request request) {
// 从请求中获取版本信息(例如从 Header 中)
String version = null;
if (request instanceof RequestDataContext) {
RequestDataContext context = (RequestDataContext) request;
HttpHeaders headers = context.getClientRequest().getHeaders();
version = headers.getFirst("version");
}
String finalVersion = version;
return serviceInstanceListSupplierProvider.get()
.select(serviceId)
.next()
.map(instances -> {
List<ServiceInstance> filteredInstances = instances;
if (finalVersion != null) {
filteredInstances = instances.stream()
.filter(inst -> finalVersion.equals(inst.getMetadata().get("version")))
.collect(Collectors.toList());
}
if (filteredInstances.isEmpty()) {
return Response.error(new NoAvailableInstanceException("No instance for version: " + finalVersion));
}
// 随机选择一个
int index = new Random().nextInt(filteredInstances.size());
return Response.of(filteredInstances.get(index));
});
}
}
然后通过配置类将自定义负载均衡器应用到 AI 服务:
@Configuration
public class LoadBalancerConfig {
@Bean
public ReactorLoadBalancer<ServiceInstance> reactorServiceInstanceLoadBalancer(
Environment environment,
LoadBalancerClientFactory loadBalancerClientFactory) {
String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
return new VersionLoadBalancer(name,
() -> loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class));
}
}
在 bootstrap.yml 中开启针对 ai-service 的自定义负载均衡:
spring:
cloud:
loadbalancer:
configurations: health-check
nacos:
discovery:
metadata:
version: v1
ai-service:
ribbon:
NFLoadBalancerRuleClassName: com.example.demo.loadbalancer.VersionLoadBalancer # 如果使用 Ribbon,但推荐使用 Spring Cloud LoadBalancer
由于 Spring Cloud 2020 以后默认使用 Spring Cloud LoadBalancer,上述配置方式更贴合新版本。
12.4.5 测试灰度发布
- 启动两个 AI 服务实例,一个 metadata.version=v1,另一个 metadata.version=v2。
- 启动 Gateway(或直接使用客户端负载均衡的调用方,如订单服务)。
- 调用时携带 Header
version: v1,请求应被路由到 v1 实例;携带version: v2路由到 v2 实例;不携带则随机选择一个。 - 通过调整灰度策略(例如按用户 ID 取模),可以实现小流量测试。
12.4.6 基于配置中心的动态切换
灰度策略可以存储在配置中心,通过动态刷新实现实时调整。例如,配置 gray.ratio=10 表示 10% 流量走新版本,然后在负载均衡器中读取该配置,根据随机数决定版本。
12.5 本章小结
通过本章的学习,你将 Spring AI 服务成功融入了 Spring Cloud 生态:
- 配置中心:使用 Nacos 管理提示词模板,实现动态刷新,无需重启服务。
- 服务发现:将 AI 服务注册到 Nacos,供其他业务服务通过负载均衡调用。
- 灰度发布:通过实例元数据和自定义负载均衡策略,实现版本感知的路由,支持平滑升级和 A/B 测试。
十三、总结与最佳实践
恭喜你完成了整个 Spring AI 学习之旅!从最初的环境搭建到最后的微服务集成,你已经系统地掌握了 Spring AI 的所有核心组件,并亲手实践了从简单聊天到复杂 RAG 知识库的构建。现在,让我们一起来回顾所学内容,并探讨在生产环境中如何做出明智的技术选型,最后为你指明继续深入的方向。
13.1 回顾 Spring AI 所有组件及适用场景
在整个教程中,我们逐步探索了以下组件,每个组件都有其独特的用途。下面的表格可以帮助你快速回顾:
| 组件类别 | 核心组件/概念 | 适用场景 | 你学到的实践 |
|---|---|---|---|
| 基础模型 | ChatClient, ChatModel | 任何需要与大模型对话的地方 | 使用 prompt().user().call().content() 发送消息,获取回复 |
| 流式响应 | stream(), Flux<ChatResponse> | 需要实时展示生成内容(打字机效果) | 结合 WebFlux 返回 text/event-stream,前端用 EventSource 接收 |
| 提示词管理 | PromptTemplate, @Value 加载模板 | 动态构造用户消息,避免硬编码 | 在 resources 中定义模板文件,用变量替换占位符 |
| 结构化输出 | entity(Class<T>) | 让 AI 返回 Java 对象,无需手动解析 JSON | 定义 POJO,调用 call().entity(Person.class) 直接获取对象 |
| 函数调用 | @Tool 注解,ChatClient.tools() | 让 AI 获取实时信息或执行操作(如查天气、查订单) | 编写工具 Bean,注册到 ChatClient,AI 自动调用 |
| 多模态 | ImageModel, AudioModel, 支持图片的 UserMessage | 图像生成、图像理解、语音合成/识别 | 使用 ImageClient 生成图片,在聊天中附加图片让 AI 描述 |
| 文档处理 | DocumentReader (PDF, TXT), TokenTextSplitter | 从各种格式文件中提取文本并分割 | 加载 PDF 或 TXT 文档,分割成适合嵌入的块 |
| 向量存储 | VectorStore (PGvector, Redis, Milvus 等) | 存储文档向量,用于相似度检索 | 配置向量数据库,调用 vectorStore.add(documents) |
| 检索增强 | QuestionAnswerAdvisor, DocumentRetriever | 让 AI 基于私有知识库回答问题(RAG) | 在 ChatClient 中添加检索顾问,自动注入检索结果 |
| 高级 RAG | QueryTransformer, HybridSearch, Reranking | 提升检索准确率,处理复杂查询 | 可扩展检索流程,结合压缩查询、多路召回等 |
| 注解式开发 | 自定义 @AiPrompt + AOP | 简化 AI 服务调用,提示词与业务代码分离 | 定义注解,切面自动渲染模板并调用 ChatClient |
| 缓存 | Spring Cache (@Cacheable) + Caffeine/Redis | 减少重复调用,提升响应速度,节省成本 | 在服务方法上添加 @Cacheable,配置缓存管理器 |
| 监控 | Micrometer + Actuator | 收集调用次数、耗时、Token 用量 | 自定义切面记录指标,通过 /actuator/metrics 暴露 |
| 微服务集成 | Spring Cloud Config, Nacos, 服务发现, 灰度发布 | 将 AI 服务融入微服务体系,实现配置中心、负载均衡、版本控制 | 使用配置中心管理提示词,注册到 Nacos,自定义负载均衡实现灰度 |
一句话总结:Spring AI 通过组件化设计,让你像搭积木一样组合这些模块,快速构建从简单到复杂的 AI 应用,并自然融入 Spring 生态。
13.2 生产环境选型建议
当你准备将应用部署到生产环境时,需要根据实际场景做出更细致的选择。以下是一些关键决策点的建议。
13.2.1 向量数据库选型
在 RAG 应用中,向量存储是核心组件。Spring AI 通过 VectorStore 抽象支持多种实现。
| 向量数据库 | 优点 | 适用场景 |
|---|---|---|
| PGvector (PostgreSQL 插件) | - 如果你已经在使用 PostgreSQL,可以复用现有数据库 - 支持 SQL 查询,与关系数据无缝结合 - 开源免费 | 中小型项目,数据量在百万级以下,希望简化架构 |
| Milvus / Zilliz Cloud | - 专业向量数据库,功能强大,支持十亿级向量 - 丰富的索引类型(IVF、HNSW) - 云服务或自托管 | 大规模生产环境,对性能和扩展性要求高 |
| Elasticsearch | - 强大的全文检索 + 向量检索混合能力 - 分布式、高可用 - 适合日志、文档类数据 | 需要同时支持关键词搜索和语义搜索,数据量大 |
| Redis (Redis Stack) | - 内存数据库,性能极高 - 支持向量搜索和二级索引 - 可作为缓存和向量存储合一 | 需要低延迟检索,数据量适中(受内存限制) |
| SimpleVectorStore (内存) | - 无需额外基础设施,测试方便 - 数据不持久化 | 开发测试,小型演示 |
建议:对于大多数 Java 后端团队,如果已有 PostgreSQL,PGvector 是最平滑的选择;如果对性能有更高要求且预算充足,可以考虑 Milvus 或 Zilliz Cloud;如果团队熟悉 Elasticsearch,它也是一个强大的多面手。
13.2.2 模型选型(云端 vs 本地)
大模型的选择直接影响成本、响应速度和数据隐私。
| 模型类型 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 云端模型 (OpenAI, Azure, 通义千问, DeepSeek 等) | - 能力强大,持续更新 - 无需自己部署硬件 - 开箱即用 | - 调用付费,长期成本高 - 数据需发送到第三方(有隐私风险) - 依赖网络 | 通用对话、需要强大推理能力的场景,对数据隐私要求不高的项目 |
| 本地模型 (Ollama, llama.cpp, Hugging Face) | - 数据完全私有 - 一次部署,免费调用(除硬件成本) - 延迟低(无网络开销) | - 需要 GPU 资源,硬件成本高 - 模型能力相对较弱(尤其小参数模型) - 部署运维复杂 | 数据隐私要求极高(如金融、医疗),或需要离线运行 |
建议:
- 初期可以用云端模型快速验证,如 OpenAI 的
gpt-4o-mini或通义千问的qwen-plus,成本较低。 - 当应用成熟且数据敏感时,可考虑用本地模型替代,例如通过 Ollama 运行
llama3或qwen2。 - Spring AI 支持通过配置轻松切换模型提供商(如
spring.ai.openai换成spring.ai.ollama),方便做 A/B 测试或灰度迁移。
13.2.3 记忆持久化方案
对于多轮对话,我们需要保存对话历史。Spring AI 提供了 ChatMemory 接口,默认实现是 InMemoryChatMemory(非持久化)。生产环境需要持久化:
- 基于 Redis:用 Redis 存储对话历史,速度快,支持过期。可以实现
ChatMemory接口,使用 Redis 的 List 或 Hash 结构。 - 基于数据库:使用 JPA 将对话存入关系库,适合需要长期保存并分析对话的场景。
- 基于向量数据库:也可以将对话作为文档存入向量库,用于后续检索,但通常对话记忆是短期需求。
建议:对于大多数 Web 应用,用 Redis 存储对话历史是最佳选择:性能好,支持自动过期,避免数据库压力。Spring 生态有 spring-data-redis,可以轻松集成。
13.2.4 性能与成本优化
- 缓存:对于常见问题(如 FAQ),使用 Spring Cache 缓存 AI 回复,可大幅减少重复调用,降低 Token 消耗。注意设置合理的过期时间。
- 流式响应:对于长回复,使用流式接口提升用户体验,同时避免超时。
- Token 监控:通过监听器或切面记录每次调用的 Token 用量,设置每日/每月上限,及时告警。
- 降级方案:当模型服务不可用或超时时,返回预设的默认回复,或切换到备用模型。
- 请求合并:对于非实时场景,可以将多个请求合并成一个批量请求发送给支持批处理的模型(如有),减少网络开销。
- 模型蒸馏:对于特定任务,可以微调一个小模型替代大模型,降低成本。
13.2.5 安全合规
- 内容审核:在用户输入和 AI 输出两端进行审核,防止不当内容。可以使用 OpenAI 的 Moderation API 或集成第三方审核服务。
- 数据脱敏:在发送给模型之前,对用户输入中的敏感信息(如身份证号、手机号)进行脱敏处理。
- API 密钥管理:使用环境变量或配置中心管理密钥,避免硬编码。定期轮换密钥。
13.3 后续学习路径推荐
你已经掌握了 Spring AI 的绝大部分功能,接下来可以朝以下方向深入:
- 深入学习 Spring AI 官方文档:Spring AI 正在快速发展,关注 官方文档 获取最新特性和最佳实践。
- 探索 RAG 高级技术:
- 混合检索:结合向量检索和关键词检索,提升准确率。
- 重排序:使用 Cross-encoder 模型对检索结果重新排序,提高相关性。
- 查询规划:对于复杂问题,拆分成多个子查询再聚合答案。
- 尝试 Agent 开发:构建能自主调用多工具的 Agent,例如规划、执行、观察的循环。Spring AI 正在发展 Agent 能力,可关注相关更新。
- 关注 Spring AI Alibaba:阿里云提供的 Spring AI 实现,适配通义千问等国产模型,适合国内用户。
- 参与社区:给 Spring AI 项目点个 Star,在 GitHub 上提 Issue 或 PR,与其他开发者交流经验。
- 结合实际业务落地:将 AI 能力集成到现有系统中,如智能客服、内部知识库、代码生成助手等,并持续优化成本和效果。
附录
A. 常见问题解答(FAQ)
Q1: 为什么我使用 ChatClient 时出现 No qualifying bean of type 'ChatClient.Builder' 错误?
A: 确保引入了正确的 Spring AI Starter 依赖(如 spring-ai-openai-spring-boot-starter),并且 Spring Boot 主类上有 @SpringBootApplication 注解。自动配置会创建 ChatClient.Builder 的 Bean。
Q2: 如何切换使用不同的模型(例如从 OpenAI 切换到 Ollama)?
A: 修改 application.yml 中的配置,将 spring.ai.openai 替换为 spring.ai.ollama,并引入对应的 Starter 依赖。业务代码中的 ChatClient 注入保持不变。
Q3: 流式响应时,前端收到乱码或无法正确解析?
A: 确保 Controller 的 produces = MediaType.TEXT_EVENT_STREAM_VALUE,并且返回的是 Flux<String> 或 Flux<ServerSentEvent>。前端使用 EventSource 时,注意设置正确的字符编码(默认 UTF-8)。
Q4: 结构化输出时,AI 返回的 JSON 无法解析为我的 Java 类? A: 检查你的 Java 类是否有无参构造和 getter/setter(或使用 record)。另外,可以在提示词中明确要求输出 JSON 格式,例如“请以 JSON 格式返回,包含 name、age 字段”。
Q5: 函数调用时,AI 不调用我期望的工具?
A: 检查工具的描述是否清晰,参数描述是否准确。可以调整 @Tool 的 name 和 description,确保 AI 能理解何时调用。另外,注意工具方法必须是 public 的,且工具类需要是 Spring Bean。
Q6: RAG 检索结果不相关怎么办? A: 尝试调整分割块大小(chunk size)和重叠(overlap),或者提高相似度阈值(minScore)。也可以考虑使用混合检索或重排序技术。
Q7: 如何获取 Token 用量?
A: 使用 ChatResponse response = chatClient.prompt().call();,然后从 response.getMetadata() 中获取。具体实现因模型而异,OpenAI 的 Token 用量可以通过 OpenAiUsage 类获取。
Q8: Spring AI 和 LangChain4j 有什么区别? A: Spring AI 是 Spring 官方项目,深度集成 Spring 生态,适合已有 Spring 技术栈的团队;LangChain4j 是社区驱动的独立框架,更加轻量,组件更丰富。两者各有优势,可根据团队偏好选择。
B. 完整代码示例仓库地址
为了方便你查阅和运行,本教程的所有代码示例已整理到一个 GitHub 仓库中:
仓库结构按照章节组织,每个示例都是独立的可运行模块,并包含详细的 README 说明。
C. 参考资源链接
- Spring AI 官方文档:spring.io/projects/sp…
- Spring AI GitHub:github.com/spring-proj…
- Spring Boot 官方文档:spring.io/projects/sp…
- Spring Cloud 官方文档:spring.io/projects/sp…
- Nacos 官网:nacos.io
- PGvector 项目:github.com/pgvector/pg…
- Ollama 官网:ollama.ai