跳转至

RAG 检索流程

KnowledgeAgent 编排完整的 RAG 链路:Query 改写 → 向量检索 + BM25 检索 → RRF 融合 → Reranker 重排序 → 阈值过滤 → LLM 生成。相似度低于阈值不强答,返回兜底话术。


RAG 全流程图

flowchart TD
    Q[用户 Query] --> Rewrite["Query 改写<br/>DeepSeek 同步改写 ~1.5s"]
    Rewrite --> Parallel["双路并发召回"]
    Parallel --> Vec["向量检索<br/>ChromaDB + BGE-large-zh<br/>1024 维 · cosine"]
    Parallel --> BM25["BM25 检索<br/>rank-bm25 关键词召回"]
    Vec --> RRF["RRF 融合<br/>k=60 · 向量 0.6 / 关键词 0.4"]
    BM25 --> RRF
    RRF --> Rerank["Reranker 重排序<br/>BGE-reranker-base CrossEncoder"]
    Rerank --> Threshold{"相似度 ≥ 0.6?"}
    Threshold -- "是" --> LLM["LLM 生成<br/>基于检索片段生成答案"]
    Threshold -- "否" --> Fallback["兜底话术<br/>知识库中未找到相关内容"]
    LLM --> Output([返回答案 + 来源])
    Fallback --> Output

    style Parallel fill:#e3f2fd,stroke:#2196f3
    style RRF fill:#e8f5e9,stroke:#4caf50
    style Rerank fill:#fff3e0,stroke:#ff9800
    style Fallback fill:#ffebee,stroke:#f44336

Query 改写

用户提问往往口语化、含指代或省略,直接检索效果不佳。QueryRewriter 用 DeepSeek 同步改写为更适合检索的形式:

特性 说明
模型 主 LLM(DeepSeek-V3)
耗时 约 1.5s(同步调用)
作用 补全指代、扩展缩写、标准化表述
可选 可关闭改写直接用原始 query 检索

改写延迟权衡

Query 改写约增加 1.5s 延迟,但显著提升召回率。对延迟敏感的场景可关闭改写,或配合 HotQueryCache 缓存抵消延迟。


向量检索

使用 BGE-large-zh-v1.5 嵌入模型 + ChromaDB 向量库做语义召回。

参数 说明
嵌入模型 BAAI/bge-large-zh-v1.5 中文语义检索专优
向量维度 1024 BGE-large-zh 输出维度
相似度度量 cosine ChromaDB hnsw:space=cosine
召回数量 VECTOR_TOP_K=25 向量路召回 top-25
def _vector_retrieve(self, question, where):
    """向量召回:embed_query → vectorstore.query。"""
    embedding_service = get_embedding_service()
    query_embedding = embedding_service.embed_query(question)
    if not query_embedding:
        # 向量化失败(BGE 降级 hash fallback 时仍可生成向量)
        return []
    # 召回阶段放宽阈值(0.0),融合阶段统一过滤
    return self.vector_store.query(
        query_embedding=query_embedding,
        top_k=self._vector_top_k,  # 默认 25
        score_threshold=0.0,
        where=where,
    )

为什么向量检索擅长语义匹配?

用户提问方式多样("密码忘了" / "登不上" / "无法登录账户"),向量检索能捕捉语义相似性,即使关键词不完全匹配也能召回正确结果。但对专有名词(产品型号、订单号)效果较差,需 BM25 补充。


BM25 检索

使用 rank-bm25 做关键词召回,补充向量检索在精确匹配上的不足。

参数 说明
检索器 rank-bm25 经典 BM25 关键词检索算法
召回数量 BM25_TOP_K=25 关键词路召回 top-25
索引构建 按需构建 + 缓存 知识库变更时自动重建
def _ensure_bm25_index(self):
    """确保 BM25 索引已构建且与向量库同步。

    通过比较向量库条目数判断是否需要重建:
    - 索引为空或条目数不一致时重建
    - 避免每次检索都全量重建,节省 CPU 与内存
    """
    current_count = self.vector_store.count()
    if self.bm25_retriever.size == 0 or self._indexed_count != current_count:
        self._build_bm25_index()  # 从向量库拉取全部 chunks 构建
        self._indexed_count = current_count

