逐日AI
第 2 周 · D10约 5 小时

查询侧优化:改写、假设文档嵌入、多路查询、后退提问与意图路由

用户的问题往往写得又短又含糊,检索不到不是索引的错。这一天把功夫下在查询进来之后、检索发生之前:改写、生成假设答案再检索、拆成多个子查询、退一步问更一般的问题,以及先判意图再决定走哪条路。

今日目标 0/3

登录后可以勾选并保存进度。

今日目标

  1. 能说清查询改写、假设文档嵌入、多路查询、后退提问四种手法各自补的是什么短板
  2. 能实现一个意图路由,把闲聊、需要检索的问题和需要多跳的问题分流到不同处理路径
  3. 能用评估证明每一种查询侧手法的净收益,并算清它们额外增加的延迟与调用次数

昨天把力气全花在检索器上:两路召回、融合、重排。今天换一头——一个字都不改检索器,只改进去的那句话。读完回到页面顶部把三条目标勾掉。

小白版讲解

病人说不舒服,医生得先问出哪里不舒服

去医院看病,你不会一进门就被推进 CT 室。医生先问:哪儿不舒服,什么时候开始的,按下去会不会更疼。这一轮问诊的产出不是诊断,是一句准确的主诉——「右下腹持续性钝痛,两天,按压加剧」。有了这句话,后面的检查才知道该拍哪儿。

检索增强生成(RAG)系统里,「用户打进来的那句话」就是病人的第一句「我不舒服」:又短又含糊。我们前九天做的所有事——切块、建索引、混合检索、重排——都是 CT 室里的设备。设备再好,主诉不准,拍出来的还是别处。

今天要做的,就是在查询进来之后、检索发生之前加一道问诊。它有个统称:查询变换(query transformation)。四种常见手法各补一种短板:

  • 查询改写补「话没说全」:指代没消解、口语化、一句话塞了两个问题。
  • 假设文档嵌入补「问句和答案长得不像」:两者本来就不在一个语域。
  • 多路查询补「只有一个说法捞不全」:同一件事文档里可能换了个词写。
  • 后退提问补「问得太细,细到语料里根本没有那一句」。

再加一层意图路由:先判断这个问题到底要不要检索。

要提前说清一件事:这一天的产出不是「把五种手法全打开」,而是一份默认配置。 每种手法都要多花至少一次模型调用,多数还要多花几次检索。所以每讲一种都要顺手报三笔账:指标涨了多少、延迟涨了多少、多了几次调用。秤在 第八天 已经造好了。

查询改写:把「话没说全」补全

查询改写是最朴素也最值钱的一种。它干三件事:补全指代、拆开复合问题、把口语转成检索友好的说法

补全指代:上一轮问「生产数据库主备切换必须由谁书面审批」,这一轮问「这个人叫什么名字」。「这个人」三个字拿去做 BM25,捞到的全是讲「人」的块。改写器要把它变成「平台组组长叫什么名字」——先行词从上一轮对话里来。

拆开复合问题:「年假能结转几天,结转的部分最晚什么时候休完」其实是两问,合在一起检索两个主题的词互相稀释,哪一边都排不上去。

口语转检索友好:「请问一下报销那个额度大概是多少啊」里,「请问」「一下」「那个」「大概」「啊」在语料里到处都是,逆文档频率接近零,唯一的作用是把查询变长、把 BM25 的长度归一化拉偏。

实现上它只是一次很短的模型调用。两个细节:温度设成 0,否则同一句话每次改得不一样,评估数字会飘;明确禁止模型回答问题,不然它会热心地把答案也写进去,那段话拿去检索纯属污染。

rewrite.js
const REWRITE_SYSTEM = [
  '你是检索系统的查询改写器。把用户这一轮的问题改写成一个可以直接拿去检索的检索式。',
  '1. 用对话历史补全指代(「这个人」「他」「那个」要换成具体名称),补不出来就保持原样。',
  '2. 去掉口语填充词与礼貌用语,只留有检索价值的实词。',
  '3. 不要回答问题,不要添加历史里没有的事实。',
  '只输出改写后的一行文本,不要解释。',
].join('\n')
 
export async function rewriteQuery(client, history, question) {
  const context = history
    .slice(-4)
    .map((t) => `${t.role === 'user' ? '用户' : '助手'}:${t.content}`)
    .join('\n')
  const res = await client.messages.create({
    model: 'claude-haiku-4-5-20251001', // 改写是短任务,用便宜快的那一档就够
    max_tokens: 512,
    temperature: 0, // 同一句话必须改成同一个检索式,否则评估数字会飘
    system: REWRITE_SYSTEM,
    messages: [{ role: 'user', content: `对话历史:\n${context || '(无)'}\n\n本轮问题:${question}` }],
  })
  return res.content.find((b) => b.type === 'text').text.trim()
}

