Spring AI 技术细节:RAG QuestionAnswerAdvisor 设计与实现
前置知识
- 了解 Spring AI ChatClient / ChatModel 的基本调用方式
- 知道 RAG(Retrieval-Augmented Generation)的两阶段架构------检索 + 生成
- 熟悉 Advisor 拦截器链的 Around Advice 模式
核心概念
核心问题
LLM 的知识受限于训练数据截止日期和上下文窗口。QuestionAnswerAdvisor 如何在 LLM 处理用户问题前自动向知识库提问(检索相关文档),并将找到的证据注入 Prompt,让 LLM 基于真实资料而非幻觉生成答案?
生活类比
像开卷考试:学生(LLM)拿到试卷(用户问题)后,先快速翻阅参考书(VectorStore 检索相关文档),把相关段落抄在草稿纸上(上下文注入),然后基于这些资料组织回答。Advisor 就是那个帮忙翻书的助教。
核心思想
作为 Advisor 链的一环,在 ChatClient 调用 LLM 前拦截请求,用用户 Query 去 VectorStore 检索相关文档,将检索结果拼接进 Prompt 上下文发送给 LLM,使回答基于真实知识而非模型记忆。
注:
博客:
https://blog.csdn.net/badao_liumang_qizhi
一、QuestionAnswerAdvisor 概述
Spring AI RAG 的模块化架构
Spring AI 对 RAG 的支持采用了模块化架构。开发者既可以自行构建自定义 RAG 流程,也可以使用开箱即用的 Advisor API 快速实现 RAG。QuestionAnswerAdvisor 正是后者------一个封装了标准 RAG 流程的内置 Advisor。
要使用 QuestionAnswerAdvisor,需要在项目中添加以下依赖:
xml
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-advisors-vector-store</artifactId>
</dependency>
QuestionAnswerAdvisor 的接口继承体系
QuestionAnswerAdvisor 实现了多个关键接口:
text
┌──────────────────────────────────────────────────────────────┐
│ Advisor (顶层接口) │
│ + getOrder(): int │
│ + getName(): String │
└────────────────────────┬─────────────────────────────────────┘
│
┌───────────────┴───────────────┐
▼ ▼
┌──────────────────┐ ┌──────────────────────┐
│ CallAroundAdvisor│ │ StreamAroundAdvisor │
│ + aroundCall() │ │ + aroundStream() │
└────────┬─────────┘ └──────────┬───────────┘
│ │
└───────────────┬───────────────┘
▼
┌──────────────────────────────────────────────────────────────┐
│ QuestionAnswerAdvisor │
│ implements CallAroundAdvisor, StreamAroundAdvisor, Ordered │
│ - vectorStore: VectorStore │
│ - searchRequest: SearchRequest │
│ - promptTemplate: PromptTemplate │
│ + before(): AdvisorChain │
│ + after(): AdvisorChain │
└──────────────────────────────────────────────────────────────┘
QuestionAnswerAdvisor 同时支持非流式 (CallAroundAdvisor)和流式 (StreamAroundAdvisor)两种调用模式,这意味着无论是普通的同步调用还是流式输出场景,RAG 增强都能无缝工作。
核心执行流程详解
text
┌─────────────────────────────────────────────────────────────────────────┐
│ QuestionAnswerAdvisor 完整执行流程 │
│ │
│ 1. 用户调用: chatClient.prompt("Spring AI 支持哪些 VectorStore?").call()│
│ │ │
│ ▼ │
│ 2. Advisor 链拦截: 框架将 Prompt 包装为 AdvisedRequest │
│ │ │
│ ▼ │
│ 3. QuestionAnswerAdvisor.before() 执行: │
│ ├─ 从 AdvisedRequest 中提取用户查询文本 │
│ ├─ 从 AdvisorContext 中读取运行时参数 (filter_expression 等)│
│ └─ 调用 VectorStore.similaritySearch() 执行检索 │
│ │ │
│ ▼ │
│ 4. 上下文增强: │
│ ├─ 将检索到的 Document 列表格式化为文本 │
│ └─ 使用 PromptTemplate 将检索结果拼接到用户消息中 │
│ │ │
│ ▼ │
│ 5. 传递给下游: 调用 advisorChain.next() 将增强后的请求传给下一个 Advisor│
│ │ │
│ ▼ │
│ 6. 下游处理: 后续 Advisor + LLM 调用 │
│ │ │
│ ▼ │
│ 7. QuestionAnswerAdvisor.after() 执行: │
│ └─ 处理/修饰响应 (可注入检索来源元数据) │
│ │ │
│ ▼ │
│ 8. 返回最终响应给调用方 │
└─────────────────────────────────────────────────────────────────────────┘
二、QuestionAnswerAdvisor 核心实现
默认模板与占位符机制
QuestionAnswerAdvisor 使用一个默认模板 来将检索到的文档与用户问题合并。模板中必须包含一个名为 question_answer_context 的占位符,检索到的文档内容会被填充到这个占位符位置。
默认模板(近似):
text
基于以下参考信息回答用户问题:
{question_answer_context}
用户问题:{query}
Spring AI 1.0.0 RC1 版本起,Advisor 使用独立的模板 ,每个 Advisor 有自己特定的占位符要求------QuestionAnswerAdvisor 需要 query 和 question_answer_context 两个占位符。
QuestionAnswerAdvisor 核心源码解析
java
// Spring AI QuestionAnswerAdvisor 核心逻辑(基于源码推断)
public class QuestionAnswerAdvisor implements CallAroundAdvisor, StreamAroundAdvisor {
private final VectorStore vectorStore;
private final SearchRequest defaultSearchRequest;
private final PromptTemplate promptTemplate;
private final int order;
@Override
public AdvisedResponse aroundCall(AdvisedRequest request,
CallAroundAdvisorChain chain) {
// 1. 提取用户查询
String query = extractUserText(request);
// 2. 构建 SearchRequest(合并默认配置 + 运行时参数)
SearchRequest searchRequest = buildSearchRequest(request);
// 3. 执行向量检索
List<Document> documents = vectorStore.similaritySearch(searchRequest);
// 4. 格式化检索结果为上下文文本
String context = formatDocuments(documents);
// 5. 使用模板增强用户消息
AdvisedRequest enhancedRequest = request.augment(userText -> {
// 将检索结果填充到 question_answer_context 占位符
String augmented = promptTemplate.render(Map.of(
"query", query,
"question_answer_context", context
));
return augmented;
});
// 6. 调用链中的下一个 Advisor
AdvisedResponse response = chain.next(enhancedRequest);
// 7. 将检索来源注入响应元数据
response.getContext().put("rag_sources", documents);
return response;
}
}
流式场景的特殊处理
对于流式输出场景,QuestionAnswerAdvisor 实现了 StreamAroundAdvisor 接口,处理逻辑与非流式类似,但需要适配响应式流(Reactive Streams)的背压(Backpressure)和异步特性。核心区别在于检索操作需要非阻塞执行 ,Builder 提供了 scheduler() 方法来指定执行检索的线程池:
java
// 为流式场景配置专用的 Scheduler
var qaAdvisor = QuestionAnswerAdvisor.builder(vectorStore)
.scheduler(Schedulers.boundedElastic()) // 使用响应式调度器
.build();
此外,protectFromBlocking() 方法可以防止在响应式上下文中意外执行阻塞操作。
三、QuestionAnswerAdvisor 配置与使用
完整的 Builder 配置选项
QuestionAnswerAdvisor.Builder 提供以下配置方法:
| 方法 | 说明 |
|---|---|
searchRequest(SearchRequest) |
配置检索参数(topK、阈值、过滤表达式等) |
promptTemplate(PromptTemplate) |
自定义上下文增强模板 |
order(int) |
设置 Advisor 在链中的执行顺序 |
scheduler(Scheduler) |
为检索操作配置响应式调度器 |
protectFromBlocking(boolean) |
防止阻塞操作 |
build() |
构建 Advisor 实例 |
运行时动态配置 Filter Expression
QuestionAnswerAdvisor 支持在运行时动态更新检索的过滤表达式,无需重新创建 Advisor 实例:
java
@Service
public class DynamicRagService {
private final ChatClient chatClient;
public String searchWithDynamicFilter(String query, String tenantId) {
// 运行时传入过滤条件
return chatClient.prompt()
.user(query)
.advisors(a -> a.param(
QuestionAnswerAdvisor.FILTER_EXPRESSION, // 内置常量
"tenant_id == '" + tenantId + "'"
))
.call()
.content();
}
/**
* 多条件动态过滤
*/
public String searchWithMultipleFilters(String query,
String tenantId,
String category,
Double minScore) {
String filter = String.format(
"tenant_id == '%s' && category == '%s' && score >= %.2f",
tenantId, category, minScore
);
return chatClient.prompt()
.user(query)
.advisors(a -> a.param(
QuestionAnswerAdvisor.FILTER_EXPRESSION, filter
))
.call()
.content();
}
}
FILTER_EXPRESSION 参数使用的过滤表达式语法与 VectorStore 的 SearchRequest.filterExpression 完全一致,具有跨向量数据库的可移植性。
自定义 PromptTemplate 的两种方式
QuestionAnswerAdvisor 的模板定制与 ChatClient 自身的模板渲染器是两个不同层级的配置:
java
@Configuration
public class CustomTemplateConfig {
@Bean
public QuestionAnswerAdvisor customTemplateAdvisor(VectorStore vectorStore) {
// 方式一:自定义 Advisor 的上下文增强模板
PromptTemplate customTemplate = new PromptTemplate("""
你是一位专业的技术顾问。请基于以下参考资料回答用户的问题。
如果参考资料不足以回答,请明确说明"根据现有资料无法回答该问题"。
【参考资料】
{question_answer_context}
【用户问题】
{query}
请用专业、清晰的语言回答,并在引用参考资料时标注来源编号。
""");
return QuestionAnswerAdvisor.builder(vectorStore)
.promptTemplate(customTemplate) // Advisor 层模板
.searchRequest(SearchRequest.builder()
.topK(5)
.similarityThreshold(0.7)
.build())
.build();
}
@Bean
public ChatClient chatClient(ChatLanguageModel model,
QuestionAnswerAdvisor advisor) {
return ChatClient.builder(model)
// 方式二:ChatClient 层的模板渲染器(在 Advisor 执行之前生效)
.templateRenderer((template, params) -> {
// 处理初始用户/系统提示词
return template.render(params);
})
.defaultAdvisors(advisor)
.build();
}
}
四、自定义 RAG Advisor
实现自定义 Advisor 的完整接口
要创建自定义 Advisor,需要实现 CallAdvisor 或 StreamAdvisor 接口(或两者)。关键方法包括 before()、after() 以及 getOrder() 和 getName()。
java
/**
* 自定义增强型 RAG Advisor - 完整实现
* - 支持多轮对话历史
* - 支持检索后重排序(Rerank)
* - 支持引用标注和来源追踪
* - 支持检索结果缓存
*/
@Component
public class EnhancedRagAdvisor implements CallAroundAdvisor, StreamAroundAdvisor {
private final VectorStore vectorStore;
private final RerankingService rerankingService;
private final Cache<String, List<Document>> cache;
private final int order;
public EnhancedRagAdvisor(VectorStore vectorStore,
RerankingService rerankingService) {
this.vectorStore = vectorStore;
this.rerankingService = rerankingService;
this.cache = Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(Duration.ofMinutes(5))
.build();
this.order = 100; // 默认顺序
}
@Override
public AdvisedResponse aroundCall(AdvisedRequest request,
CallAroundAdvisorChain chain) {
// 1. 提取查询
String query = extractQuery(request);
// 2. 检查缓存(精确匹配)
List<Document> cached = cache.getIfPresent(query);
if (cached != null && !cached.isEmpty()) {
log.info("Cache hit for query: {}", query);
return chain.next(augmentRequest(request, cached));
}
// 3. 向量检索(扩大召回范围)
List<Document> candidates = vectorStore.similaritySearch(
SearchRequest.builder()
.query(query)
.topK(20) // 扩大召回
.similarityThreshold(0.5) // 降低阈值
.build()
);
// 4. 重排序(使用 Cross-Encoder 精排)
List<Document> reranked = rerankingService.rerank(query, candidates);
List<Document> topDocs = reranked.stream()
.limit(5) // 最终取 Top5
.collect(Collectors.toList());
// 5. 写入缓存
cache.put(query, topDocs);
// 6. 带引用标注的格式化
String context = formatWithCitations(topDocs);
// 7. 增强 Prompt
AdvisedResponse response = chain.next(
augmentWithContext(request, query, context)
);
// 8. 将来源信息注入响应
response.getContext().put("rag_sources", topDocs);
response.getContext().put("rag_reranked", true);
return response;
}
@Override
public Flux<AdvisedResponse> aroundStream(AdvisedRequest request,
StreamAroundAdvisorChain chain) {
// 流式场景的响应式实现
String query = extractQuery(request);
List<Document> docs = vectorStore.similaritySearch(
SearchRequest.builder().query(query).topK(5).build()
);
String context = formatWithCitations(docs);
return chain.next(augmentRequest(request, docs))
.doOnNext(response ->
response.getContext().put("rag_sources", docs)
);
}
private String formatWithCitations(List<Document> docs) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < docs.size(); i++) {
Document doc = docs.get(i);
String source = (String) doc.getMetadata()
.getOrDefault("source", "unknown");
Double score = (Double) doc.getMetadata()
.getOrDefault("score", 0.0);
sb.append(String.format("[%d] (相关度: %.2f%%) %s [来源: %s]%n",
i + 1, score * 100, doc.getContent(), source));
}
return sb.toString();
}
private AdvisedRequest augmentRequest(AdvisedRequest request,
List<Document> docs) {
String query = extractQuery(request);
String context = formatWithCitations(docs);
// 使用模板增强
return request.augment(userText ->
String.format("""
请基于以下参考资料回答问题,并在回答中标注引用编号。
参考资料:
%s
问题:%s
""", context, query)
);
}
private String extractQuery(AdvisedRequest request) {
// 从请求中提取用户消息文本
return request.userText();
}
@Override
public int getOrder() {
return order;
}
@Override
public String getName() {
return "enhancedRagAdvisor";
}
}
五、多 Advisor 链式组合
Advisor 链的执行顺序详解
Spring AI 的 Advisor 系统以链(Chain) 方式运行,每个 Advisor 依次处理请求和响应。
关键执行规则:
- 顺序决定 :由
getOrder()方法决定,值越低越先执行 - 堆栈特性 :Advisor 链以堆栈方式运行------第一个处理请求的 Advisor,是最后一个处理响应的
- 自动添加:框架会自动添加一个"最终 Advisor"将请求发送给 LLM
text
┌─────────────────────────────────────────────────────────────────────┐
│ Advisor 链执行顺序示意 │
│ │
│ 请求方向 (Request Flow) │
│ ────────────────────────────────────────────────────────────────▶ │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │Security │───▶│ RAG │───▶│ Logger │───▶│ Cache │───▶│ LLM
│ │Advisor │ │Advisor │ │Advisor │ │Advisor │ │
│ │order=0 │ │order=100 │ │order=200 │ │order=300 │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
│ ▲ ▲ ▲ ▲ │
│ │ │ │ │ │
│ ─────┴───────────────┴───────────────┴───────────────┴────────── │
│ │
│ 响应方向 (Response Flow) │
│ ◀─────────────────────────────────────────────────────────────── │
│ 最后一个处理请求的 Advisor = 第一个处理响应 │
└─────────────────────────────────────────────────────────────────────┘
内置 Advisor 类型
Spring AI 提供了多个开箱即用的 Advisor:
| Advisor | 用途 |
|---|---|
QuestionAnswerAdvisor |
RAG 检索增强生成 |
MessageChatMemoryAdvisor |
基于消息的对话记忆管理 |
PromptChatMemoryAdvisor |
基于 Prompt 的对话记忆 |
VectorStoreChatMemoryAdvisor |
基于向量存储的长期记忆 |
SafeGuardAdvisor |
敏感词过滤和安全防护 |
SimpleLoggerAdvisor |
请求/响应日志记录 |
ToolCallAdvisor |
工具调用循环(递归 Advisor) |
递归 Advisor(Recursive Advisor)
从 Spring AI 1.1.0-M4 版本开始,引入了递归 Advisor 支持,允许 Advisor 链多次循环执行 以支持迭代工作流。ToolCallAdvisor 就是将工具调用循环实现为 Advisor 链一部分的典型例子。
java
// 递归 Advisor 使用示例(ToolCallAdvisor 模式)
@Configuration
public class RecursiveAdvisorConfig {
@Bean
public ChatClient recursiveToolClient(ChatLanguageModel model,
List<Tool> tools) {
return ChatClient.builder(model)
.defaultAdvisors(
// ToolCallAdvisor 会循环执行:调用 LLM → 检测工具调用 → 执行工具 → 再次调用 LLM
new ToolCallAdvisor(tools)
)
.build();
}
}
条件化 Advisor 链
java
@Configuration
public class ConditionalAdvisorChainConfig {
@Bean
public ChatClient conditionalClient(ChatLanguageModel model,
VectorStore vectorStore,
FeatureFlags featureFlags) {
return ChatClient.builder(model)
.defaultAdvisors(context -> {
List<Advisor> advisors = new ArrayList<>();
// 总是启用日志
advisors.add(new SimpleLoggerAdvisor());
// 根据 Feature Flag 决定是否启用 RAG
if (featureFlags.isRagEnabled()) {
advisors.add(QuestionAnswerAdvisor.builder(vectorStore)
.searchRequest(SearchRequest.builder()
.topK(featureFlags.getRagTopK())
.build())
.build());
}
// 根据环境决定是否启用安全审查
if (featureFlags.isProduction()) {
advisors.add(new SafeGuardAdvisor(sensitiveWords));
}
// 高并发场景启用缓存
if (featureFlags.isCacheEnabled()) {
advisors.add(new CachingAdvisor(cacheConfig));
}
return advisors;
})
.build();
}
}
六、检索质量优化与监控
检索质量评估
java
@Service
public class RagQualityMonitor {
private final MeterRegistry meterRegistry;
/**
* 评估单次 RAG 检索的质量
*/
public RagQualityReport evaluateQuery(String query,
List<Document> retrieved,
List<String> groundTruthIds) {
// 1. 计算召回率 (Recall)
Set<String> retrievedIds = retrieved.stream()
.map(Document::getId)
.collect(Collectors.toSet());
Set<String> truthSet = new HashSet<>(groundTruthIds);
long hitCount = retrievedIds.stream()
.filter(truthSet::contains)
.count();
double recall = truthSet.isEmpty() ? 1.0 :
(double) hitCount / truthSet.size();
// 2. 记录指标
meterRegistry.gauge("rag.recall", recall);
meterRegistry.counter("rag.retrieved_count",
"query", query,
"count", String.valueOf(retrieved.size())).increment();
// 3. 如果召回率过低,触发告警
if (recall < 0.5) {
log.warn("Low RAG recall: {} for query: {}", recall, query);
}
return new RagQualityReport(recall, retrieved.size(), hitCount);
}
}
Query 改写与查询增强
Spring AI 提供了模块化的 RAG 特性,支持围绕 Query 进行查询改写和查询增强:
java
@Service
public class QueryEnhancementService {
private final ChatLanguageModel model;
/**
* 使用 LLM 改写用户查询,提升检索效果
*/
public String rewriteQuery(String originalQuery) {
String prompt = String.format("""
将以下用户问题改写成更适合向量检索的查询语句。
要求:保留核心语义,使用关键词形式,去除口语化表达。
原始问题:%s
改写后的查询:
""", originalQuery);
return model.call(prompt);
}
/**
* 查询扩展:生成多个相关查询提高召回率
*/
public List<String> expandQuery(String query) {
// 使用同义词扩展、HyDE 等技术生成多个查询变体
// 然后对每个变体分别检索,合并结果后去重
return List.of(
query,
query + " 技术实现",
query + " 最佳实践",
query + " 案例分析"
);
}
}
七、总结
本章深入 Spring AI RAG QuestionAnswerAdvisor 的设计原理与实现细节。QuestionAnswerAdvisor 封装了 RAG 的核心循环:拦截 LLM 调用 → 向量检索 → 上下文拼接 → LLM 生成。
核心要点回顾
| 维度 | 关键内容 |
|---|---|
| 接口体系 | 实现 CallAroundAdvisor + StreamAroundAdvisor,同时支持同步和流式 |
| 依赖管理 | 需引入 spring-ai-advisors-vector-store 依赖 |
| 模板占位符 | 必须包含 question_answer_context 占位符 |
| 动态过滤 | 通过 FILTER_EXPRESSION 参数运行时注入过滤条件 |
| 执行顺序 | 由 getOrder() 控制,值越低越先执行 |
| 递归支持 | 1.1.0-M4+ 支持递归 Advisor,实现迭代工作流 |
最佳实践总结
-
检索参数调优 :根据业务场景调整
topK和similarityThreshold,避免检索过多无关文档或遗漏关键信息 -
模板设计 :自定义
PromptTemplate时,明确告知 LLM 如何处理"检索不到相关信息"的情况(如"请告知用户无法回答") -
过滤表达式:充分利用元数据过滤缩小检索范围,提升检索精度和响应速度
-
链顺序规划:合理安排 Advisor 顺序------安全审查通常最先执行,RAG 在对话记忆之后,日志和缓存放在最后
-
监控与评估:建立 RAG 检索质量监控体系,定期评估召回率和精确率
-
缓存策略:对高频查询实施缓存,减少向量检索开销
-
灰度发布:使用 Feature Flag 控制 RAG 功能的启用,支持 A/B 测试