引言
你已经搭好了一个基础 RAG 系统:用户提问 → 检索文档 → 模型回答。看起来很美好,对吧?
然后在生产环境部署的第一天,你收到了 10 条用户反馈:
- 「问的问题明明文档里有,但 AI 说找不到」
- 「AI 给的答案看起来对,但引用了错误的文档」
- 「用英文搜和中文搜得到完全不同的结果」
- 「查询稍微复杂一点,AI 就答非所问」
- ……
基础 RAG 只是一个起点。从「能用」到「好用」,还需要解决很多工程细节。一个生产级 RAG 系统通常比原型复杂 5-10 倍。
问题 1:用户的查询太模糊
查询重写(Query Rewriting)
用户原始查询:"那个政策"
→ 检索引擎不知道用户在说什么
查询重写后:"公司的年假政策是什么?"
→ 检索引擎可以精准匹配python
def rewrite_query(original_query: str, chat_history: list = None) -> str:
"""用 LLM 重写用户查询,使其更适合检索"""
messages = [
{"role": "system", "content": """你是一个查询优化器。
将用户模糊的问题改写为清晰、具体的搜索查询。
只输出改写后的查询,不要解释。"""},
]
if chat_history:
# 结合对话历史理解用户意图
messages.append({
"role": "user",
"content": f"对话历史:{chat_history}\n当前问题:{original_query}\n改写后:"
})
else:
messages.append({
"role": "user",
"content": f"改写以下查询:{original_query}"
})
response = client.chat.completions.create(
model="gpt-4o-mini", # 用便宜模型做重写
messages=messages,
temperature=0
)
return response.choices[0].message.contentHyDE:假设性文档 Embedding
思路:先用模型生成一个「假设性答案」,
再用这个答案(而不是原始问题)去做检索
原因:问题和文档的语义空间不同
问题:"公司年假多少天?"
文档:"入职满一年可享受 10 天年假"
问题 → embedding → 和文档的 embedding 距离可能较远
假设答案 → "根据规定年假是10天" → 和文档的 embedding 距离更近python
def hyde_query(query: str) -> list[float]:
"""HyDE: 用假设答案做检索"""
# 先让模型生成假设答案
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": query}],
temperature=0
)
hypothetical_answer = response.choices[0].message.content
# 用假设答案(而不是原始问题)生成 embedding
embedding_response = client.embeddings.create(
model="text-embedding-3-small",
input=hypothetical_answer
)
return embedding_response.data[0].embedding问题 2:纯向量搜索不够准
混合检索(Hybrid Search)
向量搜索的优点:理解语义,"AI" 能匹配 "人工智能"
向量搜索的缺点:精确关键词可能匹配不准
关键词搜索的优点:精确匹配关键词、编号、专有名词
关键词搜索的缺点:不理解语义
混合检索 = 向量搜索 + 关键词搜索,取长补短python
def hybrid_search(query: str, collection, top_k: int = 5):
"""混合检索:向量 + 关键词"""
# 向量搜索
vector_results = collection.query(
query_texts=[query],
n_results=top_k * 2 # 多取一些
)
# 关键词搜索(简单的词袋匹配)
query_keywords = set(jieba.cut(query))
scored_results = []
for doc, metadata in zip(
vector_results["documents"][0],
vector_results["metadatas"][0]
):
# 向量相似度分数
vector_score = ...
# 关键词匹配分数
doc_keywords = set(jieba.cut(doc))
keyword_score = len(query_keywords & doc_keywords) / len(query_keywords)
# 混合分数
final_score = 0.7 * vector_score + 0.3 * keyword_score
scored_results.append((doc, final_score))
# 按混合分数排序
scored_results.sort(key=lambda x: x[1], reverse=True)
return [doc for doc, _ in scored_results[:top_k]]Reranking:重排序
初始检索可能返回 20 个候选结果
用 Cross-Encoder 重新打分排序,选出最相关的 3-5 个
向量搜索(Bi-Encoder):
query → embedding → 与所有 doc embedding 比较距离
优点:快(可以预计算 doc embedding)
缺点:query 和 doc 各自编码,交互不深
Reranking(Cross-Encoder):
query + doc → 一起编码 → 相关性分数
优点:更准确(query 和 doc 深度交互)
缺点:慢(每个候选对都要单独计算)python
# 使用 Cohere Reranker(或本地 Cross-Encoder)
from cohere import Client
def rerank(query: str, documents: list[str], top_n: int = 3) -> list[str]:
co = Client("your-api-key")
results = co.rerank(
model="rerank-v3.5",
query=query,
documents=documents,
top_n=top_n
)
return [documents[r.index] for r in results.results]问题 3:复杂查询需要拆分
查询分解
用户问:"对比一下公司 A 产品和 B 产品的价格、功能和售后政策"
这不是一个查询,而是多个查询:
1. "A 产品价格"
2. "A 产品功能"
3. "A 产品售后政策"
4. "B 产品价格"
5. "B 产品功能"
6. "B 产品售后政策"
分别检索,合并结果,再让模型综合回答python
def decompose_query(complex_query: str) -> list[str]:
"""将复杂查询分解为多个子查询"""
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{
"role": "user",
"content": f"""将以下复杂问题分解为多个简单的子问题,
每个子问题只关注一个方面。输出 JSON 数组。
问题:{complex_query}"""
}],
response_format={"type": "json_object"},
temperature=0
)
return json.loads(response.choices[0].message.content)["sub_questions"]RAG 的评估指标
评估一个 RAG 系统好不好,主要看三个维度:
1. 检索质量(Retrieval Quality)
召回率:相关文档被检索到的比例
精确率:检索到的文档中相关的比例
MRR:第一个相关文档的排名位置
2. 生成质量(Generation Quality)
忠实度:回答是否忠于检索文档(没编造)
相关性:回答是否与问题相关
完整性:回答是否覆盖了问题的所有方面
3. 端到端质量(End-to-End)
准确率:最终回答的正确率
响应时间:端到端延迟
成本:每次查询的 API 成本| 指标 | 含义 | 目标 |
|---|---|---|
| Recall@K | 前 K 个结果中相关文档的比例 | > 0.8 |
| Faithfulness | 回答忠于文档的程度 | > 0.9 |
| Answer Relevancy | 回答与问题的相关性 | > 0.85 |
| Latency | 端到端响应时间 | < 3s |
本节小结
| 概念 | 要点 |
|---|---|
| 查询重写 | 将模糊查询改写为具体检索词 |
| HyDE | 用假设答案代替原始问题做检索 |
| 混合检索 | 向量搜索 + 关键词搜索,取长补短 |
| Reranking | 用 Cross-Encoder 对候选结果重新排序 |
| 查询分解 | 复杂问题拆分为多个子查询分别检索 |
| 评估指标 | 召回率、忠实度、相关性、延迟 |
思考题
- 为什么向量搜索不能完全取代关键词搜索?什么场景下关键词搜索更可靠?
- 如果你的 RAG 系统在简单查询上表现很好,但复杂查询经常出错,你会从哪些方面优化?
- Reranking 增加了一次额外的 API 调用。在成本和效果之间,你会如何取舍?