Query 改写(Query Rewriting) ,简单来说,就是在将用户的问题喂给大模型(LLM)或向量数据库之前,先对原始问题进行"翻译、补全和精炼"。
它的核心理念是:"用户输入的并不一定是机器检索或理解的最佳形式"。在大模型应用中(尤其是 RAG 和多轮对话 Agent),Query 改写直接决定了检索质量,而检索质量直接决定最终回答的好坏。
结合你熟悉的 LangGraph 和 RAG 背景,我把 Query 改写的核心策略、技术实现和面试话术给你彻底讲透。
1. 为什么需要 Query 改写?(痛点分析)
用户的原始输入往往存在三个致命问题:
- 指代不明(多轮对话):用户问"它多少钱?"------这里的"它"指代哪个商品?模型必须结合历史对话才能知道。
- 信息缺失(口语化):用户问"北京呢?"------是在问天气,还是问房价,还是问人口?
- 表述冗余或含糊:用户说"那个啥,就是那个能飞的东西"------模型需要将其规范化为"无人机"。
面试金句 :"在大模型应用中,Garbage In, Garbage Out 依然成立。如果直接拿用户的原始口语去向量库做相似度检索,大概率会搜出一堆不相关的内容。Query 改写是提升 RAG 召回率(Recall)性价比最高的手段之一。"
2. Query 改写的五种核心策略(面试重点)
| 策略名称 | 核心逻辑 | 适用场景 |
|---|---|---|
| 指代消解与省略补全 | 将当前问题结合历史对话,把代词替换成具体名词。 | 多轮对话 Agent。 |
| 纠错与规范化 | 修正错别字,将口语化表达转为书面语。 | 语音转文字后的场景。 |
| 查询分解 | 将一个复杂问题拆解成多个独立的子问题。 | 需要多跳推理(Multi-hop)的复杂问题。 |
| HyDE(假设性文档嵌入) | 反向思维:让 LLM 先针对问题生成一个"假想的完美答案",然后用这个答案去检索。 | 向量检索(RAG),能有效弥补 Query 和文档之间的"语义鸿沟"。 |
| 意图识别与路由 | 判断问题是"闲聊"、"知识问答"还是"代码生成",分流到不同下游。 | 多任务 Agent。 |
3. 代码实战:如何在 LangGraph 中实现 Query 改写?
在你的智能体工作流中,Query 改写通常作为**前置节点(Pre-processing Node)**存在。下面是一个基于 LangGraph 的实现示例:
python
from langgraph.graph import StateGraph, MessagesState
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
# 定义改写 Prompt
rewrite_prompt = ChatPromptTemplate.from_messages([
("system", "你是一个查询改写专家。请结合历史对话,将用户的最新问题改写为独立、清晰、包含所有必要实体的完整陈述句。"),
("placeholder", "{messages}")
])
llm = ChatOpenAI(model="qwen-plus")
rewrite_chain = rewrite_prompt | llm
# 在 LangGraph 中定义一个"改写节点"
async def query_rewriter(state: MessagesState):
"""对用户当前输入进行改写"""
# 调用 LLM 进行改写
response = await rewrite_chain.ainvoke({"messages": state["messages"]})
# 将改写后的新问题追加到消息列表,或替换最后一条用户消息
# 注意:这里最好保留原始消息用于日志,用新消息覆盖作为检索输入
rewritten_query = response.content
# 返回更新后的状态(确保后续节点拿到的是改写后的 query)
return {"rewritten_query": rewritten_query}
在 LangGraph 图中的位置 :
START → query_rewriter(改写节点) → retriever(检索节点) → generator(生成节点)→ END。
4. 进阶技巧:HyDE(面试杀手锏)
如果面试官问:"向量检索效果不好怎么办?"
你可以回答:
"我会尝试 HyDE(Hypothetical Document Embeddings,假设性文档嵌入) 策略。传统的向量检索是用'问题'去匹配'答案',但问题和答案的语言风格往往差异很大。HyDE 的思路是先让 LLM 针对问题生成一个虚构的'标准答案' ,然后用这个虚构的标准答案去做向量检索。
原理:因为虚构答案的风格与真实文档更接近,所以在向量空间中的距离更近,能显著提升检索命中率。具体实现就是多调一次 LLM 生成假设答案,但这在长文本场景(如论文、法律文书检索)中非常有效。"
5. 面试官进阶追问:"改写后的 Query 会不会改变用户意图?"
标准回答:
"这是一个非常好的问题。改写追求的是 '语义等价' ,而不是'字面相同'。我们会通过严格的 Prompt 约束(比如'只能补全省略部分,严禁臆造用户未提及的信息'),并利用 Pydantic 结构化输出校验改写后的实体是否在原始上下文中出现过。如果改写置信度较低(比如带有猜测性词汇),我们会采用并行策略:保留原始 Query 和改写后 Query 一起做检索,最后用 RRF(倒数排名融合)算法融合两路召回结果,既保留了原始意图,又提升了召回面。"
6. 总结(面试收尾话术)
"Query 改写是 RAG 和 Agent 系统中投入产出比最高的优化点。它不需要修改底层模型,也不依赖昂贵的数据标注,只需要设计好 Prompt 并挂载一个轻量级的前置节点。在我之前的项目中,通过引入上下文补全和 HyDE 策略,RAG 系统的命中率(Hit Rate)从 68% 提升到了 89%,这也是我给团队推荐的首选优化手段。"
这段代码同时涉及了 Chain 和 Graph,但它们的层次不同,需要明确区分:
rewrite_chain:是一个LangChain 表达式语言(LCEL)链 。它通过管道符|将ChatPromptTemplate和ChatOpenAI串联,形成一个可调用的处理流程。它负责"接收消息 → 调用 LLM → 返回改写结果",是纯粹的业务逻辑单元。query_rewriter:是一个LangGraph 节点函数 (异步函数)。它被设计为挂载到StateGraph上的一个节点,负责从State中读取数据,调用rewrite_chain执行改写,然后将结果写回State。它本身不是 Graph,而是 Graph 的一个组成部分。
类比理解 :如果把 LangGraph 比作一条自动化生产线,那么 Graph 是整个产线的布局 ,节点(
query_rewriter)是其中一个工位 ,而 Chain(rewrite_chain)是工位上的一台机器。机器执行具体加工任务,工位负责把原料(State)喂给机器,再把成品放回传送带(State)。
🧠 面试应答话术(如何给面试官讲清楚)
"这二者是协同工作的。
rewrite_chain是一个 LangChain 的 LCEL 链,它封装了 Prompt 和 LLM 调用逻辑,专注做文本改写。而query_rewriter是 LangGraph 中的一个节点函数,它作为图的一部分,负责从共享状态中提取消息,调用rewrite_chain,再将改写结果存入状态。这种设计将 '具体怎么做'(Chain) 与 '什么时候做、数据从哪来、往哪去'(Graph Node) 解耦,提高了代码的可测试性和可维护性。"
📌 补充说明
- 你在代码中返回了
{"rewritten_query": rewritten_query},这会在 State 中新增一个字段。如果 State 类型是MessagesState(只定义了messages),你需要确保 State 支持额外字段,或者将改写结果直接替换messages的最后一条消息(更常见做法)。这属于设计细节,但不影响"是 Chain 还是 Graph"的结论。