QAnything 阅读优化策略03——查询转换

查询转换(Query Transformation)

查询转换是将用户查询从文本形式转化为检索引擎能处理的格式(如 embedding 向量),同时做必要的清洗和保护。这是让"人类语言"适配"机器引擎"的关键桥梁。


一、是什么

查询转换是将用户查询从文本形式转化为检索引擎能处理的格式(如 embedding 向量),同时做必要的清洗和保护。

核心输入 :改写后的检索查询

核心输出:归一化的 embedding 向量 + 保护后的查询文本


二、为什么重要

检索引擎不认识"人类语言",它只认识向量。

转换质量直接影响检索质量:

  • 图片标记 ![figure] 会干扰 embedding 模型
  • 过长的 query 会导致 Rerank 模型截断
  • 不同组件需要不同的 Token 计数精度

与传统方案的对比

优化点 传统方案 传统方案的问题 QAnything 方案 优势
向量化 平均池化(mean pooling) 所有 token 平均,重要信息被稀释 CLS token 提取 [:,0] 全局语义更聚焦
归一化 不归一化 余弦相似度计算需要除法,长向量主导排序 L2 归一化 余弦相似度 = 点积,加速计算
图片处理 原样发送 ![figure](xxx.jpg) 干扰语义 发送前过滤图片标记 检索向量更纯净
Token 计数 统一用一个 tokenizer tiktoken 计 100 token,实际可能 120 三级 tokenizer + 安全余量 各组件精度匹配
推理框架 PyTorch 直接推理 推理慢,部署体积大,无算子融合 ONNX Runtime + 全图优化 推理速度提升 2-3x
分数输出 原始 logits(无界) 阈值难设,不可解释 Sigmoid + 线性拉伸 0~1 可解释,区分度高

三、怎么做

3.1 Embedding 向量化 + L2 归一化

问题:文本需要转为向量才能在 Milvus 中检索。

方案:提取 CLS token 作为全局语义表示,然后 L2 归一化。

python 复制代码
# Embedding ONNX 模型推理
embedding = outputs_onnx[0][:, 0]  # 提取 CLS token(全局语义)

# L2 归一化:使所有向量落到单位球面
norm_arr = np.linalg.norm(embedding, axis=1, keepdims=True)
embeddings_normalized = embedding / norm_arr

归一化的价值

  • 归一化后余弦相似度 = 点积,加速后续相似度计算
  • 所有向量长度一致,避免"长向量主导排序"的问题

为什么用 CLS token

  • BERT 类模型的 [:, 0] token 经过自注意力聚合,代表整句话的全局语义
  • 比平均池化更能捕捉句子的核心含义

3.2 图片引用过滤

问题 :查询中的 ![figure](xxx.jpg) 会干扰 Embedding 模型的语义理解。

方案:发送前过滤掉图片和公式标记。

python 复制代码
# Embedding 客户端:发送前过滤
def _process_query(query):
    return '\n'.join([line for line in query.split('\n')
                      if not line.strip().startswith('![figure]')
                      and not line.strip().startswith('![equation]')])

为什么不在索引阶段过滤

  • 索引阶段的图片引用是文档内容的一部分
  • 但查询阶段用户不会主动写图片标记,这些通常是系统拼接进来的

3.3 三级 Tokenizer 精度体系

问题:不同组件对 token 计数的精度要求不同,用错 tokenizer 会导致切分不准或预算超支。

Tokenizer 使用场景 为什么用它
tiktoken (GPT) LLM Token 预算计算 直接影响 API 成本,需要精确
embedding_tokenizer 文件切分判断 必须与 Embedding 模型的 max_length 对齐
rerank_tokenizer Rerank 资格判断 必须与 Rerank 模型的 max_length 对齐
python 复制代码
# LLM Token 估算:额外加安全余量
total_tokens *= 1.1   # tiktoken 精度较高,加 10%
total_tokens *= 1.2   # 如果用 cl100k_base 兜底(中文模型偏差大),加 20%

安全余量的原因

  • tiktoken 是 GPT 专用 tokenizer,与国产 LLM(如 Qwen)有偏差
  • cl100k_base 是通用 tokenizer,中文场景偏差更大
  • 加余量是为了防止实际 token 超过预算

3.4 Rerank 查询长度保护

问题:Rerank 模型 max_length = 512,如果 query 太长就没有空间放 doc。

方案:query > 300 token 时跳过 Rerank,给 doc 留出 ~200 token 空间。

python 复制代码
# 三个条件全部满足才执行 Rerank
if rerank and len(source_documents) > 1 and num_tokens_rerank(query) <= 300:
    source_documents = await self.rerank.arerank_documents(...)

为什么是 300 而不是 512

  • Rerank 输入 = query + doc,512 是总长度
  • 给 doc 留 200 token 空间,防止截断

3.5 ONNX Runtime 推理加速

问题:PyTorch 推理速度慢,部署体积大。

方案:使用 ONNX Runtime 进行模型推理。

python 复制代码
sess_options = SessionOptions()
sess_options.graph_optimization_level = GraphOptimizationLevel.ORT_ENABLE_ALL  # 全图优化
# 自动线程调度 + GPU/CPU 自适应
providers = ['CUDAExecutionProvider', 'CPUExecutionProvider']

优化项

  • 算子融合:将多个小算子合并为一个大算子
  • 常量折叠:编译期计算常量表达式
  • 自动线程调度:根据 CPU 核心数分配线程
  • 设备自适应:自动选择 GPU 或 CPU

3.6 Rerank Sigmoid 分数校准

问题:交叉编码器输出 logits(无界值),需要映射为 0~1 之间的可解释分数。

python 复制代码
def sigmoid(x):
    scores = 1 / (1 + np.exp(-x))                       # 标准 sigmoid → (0, 1)
    scores = np.clip(1.5 * (scores - 0.5) + 0.5, 0, 1) # 线性拉伸 → 增强区分度
    return scores

线性拉伸的作用

复制代码
标准 sigmoid 输出:
0.48  0.50  0.52  0.55  → 区分度低,难以设定阈值

拉伸后:
0.47  0.50  0.53  0.575 → 高分更高,低分更低,区分度增强

四、解决的问题汇总

手段 解决的问题
Embedding 归一化 余弦相似度计算效率
图片引用过滤 无效 token 干扰语义
三级 Tokenizer 不同组件精度需求不同
Rerank 长度保护 Rerank 模型输入截断
ONNX 加速 推理速度慢
Sigmoid 校准 分数无界不可解释
相关推荐
良木生香19 小时前
【C++初阶】STL—— Stack & Queue 从入门到精通:容器适配器、迭代器与经典面试题
java·开发语言·c++·算法·zookeeper
小小晓.19 小时前
C++小白记:vector
开发语言·c++·算法
tkevinjd19 小时前
力扣72-编辑距离
算法·leetcode·职场和发展
小刘学技术19 小时前
AI人工智能决策树分类器:原理、实现与应用
开发语言·人工智能·python·算法·决策树·机器学习·数据挖掘
caimouse20 小时前
mm学习笔记_04:VAD树算法与地址空间分配
笔记·学习·算法·reactos
abcy07121320 小时前
flink窗口类型
开发语言·python·算法
什巳1 天前
JAVA练习309- 二叉树的层序遍历
java·数据结构·算法·leetcode
什巳1 天前
JAVA练习306- 翻转二叉树
java·数据结构·算法·leetcode
smj2302_796826521 天前
解决leetcode第3989题网格中保持一致的最大列数
python·算法·leetcode