黑白梦黑白梦

  • 文章
  • 专栏
  • 文章
  • 专栏
全部文章

查询改写(Query Rewriting):面向多路检索的查询表示生成机制

发布于 2026-09-15约 16 分钟

查询改写是在检索执行之前,把用户原始输入或整段对话历史转换为检索侧可消费的查询表示,通常由语言模型完成。其产出形态由下游检索通道决定。本文梳理原始输入直接进入检索时的失效形式与常见的产出形态,并以多表示生成为例展开实现方式、消费路径与边界条件。

核心概念与工作模型

原始输入作为检索查询的失效形式

不同检索通道对查询形态有各自的隐含要求,把用户输入原样交给检索系统,会在以下几类场景下损失召回:

  • 指代与省略:多轮对话中的"它的成本是多少"不包含任何可匹配的实体词元,BM25 没有可查询的词;向量检索得到的向量对应"成本"这一抽象概念,缺少指代对象的约束,命中范围会扩散到所有讨论成本的文档。
  • 语义稀释:向量嵌入把整段文本压缩为单一向量,该向量是文本中各语义成分的聚合结果。当输入包含寒暄、多个话题或与当前问题无关的历史时,当前检索意图在聚合后的向量中占比下降。
  • 噪声词元的累加:BM25 的分值为查询各词元贡献之和,长查询中与目标无关的词元会成为额外的加项,参与最终排序。
  • 多意图混杂:单轮输入可能同时包含多个信息需求,而一次检索调用通常只能表达一个查询意图。

这几类失效的共同点是:问题出在查询侧的表达,而非语料或索引。

查询改写的产出形态

查询改写的产出并不固定。工程实现中常见的产出形态如下:

产出形态 产出物 消费方式 适用场景
上下文消解重写 单个独立完整的查询 作为查询送入检索 多轮对话中的指代与省略
多表示生成 面向不同通道的多个表示 分别送入对应通道,结果按名次合并 多条分值尺度不同的检索路径
元数据过滤抽取 结构化过滤条件,如实体名、时间范围 检索前的硬过滤,不参与相似度计算 语料带有可过滤的元数据字段
关键词扩展 多组面向不同召回目标的词元集合 分别用于字段名、指标、取值等目标的匹配 结构化数据的模式对齐
假设文档生成 一篇假设性的说明文档 编码为向量后检索 查询短,与文档形态差异较大
多查询扇出与分解 多个查询变体或子问题 各自检索后融合 单一表述覆盖不全,或问题含多个子需求

几点补充:

  • 形态之间不互斥:同一系统常组合使用。例如先做上下文消解得到独立查询,再在该查询之上生成假设文档;或在多表示生成的同时抽出实体名作为过滤条件。
  • 产出形态由消费方决定:检索侧有几条路径、每条路径需要什么形态的输入,决定了改写输出包含哪些字段。先确定消费端,再确定 schema。
  • 改写不必然依赖语言模型:词元抽取可由 TF-IDF、TextRank 一类本地算法完成,模型用在需要语义理解的环节,例如指代消解、面向特定目标的词元扩展、假设文档生成。

本文后续以"多表示生成"为主线展开,原因是它同时涉及结构化输出与多路检索的衔接,其余形态可以视为同一调用结构下输出字段的变化。

面向不同检索通道的查询表示

采用"多表示生成"时,改写器为每条检索通道各产出一个表示。各通道匹配查询的方式不同,同一个信息需求因此需要不同的表示形式:

表示形式 匹配机制 适配通道 典型粒度
词元列表(keywords) 词元的字面重合与统计权重 BM25、稀疏向量检索 术语、专有名词、标识符、日期
自然语言查询(searchQuery) 向量空间中的语义相似度 稠密向量检索 概念化描述

两类表示的构造约束方向相反:

  • 关键词通道要求与语料共享词元:BM25 按词元的字面出现计算分值,改写结果需要落在语料实际使用的词元上,同义词与形态变化需要显式展开。
  • 语义通道不要求词元重合:查询向量与文档向量在同一向量空间中比较,查询可以写成对意图的概念化描述,不必包含文档中出现的任何词。
  • 宽泛程度不同:同一问题下,关键词偏精确,搜索查询偏宽泛。把语义查询写得与关键词同样精确,会人为收窄向量检索的召回范围。

