Spring AI 技术细节:RAG QuestionAnswerAdvisor 设计与实现

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 需要 queryquestion_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,需要实现 CallAdvisorStreamAdvisor 接口(或两者)。关键方法包括 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 依次处理请求和响应。

关键执行规则

  1. 顺序决定 :由 getOrder() 方法决定,值越低越先执行
  2. 堆栈特性 :Advisor 链以堆栈方式运行------第一个处理请求的 Advisor,是最后一个处理响应的
  3. 自动添加:框架会自动添加一个"最终 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,实现迭代工作流

最佳实践总结

  1. 检索参数调优 :根据业务场景调整 topKsimilarityThreshold,避免检索过多无关文档或遗漏关键信息

  2. 模板设计 :自定义 PromptTemplate 时,明确告知 LLM 如何处理"检索不到相关信息"的情况(如"请告知用户无法回答")

  3. 过滤表达式:充分利用元数据过滤缩小检索范围,提升检索精度和响应速度

  4. 链顺序规划:合理安排 Advisor 顺序------安全审查通常最先执行,RAG 在对话记忆之后,日志和缓存放在最后

  5. 监控与评估:建立 RAG 检索质量监控体系,定期评估召回率和精确率

  6. 缓存策略:对高频查询实施缓存,减少向量检索开销

  7. 灰度发布:使用 Feature Flag 控制 RAG 功能的启用,支持 A/B 测试

相关推荐
有书Show1 小时前
GEO发稿平台哪家好:2026生成式引擎优化新规下的平台评估维度
大数据·人工智能
实战云1 小时前
高校AI通识课建设指南:实操平台、教学包与教材适配全解析
人工智能
LabVIEW开发1 小时前
LabVIEW储能电池绝缘测试
人工智能·labview·labview知识·labview功能·labview程序
斑马1391 小时前
Linux软件编程学习笔记(十二)——TCP并发服务器模型
java·服务器·网络
冬栈程序设计1 小时前
Springboot宿舍管理系统【670099】 -附源码(开箱即用)
java·毕业设计·springboot·课程设计·程序设计·大作业
Luhui Dev1 小时前
图片几何题如何转成可编辑图形?从识别题图到复用画板的工作流
人工智能·数学·ai·agent·luhuidev
LorryJovens1 小时前
【LAAP架构与安全伦理】LAAP框架与具身智能大脑:从认知架构到自主意识与人机共生社会--LAAP先导愿景片发布
人工智能·架构·agi
2601_962284501 小时前
北京Java全栈开发培训 Web前端开发 软件测试就业培训班
java·软件测试·web前端·全栈开发·就业培训
阿里云大数据AI技术1 小时前
数据平台开始“支撑 Agent”:DataWorks Data Agent 如何重构企业数据生产方式
人工智能·阿里云·dataworks·数据平台·data agent