QAnything 阅读优化策略05——检索

检索(Retrieval)

检索是从知识库中找到与用户查询最相关的文档的过程。QAnything 采用"宽召回 → 精排序 → 严过滤"三段式检索,层层递进,平衡召回率与精确率。


一、是什么

检索是从知识库中找到与用户查询最相关的文档的过程。

核心输入 :归一化后的查询向量 + 知识库索引

核心输出:经过过滤和排序的相关文档列表


二、为什么重要

检索决定了 RAG 的天花板,生成只能逼近这个天花板。

核心矛盾:召回率 vs 精确率

  • 检索多了 → 噪音多,LLM 被无关信息干扰("大海捞针"问题)
  • 检索少了 → 遗漏关键信息,回答不完整

与传统方案的对比

优化点 传统方案 传统方案的问题 QAnything 方案 优势
检索方式 仅向量检索 遗漏专有名词、数字等精确匹配 向量检索 + BM25 双路召回 语义 + 精确匹配互补
结果合并 两路结果直接拼接 同一文档出现两次,浪费 Token Milvus 在前 + ES 独有追加 无重复,Token 更节省
分数融合 RRF 或加权融合 Milvus(0~1)和 BM25(无界)量纲不同,融合无意义 不做分数融合,交给 Rerank 统一排 避免量纲差异导致的排序错误
排序精度 仅用向量余弦相似度排序 query 和 doc 独立编码,交互深度不足 Rerank 交叉编码器联合编码 每层注意力交互,排序更精确
过滤策略 单一阈值(如 top-k 或 score > 0.5) top-k 固定数量可能引入噪音;单一阈值无法检测质量断崖 两层过滤:绝对阈值 0.28 + 梯度断裂 50% 保底 + 检测断崖,过滤更精准
去重方式 按 doc_id 去重 不同文件相同段落(如法规原文)无法去重 按 page_content 内容去重 跨文件重复内容也能去重
断连处理 Milvus 断连 → 返回空结果 用户看到"无结果",体验差 空结果时自动重连重试 连接中断自动恢复
HTTP 重试 无重试或固定间隔重试 网络抖动直接失败 指数退避(1s→2s→4s) 给服务端恢复时间

三、怎么做

QAnything 采用三段式检索:宽召回 → 精排序 → 严过滤


阶段一:宽召回

3.1 Milvus 向量检索(语义级)

问题:需要理解"同义词"和"概念相似"(如"AI"和"人工智能")。

方案:使用 HNSW 索引进行近似最近邻搜索。

python 复制代码
# HNSW 索引:在 64 个候选点中精确搜索
search_params = {"params": {"ef": 64}}

# 按知识库分区过滤
expr = f'kb_id in {partition_keys}'

# 检索流程:query → embedding → HNSW 搜索 → top_k 子块 → 映射回父块

HNSW 原理

  • 分层图结构,高层快速定位,低层精确搜索
  • ef=64:在 64 个候选点中搜索,平衡速度和精度

3.2 Elasticsearch BM25 关键词检索(精确级)

问题:向量检索容易遗漏精确关键词(如专有名词 "Qwen2.5"、数字 "¥0.008")。

方案:使用 BM25 算法进行精确关键词匹配。

python 复制代码
# BM25 策略:基于倒排索引的精确匹配
es_store = ElasticsearchStore(strategy=BM25RetrievalStrategy())
filter = [{"terms": {"metadata.kb_id.keyword": partition_keys}}]
es_sub_docs = await es_store.asimilarity_search(query, k=top_k, filter=filter)

BM25 核心原理

  • TF(词频):一个词在文档中出现越多 → 越重要
  • IDF(逆文档频率):一个词在整个语料中出现越少 → 越有区分度

向量检索 vs BM25 对比

特性 向量检索 BM25
理解同义词
精确匹配
适合场景 语义类问题 精确关键词
速度

3.3 去重合并(不做分数融合)

问题:两路检索结果可能重复,浪费 Token。

方案:Milvus 在前(语义质量更稳定),ES 独有的追加。

python 复制代码
# Milvus 在前,ES 独有的追加
milvus_doc_ids = [d.metadata[id_key] for d in query_docs]
for d in es_sub_docs:
    if doc_id not in milvus_doc_ids:
        es_ids.append(doc_id)
query_docs.extend(es_docs)

为什么不做分数融合

  • Milvus 分数 = 余弦相似度(0~1)
  • ES 分数 = BM25(可以是负数或很大的数)
  • 量纲不同,直接融合无意义,交给 Rerank 统一排序

阶段二:精排序

3.4 Rerank 交叉编码器精排

问题:向量检索的"粗排"分数不够精确。

方案:使用交叉编码器对候选文档进行精排。

特性 双编码器(向量模型) 交叉编码器(Rerank)
编码方式 query 和 doc 独立编码 query 和 doc 拼接后联合编码
交互深度 只在最后算余弦相似度 每一层都有注意力交互
精度 较粗 更精确
速度 快(可预计算) 慢(每对重新计算)
角色 大规模初筛 少量候选精排

为什么不用交叉编码器做初筛

  • 交叉编码器需要对每个 (query, doc) 对重新计算,无法预计算
  • 如果知识库有 100 万条文档,交叉编码器要计算 100 万次,不现实

3.5 Rerank 异常降级
python 复制代码
try:
    docs = await self.rerank.arerank_documents(query, docs)