改写器的输入输出契约

改写器是一次独立的语言模型调用,其输入与输出契约分别固定在两处:

  • 输入:对话消息序列。改写器读取完整对话上下文,而不是仅最后一条用户消息,指代与省略的解析依据存在于历史轮次中。
  • 输出:结构化对象。字段结构由 schema 声明,模型按 schema 生成,调用方按 schema 消费。

输出字段的构成由消费方决定,下文示例的字段配置对应词元匹配通道与语义匹配通道这一组合。

字段的语义由 schema 中的描述文本承担。这段描述是约束模型输出行为的主要手段,它决定了模型如何理解每个字段的用途与粒度。

核心机制实现与行为验证

模型实例化

创建改写器使用的模型实例:

TypeScript
import { createAlibaba } from '@ai-sdk/alibaba';

const alibaba = createAlibaba({
  apiKey: process.env.ALIBABA_API_KEY,
  baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
});

// 改写任务输出短、结构固定,可选用延迟较低的档位
const rewriterModel = alibaba('qwen3.7-flash');

档位选择属于工程取舍。改写器位于检索之前的串行路径上,其推理耗时直接叠加到用户可感知的响应时间;同时改写本身是受限的信息抽取任务,不涉及长链条推理。两方面的权衡决定档位,替换模型只需更换此处的实例。

输出 schema 定义

定义改写结果的 schema,并在字段描述中固定用途与粒度:

TypeScript
import { z } from 'zod';

const queryPlanSchema = z.object({
  keywords: z
    .array(z.string())
    .describe(
      '用于关键词检索的词元列表。使用语料中可能出现的具体术语,包含专有名词、标识符与同义词。',
    ),
  searchQuery: z
    .string()
    .describe(
      '用于语义检索的自然语言查询。描述信息需求本身,可以比关键词更宽泛,不必与语料共享词元。',
    ),
});

type QueryPlan = z.infer<typeof queryPlanSchema>;

两处描述的差异决定了输出的行为差异:keywords 的描述强调字面匹配与同义展开,searchQuery 的描述强调概念化与宽泛度。省略描述时,模型只能依据字段名推断用途,两类输出会趋同。

改写调用

执行改写调用:

TypeScript
import {
  convertToModelMessages,
  generateText,
  Output,
  type UIMessage,
} from 'ai';

export const rewriteQuery = async (
  messages: UIMessage[],
): Promise<QueryPlan> => {
  const result = await generateText({
    model: rewriterModel,
    instructions: `你是一个检索查询改写器。
      阅读完整对话上下文,判断用户当前的信息需求。
      生成一个用于关键词检索的词元列表,以及一个用于语义检索的自然语言查询。
      词元列表使用语料中可能出现的具体术语;语义查询描述信息需求本身,可以更宽泛。`,
    output: Output.object({
      schema: queryPlanSchema,
    }),
    messages: await convertToModelMessages(messages),
  });

  return result.output;
};

调用中的三个关键点:

  • instructions:声明任务边界与两类输出的差异,与 schema 描述共同约束模型行为。
  • convertToModelMessages:把前端的 UIMessage 序列转换为模型可消费的消息序列,保留完整对话历史。
  • output: Output.object({ schema }):声明结构化输出,result.output 为按 schema 解析后的对象,其类型由 schema 推导。

输出形态验证

构造一段多轮对话作为输入:

TypeScript
const messages: UIMessage[] = [
  {
    id: '1',
    role: 'user',
    parts: [
      { type: 'text', text: '订单查询接口在高峰期会返回 504' },
    ],
  },
  {
    id: '2',
    role: 'assistant',
    parts: [
      { type: 'text', text: '下游依赖的超时配置是怎样的?' },
    ],
  },
  {
    id: '3',
    role: 'user',
    parts: [
      { type: 'text', text: '调的是支付网关,重试应该怎么设?' },
    ],
  },
];

const plan = await rewriteQuery(messages);
console.dir(plan, { depth: null });

对末尾这条追问,两类表示分别指向不同的检索目标。具体内容随模型与语料变化,以下为一次调用输出的形态:

