用户不会替你斟酌提问。多轮对话里「那第二步呢」「申请要怎么做?」这种句子,索引里没有任何指代对应物,原话直接进检索只能捞垃圾。

我给自己的企业知识库(Nuxt3 my-nuxt-app)加了 Query 改写环节,实测同一个问题改写后 BM25 命中从 0 条到 11 条。这篇讲链路和设计,代码真实在跑。

链路四步

前端 RagChat 完整 messages 历史 + mode 一起 POST
  → ask.post.ts 透传 answerWithRAGStream(query, mode, messages)
  → 步骤0:取「最后一条 user 之前」的内容为 history,调 rewriteQuery
  → 改写结果只用于检索(hybrid/bm25/vector 三模式全受益)
  → 生成侧仍用用户原话

第四条是关键分域:改写给检索看,原话给生成看。 最终喂 LLM 的 user message 必须还是用户的原始表达——不然模型会去回答「你改出来的问题」,曲解用户意图。检索要语义明确,生成要忠于原话,两边目标本来就不一样。

真实代码:改写器长什么样

核心服务 server/service/rag/query-rewrite.service.ts,system prompt 直接贴原文(这就是「先分类再改写」的实现):

const system = `你是一个 RAG 检索 query 改写器。根据对话历史,把用户最新问题
改写成一个独立、完整、语义明确的检索 query。

改写规则:
1. 指代消解:把"它"、"这个"、"那个"等指代词替换为历史对话中明确的实体
2. 省略补全:把省略主语/宾语的短句补全为完整表述
3. 模糊词具体化:把"东西"、"那个"等模糊词替换为具体指代
4. 反问句转正向意图:如"难道不是 A 更好吗" → "A 相比其他方案的优势"
5. 多意图聚焦:保留最核心的检索意图,用逗号分隔次要意图
6. 不要添加历史中不存在的信息,不要编造实体

先判断 query 类型(从以下选一个):
- context_dependent / comparative / vague_reference
- multi_intent / rhetorical / standalone

只输出 JSON,不要解释,不要 markdown:
{"type": "类型", "rewritten": "改写后的完整query"}`

调用参数(为什么这么配):

const resp = await client.chat.completions.create({
  model: 'deepseek-chat',
  messages,
  temperature: 0.1, // 改写要确定性,温度低一点
  stream: false,
  response_format: { type: 'json_object' }, // 强制 JSON,防自由发挥
})

历史截最近 10 条(history.slice(-10))——再长 prompt 膨胀,收益不大。

两道成本短路,别白花 token

改写要多调一次 LLM,不能什么query都傻改。两道闸:

// 闸一:首条消息(无历史)直接跳过
const userMsgCount = history.filter((m) => m.role === 'user').length
if (userMsgCount === 0) return null

// 闸二:启发式 shouldRewrite——不含指代信号且长度合理就不改
function shouldRewrite(query: string): boolean {
  if (query.length <= 5) return true          // ≤5字几乎一定是省略/指代
  if (REFERENCE_PATTERNS.test(query)) return true // 命中「它/这个/怎么做/什么功能…」
  return false                                 // 表达清晰,省一次调用
}

失败路径全回 null

空返回、JSON 解析失败、缺 rewritten 字段、改写结果和原话一样、调用异常——五种失败全部 return null,上层 fallback 用原 query。链路永不因为改写挂掉:改写是增益环节,不是必经环节。

每步都有 🔧 Query改写调试日志(原 query/历史/两个 prompt/耗时/token),排查时控制台一目了然。

实测

「申请要怎么做?有没有步骤」+ 2 条婚假相关历史:

  • 改写输出:{"type": "context_dependent", "rewritten": "婚假申请的具体步骤和流程是什么"}
  • 耗时 709ms,565 token
  • 该 query 进 BM25:命中 11 条(索引状态 docs=38, vocab=1936);原话直查基本捞不到东西

一次 565 token 的改写调用(deepseek-chat 计价下约几厘钱),换检索质量从不可用到可用,这笔账怎么算都划算——前提是有上面那两道短路,不该调的时候别调。

速查表

项值
位置检索第一站,排在混合检索之前
输出JSON {type, rewritten},temp 0.1,json_object
类型context_dependent/comparative/vague_reference/multi_intent/rhetorical/standalone
短路无历史跳过;≤5 字必改;命中指代词才改
兜底任何异常回原 query
分域改写给检索,原话给生成
自测对比改写前后命中条数(我从 0 到 11 条)

已知边界:目前只处理纯文本指代;retrieveOnly 调试接口不经过改写(只看原始检索)。

下一篇把 6 条规则和 6 种类型配成对照表——一类问题开一剂药。

本篇最早于 2026 年 10 月 14 日发布于小红书,此为含完整代码与数据的博客版。