<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Query改写 on 老朱的赛博工位</title><link>https://blog.zjkun.top/tags/query%E6%94%B9%E5%86%99/</link><description>Recent content in Query改写 on 老朱的赛博工位</description><generator>Hugo</generator><language>zh-CN</language><lastBuildDate>Thu, 15 Oct 2026 13:00:00 +0800</lastBuildDate><atom:link href="https://blog.zjkun.top/tags/query%E6%94%B9%E5%86%99/index.xml" rel="self" type="application/rss+xml"/><item><title>RAG 改写没头绪？6 规则配 6 类型直接抄</title><link>https://blog.zjkun.top/posts/rag-rewrite-6rules-6types/</link><pubDate>Thu, 15 Oct 2026 13:00:00 +0800</pubDate><guid>https://blog.zjkun.top/posts/rag-rewrite-6rules-6types/</guid><description>Query 改写的 6 条规则对应 6 种问法：上下文依赖→指代消解+省略补全，对比→保留双实体，独立型放行不改。一条横向护栏「不编造实体」，异常一律回原 query。附我项目 system prompt 原文和婚假实测数据。</description><content:encoded><![CDATA[<p><a href="/posts/rag-query-rewrite-4-steps/">上一篇</a>拆了 Query 改写的链路：四步+两道短路。这篇补核心对照——<strong>六条规则对应六种问法，一类问题开一剂药。</strong></p>
<h2 id="为什么先分类再改写">为什么先分类再改写</h2>
<p>病症不同，药不同。不分类一刀切地「请改写用户问题」，模型容易改错方向：把该放行的独立问题改花，把对比句「归纳」掉一个实体。我的做法是把六规则和六类型全部写进 system prompt，模型每步输出 JSON <code>{type, rewritten}</code>——<strong>先判型，再按规则改</strong>。</p>
<p>真实 prompt 关键段（<code>query-rewrite.service.ts</code>，节选）：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-fallback" data-lang="fallback"><span style="display:flex;"><span>先判断 query 类型（从以下选一个）：
</span></span><span style="display:flex;"><span>- context_dependent: 上下文依赖型（需看历史才能理解指代）
</span></span><span style="display:flex;"><span>- comparative:       对比型（A 和 B 哪个好）
</span></span><span style="display:flex;"><span>- vague_reference:   模糊指代型（那个东西怎么用）
</span></span><span style="display:flex;"><span>- multi_intent:      多意图型（同时问多个事）
</span></span><span style="display:flex;"><span>- rhetorical:        反问型（难道不是...吗）
</span></span><span style="display:flex;"><span>- standalone:        独立型（不依赖历史，表达清晰）
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>只输出 JSON，不要解释，不要 markdown：
</span></span><span style="display:flex;"><span>{&#34;type&#34;: &#34;类型&#34;, &#34;rewritten&#34;: &#34;改写后的完整query&#34;}
</span></span></code></pre></div><p>模型 deepseek-chat，<code>temperature: 0.1</code>（改写要确定性），<code>response_format: json_object</code>。</p>
<h2 id="六组配对症状--药">六组配对：症状 → 药</h2>
<table>
	<thead>
			<tr>
					<th>类型</th>
					<th>症状</th>
					<th>用药</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>context_dependent</td>
					<td>「那第二步呢」，缺指代缺主语</td>
					<td>指代消解 + 省略补全：从 history 把实体拉回来</td>
			</tr>
			<tr>
					<td>comparative</td>
					<td>一句问两个东西的区别</td>
					<td>模糊词具体化，<strong>双实体必须同时保留</strong>——两个实体就是检索词，去掉任一个都伤召回</td>
			</tr>
			<tr>
					<td>vague_reference</td>
					<td>「好用吗」「怎么弄」</td>
					<td>模糊词具体化：口语翻译成可打分的明确意图</td>
			</tr>
			<tr>
					<td>multi_intent</td>
					<td>一句话问三件事</td>
					<td>多意图聚焦：拆子问题分开检索，整句塞给向量语义直接漂</td>
			</tr>
			<tr>
					<td>rhetorical</td>
					<td>「这都查不出来？」</td>
					<td>反问转意图：情绪化反问转陈述式再去查，原话进检索几乎 0 命中</td>
			</tr>
			<tr>
					<td>standalone</td>
					<td>句子成分齐全</td>
					<td><strong>放行不改</strong>。改写成分齐全的独立问题，反而容易改坏</td>
			</tr>
	</tbody>
</table>
<p>婚假实测（同一组数据贯穿两篇）：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-fallback" data-lang="fallback"><span style="display:flex;"><span>输入：「申请要怎么做?有没有步骤」+ 2 条婚假相关历史
</span></span><span style="display:flex;"><span>输出：{&#34;type&#34;: &#34;context_dependent&#34;,
</span></span><span style="display:flex;"><span>       &#34;rewritten&#34;: &#34;婚假申请的具体步骤和流程是什么&#34;}
</span></span><span style="display:flex;"><span>耗时 709ms / 565 token
</span></span><span style="display:flex;"><span>BM25 命中：0 条 → 11 条（索引 docs=38, vocab=1936）
</span></span></code></pre></div><h2 id="一条横六型的护栏不编造实体">一条横六型的护栏：不编造实体</h2>
<p>prompt 第 6 条规则单独拎出来说，因为它是事故源头：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-fallback" data-lang="fallback"><span style="display:flex;"><span>6. 不要添加历史中不存在的信息，不要编造实体
</span></span></code></pre></div><p><code>rewritten</code> 里只能出现 history 和原问里出现过的实体——用户没提婚假，就不能「猜」他要问婚假。<strong>编造实体 = 检索奔向错误文档，比不改更糟。</strong></p>
<p>对比型还有一个专门的坑：模型爱把「A 和 B 的区别」「归纳」成只讲 A。所以规则里写死双实体必须保留，这条救过一次召回。</p>
<h2 id="宁可保守失败全回-null">宁可保守：失败全回 null</h2>
<p>改写失败、超时、JSON 不合法、改写结果和原话一样——一律 <code>return null</code>，上层降级用原 query，链路永不因改写挂掉。判型成 <code>standalone</code> 或直接跳过（无历史 / 启发式不含指代信号）都走同一个出口。</p>
<p>保守方向的选择题：改坏了捞错文档 vs 不改多捞几条垃圾——后者至少模型还能自己消化，前者直接把错误资料塞进上下文。所以宁可少改。</p>
<h2 id="速查表贴墙版">速查表（贴墙版）</h2>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-fallback" data-lang="fallback"><span style="display:flex;"><span>上下文依赖 → 指代消解 + 省略补全
</span></span><span style="display:flex;"><span>对比       → 具体化 + 保留双实体
</span></span><span style="display:flex;"><span>模糊       → 模糊词具体化
</span></span><span style="display:flex;"><span>多意图     → 拆分逐条检
</span></span><span style="display:flex;"><span>反问       → 转陈述再查
</span></span><span style="display:flex;"><span>独立       → 放行不改
</span></span><span style="display:flex;"><span>公共护栏：不编造实体 + 异常回原话
</span></span></code></pre></div><p>改改写本质是「先承认用户的问法会带来什么麻烦，再决定怎么改」。无差别套一句「请优化这个问题」的 prompt，最容易跑偏。</p>
<blockquote>
<p>本篇最早于 2026 年 10 月 15 日发布于小红书，此为含完整代码与数据的博客版。</p>
</blockquote>
]]></content:encoded></item><item><title>RAG 怕问得含糊？Query 改写四步直接抄</title><link>https://blog.zjkun.top/posts/rag-query-rewrite-4-steps/</link><pubDate>Wed, 14 Oct 2026 09:00:00 +0800</pubDate><guid>https://blog.zjkun.top/posts/rag-query-rewrite-4-steps/</guid><description>多轮对话里「那第二步呢」「申请要怎么做」直接进检索就是捞垃圾。我的 Nuxt3 知识库 Query 改写全链路：改写只用于检索、生成仍用原话，两道成本短路省 token，失败全回 null 降级。实测 BM25 命中从 0 条到 11 条。</description><content:encoded><![CDATA[<p>用户不会替你斟酌提问。多轮对话里「那第二步呢」「申请要怎么做?」这种句子，索引里没有任何指代对应物，原话直接进检索只能捞垃圾。</p>
<p>我给自己的企业知识库（Nuxt3 my-nuxt-app）加了 Query 改写环节，实测同一个问题改写后 BM25 命中从 0 条到 11 条。这篇讲链路和设计，代码真实在跑。</p>
<h2 id="链路四步">链路四步</h2>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-fallback" data-lang="fallback"><span style="display:flex;"><span>前端 RagChat 完整 messages 历史 + mode 一起 POST
</span></span><span style="display:flex;"><span>  → ask.post.ts 透传 answerWithRAGStream(query, mode, messages)
</span></span><span style="display:flex;"><span>  → 步骤0：取「最后一条 user 之前」的内容为 history，调 rewriteQuery
</span></span><span style="display:flex;"><span>  → 改写结果只用于检索（hybrid/bm25/vector 三模式全受益）
</span></span><span style="display:flex;"><span>  → 生成侧仍用用户原话
</span></span></code></pre></div><p>第四条是关键分域：<strong>改写给检索看，原话给生成看。</strong> 最终喂 LLM 的 user message 必须还是用户的原始表达——不然模型会去回答「你改出来的问题」，曲解用户意图。检索要语义明确，生成要忠于原话，两边目标本来就不一样。</p>
<h2 id="真实代码改写器长什么样">真实代码：改写器长什么样</h2>
<p>核心服务 <code>server/service/rag/query-rewrite.service.ts</code>，system prompt 直接贴原文（这就是「先分类再改写」的实现）：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-ts" data-lang="ts"><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">system</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">`你是一个 RAG 检索 query 改写器。根据对话历史，把用户最新问题
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">改写成一个独立、完整、语义明确的检索 query。
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">改写规则：
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">1. 指代消解：把&#34;它&#34;、&#34;这个&#34;、&#34;那个&#34;等指代词替换为历史对话中明确的实体
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">2. 省略补全：把省略主语/宾语的短句补全为完整表述
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">3. 模糊词具体化：把&#34;东西&#34;、&#34;那个&#34;等模糊词替换为具体指代
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">4. 反问句转正向意图：如&#34;难道不是 A 更好吗&#34; → &#34;A 相比其他方案的优势&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">5. 多意图聚焦：保留最核心的检索意图，用逗号分隔次要意图
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">6. 不要添加历史中不存在的信息，不要编造实体
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">先判断 query 类型（从以下选一个）：
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">- context_dependent / comparative / vague_reference
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">- multi_intent / rhetorical / standalone
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">只输出 JSON，不要解释，不要 markdown：
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">{&#34;type&#34;: &#34;类型&#34;, &#34;rewritten&#34;: &#34;改写后的完整query&#34;}`</span>
</span></span></code></pre></div><p>调用参数（为什么这么配）：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-ts" data-lang="ts"><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">resp</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">client</span>.<span style="color:#a6e22e">chat</span>.<span style="color:#a6e22e">completions</span>.<span style="color:#a6e22e">create</span>({
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">model</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;deepseek-chat&#39;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">messages</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">temperature</span>: <span style="color:#66d9ef">0.1</span>, <span style="color:#75715e">// 改写要确定性，温度低一点
</span></span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">stream</span>: <span style="color:#66d9ef">false</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">response_format</span><span style="color:#f92672">:</span> { <span style="color:#66d9ef">type</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;json_object&#39;</span> }, <span style="color:#75715e">// 强制 JSON，防自由发挥
</span></span></span><span style="display:flex;"><span>})
</span></span></code></pre></div><p>历史截最近 10 条（<code>history.slice(-10)</code>）——再长 prompt 膨胀，收益不大。</p>
<h2 id="两道成本短路别白花-token">两道成本短路，别白花 token</h2>
<p>改写要多调一次 LLM，不能什么query都傻改。两道闸：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-ts" data-lang="ts"><span style="display:flex;"><span><span style="color:#75715e">// 闸一：首条消息（无历史）直接跳过
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">userMsgCount</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">history</span>.<span style="color:#a6e22e">filter</span>((<span style="color:#a6e22e">m</span>) <span style="color:#f92672">=&gt;</span> <span style="color:#a6e22e">m</span>.<span style="color:#a6e22e">role</span> <span style="color:#f92672">===</span> <span style="color:#e6db74">&#39;user&#39;</span>).<span style="color:#a6e22e">length</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">userMsgCount</span> <span style="color:#f92672">===</span> <span style="color:#ae81ff">0</span>) <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">null</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// 闸二：启发式 shouldRewrite——不含指代信号且长度合理就不改
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">function</span> <span style="color:#a6e22e">shouldRewrite</span>(<span style="color:#a6e22e">query</span>: <span style="color:#66d9ef">string</span>)<span style="color:#f92672">:</span> <span style="color:#66d9ef">boolean</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">query</span>.<span style="color:#a6e22e">length</span> <span style="color:#f92672">&lt;=</span> <span style="color:#ae81ff">5</span>) <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">true</span>          <span style="color:#75715e">// ≤5字几乎一定是省略/指代
</span></span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">REFERENCE_PATTERNS</span>.<span style="color:#a6e22e">test</span>(<span style="color:#a6e22e">query</span>)) <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">true</span> <span style="color:#75715e">// 命中「它/这个/怎么做/什么功能…」
</span></span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">false</span>                                 <span style="color:#75715e">// 表达清晰，省一次调用
</span></span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h2 id="失败路径全回-null">失败路径全回 null</h2>
<p>空返回、JSON 解析失败、缺 <code>rewritten</code> 字段、改写结果和原话一样、调用异常——五种失败全部 <code>return null</code>，上层 fallback 用原 query。<strong>链路永不因为改写挂掉</strong>：改写是增益环节，不是必经环节。</p>
<p>每步都有 🔧 Query改写调试日志（原 query/历史/两个 prompt/耗时/token），排查时控制台一目了然。</p>
<h2 id="实测">实测</h2>
<p>「申请要怎么做?有没有步骤」+ 2 条婚假相关历史：</p>
<ul>
<li>改写输出：<code>{&quot;type&quot;: &quot;context_dependent&quot;, &quot;rewritten&quot;: &quot;婚假申请的具体步骤和流程是什么&quot;}</code></li>
<li>耗时 <strong>709ms</strong>，<strong>565 token</strong></li>
<li>该 query 进 BM25：命中 <strong>11 条</strong>（索引状态 docs=38, vocab=1936）；原话直查基本捞不到东西</li>
</ul>
<p>一次 565 token 的改写调用（deepseek-chat 计价下约几厘钱），换检索质量从不可用到可用，这笔账怎么算都划算——前提是有上面那两道短路，不该调的时候别调。</p>
<h2 id="速查表">速查表</h2>
<table>
	<thead>
			<tr>
					<th>项</th>
					<th>值</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>位置</td>
					<td>检索第一站，排在混合检索之前</td>
			</tr>
			<tr>
					<td>输出</td>
					<td>JSON <code>{type, rewritten}</code>，temp 0.1，json_object</td>
			</tr>
			<tr>
					<td>类型</td>
					<td>context_dependent/comparative/vague_reference/multi_intent/rhetorical/standalone</td>
			</tr>
			<tr>
					<td>短路</td>
					<td>无历史跳过；≤5 字必改；命中指代词才改</td>
			</tr>
			<tr>
					<td>兜底</td>
					<td>任何异常回原 query</td>
			</tr>
			<tr>
					<td>分域</td>
					<td>改写给检索，原话给生成</td>
			</tr>
			<tr>
					<td>自测</td>
					<td>对比改写前后命中条数（我从 0 到 11 条）</td>
			</tr>
	</tbody>
</table>
<p>已知边界：目前只处理纯文本指代；<code>retrieveOnly</code> 调试接口不经过改写（只看原始检索）。</p>
<p>下一篇把 6 条规则和 6 种类型配成对照表——一类问题开一剂药。</p>
<blockquote>
<p>本篇最早于 2026 年 10 月 14 日发布于小红书，此为含完整代码与数据的博客版。</p>
</blockquote>
]]></content:encoded></item></channel></rss>