学习时整理,如有错误欢迎指正。
注:本笔记只做入门作用,代码仅为demo演示,未作架构 及业务处理,可能存在漏洞隐患
问题改写
在 RAG 的相似度检索中,检索效果很大程度取决于用户问题的表达方式。
用户输入的提问不可控,常存在模糊、信息缺失、上下文依赖等问题,难以直接高效匹配到高质量的相关文档块。
问题重写(改写)是主流的检索优化手段,目标是:让用户的自然语言提问,转化为更适合检索的查询表达,从而显著提高检索准确率和最终生成答案的质量。
主流实现策略共四种:富化、分解、多样化、回溯提示。
分解(Decompose)
将用户复杂、多步骤或包含多个子问题的查询,拆解成若干相互独立、更简单、更具体的子问题。每个子问题可以独立进行检索,最终将多个检索结果合并,用于回答原问题。
- 接收到用户请求后,先调用一次大模型,对原始问题进行逻辑解析和拆分
- 将拆分后的子查询列表分别进行检索,收集所有相关文档块
- 将所有文档块和原始问题一并输入给生成模型,得出最终答案
prompt:
# 角色 你是一名专业的查询逻辑分析专家。 # 任务 将给定的“用户原始问题”分解为一系列**相互独立、逻辑清晰**,且可单独用于检索的子查询列表。 你的输出必须是一个标准的JSON数组格式。 # 用户原始问题 {QUESTION} # 输出格式要求(JSON Array) [ "子查询1", "子查询2", "子查询3", "..." ] (不强制要求数组元素个数,可根据真实情况输出,至少保留1个) # 输出 请直接输出JSON数组,不要包含解释或多余的文字。代码实现:
/** * 问题分解 * @param question 原始问题 * @return 子问题列表 */publicList<String>decompose(Stringquestion){PromptTemplatepromptTemplate=newPromptTemplate(DECOMPOSE_PROMPT);promptTemplate.add("QUESTION",question);Stringresult=chatModel.call(promptTemplate.create()).getResult().getOutput();returnJSON.parseArray(result,String.class);}富化
在原始查询中添加上下文信息、背景知识或必要的限制条件,以消除歧义、补充缺失的信息,使查询更完整、更具体。
核心场景:指代消除
- 示例:对话中用户问「他有什么特点?」,句中的 “他” 是指代表述,需要补充明确的指代对象,才能准确检索。
适用场景:
用户表达模糊,存在指代性表述(省略了主语或关键信息)
问题缺少历史对话细节,需补充时间、地点、实体等限制来缩小检索范围
问题使用了专业术语的简称,需要补全完整概念
实现流程:
- 接收到用户请求的问题后,先调用一次大模型,对用户的原始问题进行重写
- 将重写丰富过后的问题,再进行相似度检索,执行后续 RAG 流程
Promp:
# 角色 你是一个专业的问题重写优化器。 # 任务 根据提供的“对话历史”和“用户原始问题”,重写为一个独立、完整、且包含所有必要背景信息的新查询,用于RAG检索。 ## 对话历史: {CHAT_HISTORY} ## 原始问题: {QUESTION} # 输出 输出富化过后的新问题,不要包含多余的解释性内容代码实现:
/** * 问题富化 * @param chatHistory 对话历史 * @param question 原始问题 * @return 富化后的完整问题 */publicStringenrich(StringchatHistory,Stringquestion){PromptTemplatepromptTemplate=newPromptTemplate(ENRICH_PROMPT);promptTemplate.add(CHAT_HISTORY,chatHistory);promptTemplate.add(QUESTION,question);Stringresult=chatModel.call(promptTemplate.create()).getResult().getOutput().getText();returnresult;}多样化
对同一个用户查询生成多个语义相近或相关的变体,提升对知识库内容描述多样性的覆盖,从而增强召回率。
适用场景
- 描述风格不一致:文档的表述方式不同,例如有的写 “接口性能优化”,有的写 “提升接口响应速度”
- 用户表述差异性:不同用户的表达方式习惯不同
实现流程
- 接收到用户请求后,先调用一次大模型,为原始问题生成多个语义相近但措辞不同的查询变体
- 将原始查询和所有变体并行进行检索
- 汇聚所有的检索结果,得到全部文档块,去重后用于后续生成
Prompt
# 角色 你是一名专业的语义扩展专家。 # 任务 为给定的“原始问题”生成**3个**语义相同但**措辞完全不同、且利于检索**的查询变体,以提高检索的召回率。 你的输出必须是一个标准的JSON数组格式。 # 原始问题 {QUESTION} # 输出格式要求(JSON Array) [ "变体1", "变体2", "变体3" ] # 输出 输出富化过后的新问题,不要包含多余的解释性内容代码实现
/** * 问题多样化 * @param question 原始问题 * @return 语义相近的查询变体列表 */ public List<String> diversify(String question) { PromptTemplate promptTemplate = new PromptTemplate(DIVERSIFY_PROMPT); promptTemplate.add(QUESTION, question); String result = chatModel.call(promptTemplate.create()).getResult().getOutput().getText(); return JSON.parseArray(result, String.class); }回溯提示(Step-Back Prompting)
核心思想是先引导模型 “后退一步”,从具体问题中抽象出更一般的原理、概念或背景知识;再基于这些抽象信息进行推理或检索,最终回答原始问题。
典型场景
问题过于具体,知识库中没有完全匹配的条目,需要匹配通用规则 / 政策。
- 原始具体问题:我老舅结婚,我可以请几天假去参加她的婚礼。
- 抽象回溯问题:探亲假的请假政策是怎么样的?
实现流程:
- 将具体问题转化为更抽象的通用问题
- 使用这个抽象问题(或与原始问题结合)向知识库检索相关信息
- 将检索到的通用背景知识与原始具体问题结合,由 LLM 生成更准确、有深度的回答
Prompt
# 角色 你是一个擅长抽象思维和原理推理的专家。 # 任务 请根据用户提出的具体问题,先“后退一步”,将其转化为一个更通用、更本质的问题,聚焦于背后的原理、规律、概念或一般性知识,而不是具体细节。 # 原始问题 {QUESTION} # 输出 请只输出改写后的“后退问题”,不要解释,不要包含原始问题,也不要回答它。代码实现
/** * 回溯提示(Step-Back) * @param question 原始具体问题 * @return 抽象后的通用问题 */publicStringstepBack(Stringquestion){PromptTemplatepromptTemplate=newPromptTemplate(STEP_BACK);promptTemplate.add(QUESTION,question);Stringresult=chatModel.call(promptTemplate.create()).getResult().getOutput().getText();returnresult;}合并
# 角色 你是一名专业的查询优化专家,擅长对用户问题进行分析、重写、拆分、扩展和抽象,以提升RAG检索的准确率与召回率。 # 输入 ## 对话历史 {CHAT_HISTORY} ## 用户原始问题 {QUESTION} # 任务 围绕用户问题完成以下四项工作。 --- ## 任务1:查询重写(Rewrite) 结合对话历史,将用户当前问题改写为一个能够脱离上下文独立理解的完整查询。 要求: - 保留用户真实意图; - 补充必要背景信息; - 消除代词、省略、上下文依赖; - 不添加用户未表达的信息; - 输出一句完整的问题。 --- ## 任务2:查询分解(Decomposition) 如果问题包含多个相互独立的信息需求,则拆分为多个可以单独检索的子查询。 要求: - 每个子查询能够独立用于检索; - 子查询之间逻辑独立; - 不重复; - 如果问题本身无需拆分,则仅输出一个子查询。 --- ## 任务3:语义扩展(Semantic Expansion) 基于重写后的查询,生成3个语义完全一致但表达方式不同的查询变体。 要求: - 不改变原始含义; - 使用不同措辞; - 有利于提升Embedding召回率; - 不增加新的需求。 固定生成3条。 --- ## 任务4:Step-Back 问题 将用户的问题提升到更一般、更本质、更抽象的层次,形成一个能够帮助理解原问题的"后退问题"。 要求: - 聚焦原理、规律、概念、机制; - 不回答原问题; - 不包含具体场景; - 不只是简单改写,而是更高层抽象。 --- # 输出格式 严格输出以下JSON,不允许输出其它内容。 ```json { "rewrite": "独立完整的新问题", "decomposition": [ "子查询1", "子查询2" ], "expansion": [ "变体1", "变体2", "变体3" ], "step_back": "更加抽象、更本质的问题" } ``` # 输出要求 - 必须输出合法JSON。 - 所有字段均必须存在。 - decomposition 至少包含1项。 - expansion 固定包含3项。 - 不输出解释、标题、Markdown或其它文字。查询路由
RAG 路由机制(Routing)
在实际 RAG 系统中,不同类型的问题往往需要不同的处理方式。
例如:
- 不同的问题需要不同的 Prompt;
- 不同的问题需要检索不同的数据源;
- 不同的问题甚至需要调用不同的 Agent 或 Tool。
因此,在正式进入 RAG 检索之前,通常会增加一层Routing(路由),先由大模型理解用户意图,再决定后续执行路径。
整个 Routing 可以分为两类:
- Prompt Routing(提示词路由)
- Query Routing(数据源路由)
Prompt Routing(提示词路由)
Prompt Routing 指的是:
根据用户问题的意图,自动选择最适合的 System Prompt,再交给大模型回答。
如果所有问题都使用同一套 Prompt,很难兼顾不同业务场景。
因此,可以提前设计多套 Prompt,大模型先进行意图分类,再选择对应 Prompt,从而获得更加专业的回答。
整体流程如下:
本质上,就是:
先分类,再回答。
例如,一个医疗 AI 同时支持:
- 病情咨询
- 用药咨询
两种问题虽然都属于医疗领域,但需要不同的专业 Prompt。
实现:
可以通过多个@SystemMessage定义不同 Prompt,再增加一个意图识别方法。
@AiServicepublicinterfaceMedicalPromptRoutingService{@SystemMessage(""" 你是一名专业医生,请从疾病、症状、诊断等角度回答患者的问题。 """)Flux<String>doctorConsultation(StringuserMessage);@SystemMessage(""" 你是一名专业药学专家,请从药物适应症、剂量、副作用、禁忌等方面回答问题。 """)Flux<String>pharmacistConsultation(StringuserMessage);@SystemMessage(""" 判断用户问题属于哪种类型: 病情、诊断、症状 —— 返回 DOCTOR 药物、剂量、副作用 —— 返回 PHARMACIST 仅返回 DOCTOR 或 PHARMACIST。 """)StringdetermineConsultationType(StringuserMessage);}Query Routing(数据源路由)
企业知识通常存储在不同的数据源中:
- PDF、Word、Markdown 等文档
- MySQL、PostgreSQL 等关系型数据库
- Neo4j 等图数据库
- Elasticsearch 等搜索引擎
不同的数据适合不同的检索方式。
例如:
文档知识适合向量检索;
组织架构、人物关系适合图查询;
统计数据、订单信息适合 SQL 查询。
因此,在检索之前,需要先判断用户的问题最适合查询哪一种数据源。
常见路由类型:
| 问题类型 | 路由目标 | 典型场景 |
|---|---|---|
| 文档问答、知识检索、语义搜索 | Vector Database | RAG 文档检索 |
| 人物关系、组织架构、知识图谱 | Graph Database | 实体关系查询 |
| 精确查询、统计分析、业务数据 | Relational Database | SQL 查询 |
| 实时网页信息 | Web Search | 新闻、天气、股票 |
| 外部系统能力 | API / Tool | 天气接口、订单系统、MCP Tool |
实际项目中,Vector Database 通常作为默认路由,无法明确分类时优先进行语义检索。
实现
publicclassQueryRouteService{privatestaticfinalStringROUTE_PROMPT=""" 你需要判断用户问题最适合使用哪种数据源进行检索。 文档知识、语义搜索、知识问答 返回:VECTOR 实体关系、知识图谱、组织架构 返回:GRAPH SQL、统计分析、精确字段查询 返回:RELATIONAL 实时互联网信息 返回:WEB 调用外部接口或工具 返回:TOOL 无法确定时默认返回 VECTOR。 仅返回: VECTOR GRAPH RELATIONAL WEB TOOL 用户问题: {QUESTION} """;@AutowiredprivateChatModelchatModel;publicStringroute(Stringquery){PromptTemplatetemplate=newPromptTemplate(ROUTE_PROMPT);template.add("QUESTION",query);returnchatModel.call(template.create()).getResult().getOutput().getText();}}查询构造
Text2SQL
在 RAG 的查询路由流程中,不同类型的数据源适配的查询方式不同:
- 向量数据库:可直接基于用户自然语言做语义相似度检索
- 关系型数据库、图数据库:无法直接识别自然语言,需要先将用户问题转换为对应的结构化查询语句(SQL / Cypher),再执行检索
查询构造就是这一环节的核心能力:根据路由结果,提前为目标数据源生成可执行的查询语句,最典型的落地场景是 Text-to-SQL(自然语言转 SQL)。
核心概念:
Text2SQL(自然语言到 SQL)是融合自然语言处理(NLP)与数据库查询技术的 AI 能力,目标是将用户用自然语言(中文、英文等)表达的查询意图,自动转换为语法正确、可执行的 SQL 查询语句,降低非技术人员访问数据的门槛,提升数据查询效率。
主流实现方式是基于大模型,结合表结构信息与约束规则,生成符合业务要求的 SQL。
提示词模板:
模板包含 3 个动态占位符,调用时实时替换:
{tables}:数据库表结构与字段注释信息,是大模型生成 SQL 的核心依据{user_query}:用户的自然语言查询问题{today}:当前日期,用于处理「最近几天」「今天」等时间类查询,保证时间逻辑准确
完整提示词:
# 角色 你是一个SQL专家。请根据以下表结构信息将用户问题转换为SQL查询语句。特别注意,你只能查询,不能做修改、删除等操作。 # 表结构信息 {tables} # 用户问题 {user_query} # 要求 1. 只返回SQL语句,不需要包含任何解释和说明 2. 确保SQL语法正确 3. 使用上下文中提供的表名和字段名 4. 如果根据所提供的表无法做查询,请直接返回空字符串"" # 其他说明 今天是:{today}Java 完整实现代码(Spring AI + JdbcTemplate):
importorg.springframework.stereotype.Service;importorg.springframework.ai.chat.client.ChatClient;importorg.springframework.ai.chat.model.ChatModel;importorg.springframework.ai.prompt.Prompt;importorg.springframework.ai.prompt.PromptTemplate;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.jdbc.core.JdbcTemplate;importorg.springframework.util.StringUtils;importjavax.annotation.PostConstruct;importjava.text.SimpleDateFormat;importjava.util.Date;importjava.util.Map;@ServicepublicclassSqlQueryService{privateChatClientchatClient;@AutowiredprivateChatModelchatModel;@AutowiredprivateJdbcTemplatejdbcTemplate;@PostConstructpublicvoidinit(){chatClient=ChatClient.builder(chatModel).build();}表结构常量(示例:员工信息表)
privatestaticfinalStringTABLES=""" CREATE TABLE "app"."staff_info" ( "id" bigint NOT NULL , "emp_id" varchar(64) NOT NULL , "name" varchar(128) NOT NULL , "job" varchar(128) NOT NULL , "entry_time" date NOT NULL , "gender" varchar(32) NOT NULL DEFAULT 'F'::character varying , "birthday" date NOT NULL , "educational_background" varchar(64) NOT NULL , "director_id" varchar(64) , "dept_id" varchar(64) NOT NULL , "duty" varchar(512) , "motto" varchar(1024) , "pic_url" varchar(512) , CONSTRAINT "pk_app_staff_info" PRIMARY KEY ("id") ) WITH ( FILLFACTOR = 100, OIDS = FALSE ); ALTER TABLE "app"."staff_info" OWNER TO llmentor; COMMENT ON COLUMN "app"."staff_info"."emp_id" IS '工号'; COMMENT ON COLUMN "app"."staff_info"."name" IS '姓名'; COMMENT ON COLUMN "app"."staff_info"."job" IS '岗位'; COMMENT ON COLUMN "app"."staff_info"."entry_time" IS '入职时间'; COMMENT ON COLUMN "app"."staff_info"."gender" IS '性别: M表示女性,F表示男性'; COMMENT ON COLUMN "app"."staff_info"."birthday" IS '出生年月日'; """;提示词常量 + SQL 生成方法
privatestaticfinalStringTEXT_2_SQL_PROMPT=""" # 角色 你是一个SQL专家。请根据以下表结构信息将用户问题转换为SQL查询语句。特别注意,你只能查询,不能做修改、删除等操作。 # 表结构信息 {tables} # 用户问题 {user_query} # 要求 1. 只返回SQL语句,不需要包含任何解释和说明 2. 确保SQL语法正确 3. 使用上下文中提供的表名和字段名 4. 如果根据所提供的表无法做查询,请直接返回空字符串"" # 其他说明 今天是:{today} """;/** * 自然语言转SQL * @param query 用户自然语言问题 * @return 生成的SQL语句 */publicStringtext2sql(Stringquery){PromptTemplatepromptTemplate=newPromptTemplate(TEXT_2_SQL_PROMPT);SimpleDateFormatsdf=newSimpleDateFormat("yyyy-MM-dd");Promptprompt=promptTemplate.create(Map.of("user_query",query,"tables",TABLES,"today",sdf.format(newDate())));Stringsql=chatClient.prompt(prompt).call().content();System.out.println("sql:"+sql);returnsql;}执行 SQL 查询方法
/** * 生成SQL并执行查询,返回封装结果 * @param query 用户自然语言问题 * @param clazz 结果映射的实体类 * @return 查询结果 */publicObjectsqlQuery(Stringquery,Classclazz){if(StringUtils.isBlank(query)){returnnull;}Stringsql=text2sql(query);if(StringUtils.isBlank(sql)){returnnull;}returnjdbcTemplate.queryForObject(sql,clazz);}}整体执行流程
- 接收用户的自然语言查询请求
- 调用
text2sql方法,传入表结构、当前日期与用户问题,通过大模型生成 SQL 语句 - 校验生成的 SQL 是否有效,无效则直接返回
- 调用
sqlQuery方法,通过 JdbcTemplate 执行 SQL,返回结构化查询结果 - 查询结果可进一步作为上下文,送入大模型生成最终的自然语言回答
问题澄清
用户提问信息不全、模糊、有歧义时,AI 主动通过多轮对话提问补齐关键参数,拿到全部必要信息后再正式生成答案,
这个交互补齐信息的逻辑就是问题澄清。
实现:
主要依赖两个功能:
- 对话记忆存储:保存每一轮用户提问 + AI 回复,汇总已收集到的全部用户需求
- Prompt 规则约束:规定什么时候追问澄清、什么时候直接生成方案
系统提示词:
你是旅行规划助手,严格遵循下面规则: 一、对话语气:热情友好,像朋友自然聊天 二、核心流程分两个阶段 【阶段1:信息收集/问题澄清阶段】 1. 先梳理当前对话已获取信息,判断是否缺少核心必填字段:目的地、出行时间、出行人数、大致预算 2. 信息不全时,主动追问补齐,每次最多只提1~2个问题,话术自然,禁止生硬审问 开头模板参考:听起来很棒!具体想... 引导提问示例:这个地方我很推荐!你的出行预算大概在多少合适? 3. 禁止一次性抛出一堆问题,避免用户反感 【阶段2:规划生成阶段】 1. 当目的地、时间、人数、预算四项核心信息全部收集完整,停止追问,直接输出详细旅行方案 2. 方案必须包含:每日详细日程、住宿推荐、交通方案、游玩活动安排 3. 额外补充实用出行贴士、注意事项 三、硬性要求:全程基于历史对话已有信息作答,不能遗忘前面用户说过的内容publicStringchatWithTraveler(StringmemoryId,StringuserInput){// 1. 获取当前对话记忆ChatMemorymemory=chatMemoryManager.getMemory(memoryId);// 2. 添加用户新消息memory.add(UserMessage.builder().text(userInput).build());// 3. 组装系统提示词+上下文Promptprompt=createTravelPrompt(memory.get(),userInput);// 4. 调用大模型Stringresp=chatClient.call(prompt).getResult().getOutput().getText();// 5. 保存AI回复到记忆memory.add(AssistantMessage.builder().text(resp).build());returnresp;}