<?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>多轮对话 on 老朱的赛博工位</title><link>https://blog.zjkun.top/tags/%E5%A4%9A%E8%BD%AE%E5%AF%B9%E8%AF%9D/</link><description>Recent content in 多轮对话 on 老朱的赛博工位</description><generator>Hugo</generator><language>zh-CN</language><lastBuildDate>Wed, 14 Oct 2026 09:00:00 +0800</lastBuildDate><atom:link href="https://blog.zjkun.top/tags/%E5%A4%9A%E8%BD%AE%E5%AF%B9%E8%AF%9D/index.xml" rel="self" type="application/rss+xml"/><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>