【SpringAI源码解析】RetrievalAugmentationAdvisor多轮对话历史记录解析
RAG多轮对话历史记录解析
Q:Query中的history从何而来?
A:history其实是基于ChatMemory的,我们在chatclient中先后配置了MessageChatMemoryAdvisor和retrievalAugmentationAdvisor。
▼java复制代码ChatClient chatClient = ChatClient.builder(chatModel) .defaultAdvisors(new MyLoggerAdvisor(), MessageChatMemoryAdvisor.builder(chatMemory).build()//添加Memory , retrievalAugmentationAdvisor//RAG ) .build();
学过Advisor的都知道,这东西可以理解成一个过滤器Filter,在MessageChatMemoryAdvisor中会创建一个新的****ChatClientRequest,并把有所的历史会话记录都放在这个新ChatClientRequest中,那么该Advisor放行后,传入下一个Advisor的ChatClientRequest就带有了所以的历史记录。
▼java复制代码@Override public ChatClientRequest before(ChatClientRequest chatClientRequest, AdvisorChain advisorChain) { String conversationId = getConversationId(chatClientRequest.context(), this.defaultConversationId); // 1. Retrieve the chat memory for the current conversation. List<Message> memoryMessages = this.chatMemory.get(conversationId); // 2. Advise the request messages list. List<Message> processedMessages = new ArrayList<>(memoryMessages); processedMessages.addAll(chatClientRequest.prompt().getInstructions());//获取全部的Message信息 // 3. Create a new request with the advised messages. ChatClientRequest processedChatClientRequest = chatClientRequest.mutate() .prompt(chatClientRequest.prompt().mutate().messages(processedMessages).build()) .build(); // 4. Add the new user message to the conversation memory. UserMessage userMessage = processedChatClientRequest.prompt().getUserMessage();//逆序获取最新的UserMessage this.chatMemory.add(conversationId, userMessage); return processedChatClientRequest; }
Q:RAG的Advisor就一定会读取所有的history信息作为向量搜索的文本吗?
A:答案是NO!,看看后面会发生什么。
我们在使用RAG的时候是开启了RetrievalAugmentationAdvisor或QuestionAnswerAdvisor。
而在QuestionAnswerAdvisor的源码中,其内部befor方法只使用了当前的UserMessage,也就是在向量转换检索时,并没有使用history信息融合,比如说在多轮对话中历史记录是:
-
UserMessage:“小米手机怎么样”
-
AssistantMessage:“小米手机xxxxxx很不错”
-
现在用户输入(UserMessage):“它多少钱?”
-
然后QuestionAnswerAdvisor就拿着**“它多少钱?”**向量化去数据库搜索。
-
**数据库懵了:”它“是谁呢???? 检索不到任何关于“小米手机”的文档。**搜索结果为null。
-
最终导致大模型收到的Prompt信息有:上下文历史记录(开启了ChatMemeory才会有)、RAG搜到的Document(可能为null,或者乱七八糟其他的东西)、当前message“它多少钱?”。
-
最终大模型告诉你(AssistantMessage):“抱歉,我不知道小米手机的具体信息。”
这不炸了吗?所以说多轮对话RAG还得是RetrievalAugmentationAdvisor
那么我们看看RetrievalAugmentationAdvisor是怎么完成基于历史对话的语义改写的。
RetrievalAugmentationAdvisor类部分源码如下:
▼java复制代码public ChatClientRequest before(ChatClientRequest chatClientRequest, @Nullable AdvisorChain advisorChain) { Map<String, Object> context = new HashMap(chatClientRequest.context()); // 【关键点】这里解答了之前的疑问:Query中的History是哪里来的? // 它是从 chatClientRequest 里现场提取 UserMessage(text) 和 Instructions(history) 组装的 Query originalQuery = Query.builder() .text(chatClientRequest.prompt().getUserMessage().getText())//当前的usermessage .history(chatClientRequest.prompt().getInstructions())//获取上history信息 .context(context).build(); Query transformedQuery = originalQuery; // 遍历所有的 Transformer,对 Query 进行一轮轮的修改 // 例如:QueryRewrite 就在这里发生 for(QueryTransformer queryTransformer : this.queryTransformers) { transformedQuery = queryTransformer.apply(transformedQuery); } // 如果配置了扩展器,就把一个 Query 变成 List<Query> (比如扩充关键词) // 如果没配置,列表里就只有那一个 transformedQuery List<Query> expandedQueries = this.queryExpander != null ? this.queryExpander.expand(transformedQuery) : List.of(transformedQuery); // 针对每一个 Query,都开启一个异步任务去检索 //如果 Query 被扩展成了 3 个,这里会同时发 3 个请求给 VectorStore,而不是串行排队 Map<Query, List<List<Document>>> documentsForQuery = (Map)expandedQueries.stream() .map((query) -> CompletableFuture.supplyAsync( () -> this.getDocumentsForQuery(query), // 调用 retriever.retrieve(query) this.taskExecutor)) .toList() .stream() .map(CompletableFuture::join) .collect(Collectors.toMap(Map.Entry::getKey, (entry) -> List.of((List)entry.getValue()))); // 把刚才并发查回来的多组文档,合并成一组,默认是 ConcatenationDocumentJoiner List<Document> documents = this.documentJoiner.join(documentsForQuery); // 拿着合并后的一大堆文档,进行后处理 // 【关键】Cross-Encoder Rerank (精排) 就可以放在这里执行! for(DocumentPostProcessor documentPostProcessor : this.documentPostProcessors) { documents = documentPostProcessor.process(originalQuery, documents); } // 1. 把最终选定的文档,存入 Context (为了 after 阶段使用) context.put("rag_document_context", documents); // 2. 【核心】把文档内容拼接到用户的问题里,queryAugmenter默认使用ContextualQueryAugmenter //负责把 documents 变成 String,填入 Prompt 模板 Query augmentedQuery = this.queryAugmenter.augment(originalQuery, documents); // 3. 修改 Request,把原来的"User Message"替换成"带上下文的 User Message" return chatClientRequest.mutate().prompt(chatClientRequest.prompt().augmentUserMessage(augmentedQuery.text())).context(context).build(); } private Map.Entry<Query, List<Document>> getDocumentsForQuery(Query query) { List<Document> documents = this.documentRetriever.retrieve(query); return Map.entry(query, documents); } public ChatClientResponse after(ChatClientResponse chatClientResponse, @Nullable AdvisorChain advisorChain) { //xxxxx }
可以看到第5行构建Query对象时把chatClientRequest中所有的Message都拿出来了(这归功于ChatMemory在上一层Advisor中构建了一个新的Request存放了所有的Message)。并且经过了QueryTransformer,有了 RewriteQueryTransformer 不就可以重写Query了吗?
开干!
▼java复制代码@Bean public ChatClient coffeeChatClient(DashScopeChatModel dashScopeChatModel, MyRedisChatMemory myRedisChatMemory, ToolCallbackProvider toolCallbackProvider, VectorStore vectorStore) { VectorStoreDocumentRetriever vectorStoreDocumentRetriever = VectorStoreDocumentRetriever.builder() .topK(3) .similarityThreshold(0.5) .vectorStore(vectorStore) .build(); // 1. 定义改写器 (需要用到 chatModel,因为要调大模型) QueryTransformer rewriteTransformer = RewriteQueryTransformer.builder() .chatClientBuilder(ChatClient.builder(dashScopeChatModel)) // 绑定模型 .build(); RetrievalAugmentationAdvisor retrievalAugmentationAdvisor = RetrievalAugmentationAdvisor.builder() .documentRetriever(vectorStoreDocumentRetriever)//检索器 .queryTransformers(rewriteTransformer) // 【关键】注入改写器! .build(); ChatClient chatClient = ChatClient.builder(dashScopeChatModel) .defaultAdvisors(new MyLoggerAdvisor(), MessageChatMemoryAdvisor.builder(myRedisChatMemory).build()//添加Memory , retrievalAugmentationAdvisor//RAG ) .defaultSystem("你是小鱼茶室的服务员,你需要回答用户的问题,可以使用知识库补充语料,并时按需使用工具") .defaultTools(new DateTimeTools()) // .defaultToolCallbacks(toolCallbackProvider.getToolCallbacks()) .build(); return chatClient; }
测试

改写后详细信息


第二轮对话

可以看出把“它”改写成了“这款饮品”说明起效果了。虽然没有把“它”直接改写为“咖啡”,但是改写成“这款饮品”已经能看出增强语义了,大家可以多试几次兴许有一次能修改的很完美。
总结
RewriteQueryTransformer成功改写了query拿到新的transformedQuery去向量数据库查询知识,注意!这里并不是修改Prompt记录,只是根据Prompt和History重写Query去向量数据库查找对应的知识,增加知识的命中率,并不影响chatMemory记录Prompt,并且日志也会输出原始的Query信息,所以还是debug模式去验证是否有效。
| 特性 | QuestionAnswerAdvisor (简单版) | RetrievalAugmentationAdvisor (高级版) |
|---|---|---|
| Prompt 拼接 | ✅ 负责拼接文档 (历史由别的 Advisor 拼) | ✅ 负责拼接文档 (历史由别的 Advisor 拼) |
| 检索依据 | ❌ 只看当前这一句话 (容易搜不到) | ✅ 看历史 + 当前话 (通过 Transformer 重写 Query) |
| 是否有 Query 重写 | 默认没有 (需要自己扩展) | ✅ 内置支持 (queryTransformers) |
| 适用场景 | 单轮问答 (如搜索引擎) | 多轮对话 (如聊天机器人) |