三笔账:每问多一次模型调用,输出只有一行,可以用便宜快的那一档模型;延迟多一次往返指标在单轮问答上基本不动——实验里开不开它指标一模一样。价值全在多轮,本节最后展开。

假设文档嵌入:先编一段答案,再拿答案去检索

这是四种手法里最反直觉的一种,英文叫 HyDE(hypothetical document embeddings,假设文档嵌入):先让模型针对这个问题编一段答案,然后不拿问题去检索,拿这段编出来的答案去检索。 第一次听会觉得荒谬——模型编的东西可能全是错的,拿错的东西去检索怎么会更准?

关键在于问题和答案不在同一个语义空间里。用户问「出差住一线城市每晚住宿费上限是多少」,这是疑问句;语料里那一段写的是「一线城市每晚住宿标准为人民币若干元,超出部分需部门负责人签批」——陈述句、有具体数字、公文腔。两段文字讲同一件事,但文体、句式、用词全不一样,而向量检索比的正是语义相似度。

HyDE 就是把疑问句先变成一段长得像制度条文的假文本:「关于一线城市住宿标准,按公司规定执行,每晚上限为人民币若干元。」里面的数字可能是瞎编的,但没关系——我们不把它给用户看,只拿它的向量去找邻居。它和真正的那一段在同一个语域、同一批公文词,距离自然近得多。

三笔账:每问多一次模型调用,而且是四种手法里最贵的一次——假设文档要写 80 到 150 字,输出 token 是改写的十几倍;检索次数翻倍指标这一笔账,我们的离线实验答不了。离线模式下这一步是规则替身,只会套一层文体词,编不出「上限若干元」这种具体内容——而那恰恰是 HyDE 起作用的地方。离线跑出来的涨跌只说明这条路接得通,既不能证明 HyDE 有用,也不能证明它没用。这一笔账必须连上真模型自己跑。

多路查询:一个问题,几个角度,结果用倒数排名融合合起来

多路查询的想法很简单:同一件事,文档里可能换了个词写。用户问「消费者扩容要走什么流程」,纪要里写的是「属于二级变更,需按规范在变更评审会上提单」。「扩容」和「变更」、「流程」和「规范」,字面一个都不重合。

所以让模型生成两三个换角度的检索式:一个偏关键词(去掉虚词只留名词和数字),一个偏文档用语,加上原问题本身,各查一遍再融合成一份。

融合用第九天那套倒数排名融合(reciprocal rank fusion),常数取 60,同分按 id 兜底保证可复现——原理见 第九天,这里复用同一个函数。留意一点:第九天融合的是「两个不同检索器」的名次,今天融合的是「同一个检索器、不同检索式」的名次。融合的轴变了,函数不用改——这正是它比按分数加权强的地方:它压根不看分数。

multi-query.js
const RRF_K = 60 // 原论文常用取值,全课统一
 
export function rrfFuse(rankings, k = RRF_K) {
  const scores = new Map()
  for (const ranking of rankings) {
    ranking.forEach((id, index) => {
      // 名次从 1 起算
      scores.set(id, (scores.get(id) ?? 0) + 1 / (k + index + 1))
    })
  }
  return [...scores.entries()]
    .map(([id, score]) => ({ id, score }))
    .sort((a, b) => b.score - a.score || (a.id < b.id ? -1 : 1)) // 同分按 id 排,保证可复现
}
 
export async function multiQuerySearch(retriever, variants) {
  const rankings = []
  for (const q of variants) {
    const hits = await retriever.retrieve(q) // 每个检索式各查一遍,这是成本涨得最快的一项
    rankings.push(hits.map((h) => h.chunkId))
  }
  return rrfFuse(rankings)
}

三笔账:每问多一次模型调用(一次生成全部变体,不要一个变体调一次);检索次数按变体数翻倍,三个变体就是三倍;指标是负收益——召回率没动,nDCG 从 0.671 掉到 0.665。30 篇语料里本来就没有「换个角度才能捞到」的长尾,多出来的两路只是把同一批结果重新洗了一遍牌。语料上万篇再回来重测,结论很可能反过来。

后退提问:先问更一般的问题,拿到背景再回来

后退提问(step-back prompting)针对的是另一种失败:问题问得太具体,具体到语料里根本没有那一句

