1.什么是Query改写?
前面聊了这么多检索技术,但有一个前提一直被忽略 ------ 用户得先提出一个好问题。 现实中用户是怎么提问的:
用户输入:「那个接口怎么又挂了」真实意图:支付服务订单创建接口最近一次故障原因是什么
对检索器来说,"接口""挂了" 两个词的信息量几乎为零 ------Embedding 模型只能猜个大概方向,BM25 更是一头雾水。 很多线上项目最后发现,Embedding 没问题、向量库没问题、Rerank 没问题,真正的问题是用户不会提问。垃圾 query 进去,后续链路再强也白搭。
解决方案是 Query Rewrite(查询改写) ------ Query 改写,就是在搜索知识库前,先把用户原来的问题改成"更容易搜到资料"的问题。 在检索之前,先让 LLM 把用户的模糊问题改写成适合检索的明确查询。可以结合历史对话做指代消解("那个接口"→"支付服务订单创建接口"),也可以补充业务上下文。改写后的问题再送入后续的召回和精排流程。
Query Rewrite 在实践中往往是收益最高的单点优化之一,大部分 RAG 框架(LangChain、LlamaIndex 等)都内置了这一步。
2.怎么进行Query改写?有哪些方案?
-
首先不是所有的问题都需要query改写,只有模糊追问、口语表达、依赖上下文的问题才需要改写。比方说用户提的问题就像我们以前用搜索引擎查Bug的时间,错误码,错误信息,这种有关键信息的这种提问就是一个好的提问,这种就不需要改写问题,可以直接去搜。
-
如果用户的提问中带有明确的代词,那个", "这个", "它", "上次", "这次", "又", "那",这种很明显是需要通过聊天记录去找找这次代词是指的什么,那这种肯定是需要把历史聊天记录,当前用户提问交给模型进行query改写
-
如果用户提问的问题很短,5个字以下这种一般也是需要query改写的
python
import re
def need_llm_judge(query: str) -> bool:
# 1. 有错误码、订单号、接口路径:保留原问题,直接搜
if re.search(r"ERR-\d+|ORD-\w+|/api/[\w/-]+", query):
return False
# 2. 有明显指代词:交给 LLM 判断
refer_words = ["那个", "这个", "它", "上次", "这次", "又", "那"]
if any(word in query for word in refer_words):
return True
# 3. 问题太短,通常信息不足:交给 LLM 判断
if len(query) < 10:
return True
# 4. 其他完整问题:先直接搜
return False
python
if need_llm_judge(query):
plan = plan_query(query, chat_history) # 交给 LLM 判断
else:
plan = QueryPlan(
action="direct_search",
rewritten_query=None,
reason="规则判断:问题完整或包含精确关键词"
)
下面是完整例子
python
import os
import re
from pathlib import Path
from typing import Literal, Optional
from dotenv import load_dotenv
from langchain.agents import create_agent
from langchain.chat_models import init_chat_model
from pydantic import BaseModel, Field
# 无论你从哪个目录执行命令,都读取"本文件同级目录"的 .env。
load_dotenv(Path(__file__).with_name(".env"))
class QueryPlan(BaseModel):
"""LLM 必须返回的固定格式。"""
action: Literal["direct_search", "rewrite", "clarify"] = Field(
description=(
"direct_search:问题完整,直接检索;"
"rewrite:问题依赖历史,需要改写;"
"clarify:信息不足,需要追问用户"
)
)
rewritten_query: Optional[str] = Field(
default=None,
description="仅 action=rewrite 时填写;必须是可以单独检索的完整问题。",
)
reason: str = Field(description="简短说明为什么作出这个判断。")
def need_llm_judge(query: str) -> bool:
"""用便宜的规则筛选:只有不确定的情况才调用 LLM。"""
# 错误码、订单号、接口路径等通常是精确检索词,保留原样直接搜索。
exact_token_pattern = r"ERR-\d+|ORD-\w+|/api/[\w/-]+"
if re.search(exact_token_pattern, query, flags=re.IGNORECASE):
return False
# 这些词常常说明当前问题依赖上一轮聊天,需要 LLM 进一步判断。
refer_words = ["那个", "这个", "它", "上次", "这次", "又", "那"]
if any(word in query for word in refer_words):
return True
# 很短的问题往往缺少对象,例如"怎么处理?"。
return len(query) < 10
def build_query_router():
"""用 init_chat_model 创建模型,再用 create_agent 创建查询路由 Agent。"""
if not os.getenv("CLOSEAI_API_KEY"):
raise RuntimeError(
"没有读取到 CLOSEAI_API_KEY。请在 .env 中填写后再运行。"
)
model = init_chat_model(
model="gpt-5.4-mini",
model_provider="openai",
api_key=os.getenv("CLOSEAI_API_KEY"),
base_url=os.getenv("CLOSEAI_BASE_URL"),
temperature=0, # 判断任务希望稳定,不希望每次随机变化。
)
# response_format=QueryPlan:要求 Agent 的最终结果必须符合 QueryPlan。
# 因此 invoke() 的结果中会有 result["structured_response"]。
return create_agent(
model=model,
tools=[],
system_prompt="""
你是企业知识库的检索查询路由器,不负责回答用户问题。
你只能在以下三个 action 中选择一个:
1. direct_search:当前问题脱离历史也完整清晰,可以直接检索。
2. rewrite:当前问题依赖历史;请写出可独立检索的 rewritten_query。
3. clarify:历史也无法确定用户的具体对象;不要猜测,要求用户补充。
重要规则:
- 不能编造历史里没有的业务名称、接口名、时间或事实。
- 错误码、订单号、接口路径、产品名等精确词必须原样保留。
- 不要回答问题,只输出 QueryPlan。
""".strip(),
response_format=QueryPlan,
)
def plan_query(
query: str,
chat_history: str,
query_router,
) -> QueryPlan:
"""调用 LLM,判断当前问题应该怎么进入检索流程。"""
user_prompt = f"""
最近对话历史:
{chat_history or "(没有历史对话)"}
当前用户问题:
{query}
"""
result = query_router.invoke({
"messages": [{"role": "user", "content": user_prompt}]
})
# create_agent 的结构化输出放在 structured_response 这个 key 中。
return result["structured_response"]
def decide_query_action(
query: str,
chat_history: str,
query_router,
) -> QueryPlan:
"""整个入口:明显情况走规则;模糊情况才交给 LLM。"""
if not need_llm_judge(query):
return QueryPlan(
action="direct_search",
rewritten_query=None,
reason="规则判断:问题完整,或包含错误码、订单号、接口路径等精确关键词。",
)
return plan_query(query, chat_history, query_router)
def show_case(title: str, query: str, history: str, query_router) -> None:
"""打印一组测试案例的输入和输出。"""
plan = decide_query_action(query, history, query_router)
print(f"\n{'=' * 60}\n案例:{title}")
print(f"历史:{history or '(没有)'}")
print(f"当前问题:{query}")
print(f"判断结果:{plan.model_dump_json(indent=2)}")
def main() -> None:
# 创建一个处理query重写的agent
query_router = build_query_router()
# 案例 1:有错误码,规则直接决定保留原问题检索,不调用 LLM。
show_case(
title="精确错误码:直接检索",
query="ERR-1024 在订单创建时是什么意思?",
history="",
query_router=query_router,
)
# 案例 2:有"那个接口",LLM 从历史中补全对象并生成检索语句。
show_case(
title="依赖历史:改写",
query="那个接口又挂了?",
history="用户:支付服务的订单创建接口今天连续超时。",
query_router=query_router,
)
# 案例 3:没有历史,LLM 不应猜测"这个"是什么,而应选择 clarify。
show_case(
title="信息不足:追问",
query="这个怎么处理?",
history="",
query_router=query_router,
)
if __name__ == "__main__":
main()
其实普通的项目的刚开始做到这一步已经可以了,对于需要query改写的问题都是将原问题和改写后的问题一块去检索,这个时间由于两路检索,所以需要将检索的两路结果进行Rerank。