Skip to content

引言

你已经搭好了一个基础 RAG 系统:用户提问 → 检索文档 → 模型回答。看起来很美好,对吧?

然后在生产环境部署的第一天,你收到了 10 条用户反馈:

  1. 「问的问题明明文档里有,但 AI 说找不到」
  2. 「AI 给的答案看起来对,但引用了错误的文档」
  3. 「用英文搜和中文搜得到完全不同的结果」
  4. 「查询稍微复杂一点,AI 就答非所问」
  5. ……

基础 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.content

HyDE:假设性文档 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:纯向量搜索不够准

向量搜索的优点:理解语义,"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 对候选结果重新排序
查询分解复杂问题拆分为多个子查询分别检索
评估指标召回率、忠实度、相关性、延迟

思考题

  1. 为什么向量搜索不能完全取代关键词搜索?什么场景下关键词搜索更可靠?
  2. 如果你的 RAG 系统在简单查询上表现很好,但复杂查询经常出错,你会从哪些方面优化?
  3. Reranking 增加了一次额外的 API 调用。在成本和效果之间,你会如何取舍?