用户问「把某个工作区的链路追踪采样率临时调到百分之百,属于几级变更,最晚提前几个工作日提单」。语料里没有任何一段专门讲「采样率调整属于几级变更」——变更分级写在一篇制度文档里,采样率写在另一篇监控文档里,直接检索这个长问题两边的词互相稀释,谁都排不上去。

后退一步问:「变更评审的分级规则与提单时限是什么」。这是更一般的问题,语料里恰好有一整节在讲它;拿到背景之后再回到具体问题,答案就在背景里。

实现上和多路查询同构,区别只在提示词——多路查询要的是同一层级的不同说法,后退提问要的是上一层级的总纲

三笔账:每问多一次模型调用(输出只有一行);检索次数翻倍;指标这一笔账和假设文档嵌入同病相怜,离线下它同样是规则替身。但它有一个 HyDE 没有的确定优势:输出短,所以无论效果如何,它都是四种扩展手法里最便宜的一个

意图路由:不是所有问题都需要检索

前面四种手法都在优化「怎么检索得更准」。意图路由问的是另一个问题:这一问到底要不要检索?

用户说「你好」「谢谢,你还能帮我查什么」,这类问题检索一万次也检索不出答案。但如果管道是「所有输入一律走检索」,它们照样会触发向量化、数据库查询、上下文拼接,然后把一堆不相干的块塞进提示词,模型还得费劲从中挣脱出来回一句「你好」。钱花了,准确率还掉了。

所以在管道最前面加一个很轻的分类器,分成三条路:

  • 直接回答:闲聊、寒暄、问系统能力。零次检索。
  • 单跳检索:一篇文档里就能找到答案。走标准流程。
  • 多跳检索:必须先查到一个中间事实(某个角色是谁、该走哪个规范),再拿它去查第二篇。走两轮检索。

分类器可以很便宜——一次短调用,只让它输出三个词之一。

router.js
const INTENT_SYSTEM = [
  '你是检索系统的意图分类器。判断这个问题该走哪条路,只输出一个词:',
  'direct     —— 打招呼、道谢、问「你能做什么」这类问题,检索知识库没有意义;',
  'single-hop —— 一篇文档里就能找到答案;',
  'multi-hop  —— 必须先查到一个中间事实,再用它去查第二篇。',
].join('\n')
 
export async function route(client, retriever, query, budgets) {
  const res = await client.messages.create({
    model: 'claude-haiku-4-5-20251001',
    max_tokens: 16, // 只要一个词,别给它写作文的机会
    temperature: 0,
    system: INTENT_SYSTEM,
    messages: [{ role: 'user', content: query }],
  })
  const text = res.content.find((b) => b.type === 'text').text
  // 判不出来一律退回单跳:兜底要偏向「多花一点钱」,不能偏向「答不出来」
  const intent = text.includes('direct') ? 'direct' : text.includes('multi') ? 'multi-hop' : 'single-hop'
  if (intent === 'direct') return { intent, candidates: [], budget: 0 }
  const candidates = await retriever.retrieve(query)
  // 多跳要同时装下两篇文档,600 token 常常只够装第一篇——分流的价值不只是省钱
  return { intent, candidates, budget: intent === 'multi-hop' ? budgets.multiHop : budgets.default }
}

注意代码里那句「多跳要同时装下两篇文档」。第八天定的上下文预算是 600 token,对单篇可答的问题绰绰有余,对多跳问题常常只够装第一篇。认出多跳之后单独给它更高的预算,实测把多跳召回率从 50% 抬到了 100%。分流因此有两层价值:不该花的不花,该花的地方才有钱花

值得把这两道原本失败的多跳题拆开看,它们坏在不同的地方,也被不同的机制修好。第一道的第二篇答案文档根本没进候选池——两篇文档之间字面一个词都不重合,检索器再排序也变不出没捞到的东西;桥接那一轮拿「平台组组长」重新检索才把它捞进来。第二道正相反:那篇文档在基线里已经排到第 4 名,但两路原始分都没过门槛(关键词一路 0 分,向量一路 0.151,差一点点),被拒答闸门挡在外面;桥接那一轮用《变更评审规范》这个线索去查,同一篇文档拿到很高的关键词分,以自己的原始分过了闸,顺带升到第 1 名。候选池里没有,和候选池里有但被挡住,是两种病,别用一种药去治。

兜底怎么设计:判不出来一律退回单跳。把多跳判成单跳只是少查一轮、答得不全,把闲聊判成单跳只是白花一次检索;反过来把该检索的判成直接回答,模型手里一点材料都没有,只能编,那是最贵的一种错。兜底方向要偏向「多花一点钱」,不能偏向「答不出来」。

多轮对话:不消解指代,检索必然跑偏

