<?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/%E6%A3%80%E7%B4%A2/</link><description>Recent content in 检索 on 老朱的赛博工位</description><generator>Hugo</generator><language>zh-CN</language><lastBuildDate>Tue, 20 Oct 2026 14:05:00 +0800</lastBuildDate><atom:link href="https://blog.zjkun.top/tags/%E6%A3%80%E7%B4%A2/index.xml" rel="self" type="application/rss+xml"/><item><title>RAG 父页面召回？这 3 种场景不值得做</title><link>https://blog.zjkun.top/posts/rag-parent-document-retrieval/</link><pubDate>Tue, 20 Oct 2026 14:05:00 +0800</pubDate><guid>https://blog.zjkun.top/posts/rag-parent-document-retrieval/</guid><description>父页面召回（Parent Document Retrieval）是最容易被盲抄的 RAG 功能。我研究了 2 天，结论：我不做。3 种不值得做的场景、2 种值得做的场景、3 秒自查法，以及我的真实数据（172 个小 chunk、混合检索+精排 top5 命中稳定）。功能不是看别人做了就抄，是看自己的数据形状。</description><content:encoded><![CDATA[<p>做 RAG 最容易被盲抄的一个功能是父页面召回（Parent Document Retrieval）。我研究了 2 天，结论：<strong>我不做，而且很多人一开始就做错了。</strong></p>
<p>30 秒拿走一份判断清单，下面是完整论证版。</p>
<h2 id="它是什么治什么病">它是什么，治什么病</h2>
<p>一句话：<strong>索引小块管查得准，返回父文档管上下文全。</strong></p>
<p>检索命中一个小 chunk 后，不直接把它喂给模型，而是顺着元数据找回它所属的大单元（父段落/整页/整文档），用大单元做生成上下文。</p>
<p>它治的病只有一个：<strong>分块把意义切断了</strong>——结论依赖前文铺垫，小块单独拿出来语义不成立。</p>
<p>副作用同样明确：prompt 塞进大量无关内容、模型注意力被稀释（回答变吵）、token 成本变高、时延变长。</p>
<p><strong>它是治「切断意义」的药，不是补脑药。</strong> 没这个病就别吃。</p>
<h2 id="3-种场景不值得做">3 种场景不值得做</h2>
<ol>
<li><strong>段落自包含</strong>：问答对、条款、产品参数这类文体，按分隔符切开后每块意义完整。小块已经够用，展开父页只是往 prompt 里灌水。</li>
<li><strong>语料不大且检索稳</strong>：混合检索+重排做完，top3/top5 一直命中得很稳，根本不需要回父页凑上下文——你的问题在「查得准」，不在「上下文全」。</li>
<li><strong>抽取型答案</strong>：只要从文档里抽一个字段/一条规定。返回整页反而给模型发挥空间，它开始在你不该有自由度的地方自行总结。</li>
</ol>
<h2 id="2-种场景值得做">2 种场景值得做</h2>
<ol>
<li><strong>长文档、结论依赖上下文才成立</strong>：法律意见书、实验报告，「综上所述」的「上述」就是病。</li>
<li><strong>怎么切都装不下一个答案</strong>：切小块检索不准、切大块上下文还是不够——说明你缺的不是查得准，是上下文完整。这时父页面召回才是正解。</li>
</ol>
<h2 id="我的数据形状为什么落在不做区">我的数据形状：为什么落在「不做」区</h2>
<p>语料是部门制度类文档，txt/word/pdf/xlsx 上传解析成纯文本后走我的递归切分器（<code>chunker.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-ts" data-lang="ts"><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">DEFAULT_TARGET_CHARS</span> <span style="color:#f92672">=</span> <span style="color:#ae81ff">800</span>   <span style="color:#75715e">// 目标字符数 ≈ 上限值
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">// 短文档：直接一片（核心保护，避免过切）
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">cleaned</span>.<span style="color:#a6e22e">length</span> <span style="color:#f92672">&lt;=</span> <span style="color:#a6e22e">targetChars</span>) {
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">return</span> [{ <span style="color:#a6e22e">content</span>: <span style="color:#66d9ef">cleaned</span>, <span style="color:#a6e22e">chunk_order</span>: <span style="color:#66d9ef">1</span>, <span style="color:#a6e22e">page_no</span>: <span style="color:#66d9ef">pageNo</span> }]
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span><span style="color:#75715e">// 超长才逐层下降粒度：标题 → 空行段落 → 句号断句 → 固定兜底
</span></span></span></code></pre></div><p>制度文档的特点：条款天然自包含，一条「婚假申请流程」就是一个完整语义单元，800 字符目标 + 贪心合并（每片尽量落在 [400, 800] 区间）切出来的就是<strong>本来就自包含的小块</strong>。全库 <strong>172 个 chunk</strong>，混合检索（BM25+向量 RRF 融合）+ 精排取前 5 之后，命中一直挺稳——正好命中上面「不值得做」的前两种。</p>
<p>还有一个隐性成本：<strong>父页面召回需要 chunk→parent 的元数据链路</strong>（parent_id/页文档正文回查）。我的 chunk 有 <code>page_no</code> 和全文档连续 <code>chunk_order</code>，页级回查技术上做得动，但文档级展开等于把「检索」降级成「整本塞」——为我这 172 个块的语料引入这层结构和运行期回查，收益负期望。</p>
<p>所以父页面召回对我是<strong>锦上添花，不是药</strong>。功能不是看别人做了就抄，是看自己的数据形状。</p>
<h2 id="3-秒自查法建议抄走">3 秒自查法（建议抄走）</h2>
<p>随机抽库里 5 个段落，只问一句：</p>
<blockquote>
<p><strong>「只留这一段，答案还成立吗？」</strong></p>
</blockquote>
<ul>
<li>多数成立 → 不用做父页面，钱花在检索精排上更值</li>
<li>多数不成立 → 再动手，而且先试便宜的「取相邻片段」（命中 chunk 前后各带 1 条），还解决不了才上父文档级展开</li>
</ul>
<h2 id="速查表">速查表</h2>
<table>
	<thead>
			<tr>
					<th>判断</th>
					<th>动作</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>条款/FAQ/参数，块块自包含</td>
					<td>不做，省成本</td>
			</tr>
			<tr>
					<td>top5 命中稳定</td>
					<td>不做，问题不在这</td>
			</tr>
			<tr>
					<td>只要抽字段</td>
					<td>不做，整页反而引入发挥空间</td>
			</tr>
			<tr>
					<td>长文结论依赖铺垫</td>
					<td>值得做</td>
			</tr>
			<tr>
					<td>怎么切都不装不下答案</td>
					<td>值得做（先试相邻片段）</td>
			</tr>
	</tbody>