except Exception:
    # 降级:用 embedding 余弦相似度打分
    embed1 = await self.embeddings.aembed_query(query)
    for doc in docs:
        embed2 = await self.embeddings.aembed_query(doc.page_content)
        doc.metadata['score'] = cosine_similarity(embed1, embed2)

3.6 Embedding/Rerank 客户端并发批处理
python 复制代码
# 大批量拆成多个小批次,并发发送
tasks = [self._get_embedding_async(session, texts[i:i+batch_size])
         for i in range(0, len(texts), batch_size)]
results = await asyncio.gather(*tasks)  # 并发等待

阶段三:严过滤

3.7 第一层------绝对阈值过滤

问题:去掉绝对不相关的噪音文档。

python 复制代码
# 保留 rerank 分数 >= 0.28 的文档
filtered = [doc for doc in docs if doc.metadata['score'] >= 0.28]
# 如果过滤后为空,保留全部(总比什么都不返回好)

0.28 的含义:经验阈值,低于此分数的文档基本不相关。


3.8 第二层------梯度断裂检测

问题:在保留的文档中,找到高质量和低质量的分界线。

python 复制代码
saved_docs = [docs[0]]  # 最高分文档
for doc in docs[1:]:
    relative_diff = (saved_docs[0].score - doc.score) / saved_docs[0].score
    if relative_diff > 0.5:  # 50% 质量断崖
        break                # 后面的全不要
    else:
        saved_docs.append(doc)

执行示例

复制代码
分数: [0.92, 0.85, 0.80, 0.40, 0.35]

0.92 → 保留(最高分)
0.85 → (0.92-0.85)/0.92 = 7.6%  < 50% → 保留
0.80 → (0.92-0.80)/0.92 = 13%   < 50% → 保留
0.40 → (0.92-0.40)/0.92 = 56.5% > 50% → 截断!

最终保留: [0.92, 0.85, 0.80]

两层配合的逻辑

  • 只用绝对阈值:可能所有文档都 > 0.28 但后面几个已经很差
  • 只用相对差异:如果都很低(如 0.35, 0.30, 0.28),相对差异不大但都不该要
  • 两层组合:先保底(去绝对噪音),再检测断裂(去相对噪音)

3.9 内容级去重
python 复制代码
# 按 page_content 文本内容去重(非 doc_id)
def deduplicate_documents(source_docs):
    unique_docs = set()
    for doc in source_docs:
        if doc.page_content not in unique_docs:
            unique_docs.add(doc.page_content)
            deduplicated_docs.append(doc)

为什么不用 doc_id:不同文件可能有完全相同的段落(如法规原文),按内容去重更有效。


3.10 已删除文档过滤 + 默认分数兜底
python 复制代码
# 过滤已删除的文档
if retriever.mysql_client.is_deleted_file(doc.metadata['file_id']):
    continue

# 没有分数的文档用位置倒序兜底
if 'score' not in doc.metadata:
    doc.metadata['score'] = 1 - (idx / len(query_docs))

3.11 Milvus 自动重连 + HTTP 重试
python 复制代码
# Milvus 空结果时重建连接
if len(query_docs) == 0:
    retriever.vectorstore_client = VectorStoreMilvusClient()
    query_docs = await retriever.get_retrieved_documents(...)

# HTTP 指数退避重试
retry = Retry(total=3, backoff_factor=1, status_forcelist=[500, 502, 503, 504])
# 重试间隔:1s → 2s → 4s

四、解决的问题汇总

阶段 手段 解决的问题
宽召回 Milvus 向量检索 语义级文档匹配
宽召回 ES BM25 检索 精确关键词匹配
宽召回 去重合并 两路结果重复
精排序 Rerank 交叉编码器 粗排精度不足
精排序 异常降级 Rerank 服务故障
精排序 客户端并发批处理 大批量推理效率
严过滤 绝对阈值 ≥ 0.28 绝对噪音
严过滤 梯度断裂 > 50% 相对噪音
严过滤 内容级去重 跨文件相同段落
严过滤 已删除过滤 数据一致性
鲁棒性 自动重连 Milvus 连接中断
鲁棒性 HTTP 指数退避 网络不稳定

相关推荐
zhangfeng11336 分钟前
2026-08-10/11 AI 领域重要动态筛选(侧重 AI coding 与具身智能)
人工智能·microsoft
leoZ23116 分钟前
Vue3 还原一个企业级后台-01-项目背景与选题
图像处理·人工智能·chatgpt·智慧城市·边缘计算·openvino·dreamfusion
ltqvibe25 分钟前
企业数智化中台的五层架构——AI框架为什么需要纵向贯通
大数据·人工智能·架构
兮动人26 分钟前
AI 开始接管工作台:谁会成为下一代电脑入口?
人工智能·ai·chatgpt·codex·workbuddy
办公室马主任34 分钟前
制造业数字化的系统架构:从车间数据到经营闭环怎么设计
人工智能·系统架构·制造
Claire_8842 分钟前
基于知识图谱构建的学习资料分类整理方案:以多模态检索与智能生成为例
人工智能·powerpoint
花椒技术1 小时前
一个人已经有 Agent 了,我们为什么还要建设统 Agent 平台
人工智能·agent·ai编程
Olafur_zbj1 小时前
【AI】CUDA的新编程模型:tile编程模型 的好处
人工智能
不爱学英文的码字机器1 小时前
推荐算法梳理,六种主流模型与九步训练流程
算法·机器学习·推荐算法