查询改写在单轮问答上几乎不涨指标,但一进多轮对话,它就从「锦上添花」变成「有没有」的分界线。实验里跑了这样一段对话:

第一轮问「生产数据库主备切换必须由谁书面审批」,命中那篇值班流程文档,里面写着「必须由平台组组长书面审批」。第二轮问「这个人叫什么名字」。

不消解直接检索这七个字,实测是:一条候选都过不了门槛,系统只能拒答。 注意失败方式——不是「答错了」,是「明明语料里有答案,系统却说找不到」。用户上一句刚问完,下一句就被告知查不到,体验是崩塌式的。

改写之后这句话变成「平台组组长叫什么名字」。同一个检索器、同一份索引,这一次那篇组织架构文档进了上下文——「平台组组长是周敏」只写在那一篇里。问题从答不出来变成答得出来,我们只改了那句话。

这还牵出一个更深的问题:那次拒答本身是对的(检索侧确实什么都没捞到),但原因不是「语料里没有」,而是「我们把问题问坏了」。生成侧的拒答策略在 第六天 已经定好,管的是「有材料但不足以支撑」;今天说的是在材料被检索之前就已经跑偏

源码导读

动手实验

🧪 D10 实验:四种查询变换加意图路由的实现,以及它们在评估集上的收益与成本对照

代码位置:labs/rag-14days/day-10-query-transformations

验收标准:

  1. MOCK=1 pnpm start 跑完,验收清单 8 项全绿(starter/ 原样跑有 6 项是叉)。
  2. 对照表里「多路查询」那一行的检索次数是 60,基线是 20——多路查询的成本要看得见。
  3. 「意图路由(多跳加预算)」那一行的多跳召回率是 100%,且单跳召回率没有下降。
  4. 第二轮对话不改写时一条候选都过不了门槛,改写后那篇组织架构文档进了上下文。
  5. 闲聊那一轮的检索次数是 0。

动手之前确认三件事。一是你已跑通第八天的评估脚本,今天的 20 道题、指标口径、上下文预算全部沿用它。二是别拿平均倒数排名当证据:第八天基线上它已经是 1.0000,题目从语料反向出、字面重合度高,第一名永远是答案文档——这是指标饱和不是系统满分,今天开关任何手法它都不会动;单文档那一档召回率同样已经 100%。这一天全部的证据只能出在多跳召回率、nDCG 和拒答率上。 三是读一遍实验 README 的「MOCK 说明」——离线模式下五种手法都是确定性规则替身,跑出来的指标只证明代码路径通了,不代表真实模型的效果

  1. 实现查询改写与指代消解,跑那段三轮对话,看第二轮从「只能拒答」变成「组织架构文档进了上下文」。
  2. 实现假设文档与多路查询,几份名次用第九天的倒数排名融合合起来,确认检索次数从 20 涨到 60。
  3. 写意图分类分流三条路,并给多跳单独提高上下文预算,看多跳召回率从 50% 跳到 100%。
  4. 逐个开关五种手法,把每一档的召回率、nDCG、模型调用次数、检索次数、上下文 token 记进同一张表。
  5. 写下你的默认配置,逐条说明为什么关掉了其中三项——这才是今天真正的作业。

面试题

今天 4 道题在下方题库区,侧重查询理解的手法选择、多轮对话下的指代消解、额外调用带来的延迟账。展开后先看「分析过程」再看要点——照着推导练比背要点管用。标注「国内高频 / 海外高频」方便按目标市场取舍。

检查清单与明日预告

  • 能说清查询改写、假设文档嵌入、多路查询、后退提问四种手法各自补的是什么短板
  • 能实现一个意图路由,把闲聊、需要检索的问题和需要多跳的问题分流到不同处理路径
  • 能用评估证明每一种查询侧手法的净收益,并算清它们额外增加的延迟与调用次数
  • 能说清为什么「先改写,再路由」,以及为什么平均倒数排名今天不能当证据
  • 实验 5 条验收标准全通过,并写下了默认配置与关掉某几项的理由
  • 4 道面试题不看要点也能答出至少 3 道

明天(D11)回到索引侧,做父子文档、摘要索引和上下文检索。为什么是这个顺序:今天证明了「检索器不变,只改查询就能把多跳召回率从 50% 抬到 100%」——查询侧的便宜钱先赚干净,剩下的才轮得到索引结构出手。明天处理的正是查询侧解决不了的那一类:块被切碎后丢掉了定位信息,改多少遍查询都补不回来。

