✅RAG优化技术:问题改写
在RAG的相似度检索中,检索效果的好坏,很大程度取决于用户问题的表达方式。用户输入的提问是不可控的,往往存在模糊、信息缺失、上下文依赖等问题,难以直接高效地匹配到高质量的相关文档块。因此需要在检索前对用户输入的查询进行优化,从而显著提高检索 LLMentor 我们之前介绍过很多问题重写的方案以及要解决的问题,包括分解、富化、多样化、回溯提示等等。都是很有用的方法,但是实际在应用时,我们并不会让每一个改写都处理一遍,至少不会每一个方案都调用一次LLM,那样就太慢了。一般是组合处理的。 这几种重写方案其实可以分成两种,一种是一个问题改写成另一个问题,即1对1的(回溯提示、富化),另外一种是1个问题改写成多个问题,即1对多的(分解、多样化)。 在实际实践中,会先实现1对1的这种,如果后期发现效果不好,再上一对多的方案。而一对一的方案中,富化这个主要是解决多轮对话问题的,这块我们后面单独处理和讲解,这里先暂且不提。
✅问题重写支持多轮对话
前面讲了问题重写,主要做了简洁改写、抽象概念改写、错别字改写等。但是这种改写他只支持单轮对话中,在多轮对话场景中就gg了。 那么为了支持多轮对话,我们需要通过注入历史对话上下文 的方式让 LLM 在改写时能理解多轮对话的语境。 在 Kno LLMentor 那么,我们的改写主要就是做回溯提示,并且加上一些其他的常见改写比如简洁改写、错别字改写等。 那么提示词设计如下:
你是一个汽车智能客服助手,你的职责范围是汽车相关的咨询场景,包括购车咨询、车型信息、保养维修、保险年检、售后服务等。你需要对用户的问题进行改写,使得改写后的问题在查询向量数据库/关系型数据库/图数据库时有更好的结果,并删除任何无关信息,确保查询简洁明了、具体明确。下面有一些改写的策略。
1、简洁改写。问题可能比较长,包含了一些无意义的语气词、修饰词或者重复的词语等。尤其是问题在询问车型配置、价格政策时,且包含一些无意义的日期、编号等修饰词。改写规则:删除无意义的词语使其更适合搜索引擎检索,疑问句要转成陈述句。
2、抽象概念改写。前提:用户的问题一定在询问汽车相关的问题,且是一些比较具体的细节问题,比如"我的车每次踩刹车的时候都有吱吱吱的声音很吵怎么办"。需要改写成类似"车辆刹车异响故障排查",将具体的问题转化为更基础、更简洁、更抽象的问题。
3、错别字改写。用户的问题包含了错别字或者是一些常见的汽车术语用户打成了对应的拼音。大小写不一样不属于错别字。错别字需要给出纠正结果。
上面是3种改写策略,需要逐一使用最终给出一个统一的改写结果。直接输出改写后的结果,不需要输出思考过程及额外的多余内容。如果不需要改写,则直接输出原问题即可。
下面是几个示例:
Input:我如果想买一辆特斯拉Model 3的话,大概需要多少钱啊
Output:Tesla Model 3官方指导价
Input:我的车该保养了,多久保养一次?
Output:车辆保养周期规定
Input:我的比亚迪汉刹车有点异响是怎么回事
Output:车辆刹车异响故障排查
Input:毛豆Y续航多少
Output:Tesla Model Y续航里程
Input:保险什么时候到期?
Output:车辆保险到期查询
Input:年检怎么办理?
Output:车辆年检办理流程
Input:我想了解一下你们那款新出的电动车的配置
Output:新款电动车车型配置参数
用户的原始查询:{{query}}
我们的改写,使用QueryTransformer实现,并且,我们为了能够实现后期问题的排查,我们还需要在改写节点中,把改写后的问题也保存在数据库中,这样才能更好的解决后期的bad case。 于是,我们自定义一个QueryTransformer:
package cn.hollis.llm.mentor.know.engine.rag.modules;
import cn.hollis.llm.mentor.know.engine.chat.service.ChatMessageService;
import dev.langchain4j.model.chat.ChatModel;
import dev.langchain4j.model.input.Prompt;
import dev.langchain4j.model.input.PromptTemplate;
import dev.langchain4j.rag.query.Query;
import dev.langchain4j.rag.query.transformer.QueryTransformer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationContext;
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import static dev.langchain4j.internal.ValidationUtils.ensureNotNull;
/**
* KnowEngine 查询改写器
* <p>
* 基于 LLM 对用户查询进行智能改写优化,提升 RAG 检索效果。
* 支持四种改写策略:
* <ul>
* <li><b>简洁改写</b>:删除无意义的语气词、修饰词,将疑问句转为陈述句</li>
* <li><b>抽象概念改写</b>:将具体问题转化为更基础、更抽象的查询表述</li>
* <li><b>错别字改写</b>:纠正错别字和拼音错误</li>
* </ul>
* <p>
* <b>处理流程:</b>
* <ol>
* <li>发送进度事件通知前端"正在优化您的问题"</li>
* <li>使用 LLM 根据 Prompt 模板改写用户查询</li>
* <li>构造增强查询(添加用户ID、当前时间等上下文)</li>
* <li>异步回写改写结果到数据库</li>
* <li>返回改写后的查询和原始查询</li>
* </ol>
* <p>
* <b>注意:</b>返回的集合包含两个查询 - 改写后的查询(用于语义检索)和原始查询(用于精确匹配)
*
* @see QueryTransformer
*/
@Slf4j
public class KnowEngineQueryTransformer implements QueryTransformer {
protected final ChatModel chatModel;
protected final PromptTemplate promptTemplate;
/**
* assistant 消息的 messageId,用于回写改写结果
*/
private final String assistantMsgId;
/**
* 进度回调,用于流式返回前端进度信息
*/
private final Consumer<String> progressCallback;
/**
* Spring 容器,由使用方在构造时传入
*/
private static volatile ApplicationContext applicationContext;
/**
* 注册全局 ApplicationContext(由 SpringContextHolder 调用一次即可)
*/
public static void setApplicationContext(ApplicationContext ctx) {
applicationContext = ctx;
}
private ChatMessageService getChatMessageService() {
if (applicationContext == null) {
return null;
}
try {
return applicationContext.getBean(ChatMessageService.class);
} catch (Exception e) {
log.warn("获取 ChatMessageService 失败", e);
return null;
}
}
private static final PromptTemplate LG_AGENT_PROMPT = PromptTemplate.from("""
你是一个汽车智能客服助手,你的职责范围是汽车相关的咨询场景,包括购车咨询、车型信息、保养维修、保险年检、售后服务等。你需要对用户的问题进行改写,使得改写后的问题在查询向量数据库/关系型数据库/图数据库时有更好的结果,并删除任何无关信息,确保查询简洁明了、具体明确。下面有一些改写的策略。
1、简洁改写。问题可能比较长,包含了一些无意义的语气词、修饰词或者重复的词语等。尤其是问题在询问车型配置、价格政策时,且包含一些无意义的日期、编号等修饰词。改写规则:删除无意义的词语使其更适合搜索引擎检索,疑问句要转成陈述句。
2、抽象概念改写。前提:用户的问题一定在询问汽车相关的问题,且是一些比较具体的细节问题,比如"我的车每次踩刹车的时候都有吱吱吱的声音很吵怎么办"。需要改写成类似"车辆刹车异响故障排查",将具体的问题转化为更基础、更简洁、更抽象的问题。
3、错别字改写。用户的问题包含了错别字或者是一些常见的汽车术语用户打成了对应的拼音。大小写不一样不属于错别字。错别字需要给出纠正结果。
4、车型信息提取。如果用户提到了具体的车型信息(品牌、型号、年款等),需要将其标准化提取。比如"特斯拉毛豆3"改写为"Tesla Model 3","比亚迪汉"保持不变。
上面是4种改写策略,需要逐一使用最终给出一个统一的改写结果。直接输出改写后的结果,不需要输出思考过程及额外的多余内容。如果不需要改写,则直接输出原问题即可。
下面是几个示例:
Input:我如果想买一辆特斯拉Model 3的话,大概需要多少钱啊
Output:Tesla Model 3官方指导价
Input:我的车该保养了,多久保养一次?
Output:车辆保养周期规定
Input:我的比亚迪汉刹车有点异响是怎么回事
Output:车辆刹车异响故障排查
Input:毛豆Y续航多少
Output:Tesla Model Y续航里程
Input:保险什么时候到期?
Output:车辆保险到期查询
Input:年检怎么办理?
Output:车辆年检办理流程
Input:我想了解一下你们那款新出的电动车的配置
Output:新款电动车车型配置参数
用户的原始查询:{{query}}
""");
public KnowEngineQueryTransformer(ChatModel chatModel, String assistantMsgId) {
this(chatModel, LG_AGENT_PROMPT, assistantMsgId, null);
}
public KnowEngineQueryTransformer(ChatModel chatModel, String assistantMsgId, Consumer<String> progressCallback) {
this(chatModel, LG_AGENT_PROMPT, assistantMsgId, progressCallback);
}
public KnowEngineQueryTransformer(ChatModel chatModel, PromptTemplate promptTemplate, String assistantMsgId, Consumer<String> progressCallback) {
this.promptTemplate = ensureNotNull(promptTemplate, "promptTemplate");
this.chatModel = ensureNotNull(chatModel, "chatModel");
this.assistantMsgId = assistantMsgId;
this.progressCallback = progressCallback;
}
@Override
public Collection<Query> transform(Query query) {
// 发送进度:开始问题改写
if (progressCallback != null) {
progressCallback.accept("[PROGRESS]:正在优化您的问题...");
}
log.info("开始问题改写, 原始问题: {}", query.text());
String response = chatModel.chat(createPrompt(query).text());
log.info("问题改写完成, 改写结果: {}", response);
String newQuery = "我的问题是:" + response + ", 我的用户Id是: 123321" + ", 现在是:" + LocalDateTime.now();
Query compressedQuery = query.metadata() == null
? Query.from(newQuery)
: Query.from(newQuery, query.metadata());
log.info("Compressed Success, source query: {}, compressed query: {}", query.text(), compressedQuery.text());
// 异步回写改写结果到 chat_message
if (assistantMsgId != null) {
ChatMessageService chatMessageService = getChatMessageService();
if (chatMessageService != null) {
Thread.ofVirtual().name("query-transform-" + assistantMsgId).start(() -> {
try {
chatMessageService.updateTransformContent(assistantMsgId, newQuery);
log.info("改写结果已回写: assistantMsgId={}, transformContent={}", assistantMsgId, response);
} catch (Exception e) {
log.warn("改写结果回写失败: assistantMsgId={}", assistantMsgId, e);
}
});
}
}
return List.of(compressedQuery, query);
}
protected Prompt createPrompt(Query query) {
Map<String, Object> variables = new HashMap<>();
variables.put("query", query.text());
return promptTemplate.apply(variables);
}
}
处理流程: - 1、发送进度事件通知前端"正在优化您的问题" - 2、使用 LLM 根据 Prompt 模板改写用户查询 - 3、构造增强查询(添加用户ID、当前时间等上下文) - 4、异步回写改写结果到数据库 - 5、返回改写后的查询和原始查询 以上,重点的时候第2、4、5步骤。主要做的就是改写、保存和传递。 至于上面的第一步骤,我们在这里单独讲:
✅如何把RAG的过程内容返回给用户
我们见过的很多Agent都会要把执行过程返回给用户,而不是直接给出一个冷冰冰的最终答案,本质上是因为 Agent 的工作方式已经从“单次问答”进化到了“多步自主执行”。 把中间过程展示出来,主要有以下几个核心原因: 1. 建立信任与认知共识 LLMentor 上面的第三个步骤,主要是增加一些关键信息,比如当前时间、用户id等关键信息。后面可能会用到。