BM25 索引懒构建

BM25 索引在首次检索时才从 ChromaDB 拉取全部 chunks 构建,并缓存 _indexed_count 标记当前索引状态。知识库变更(条目数变化)时自动重建,无需手动触发。


RRF 融合

向量与 BM25 两路召回后,用 RRF(Reciprocal Rank Fusion) 加权融合排名,取长补短。

融合公式

score(chunk) = Σ weight_i × 1 / (k + rank_i)
  • rank_i:某 chunk 在第 i 路召回中的排名(从 1 开始)
  • k=60:经验平滑值,避免 rank=1 的结果过度主导
  • 权重:向量路 0.6 + 关键词路 0.4 = 1.0

实现代码

def _rrf_fuse(self, vector_hits, bm25_hits):
    """RRF 加权融合:score = Σ weight_i * 1/(k + rank_i)。

    rank 从 1 开始(rank=1 表示该路第 1 名);
    k=60 是经验值,平滑排名差异避免 top-1 过度主导。
    """
    scores = {}
    # 向量路:按 similarity 降序赋 rank
    vector_ranked = sorted(vector_hits, key=lambda h: h.get("similarity", 0.0), reverse=True)
    for rank, hit in enumerate(vector_ranked, start=1):
        chunk_id = str(hit.get("id", ""))
        scores[chunk_id] = scores.get(chunk_id, 0.0) + self._rrf_vector_weight * (1.0 / (self._rrf_k + rank))
    # 关键词路:BM25 已按分数降序返回,直接赋 rank
    for rank, (chunk_id, _) in enumerate(bm25_hits, start=1):
        scores[chunk_id] = scores.get(chunk_id, 0.0) + self._rrf_keyword_weight * (1.0 / (self._rrf_k + rank))
    # 按融合分数降序排列
    return sorted(scores.items(), key=lambda x: x[1], reverse=True)
为什么向量权重更高(0.6 vs 0.4)?

客服场景下用户提问方式多样,语义匹配比关键词匹配更重要。但关键词匹配能捕获专有名词(产品型号、订单号、错误码),因此保留 40% 权重。项目实测该配比下 Recall@5=1.0。


Reranker 重排序

RRF 融合后的排序质量仍有限(基于排名而非语义),用 CrossEncoder 对 query-chunk 对做精排:

参数 说明
模型 BAAI/bge-reranker-base BGE 中文重排序模型
取 Top-K RERANK_TOP_K=5 重排序后取前 5 进入最终答案
降级 cosine 相似度 模型加载失败时降级
class Reranker:
    """重排序器:CrossEncoder 优先,cosine 兜底。

    模型延迟加载:首次 rerank 时才尝试加载,避免导入阶段拖慢启动。
    加载失败后标记 _use_fallback,后续不再重试,节省开销。
    """

为什么需要重排序?

双塔向量检索(query 和 doc 分别编码再算相似度)丢失了 query-doc 交互信息;CrossEncoder 直接建模 (query, doc) 对的交互,比双塔更精准。先粗召回(25 条)再精排(取 5 条),兼顾召回率与排序质量。

重排序降级

模型加载失败时降级到 cosine 相似度重排序,复用 embedding 向量:

# 加载失败后标记 _use_fallback,后续不再重试
if self._use_fallback:
    # 用 query 与 chunk 的 embedding cosine 相似度排序
    # 虽不如 CrossEncoder 精准,但优于无重排序

相似度阈值与兜底

重排序后按 SIMILARITY_THRESHOLD=0.6 过滤,低于阈值不强答:

# RRF 分数无统一量纲,阈值过滤改用 rerank 后的归一化分数
_DEFAULT_SCORE_THRESHOLD = 0.6

# 低于阈值的召回视为弱相关,过滤掉
if score_threshold > 0:
    retrieved = [c for c in retrieved if c.score >= score_threshold]

兜底话术

检索为空或全部低于阈值时,返回固定兜底回复,不调用 LLM 编造

if not chunks:
    # 检索为空:直接返回兜底,避免无意义调用 LLM
    return KnowledgeAnswer(
        answer="抱歉,知识库中未找到相关内容。",
        sources=[],
        hit=False,
        confidence=0.0,
    )

幻觉率 = 0

严格的阈值过滤 + 兜底机制确保系统不会基于弱相关片段编造答案。项目实测幻觉率 = 0,远优于 ≤ 0.10 的目标。


LLM 生成

检索到高质量片段后,构造 prompt 交给 LLM 生成最终答案:

系统提示

SYSTEM_PROMPT = (
    "你是一名企业客服助手。请严格基于下方提供的知识片段回答用户问题,"
    "不要编造未在片段中出现的信息。若知识片段不足以回答问题,"
    "请明确说明「知识库中未找到相关内容」。"
    "回答末尾需以「来源:」开头列出引用的知识片段来源,"
    "格式如「来源:产品FAQ.md 第3页」,多个来源用逗号分隔。"
)

Prompt 构造

# 单条片段文本在 prompt 中的字符上限,控制 token 成本
MAX_CHUNK_CHARS = 800
# 单条片段的引用上限:避免来源行过长
MAX_SOURCE_COUNT = 3

def _build_prompt_messages(question, chunks):
    """构造含检索片段的 prompt,约束 LLM 只基于片段回答。"""
    context = "\n\n".join(
        f"【片段{i+1}{chunk.text[:MAX_CHUNK_CHARS]}\n来源:{chunk.source}"
        for i, chunk in enumerate(chunks)
    )
    return [
        {"role": "system", "content": SYSTEM_PROMPT},
        {"role": "user", "content": f"知识片段:\n{context}\n\n问题:{question}"},
    ]

评估指标

在真实 DeepSeek LLM + BGE 嵌入环境下验证:

指标 目标 实测 达标 说明
Recall@5 ≥ 0.85 1.0 前 5 条召回覆盖全部正确答案
Hit Rate ≥ 0.90 0.9333 Top-1 命中正确答案的比例
MRR 平均倒数排名(越接近 1 越好)
幻觉率 ≤ 0.10 0.0 阈值过滤 + 兜底确保不编造

如何跑评估

# 跑检索评估(Recall@K / Hit Rate / MRR / 幻觉率)
curl -X POST http://localhost:8000/api/v1/evaluation/run \
  -H "Content-Type: application/json" \
  -d '{"top_k": 5}'

检索参数调优

参数 默认 调优建议
VECTOR_TOP_K 25 召回率不足时调大(如 30),延迟敏感时调小(如 15)
BM25_TOP_K 25 同上,与向量路保持接近
RRF_K 60 一般无需调整,过大削弱排名差异,过小 top-1 过度主导
RRF_VECTOR_WEIGHT 0.6 语义匹配重要时调大(如 0.7),关键词匹配重要时调小
RERANK_TOP_K 5 答案需更多上下文时调大(如 8),延迟敏感时调小(如 3)
SIMILARITY_THRESHOLD 0.6 召回率不足时调低(如 0.5),误召回多时调高(如 0.7)

调参后清缓存

修改检索参数后需调用 POST /api/v1/performance/cache/invalidate 清空 HotQueryCache,否则已缓存的旧结果不会更新。


相关文档

主题 链接
多 Agent 协同(KnowledgeAgent 在其中的角色) 多 Agent 协同
降级策略(BGE/Reranker 降级) 降级策略
配置说明(检索参数详解) 配置说明