字段 输出形态 检索目标
keywords ["支付网关", "重试", "超时", "504"] 包含上述术语的文档
searchQuery 下游依赖调用失败后的重试与降级处理策略 讨论调用链路稳定性设计的文档,不要求出现上述术语

差异集中在两处:

  • 词元是否必须出现:keywords 中的每一项都需要在语料中有对应的字面出现,BM25 才会给出非零分值;searchQuery 的表述可以不出现在任何文档中,向量空间中的相似度仍然有效。
  • 覆盖范围:searchQuery 覆盖"调用链路稳定性"这一上位概念,可以命中《分布式调用链路的稳定性设计》一类未出现上述词元的文档。

检索管线接入

接入检索管线:

TypeScript
const plan = await rewriteQuery(messages);

// 词元列表进入关键词匹配通道
const keywordHits = await searchByKeywords(plan.keywords);

// 自然语言查询进入语义匹配通道
const semanticHits = await searchBySemantics(
  plan.searchQuery,
);

// 两路结果按名次合并
const merged = reciprocalRankFusion([
  keywordHits,
  semanticHits,
]);

其中 searchByKeywords 与 searchBySemantics 为管线中已有的检索函数,reciprocalRankFusion 负责合并两路结果。

  • 关键词通道:plan.keywords 作为词元列表传入 BM25 或稀疏向量检索。
  • 语义通道:plan.searchQuery 先经嵌入模型编码为查询向量,再与文档向量比较。
  • 合并阶段:两个通道的分值尺度不同,按分值直接相加会使量级更大的一路主导排序,因此按名次合并。

两个通道的查询表示由同一次调用产出,无需为每条检索路径单独发起模型调用。

产出形态的扩展

更换输出 schema 与消费端的分发方式,同一套调用结构可以得到其它产出形态。

单查询重写与元数据过滤抽取的组合:

TypeScript
// 单查询 + 元数据过滤:所有通道共用同一条查询
const singleQueryPlanSchema = z.object({
  rewrittenQuery: z
    .string()
    .describe('消解指代后的独立完整查询'),
  entityName: z
    .string()
    .optional()
    .describe('提问中提及的实体名称,用于检索前过滤'),
});

// 调用结构与前述一致,schema 已替换
const plan = await rewriteQuery(messages);

// 同一条查询送入所有通道,过滤条件在检索内生效
const hits = await Promise.all([
  searchByVector(plan.rewrittenQuery, plan.entityName),
  searchByWeb(plan.rewrittenQuery),
]);

多组关键词扩展:

TypeScript
// 按召回目标拆分字段,各组词元分别用于不同的匹配目标
const keywordPlanSchema = z.object({
  columnTerms: z
    .array(z.string())
    .describe('用于匹配字段名的词元'),
  metricTerms: z
    .array(z.string())
    .describe('用于匹配指标名的词元'),
  valueTerms: z
    .array(z.string())
    .describe('用于匹配字段取值的词元'),
});

两种扩展与前述示例的差别只在字段构成与消费端的分发方式:改写器的调用、结构化输出的解析、失败降级路径均可直接复用。选择哪种产出形态,依据是检索侧需要什么形态的输入,而不是改写器本身能生成什么。

边界条件与典型差异

指代消解的依赖范围

改写质量取决于对话历史能否提供指代对象:

  • 历史完整时:指代对象可被解析,改写在输入基础上补全实体与约束。
  • 历史被截断时:上下文窗口限制导致早期轮次被裁剪后,指代对象丢失,改写器只能依据残缺上下文推测,输出可能偏离原始意图。
  • 无历史可依时:单轮输入中已包含完整信息需求的场景,改写不引入新信息,仅改变表示形式。

改写引入的约束偏移

改写器在生成词元列表时可能补充输入中未出现的限定条件。例如输入为"部署流程",改写结果若引入"生产环境"这一限定词,检索范围会被收窄到生产环境相关文档,其它环境的同类文档被排除在外。这类偏移在关键词通道上影响直接:BM25 的分值是各词元贡献的累加,多余词元既引入噪声,也改变了排序依据。控制方式是在 schema 描述中限定词元的来源范围,并将词元数量维持在较小规模。

