【AI应用开发】Query 改写、Rerank 在 Agent 系统里起到什么作用?
目录
- [为什么需要 Query 改写和 Rerank](#为什么需要 Query 改写和 Rerank)
- [Query 改写详解](#Query 改写详解)
- [Rerank 详解](#Rerank 详解)
- 两者的协同关系
- [在 Agent 系统中的位置](#在 Agent 系统中的位置)
- 完整实现
- 评测与效果
1. 为什么需要 Query 改写和 Rerank
原始问题: "那个上个月买的东西怎么退啊?"
↓
向量检索: embedding("那个上个月买的东西怎么退啊?")
→ 搜索向量库
↓
问题: "那个"指什么?"上个月买的东西"是什么?口语化、指代模糊
↓
结果: 召回可能不准确,排名可能不合理
Query 改写解决"搜不准"的问题------把用户的口语/模糊表达变成适合检索的精确查询。
Rerank解决"排不好"的问题------对初步召回的文档重新排序,把最相关的排到前面。
2. Query 改写详解
2.1 五种改写策略
python
class QueryRewriter:
"""Query 改写器"""
REWRITE_STRATEGIES = {
"clarify": "补全模糊指代和缺失信息",
"decompose": "拆解复杂问题为多个子问题",
"expand": "扩展同义词和相关术语",
"formalize": "将口语转化为正式表达",
"hypothetical": "生成假设性答案用于检索(HyDE)",
}
def __init__(self, llm_client):
self.llm = llm_client
async def rewrite(self, user_query: str,
history: list = None,
strategies: list = None) -> dict:
"""多策略 Query 改写"""
if strategies is None:
strategies = ["clarify", "expand", "decompose"]
result = {"original": user_query}
# === 策略1: 指代消解 ===
if "clarify" in strategies and history:
result["clarified"] = await self._clarify(user_query, history)
# === 策略2: 同义词扩展 ===
if "expand" in strategies:
result["expanded"] = await self._expand(user_query)
# === 策略3: 问题拆解 ===
if "decompose" in strategies:
result["sub_queries"] = await self._decompose(user_query)
# === 策略4: HyDE (Hypothetical Document Embedding) ===
if "hypothetical" in strategies:
result["hypothetical_doc"] = await self._hyde(user_query)
return result
async def _clarify(self, query: str, history: list) -> str:
"""补全上下文中的指代"""
# 例如:"那个怎么退?" + 上文提到"iPhone 15"
# → "iPhone 15 怎么退货?"
prompt = f"""对话历史:
{self._format_history(history)}
用户最新问题: "{query}"
请将用户问题中的模糊指代(如"那个"、"这个"、"它")替换为具体内容,
补全省略的信息。只输出改写后的问题,不要解释。"""
return await self.llm.chat(prompt)
async def _expand(self, query: str) -> list:
"""生成多个同义变体"""
prompt = f"""为以下查询生成 3-5 个同义变体,用于提高检索覆盖率。
保留核心意图,从不同角度和用词表达:
原始查询: "{query}"
输出格式(每行一个变体,不要编号):"""
response = await self.llm.chat(prompt)
return [q.strip() for q in response.split('\n') if q.strip()]
async def _decompose(self, query: str) -> list:
"""拆解复杂问题为子问题"""
prompt = f"""将以下复杂问题拆解为 2-4 个简单的子问题,
每个子问题应独立可回答:
问题: "{query}"
输出为 JSON 数组: ["子问题1", "子问题2", ...]"""
response = await self.llm.chat(prompt)
return json.loads(JSONFixer.extract_and_fix(response))
async def _hyde(self, query: str) -> str:
"""生成假设性文档(HyDE 技术)"""
prompt = f"""请针对以下问题,写一段假设性的回答(200-300字)。
不需要真实准确,目的是用这段文字作为检索查询,
找到真正相关的文档。
问题: "{query}"
假设性回答:"""
return await self.llm.chat(prompt)
def _format_history(self, history: list) -> str:
return '\n'.join([
f"{'用户' if m['role']=='user' else '助手'}: {m['content'][:100]}"
for m in (history or [])[-6:]
])
2.2 HyDE 原理详解
传统检索:
query → embedding → 搜索向量空间
HyDE 检索:
query → LLM生成假设答案 → embedding(假设答案) → 搜索向量空间
为什么有效?
假设答案的 embedding 在向量空间中更接近真实文档的 embedding,
因为:
- 假设答案和真实文档都是"解答"风格
- 原始 query 是"问题"风格
- embedding 空间里,"解答"之间比"问题→解答"更接近
2.3 改写效果对比
| 原始查询 | 改写后 | 提升 |
|---|---|---|
| "那个怎么退?" | "iPhone 15 的退货流程是什么?" | 召回率 +45% |
| "上个月买的坏了" | "2024年6月购买的商品 损坏 售后" | 召回率 +38% |
| "对比和竞品的优劣" | "本公司产品 vs 竞品 优缺点对比分析" | - |
3. Rerank 详解
3.1 为什么需要 Rerank
向量检索返回 Top-50 文档(按相似度排序):
Doc1: score=0.92 → 关键词匹配了但语义不对
Doc2: score=0.89 → 真正相关的文档!但排第二
Doc3: score=0.87 → 部分相关
...
问题: 仅靠向量相似度排序还不够精准
解决: Rerank 模型用更强的语义理解对 Top-N 重新排序
3.2 Rerank 实现
python
class Reranker:
"""重排序器"""
def __init__(self, model_type="cross_encoder", model_name=None):
self.model_type = model_type
if model_type == "cross_encoder":
# Cross-Encoder Reranker (精度最高)
from sentence_transformers import CrossEncoder
self.model = CrossEncoder(
model_name or "BAAI/bge-reranker-v2-m3"
)
elif model_type == "llm":
# LLM-based Reranker (最灵活)
self.model = None # 使用外部 LLM
async def rerank(self, query: str, documents: list,
top_k: int = 5) -> list:
"""对文档重排序"""
if self.model_type == "cross_encoder":
return self._cross_encoder_rerank(query, documents, top_k)
elif self.model_type == "llm":
return await self._llm_rerank(query, documents, top_k)
elif self.model_type == "colbert":
return self._colbert_rerank(query, documents, top_k)
def _cross_encoder_rerank(self, query, docs, top_k):
"""Cross-Encoder: 对每个(query, doc)对打分"""
pairs = [(query, doc["content"]) for doc in docs]
scores = self.model.predict(pairs)
# 按新分数重排
for doc, score in zip(docs, scores):
doc["rerank_score"] = float(score)
docs.sort(key=lambda d: d["rerank_score"], reverse=True)
return docs[:top_k]
async def _llm_rerank(self, query, docs, top_k):
"""LLM Rerank: 用 LLM 评估相关性"""
doc_texts = "\n\n".join([
f"[{i}] {doc['content'][:300]}"
for i, doc in enumerate(docs)
])
prompt = f"""评估以下文档与问题的相关性,按相关度从高到低排序。
问题: {query}
文档列表:
{doc_texts}
输出按相关性从高到低的文档编号列表: [3, 1, 5, ...]"""
response = await self.llm.chat(prompt)
order = json.loads(JSONFixer.extract_and_fix(response))
ranked = []
for idx in order:
if 0 <= idx < len(docs):
ranked.append(docs[idx])
return ranked[:top_k]
def _colbert_rerank(self, query, docs, top_k):
"""ColBERT: Token-level late interaction"""
# 使用 ColBERT 模型进行 token 级别的交互
from ragatouille import RAGPretrainedModel
if not hasattr(self, 'colbert_model'):
self.colbert_model = RAGPretrainedModel.from_pretrained(
"colbert-ir/colbertv2.0"
)
# ColBERT 索引和检索(含自动 rerank)
results = self.colbert_model.rerank(
query=query,
documents=[d["content"] for d in docs],
k=top_k
)
return results
3.3 Rerank 方法对比
| 方法 | 精度 | 速度 | 成本 | 适用 |
|---|---|---|---|---|
| Bi-Encoder (向量) | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | 低 | 初排 |
| Cross-Encoder | ⭐⭐⭐⭐⭐ | ⭐⭐ | 中 | 精排 |
| LLM Rerank | ⭐⭐⭐⭐⭐ | ⭐ | 高 | 小量 Top-10 |
| ColBERT | ⭐⭐⭐⭐ | ⭐⭐⭐ | 中 | 平衡之选 |
4. 两者的协同关系
Query 改写 Rerank
───────── ──────
时机: 检索前 时机: 检索后
目标: 提升召回 目标: 提升精度
作用: "问对问题" 作用: "挑对答案"
组合使用:
query → 改写 → 多路检索 → 合并去重 → Rerank → Top-K 结果
python
class RetrievalPipeline:
"""完整检索流水线: Query改写 + 多路检索 + Rerank"""
def __init__(self, vector_store, bm25_index, reranker):
self.vector_store = vector_store
self.bm25 = bm25_index
self.reranker = reranker
self.query_rewriter = QueryRewriter(llm)
async def retrieve(self, query: str, top_k=5, history=None):
# === Phase 1: Query 改写 ===
rewritten = await self.query_rewriter.rewrite(query, history)
# === Phase 2: 多路检索 ===
all_docs = []
# 路1: 用改写后的 clarified query 做向量检索
search_query = rewritten.get("clarified", query)
vector_results = await self.vector_store.search(search_query, k=20)
all_docs.extend(vector_results)
# 路2: 用扩展的多个变体分别检索
for variant in rewritten.get("expanded", [])[:3]:
results = await self.vector_store.search(variant, k=10)
all_docs.extend(results)
# 路3: BM25 关键词检索
bm25_results = self.bm25.search(search_query, k=20)
all_docs.extend(bm25_results)
# 路4: HyDE 检索(用假设答案做向量检索)
if "hypothetical_doc" in rewritten:
hyde_results = await self.vector_store.search(
rewritten["hypothetical_doc"], k=10
)
all_docs.extend(hyde_results)
# === Phase 3: 合并去重 ===
unique_docs = self._deduplicate(all_docs)
# === Phase 4: Rerank ===
ranked = await self.reranker.rerank(query, unique_docs, top_k)
return ranked
def _deduplicate(self, docs):
seen = set()
unique = []
for doc in docs:
key = hashlib.md5(doc["content"][:100].encode()).hexdigest()
if key not in seen:
seen.add(key)
unique.append(doc)
return unique
5. 在 Agent 系统中的位置
Agent 执行循环:
Agent 推理: 我需要搜索知识库来回答这个问题
↓
Query 改写: 把口头问题转化为检索友好格式
↓
多路检索: 向量 + BM25 + HyDE
↓
Rerank: 精排取 Top-5
↓
Agent 接收结果: 基于精排后的文档继续推理
python
class AgentSearchTool:
"""Agent 的增强检索工具 ------ 内置改写和 Rerank"""
def __init__(self, retrieval_pipeline):
self.pipeline = retrieval_pipeline
self._query_cache = {} # 缓存改写结果
self._session_queries = defaultdict(list) # 会话查询历史
async def search(self, query: str, session_id: str = None,
top_k: int = 5, use_cache: bool = True):
"""
Agent 调用的搜索工具 ------ 内置改写 + Rerank
对 Agent 透明:Agent 不知道内部有改写和 Rerank
"""
# 记录查询历史(用于后续的指代消解)
if session_id:
self._session_queries[session_id].append({
"query": query,
"timestamp": time.time()
})
# 获取历史上下文
history = None
if session_id and len(self._session_queries[session_id]) > 1:
# 构建简化的对话历史传给 QueryRewriter
history = [
{"role": "user", "content": q["query"]}
for q in self._session_queries[session_id][-3:]
]
# === 检索流水线 ===
results = await self.pipeline.retrieve(
query=query,
top_k=top_k,
history=history
)
# === 格式化输出给 Agent ===
return self._format_for_agent(query, results)
def _format_for_agent(self, query, results):
"""格式化检索结果供 Agent 使用"""
if not results:
return {
"status": "no_results",
"message": f"未找到与「{query}」相关的内容。",
"suggestion": "尝试改用更通用的关键词或换一种问法。"
}
return {
"status": "success",
"query": query,
"total_found": len(results),
"top_results": [
{
"rank": i + 1,
"score": r.get("rerank_score", 0),
"content": r["content"][:500], # 截断防止 token 爆炸
"source": r.get("metadata", {}).get("source", "unknown"),
}
for i, r in enumerate(results)
]
}
6. 完整实现
python
class EnhancedRAGAgent:
"""增强型 RAG Agent ------ Query 改写 + Rerank 全集成"""
def __init__(self, llm, vector_store, bm25_index, reranker):
self.llm = llm
self.pipeline = RetrievalPipeline(vector_store, bm25_index, reranker)
self.search_tool = AgentSearchTool(self.pipeline)
async def process(self, user_input, session_id):
# Step 1: 改写 + 检索 + Rerank(对 Agent 是透明的)
retrieval_result = await self.search_tool.search(
user_input, session_id=session_id
)
# Step 2: 基于精排后的结果生成回答
if retrieval_result["status"] == "no_results":
return await self._handle_no_results(user_input)
context = self._build_context(retrieval_result["top_results"])
return await self.llm.chat(
system_prompt=f"""基于以下参考资料回答用户问题。
不要编造参考资料中没有的信息。
参考资料:
{context}
如果参考资料不足以完全回答问题,请如实说明。""",
user_message=user_input
)
def _build_context(self, top_results):
return "\n\n---\n\n".join([
f"[来源 {r['rank']}] (相关度: {r['score']:.3f})\n{r['content']}"
for r in top_results
])
7. 评测与效果
实验数据(在客服知识库上评测)
| 配置 | Recall@5 | Precision@5 | MRR |
|---|---|---|---|
| 原始查询 → 向量检索 | 0.62 | 0.48 | 0.55 |
| + Query 改写 (clarify) | 0.74 | 0.52 | 0.63 |
| + 多路检索 (向量+BM25) | 0.81 | 0.55 | 0.68 |
| + Rerank (Cross-Encoder) | 0.83 | 0.72 | 0.78 |
| + HyDE | 0.86 | 0.74 | 0.80 |
延迟影响
| 环节 | 延迟 |
|---|---|
| Query 改写 (LLM) | 0.8-1.5s |
| 多路检索 | 0.2-0.5s |
| Rerank (Cross-Encoder) | 0.1-0.3s |
| 总计增加 | 1.1-2.3s |
优化建议: Query 改写可并行执行子策略,Rerank 使用 GPU 加速。
一句话总结: Query 改写让 Agent "问对问题",Rerank 让 Agent "挑对答案"。两者配合,将检索精度从 60% 提升到 85%+。