面试题库

  • 假设文档嵌入(HyDE)为什么有效?它在什么情况下会把检索带偏?Why does HyDE (hypothetical document embeddings) work, and when does it steer retrieval in the wrong direction?
    国内高频海外高频进阶#hyde#query-transformation#retrieval-quality

    分析过程 · 先想清楚再作答

    1. 这题的题眼在后半句。前半句网上到处都能抄到,能不能说清「什么时候不该用」才是区分度所在——只答前半句的人,多半没在真实语料上跑过。
    2. 先给机制:向量检索比的是语义相似度,而用户的疑问句和文档里的制度条文在文体、句式、用词上都不同类。HyDE 先让模型编一段「长得像目标文档」的假文本,用它的向量去找邻居,等于把查询搬进了文档所在的那个语域。
    3. 紧接着点破一个常见误解:这段假文本的**事实对不对根本不重要**,因为它不给用户看,只贡献一个向量方向。理解到这一层,才算真懂它为什么不怕模型瞎编。
    4. 带偏有两种典型情况。一是模型编得太具体,给出语料里根本不存在的字段名或流程名,向量朝着一个不存在的方向去了;二是语料里压根没有答案,本该拒答的问题被编出来的假文档匹配到几个「看起来挺像」的邻居,拒答率掉下去、瞎编率涨上来。
    5. 说完风险要给对策,这一步最见工程经验:门槛卡在**每一路检索器的原始分**上而不是融合分上(融合分是相对的,最不相干的一批也能拿最高分);以及把假设文档当成**第二个检索式与原问题融合**,而不是直接替换原问题——替换在模型编歪时会把原问题的信号一起丢掉。
    6. 可预期的追问是「它多花多少钱」。答:假设文档要写上百字,输出 token 是查询改写的十几倍,是查询侧四种手法里最贵的一次调用,而且检索次数翻倍。所以它通常不该默认打开,应该进 A/B 队列。

    How to reason about it · think before answering

    1. The tell is in the second half. Anyone can recite why HyDE works; only someone who has run it on real data can say when it hurts.
    2. Give the mechanism first: dense retrieval compares semantic similarity, but a user's question and a policy paragraph differ in register, syntax and vocabulary. HyDE has the model draft a fake passage that looks like the target document, then retrieves with that vector — effectively moving the query into the documents' register.
    3. Then kill the common misreading: the factual accuracy of the draft does not matter, because it is never shown to the user. It only contributes a direction in embedding space.
    4. Two failure modes. The model invents an over-specific field or process name that does not exist in the corpus, and the vector chases something imaginary. Or the corpus genuinely has no answer, and the fabricated passage finds plausible-looking neighbours anyway — abstention rate drops and hallucination rate climbs.
    5. Pair the risk with a mitigation: gate admission on each retriever's raw score, never on the fused score (fused scores are relative, so even the worst batch tops out at 1.0); and treat the hypothetical document as a second query fused with the original rather than a replacement, so a bad draft can only dilute the signal, not erase it.
    6. Expect the follow-up on cost. The draft runs to a hundred-plus output tokens, an order of magnitude more than a rewrite, and it doubles retrieval calls. That is why it belongs in an A/B queue, not in the default config.

    答题要点

    • 有效的原因是语域对齐:疑问句和制度条文本来不在一个语义邻域,假设文档把查询搬到了文档那一侧。
    • 假文本的事实对错不重要,它只贡献一个向量方向,不展示给用户。
    • 带偏的两种情况:编得太具体,追一个语料里不存在的方向;本该拒答的问题被假文档匹配上,拒答率下降。
    • 两条护栏:门槛卡原始分不卡融合分;把假设文档当第二个检索式融合,而不是替换原问题。
    • 成本上它是查询侧最贵的一项(长输出加检索次数翻倍),默认关闭、按场景 A/B。

    Key points

    • It works by register alignment: a question and a policy paragraph sit in different neighbourhoods, and the fake passage moves the query into the document's.
    • The draft's factual accuracy is irrelevant — it only supplies a direction and is never shown to the user.
    • It misfires when the model invents over-specific details, or when the corpus has no answer and the fabrication finds plausible neighbours anyway.
    • Two guardrails: gate on raw per-route scores, not fused ones; fuse the hypothetical document with the original query instead of replacing it.
    • It is the most expensive query-side technique (long output plus doubled retrievals), so keep it off by default and A/B it.
  • 多轮对话里怎么处理指代?不做指代消解最典型的翻车场景是什么?How do you handle coreference in multi-turn RAG, and what is the classic failure when you skip it?
    国内高频海外高频基础#coreference#multi-turn#query-rewriting

    分析过程 · 先想清楚再作答

    1. 这是一道送分题,但送分题也有高下之分:只说「把历史拼进查询里」的答案,会被追问一句「历史越拼越长怎么办」就卡住。
    2. 先把做法说清:在检索之前加一次很短的改写调用,输入是最近几轮对话加本轮问题,输出是一行可以直接检索的检索式;温度设 0 保证同一句话每次改成同一个结果,并在提示词里明确禁止模型顺手回答问题。
    3. 为什么不是「把历史整个拼进查询」:历史越拼越长,噪声词把逆文档频率摊薄,检索反而更差;而且历史里包含上一轮的答案,等于拿答案去检索答案。改写的产出是一句话,不是一段历史。
    4. 最典型的翻车场景要举实例:上一轮问「这个操作必须由谁审批」,答「必须由某某组组长审批」;这一轮问「这个人叫什么名字」。不消解直接检索这七个字,实测是**一条候选都过不了门槛,系统只能拒答**。注意失败方式不是答错,是「明明语料里有答案却说找不到」,用户体验是崩塌式的。
    5. 补一条顺序上的坑:改写必须在意图路由**之前**。「这个人」是典型的多跳信号词,路由看到它会判成多跳、白跑一轮;改写之后它只是个普通单跳问题。同理,多路查询、后退提问也都要建立在改写后的那句话上,否则错误被放大好几倍。
    6. 可预期的追问是「怎么知道要不要改写」。答:短问题、含指代词、含省略(「那审计日志呢」)时才触发,纯新话题跳过——这一步很便宜,但能省掉一大半调用。

    How to reason about it · think before answering

    1. This is a warm-up question, but there is still a gap between answers. Saying "just concatenate the history into the query" invites a follow-up about growing histories that most candidates cannot handle.
    2. State the mechanism: insert a short rewrite call before retrieval that takes the last few turns plus the current question and returns one retrieval-ready line. Set temperature to 0 so the same input always yields the same query, and forbid the model from answering the question in the prompt.
    3. Explain why concatenation is worse: history grows without bound, filler words dilute inverse document frequency, and the previous answer leaks in — you end up retrieving an answer with an answer. The rewriter emits one sentence, not a transcript.
    4. Make the failure concrete. Turn one: "who must sign off on this operation?" Answer: "the platform team lead." Turn two: "what is that person's name?" Retrieved unresolved, not a single candidate clears the admission gate and the system refuses — even though the corpus contains the answer. The failure is not a wrong answer, it is a false "not found" right after the user's own question.
    5. Add the ordering trap: rewrite before intent routing. A pronoun is a classic multi-hop signal, so an unresolved query gets routed to the expensive path for nothing; after rewriting it is an ordinary single-hop question. Multi-query and step-back must also sit downstream of the rewrite, or one unresolved pronoun becomes three.
    6. Expect "how do you decide when to rewrite?" Trigger on short queries, pronouns and elliptical follow-ups; skip on a clearly new topic. The check is nearly free and removes most of the calls.

    答题要点

    • 在检索前加一次短改写调用,输入最近几轮加本轮问题,输出一行检索式,温度 0,禁止模型回答问题。
    • 不要把历史整段拼进查询:越拼越长、噪声稀释逆文档频率,还会拿上一轮的答案去检索。
    • 典型翻车:上一轮的「这个人 / 他 / 那个」不消解,检索一条都过不了门槛,系统在有答案的情况下拒答。
    • 失败方式是「假的查不到」,比答错更伤体验,因为用户刚刚才问过同一件事。
    • 顺序:先改写、再路由,多路查询与后退提问都建立在改写后的查询上。

    Key points

    • Add a short rewrite call before retrieval: last few turns plus current question in, one retrieval line out, temperature 0, answering explicitly forbidden.
    • Do not splice the whole history into the query — it grows unbounded, dilutes IDF, and leaks the previous answer into the search.
    • Classic failure: an unresolved pronoun means no candidate clears the gate, so the system refuses a question the corpus can answer.
    • That false "not found" hurts more than a wrong answer, since the user just asked about the same thing.
    • Order matters: rewrite first, then route; multi-query and step-back both build on the rewritten query.
  • 上线查询改写之后每问多了一次模型调用,端到端延迟涨了一倍,你怎么判断这笔开销值不值?Query rewriting adds a model call per question and doubles end-to-end latency. How do you decide whether it is worth paying?
    国内高频海外高频深入#cost-tradeoff#latency#query-rewriting

    分析过程 · 先想清楚再作答

    1. 这题考的是「能不能把工程判断落到数字上」。凡是回答「改写当然要做,能提升效果」的,一律判为没做过——他连收益是多少都没量。
    2. 先问自己一句:**收益出现在哪一类流量上**。这一条几乎决定了答案。查询改写在单轮问答上的收益接近零(我们在 20 道单轮题上实测开关它指标一模一样),收益全在多轮追问。所以第一步是去线上日志里查多轮会话占比,占比很低的话这笔钱不该花在全量流量上。
    3. 第二步是把三笔账摆齐,缺一笔就不能下判断:指标涨了多少(用固定的标准答案集跑,不要用感觉)、延迟涨了多少(改写是短输出任务,可以换便宜快的那一档模型,往往只多两三百毫秒而不是翻倍)、多了几次调用(改写是一次,多路查询是一次调用加几次检索,成本结构完全不同,别混着算)。
    4. 第三步是找**便宜的替代路径**再比一次:只在命中触发条件时才改写(短问题、含指代词、含省略),纯新话题直接跳过;改写结果按会话缓存;用小模型跑改写而不是主模型。这三招通常能把这笔开销压掉一大半,而收益几乎不掉。
    5. 结论要落成一条可执行的判据:**收益乘以受影响流量占比,除以增加的延迟与成本**,跟你手上其他候选优化排个序。改写通常能排到很前面,因为它的失败方式是「用户明明追问同一件事却被告知查不到」,那是会直接导致弃用的体验故障,不只是指标掉几个点。
    6. 可预期的追问是「延迟真的不能接受怎么办」。答:把改写和第一次检索**并行发**,用原查询先检索一路,改写回来后再补一路,两路用倒数排名融合合起来——延迟只多一个 max 而不是一个加法,代价是多一次检索。

    How to reason about it · think before answering

    1. This question is about turning an engineering judgement into numbers. "Rewriting obviously helps quality" is a fail — the candidate never measured the gain.
    2. Start with one question that nearly settles it: which slice of traffic does the gain land on? Query rewriting buys almost nothing on single-turn questions (we measured identical metrics with it on and off across 20 single-turn items); the entire payoff is in follow-up turns. So step one is to pull the share of multi-turn sessions from production logs.
    3. Step two is to lay out all three ledgers, because one alone cannot support a decision: how much the metrics moved on a fixed golden set, how much latency grew (a rewrite is a short-output task, so a cheap fast model often costs a few hundred milliseconds rather than doubling anything), and how many extra calls were added — one model call for rewriting versus one call plus several retrievals for multi-query is a completely different cost shape.
    4. Step three is to price the cheaper variants before deciding: rewrite only when a trigger fires (short query, pronoun, ellipsis), cache rewrites per session, and run a small model instead of the main one. These usually remove most of the cost while keeping the gain.
    5. Land on a usable rule: gain times affected traffic share, divided by added latency and cost, ranked against your other candidate optimisations. Rewriting usually ranks high because its failure mode is a false "not found" immediately after the user's own question — an abandonment-grade experience bug, not a few metric points.
    6. Expect "what if the latency genuinely is unacceptable?" Fire the rewrite and the first retrieval in parallel: search with the raw query immediately, search again when the rewrite returns, and fuse both rankings. You pay a max instead of a sum, at the cost of one extra retrieval.

    答题要点

    • 先定位收益落在哪一类流量:改写在单轮上接近零收益,价值全在多轮追问,先查多轮会话占比。
    • 三笔账缺一不可:标准答案集上的指标变化、增加的延迟、增加的调用次数与 token 成本。
    • 先试便宜的替代路径:条件触发、按会话缓存、用小模型跑改写,通常能压掉大半开销。
    • 判据是「收益 × 受影响流量占比 ÷ 增加的延迟与成本」,再和其他候选优化排序。
    • 延迟真的卡死时,把改写与首次检索并行发,两路名次用倒数排名融合,延迟从加法变成取最大值。

    Key points

    • Locate the gain first: rewriting is near-zero on single-turn traffic and pays off on follow-ups, so start from the share of multi-turn sessions.
    • All three ledgers are mandatory: metric delta on a golden set, added latency, added calls and token cost.
    • Try the cheap variants before deciding: conditional triggering, per-session caching, and a small model for the rewrite.
    • Decide on gain times affected traffic share over added latency and cost, then rank it against your other optimisations.
    • If latency is a hard constraint, fire the rewrite in parallel with the first retrieval and fuse both rankings, turning a sum into a max.
  • 意图路由判错了会怎样?你会怎么设计兜底?What happens when intent routing misclassifies, and how would you design the fallback?
    国内高频海外高频进阶#intent-routing#fallback#observability

    分析过程 · 先想清楚再作答

    1. 这题在考「有没有想过错误的方向」。路由是分类器,分类器一定会错;只答「多加训练数据提高准确率」的,等于没回答兜底怎么设计。
    2. 先把错误按方向拆开,这一步是整题的骨架:三条路(直接回答、单跳检索、多跳检索)两两误判,代价完全不对称。把该检索的判成直接回答,模型手里一点材料都没有,只能编,这是最贵的一种错;把闲聊判成单跳,只是白花一次检索;把多跳判成单跳,只是少查一轮、答得不全。
    3. 结论顺势就出来了:**兜底方向要偏向「多花一点钱」,判不出来一律退回单跳检索。** 单跳是三条路里错得最轻的一条,而且它的错误是可恢复的——材料不全模型还能说「资料里只查到一半」,材料为空它就只能编。
    4. 再补一层运行时兜底,比事前分类更管用:分类成直接回答之后,如果模型的回答里出现了具体数字、金额、日期这类需要出处的内容,就回退去检索一次再答;分类成单跳之后,如果检索侧一条都没过门槛,就升级走多跳或直接拒答。**用后一步的观测结果纠正前一步的判断**,这是路由系统最实用的一条设计。
    5. 还要提一句可观测性:路由的每一次判定都要落日志,带上原始问题、判定结果、后续是否发生了兜底升级。没有这份日志,你既不知道路由准不准,也没法攒出下一版的训练集。
    6. 可预期的追问是「什么时候干脆别做路由」。答:流量里闲聊占比很低、且多跳问题很少时,路由省下的钱还不够付分类调用的钱,这时候直接全部走单跳更划算——我们在 30 篇语料的实验里就看到,路由真正的收益并不在省检索,而在于认出多跳之后给它更高的上下文预算。

    How to reason about it · think before answering

    1. This tests whether you have thought about the direction of the error. A router is a classifier and classifiers misfire; "add more training data" is not a fallback design.
    2. Break the errors down by direction — that is the backbone of the answer. Across three routes (direct answer, single-hop, multi-hop) the six confusions carry wildly asymmetric costs. Routing a retrieval-worthy question to a direct answer leaves the model with no material at all, so it fabricates: the most expensive error. Routing chit-chat to single-hop merely wastes one retrieval. Routing multi-hop to single-hop just yields an incomplete answer.
    3. The conclusion follows: bias the fallback toward spending a little more, and default to single-hop retrieval whenever the classifier is unsure. Single-hop is the cheapest error to make, and it is recoverable — with partial material the model can still say it only found half the answer; with no material it can only invent one.
    4. Add a runtime fallback, which beats better up-front classification: after a direct-answer routing, if the draft reply contains figures, amounts or dates that need a source, fall back to retrieval and answer again; after a single-hop routing, if no candidate clears the admission gate, escalate to multi-hop or abstain. Correcting the earlier decision with the later observation is the single most useful pattern in routing systems.
    5. Mention observability: log every routing decision with the raw question, the label, and whether a fallback fired. Without that log you know neither how accurate the router is nor what to train the next version on.
    6. Expect "when should you skip routing entirely?" When chit-chat is a small share of traffic and multi-hop questions are rare, the classification call costs more than it saves. In our 30-document lab the real gain from routing was not saved retrievals but the ability to give recognised multi-hop questions a larger context budget.

    答题要点

    • 三条路的误判代价不对称:把该检索的判成直接回答最贵(模型没材料只能编),把闲聊判成单跳只是白花一次检索。
    • 兜底方向偏向多花钱:判不出来一律退回单跳检索,它是错得最轻且可恢复的一条路。
    • 加运行时兜底:直接回答里出现需要出处的数字就补一次检索;单跳检索一条都没过门槛就升级或拒答。
    • 每一次路由判定都落日志(原始问题、判定结果、是否触发兜底),既用于监控也用于攒下一版训练集。
    • 闲聊与多跳占比都很低时,路由省的钱付不起分类调用,直接全走单跳更划算。

    Key points

    • The three routes have asymmetric error costs: sending a retrieval-worthy question to a direct answer is the worst, while routing chit-chat to single-hop only wastes one retrieval.
    • Bias the fallback toward spending more: default to single-hop whenever the classifier is unsure, since that error is the mildest and is recoverable.
    • Add runtime fallbacks: re-retrieve if a direct answer contains figures that need a source; escalate or abstain if no single-hop candidate clears the gate.
    • Log every routing decision — raw question, label, whether a fallback fired — for both monitoring and the next training set.
    • When chit-chat and multi-hop are both rare, the classification call costs more than it saves; route everything to single-hop instead.

评论

登录后即可参与讨论

还没有评论,来说第一句。