</table>
<p>我的清单里后续真要排的是 eval 验证集和生成侧引用校验——先有尺子再谈优化，这是下一个话题。</p>
<blockquote>
<p>本篇最早于 2026 年 10 月 20 日发布于小红书，此为博客完整版。</p>
</blockquote>
]]></content:encoded></item><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 效果差怎么调试？面试官要听的排查思路</title><link>https://blog.zjkun.top/posts/rag-debug-playbook/</link><pubDate>Sat, 10 Oct 2026 08:30:00 +0800</pubDate><guid>https://blog.zjkun.top/posts/rag-debug-playbook/</guid><description>RAG 答得差，别急着换模型。我按「入库→检索→融合→精排→生成」五层给每层配了真实调试工具：三模式开关（召回策略唯一变量）、单路调试接口、retrieveOnly 只看检索、名次/得分日志。附七步建库链路对照的排查清单。</description><content:encoded><![CDATA[<p>「RAG 效果差」是个笼统病，直接开药都是瞎蒙。面试官爱听的不是你换过几个模型，而是<strong>会不会分层定位</strong>：答案质量差，可能死在入库、检索、融合、精排、生成任何一层。</p>
<p>我把自己 Nuxt3 知识库（my-nuxt-app）的调试工具链整个做成了一套排查方法，这篇按层过一遍。</p>
<h2 id="总原则控制变量--只看检索不生成">总原则：控制变量 + 只看检索不生成</h2>
<p>两条铁律贯穿所有排查：</p>
<ol>
<li><strong>召回策略是唯一变量</strong>。我把检索做成 <code>hybrid / bm25 / vector</code> 三档模式开关，同一问题各跑一遍，后面的精排、生成完全一致——谁拖后腿一眼看见。</li>
<li><strong>检索和生成分开验</strong>。有 <code>retrieveOnly</code> 开关：只看召回了哪些片段、名次如何，不跑生成。答案不对，先确认是「没捞到」还是「捞到了模型没用好」，这两件事的修法完全不同。</li>
</ol>
<h2 id="分层排查">分层排查</h2>
<h3 id="层-1入库文档分块向量">层 1：入库（文档→分块→向量）</h3>
<p>七步建库链路：知识整理 → 清理格式化 → <strong>内容切分</strong> → 向量化 → 挂元数据 → 载入向量库 → 集成。效果差的病根很多在这：切太碎丢上下文，切太粗检索不准。</p>
<p>我项目的真实分块参数：递归切分器 <code>chunker.ts</code>，目标 800 字符（标题→空行段落→句号→硬切四层降级，短文档直接一片不硬切）；BM25 侧启动全量预热——最新实测：<strong>172 个 chunk / 3015 个词项，建索引 2688ms</strong>（TiDB 拉取+分词+倒排全算上）。</p>
<p>排查动作：随机抽几个 chunk 看语义是否自包含；问一个已知有答案的问题，确认目标 chunk 的文本入库时没被解析器吃掉（表格/PDF 是重灾区）。</p>
<h3 id="层-2检索两路单测">层 2：检索（两路单测）</h3>
<p>关键词路和向量路各留了单路调试接口：<code>/api/rag/keyword-search</code>、<code>/api/rag/vector-search</code>——<strong>单路粗召、不精排</strong>，专治「不知道哪路烂」。</p>
<p>典型病症对照：</p>
<ul>
<li>专有名词/条款号/型号搜不到 → 关键词路问题：查分词（我的 BM25 用中文分词器切 query 和文档，分词烂则倒排全烂）、查索引预热是否成功（控制台看 docs/vocab 数）</li>
<li>换个说法就搜不到 → 只有 BM25 在干活，向量路没接上或阈值过严</li>
<li>召回一堆语义相近但没用的 → 向量路正常但缺精排，去层 4</li>
</ul>
<h3 id="层-3融合rrf-名次日志">层 3：融合（RRF 名次日志）</h3>
<p>两路各召 top20，RRF 融合（<code>score=Σ1/(60+rank)</code>）。<code>FusedResult</code> 里我专门存了每个 id 的 <code>ranks: [向量路名次, BM25路名次]</code>（未命中记 null），服务端控制台按模式打印——融合后排名怪，直接看是「一路强推」还是「两路共认」。</p>
<p>坑：别把余弦分和 BM25 分直接加权，量纲不同（0~1 vs 无上限），必须转名次。</p>
<h3 id="层-4精排">层 4：精排</h3>
<p>粗召本质是盲选——入库时分块单独算的向量，从没和你的问题同框比过。精排用 cross-encoder（bge-reranker-v2-m3）把「问题+候选」成对打分，融合 top20 → 精排 top5。</p>
<p>排查信号：召回明明有正确答案却排在 15 名开外进不了上下文 → 看 rerank 分数；rerank 服务挂了会自动降级按 RRF 分排，日志里有标记，别把降级的锅算到检索头上。</p>
<h3 id="层-5生成">层 5：生成</h3>
<p>检索没问题、答案还是烂，才轮到生成层：prompt 里资料怎么拼、有没有约束「只用资料作答、引用标来源页码」、模型是不是被无关片段带偏（top5 塞太多噪声也是病，回头减 FINAL_TOP_K）。</p>
<h2 id="常见三病因课件口径--我的实战对应">常见三病因（课件口径 + 我的实战对应）</h2>
<ol>
<li>文档处理和切割颗粒度问题 → 层 1</li>
<li>召回不准，想命中的片段排序太靠后 → 层 2/3/4</li>
<li>检索到了但整合生成没用对 → 层 5</li>
</ol>
<p>对应五个提准手段（我项目落了的标 ✓）：切分技巧 ✓（递归四层+短文档保护）、取相邻片段（父页面类方案，评估后我没做）、多召回+rerank ✓（20→5）、混合检索 ✓（BM25+向量）、Query 重写 ✓（改写只进检索）。</p>
<h2 id="面试话术一句话">面试话术一句话</h2>
<p>「我调试 RAG 的路径是分层二分：先用 retrieveOnly 把检索和生成切开，再用三模式开关把两路召回切开，每层都有接口和名次日志。定位到层之后，改参数—回归—再定位，不靠玄学。」</p>
<p>这就是面试官要听的：<strong>不是你知道多少技巧，而是你有能把问题范围每次减半的方法。</strong></p>
<blockquote>
<p>本篇最早于 2026 年 10 月 10 日发布于小红书，此为博客完整版。</p>
</blockquote>
]]></content:encoded></item></channel></rss>