QuestionAnswerAdvisor 类是一个用于增强问答系统的组件,它结合了检索增强生成(RAG, Retrieval-Augmented Generation)的能力,通过从外部知识库(如向量数据库)中检索相关信息,动态生成更准确的答案。
当用户提出问题时,QuestionAnswerAdvisor 会首先对知识库进行检索,并将匹配到的相关引用文本添加到用户提问的后面,从而为生成的回答提供更为丰富和准确的上下文。此外,该 Advisor 还设定了一个默认提示词,旨在确保回答的质量和相关性。如果在知识库中无法找到匹配的文本,系统将可能拒绝回答用户的问题。
整体来说,QuestionAnswerAdvisor 类的主要作如下:
检索相关文档:从 VectorStore(如 Pinecone、Weaviate、Redis 等)中查找与问题最匹配的文本片段。
增强 Prompt 上下文:将检索到的文档作为额外上下文注入到 AI 模型的 Prompt 中。
生成更准确的答案:利用检索到的信息,让 AI 生成更符合事实、更少幻觉(Hallucination)的回答。
下面通过简单流程图介绍 QuestionAnswerAdvisor 类的作用:
和其它几个内置 Advisor 不同,QuestionAnswerAdvisor 提供了一个公开的构造方法,仅仅接收一个 VectorStore 示例,用来从向量数据库中检索相似的数据。定义如下:
public QuestionAnswerAdvisor(VectorStore vectorStore) { this(vectorStore, SearchRequest.builder().build(), DEFAULT_PROMPT_TEMPLATE, BaseAdvisor.DEFAULT_SCHEDULER, 0); } QuestionAnswerAdvisor(VectorStore vectorStore, SearchRequest searchRequest, @Nullable PromptTemplate promptTemplate, @Nullable Scheduler scheduler, int order) { Assert.notNull(vectorStore, "vectorStore cannot be null"); Assert.notNull(searchRequest, "searchRequest cannot be null"); this.vectorStore = vectorStore; this.searchRequest = searchRequest; this.promptTemplate = promptTemplate != null ? promptTemplate : DEFAULT_PROMPT_TEMPLATE; this.scheduler = scheduler != null ? scheduler : BaseAdvisor.DEFAULT_SCHEDULER; this.order = order; }
虽然有了一个公开的构造方法,但是该类还是提供了一个静态 builder(VectorStore vectorStore) 方法,用来构建 QuestionAnswerAdvisor.Builder 对象,定义如下:
public static Builder builder(VectorStore vectorStore) { return new Builder(vectorStore); }
而该 Builder 对象可以通过链式调用,对 QuestionAnswerAdvisor 进行参数配置,快速构建自定义配置的 QuestionAnswerAdvisor 对象。
QuestionAnswerAdvisor.Builder 是一个用于构建 QuestionAnswerAdvisor 实例的构建器类(Builder Pattern),提供了如下几个成员变量:
public static final class Builder { private final VectorStore vectorStore; private SearchRequest searchRequest = SearchRequest.builder().build(); private PromptTemplate promptTemplate; private Scheduler scheduler; private int order = 0; private Builder(VectorStore vectorStore) { Assert.notNull(vectorStore, "The vectorStore must not be null!"); this.vectorStore = vectorStore; } //... }
其中:
vectorStore 必填,底层向量存储(如 Pinecone、Redis、PgVector 等),用于执行相似性搜索。
searchRequest 搜索请求配置(默认空配置),可设置 topK、相似度阈值等参数。
promptTemplate 提示模板,用于将搜索结果的文档和用户问题组合成最终提问。
scheduler (可选)调度器,用于控制任务执行顺序(如异步处理)。
order (可选)定义 Advisor 的执行顺序(默认 0)。
注意,如果用户没有提供 promptTempalte,则采用默认的 DEFAULT_PROMPT_TEMPLATE,代码如下:
private static final PromptTemplate DEFAULT_PROMPT_TEMPLATE = new PromptTemplate( "{query}\n\nContext information is below, surrounded by ---------------------\n\n" + "---------------------\n" + "{question_answer_context}" + "\n---------------------\n\n" + "Given the context and provided history information and not prior knowledge,\n" + "reply to the user comment. If the answer is not in the context, inform\n" + "the user that you can't answer the question.\n");
将上面提示词整理后如下:
{query} Context information is below, surrounded by --------------------- --------------------- {question_answer_context} --------------------- Given the context and provided history information and not prior knowledge, reply to the user comment. If the answer is not in the context, inform the user that you can't answer the question.
翻译成中文:
{query} 上下文信息如下, 由 --------------------- 包围 --------------------- {question_answer_context} --------------------- 根据提供的上下文和历史信息进行回复,而不依据已掌握的知识。 若答案不在上下文中,告知用户你无法回答该问题。
下面是一次发起请求的日志信息:
ChatClientRequest[prompt=Prompt{messages=[SystemMessage{textContent='使用中文进行回复', messageType=SYSTEM, metadata={messageType=SYSTEM}}, UserMessage{content='张三是谁? Context information is below, surrounded by --------------------- --------------------- 张三,男,汉族,1996 年 7 月 26 日出生,现年 30 岁,毕业于四川大学计算机工程系。凭借扎实的专业教育背景,他在技术领域展现出强劲实力。 张三尤其擅长算法设计与优化,能以高效逻辑解决复杂问题;在编程语言方面,对 C/C++ 运用娴熟,凭借该语言的高性能优势,完成众多高质量项目。同时,他在游戏开发领域造诣颇深,从游戏逻辑搭建到画面渲染优化,都能出色完成。此外,他对 AI 有着深入研究,善于将 AI 技术融入游戏开发,为游戏增添智能化体验,是兼具多元技能与创新思维的复合型技术人才。 --------------------- Given the context and provided history information and not prior knowledge, reply to the user comment. If the answer is not in the context, inform the user that you can't answer the question. ', properties={messageType=USER}, messageType=USER}], modelOptions=OpenAiChatOptions: {"streamUsage":false,"model":"gpt-3.5-turbo","temperature":0.7}}, context={chat_memory_conversation_id=sessionId-202572020514, qa_retrieved_documents=[Document{id='de8eb35d-cbe8-4b00-a8c7-56f782ffa7c6', text='张三,男,汉族,1996 年 7 月 26 日出生,现年 30 岁,毕业于四川大学计算机工程系。凭借扎实的专业教育背景,他在技术领域展现出强劲实力。 张三尤其擅长算法设计与优化,能以高效逻辑解决复杂问题;在编程语言方面,对 C/C++ 运用娴熟,凭借该语言的高性能优势,完成众多高质量项目。同时,他在游戏开发领域造诣颇深,从游戏逻辑搭建到画面渲染优化,都能出色完成。此外,他对 AI 有着深入研究,善于将 AI 技术融入游戏开发,为游戏增添智能化体验,是兼具多元技能与创新思维的复合型技术人才。', media='null', metadata={distance=0.1451374989197608}, score=0.8548625010802392}]}]
根据日志可知,检索到的文档内容被拼接在 UserMessage(用户消息)中。
在 Spring AI 1.0.0 中,SearchRequest 是用于向向量数据库(VectorStore)发送搜索请求的核心类,它封装了搜索所需的各种参数,包括查询向量、返回结果数量、过滤条件等。
部分源码如下:
public class SearchRequest { public static final double SIMILARITY_THRESHOLD_ACCEPT_ALL = 0.0; public static final int DEFAULT_TOP_K = 4; // 默认 TOP_K 为 4 private String query = ""; // top-K private int topK = 4; // 相似度阈值 private double similarityThreshold = 0.0; // 过滤表达式 @Nullable private Filter.Expression filterExpression; //... }
下面通过使用 Spring AI 内置的 SimpleVectorStore 内存向量数据库来演示 QuestionAnswerAdvisor 的效果。在创建 SimpleVectorStore 时,手动向其中添加一个关于“张三”介绍的文档,后续使用。
在 resources 目录下创建 application.yml 配置文件,如下:
spring: application: name: springai_demo1 # AI配置 ai: # openai相关配置 openai: # 基础地址 base-url: https://api.xty.app # AI KEY api-key: sk-vHTHX8D3w*******8A23e48AbB600 # 聊天模型配置 chat: options: model: gpt-3.5-turbo # 图片模型配置 image: options: # 需要高级接口 model: dall-e-3 chat: memory: # 会话历史持久化 repository: jdbc: # 启动时不自动执行初始化SQL脚本,手动创建数据表 initialize-schema: never client: # true 开启,自动注入 ChatClient,false 关闭,需要手动创建 ChatClient enabled: true # 日志配置 logging: charset: console: UTF-8 level: root: info org.springframework.ai: debug
使用 @Configuration 注解创建名为 AiConfig 的配置类,然后创建 VectorStore、QuestionAnswerAdvisor 和 SimpleLoggerAdvisor 的实例,SimpleLoggerAdvisor 用来输出日志信息。如下:
package com.hxstrive.springai.springai_openai.advisor_QuestionAnswerAdvisor.config; import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor; import org.springframework.ai.chat.client.advisor.vectorstore.QuestionAnswerAdvisor; import org.springframework.ai.document.Document; import org.springframework.ai.embedding.EmbeddingModel; import org.springframework.ai.vectorstore.SimpleVectorStore; import org.springframework.ai.vectorstore.VectorStore; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.util.ArrayList; import java.util.List; /** * AI配置 * @author Administrator */ @Configuration public class AiConfig { @Bean public VectorStore vectorStore(EmbeddingModel embeddingModel) { // 使用内存实现的简单向量存储(生产环境需替换为 Pinecone 等) SimpleVectorStore vectorStore = SimpleVectorStore.builder(embeddingModel).build(); List<Document> documentList = new ArrayList<>(); documentList.add(new Document("张三,男,汉族,1996 年 7 月 26 日出生,现年 30 岁,毕业于四川大学计算机工程系。" + "凭借扎实的专业教育背景,他在技术领域展现出强劲实力。\n" + "张三尤其擅长算法设计与优化,能以高效逻辑解决复杂问题;在编程语言方面,对 C/C++ 运用娴熟,凭借该语言的高性能优势," + "完成众多高质量项目。同时,他在游戏开发领域造诣颇深,从游戏逻辑搭建到画面渲染优化,都能出色完成。" + "此外,他对 AI 有着深入研究,善于将 AI 技术融入游戏开发,为游戏增添智能化体验,是兼具多元技能与创新思维的复合型技术人才。")); vectorStore.add(documentList); return vectorStore; } @Bean public QuestionAnswerAdvisor questionAnswerAdvisor(VectorStore vectorStore) { return QuestionAnswerAdvisor.builder(vectorStore).build(); } @Bean public SimpleLoggerAdvisor simpleLoggerAdvisor() { return new SimpleLoggerAdvisor(); // 默认配置 } }
创建一个名为 AIController 的 controller,然后将前面创建的 SimpleLoggerAdvisor、QuestionAnswerAdvisor 注入到其中,最后,通过 ChatClient 的 defaultAdvisors() 方法添加默认的 Advisor,如下:
package com.hxstrive.springai.springai_openai.advisor_QuestionAnswerAdvisor.controller; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor; import org.springframework.ai.chat.client.advisor.vectorstore.QuestionAnswerAdvisor; import org.springframework.ai.chat.memory.ChatMemory; import org.springframework.ai.chat.model.ChatModel; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @RestController public class AIController { @Autowired private ChatModel chatModel; @Autowired private SimpleLoggerAdvisor simpleLoggerAdvisor; @Autowired private QuestionAnswerAdvisor questionAnswerAdvisor; @GetMapping("/ai/simple") public String completion(@RequestParam("userText") String userText) { // 构建 RAG advisor 时需要完成 vectorStore 初始化 ChatClient chatClient = ChatClient.builder(chatModel) .defaultSystem("使用中文进行回复") .defaultAdvisors( simpleLoggerAdvisor, questionAnswerAdvisor ) .build(); // conversationId 区分不同对话,定位对应的历史记录 String conversationId = "sessionId-202572020514"; return chatClient.prompt() // 运行时设置会话ID参数 .advisors(advisor -> { advisor.param(ChatMemory.CONVERSATION_ID, conversationId); }) .user(userText) .call() .content(); } }
运行程序,使用浏览器访问 http://localhost:8080/ai/simple 接口,问“张三是谁?”,如下图:
AI 大模型成功回答了张三是谁,这些回答信息是不是很眼熟。不错,就是我们在创建 VectorStore 是手动添加的“关于张三介绍”的文档。后端服务输出日志如下:
从日志中可以看出,采用了默认的提示语模板。
如果我们将“关于张三介绍”的文档注释掉,如下:
@Configuration public class AiConfig { @Bean public VectorStore vectorStore(EmbeddingModel embeddingModel) { // 使用内存实现的简单向量存储(生产环境需替换为 Pinecone 等) SimpleVectorStore vectorStore = SimpleVectorStore.builder(embeddingModel).build(); // List<Document> documentList = new ArrayList<>(); // documentList.add(new Document("张三,男,汉族,1996 年 7 月 26 日出生,现年 30 岁,毕业于四川大学计算机工程系。" + // "凭借扎实的专业教育背景,他在技术领域展现出强劲实力。\n" + // "张三尤其擅长算法设计与优化,能以高效逻辑解决复杂问题;在编程语言方面,对 C/C++ 运用娴熟,凭借该语言的高性能优势," + // "完成众多高质量项目。同时,他在游戏开发领域造诣颇深,从游戏逻辑搭建到画面渲染优化,都能出色完成。" + // "此外,他对 AI 有着深入研究,善于将 AI 技术融入游戏开发,为游戏增添智能化体验,是兼具多元技能与创新思维的复合型技术人才。")); // vectorStore.add(documentList); return vectorStore; } //... }
再次重新运行程序,问“张三是谁?”,如下:
这次,AI 大模型不知道张三是谁了,因为我们没有提供关于张三的知识库信息。后端输出日志如下:
上述日志中,“Context information is below, surrounded by ---------------------”部分的内容为空。
QuestionAnswerAdvisor 有会话记忆的功能吗?直接通过对话验证就是,问“我刚才问了什么?”,如下:
从回答可知,AI 不记得刚刚上面我们问的“张三是谁?”这个问题,服务端日志如下:
关于 QuestionAnswerAdvisor 的更多知识,请参考官方文档和研读源码。