改写无法突破的检索边界

改写改变的是查询的表示形式,不改变语料内容。语料中不存在对应信息时,任何改写都无法召回。改写带来的收益来自查询表示与检索通道的匹配程度,而非知识的补充。这一边界决定了改写的作用位置:它处理查询侧的表达问题,检索质量的下限仍由语料与索引决定。

需要跳过改写的场景

改写是一次额外的模型调用,位于检索之前的关键路径上。以下场景中其收益不足以覆盖成本:

  • 输入本身即完整查询:术语查询、标识符查询、错误码查询,其表达形式已与目标通道匹配。
  • 语料规模很小:候选集本身有限时,检索结果的区分度主要由语料决定。
  • 对首字延迟敏感:改写调用与检索调用串行,前者的耗时完整叠加在后者之前。

判断依据是输入的表达完整度:输入越接近独立完整的查询,改写的边际收益越低。

工程实践建议

延迟预算

改写位于检索管线的最前端,其耗时叠加在后续所有环节之前:

  • 档位选择:改写是受限的信息抽取任务,输出长度有限,可与主线对话使用不同档位的模型。
  • 输出长度约束:在 schema 中限制词元数量与查询长度,减少生成阶段的 token 消耗。
  • 并发编排:改写与检索之间存在数据依赖,无法并行执行;改写可与其它不依赖查询结果的前置步骤并行。

结果复用与缓存

同一轮对话可能触发多次检索,例如检索工具被连续调用。改写结果可以按对话历史的指纹缓存,在历史未发生变化时直接复用,避免重复调用。

失败降级

改写是增强环节,其失败不应中断检索。结构化输出失败时 Output.object 抛出 NoObjectGeneratedError,调用失败则以其它错误形式抛出。两种情况都应回退到原始输入,而降级路径需要在调用处显式实现。

实现降级包装:

TypeScript
import {
  NoObjectGeneratedError,
  type UIMessage,
} from 'ai';

// QueryPlan 与 rewriteQuery 见上文的定义

export const resolveQueryPlan = async (
  messages: UIMessage[],
  fallbackText: string,
): Promise<QueryPlan> => {
  try {
    return await rewriteQuery(messages);
  } catch (error) {
    // 区分失败原因,便于监控与告警
    const reason = NoObjectGeneratedError.isInstance(error)
      ? 'schema 未满足'
      : '调用失败';
    console.warn(`查询改写降级:${reason}`);

    // 回退到原始输入,检索退化为未改写形态
    return { keywords: [], searchQuery: fallbackText };
  }
};

降级路径需要在调用处显式实现。回退查询的构造方式应保证其至少能被语义通道消费,因此 searchQuery 取原始用户输入,keywords 留空以避免无效的词元匹配。

与融合、重排的分工

检索管线中三个环节处理的问题不同:

环节 作用位置 解决的问题
查询改写 召回之前 查询表示与检索通道不匹配
名次融合 多路召回之后 各通道分值尺度不可比
重排 融合之后 候选集内的精细相关性排序

改写决定用什么查询去检索,融合决定如何合并多路结果,重排决定如何对候选精排。三者顺序固定:先确定查询,再执行召回,最后合并与精排。改写的输入是查询表示,不涉及候选集;融合与重排的输入是候选集,不再改变查询。

目录
核心概念与工作模型原始输入作为检索查询的失效形式查询改写的产出形态面向不同检索通道的查询表示改写器的输入输出契约核心机制实现与行为验证模型实例化输出 schema 定义改写调用输出形态验证检索管线接入产出形态的扩展边界条件与典型差异指代消解的依赖范围改写引入的约束偏移改写无法突破的检索边界需要跳过改写的场景工程实践建议延迟预算结果复用与缓存失败降级与融合、重排的分工

本文收录于专栏

AI 应用开发笔记

沉淀大模型智能体等 AI 应用的开发与落地笔记

0 篇文章更新于 2026-08-23
上一篇倒数排名融合(RRF):基于名次的多路检索结果合并机制

©2015-2026 黑白梦 粤ICP备15018165号

联系: heibaimeng@foxmail.com