<?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>RAG on 老朱的赛博工位</title><link>https://blog.zjkun.top/tags/rag/</link><description>Recent content in RAG on 老朱的赛博工位</description><generator>Hugo</generator><language>zh-CN</language><lastBuildDate>Wed, 21 Oct 2026 16:52:00 +0800</lastBuildDate><atom:link href="https://blog.zjkun.top/tags/rag/index.xml" rel="self" type="application/rss+xml"/><item><title>前端老鸟手搓企业知识库：RAG 全链路落地实录</title><link>https://blog.zjkun.top/posts/rag-fulllink-handcraft/</link><pubDate>Wed, 21 Oct 2026 16:52:00 +0800</pubDate><guid>https://blog.zjkun.top/posts/rag-fulllink-handcraft/</guid><description>RAG 系列总开篇：一个做了12年前端的人，怎么按「存」和「找」两条线亲手跑通企业知识库问答——bge-m3+Chroma 向量化、手写 BM25+RRF 混合粗召、bge-reranker 精排、DeepSeek 流式生成。不讲概念，只记链路、参数和踩过的坑，每个环节附对应的深拆文章。</description><content:encoded><![CDATA[<p>先交代身份：我叫老朱，33 岁，写了 12 年前端为主的全栈（Vue/React/TS 为主），最近在做求职方向转换，主攻 AI 应用。手上这个企业知识库问答系统（RAG），前端是我写的，Node 服务端（Nuxt 的 Nitro server）也是我写的，整条链路从头手搓到尾。</p>
<p>外面讲 RAG 的文章一开口就是「检索增强生成」的三段论定义，我不重复这个。<strong>这篇只回答一个问题：这套系统是怎么跑通的。</strong> 全文按两条线走——「存」和「找」。每个环节末尾有一句话小结，链到我已经写好的深拆文章，想抠细节的点开看，不想抠的看完这条线也够你搭出同款。</p>
<h2 id="定调rag-就两件事存和找">定调：RAG 就两件事，存和找</h2>
<p>一句话把 RAG 说尽：把公司文档喂给大模型之前，先检索出最相关的几段，让模型只根据资料回答。所以系统的上限在哪里？<strong>我的结论是：RAG 的上限在检索，不在大模型。</strong> 模型只负责把资料组织成人话，检索决定它能看到什么。资料给错了，再贵的模型也只是把错误说得更流畅。</p>
<p>顺带回答一个选型问题：为什么是 RAG 而不是微调？这是整个系列的前置判断，我在 <a href="/posts/rag-vs-finetune-3-modes/">「RAG 还是微调？大模型应用 3 种模式别选错」</a> 里拆过——企业知识库这种「资料持续更新、答案必须可溯源」的场景，RAG 是默认答案，不在这里展开。</p>
<p>下面进入正题。</p>
<h2 id="存这条线让知识库有进有出">存这条线：让知识库有进有出</h2>
<h3 id="环节一文档入库本质是一套状态机">环节一：文档入库，本质是一套状态机</h3>
<p>第一版我图省事，上传即入库，很快就后悔了——知识库变成垃圾堆，什么过期报价单、写了一半的文档都在里面被检索出来。现在的设计：</p>
<ul>
<li><strong>格式</strong>：支持 txt / docx / pdf / xlsx，上传后统一解析成纯文本；</li>
<li><strong>元数据</strong>：文档表带完整生命周期字段——密级、访问范围、归属部门、版本号，改了哪版一清二楚；</li>
<li><strong>状态机</strong>：草稿 → 入库 → 已发布 → 下架。只有 published 状态的文档块参与检索；发布后锁死，想改必须先下架，重新入库会覆盖旧向量。</li>
</ul>
<p>这个状态机不值钱吗？太值钱。密级和访问范围让「哪个部门能问什么」有了落点，这是企业知识库和个人玩具的分水岭。</p>
<p>一句话小结：<strong>入库不是把文件塞进数据库，是给它上户口。</strong> 至于文档怎么切块、切完检索到小块要不要回查整篇，我另外写过 <a href="/posts/rag-parent-document-retrieval/">「RAG 父页面召回？这 3 种场景不值得做」</a>，先把边界讲清楚，别盲目上父文档召回。</p>
<h3 id="环节二切块--向量化三个坑替你们踩了">环节二：切块 + 向量化，三个坑替你们踩了</h3>
<p>长文档切成小 chunk，用 <strong>bge-m3</strong> 转成向量，存进 <strong>Chroma</strong> 向量库（我库里从头到尾就一个 collection，没必要整花活）。提问时把问题也转向量，找距离最近的片段——这就是向量检索的全部原理，剩下的全是工程细节。</p>
<p>踩坑实录，三条都真金白银花时间：</p>
<ol>
<li>Chroma 查询的 <code>include</code> 参数要传<strong>字符串数组</strong>，随手传个字符串就报错；</li>
<li>metadata 字段名<strong>统一用下划线</strong>，混用驼峰和下划线会让你查库时一脸懵；</li>
<li><strong>query 和入库必须用同一个 embedding 模型</strong>——换模型不是升级，是两个宇宙的对不上，换模型就得全量重灌，新旧向量绝不混库（同维度也不行）。</li>
</ol>
<p>一句话小结：<strong>向量检索翻车，八成是入库和查询不同路。</strong></p>
<h2 id="找这条线四步把答案捞上来">找这条线：四步把答案捞上来</h2>
<h3 id="环节三双路粗召向量--手写-bm25">环节三：双路粗召——向量 + 手写 BM25</h3>
<p>纯向量检索有个死穴：<strong>专有名词、缩写、产品型号会翻车</strong>，向量对这种精确匹配就是不准。所以关键词这一路必须有。</p>
<p>我没上 ES 这种现成轮子，BM25 引擎是自己手写的（这大概是最能体现「手搓」的一个环节）：</p>
<ul>
<li>jieba 中文分词 + 倒排索引，核心参数 k1=1.5、b=0.75，没有引第三方 BM25 库；</li>
<li>Nitro 服务启动时从数据库里 published 状态的 chunks 预热建索引，<strong>零冷启动</strong>，预热失败也不阻塞服务启动；</li>
<li>召回后回查数据库补全文，向量库只管「找到哪块」，不管「给出全文」。</li>
</ul>
<p>检索时两路并行：向量和 BM25 <strong>各粗召 top-20</strong>，用 <code>Promise.allSettled</code> 包着——单路挂了，另一路自动兜底，服务不停摆。</p>
<p>一句话小结：<strong>混合检索 = 广撒网两路并行 + 一路挂掉另一路兜底。</strong> 四步怎么搭、每步参数怎么定，看 <a href="/posts/rag-hybrid-search-4-steps/">「RAG 检索总跑偏？混合检索四步直接抄」</a>。</p>
<h3 id="环节四rrf-融合扔掉分数只认名次">环节四：RRF 融合——扔掉分数，只认名次</h3>
<p>两路都召回了，分数没法直接比：向量给的是余弦相似度，BM25 给的是另一套量纲，硬加权平均是玄学。</p>
<p>我的方案是 <strong>RRF（Reciprocal Rank Fusion），k=60</strong>：分数全部扔掉，只用名次。两路都认的片段自然浮到前面，融合后取 top-20。每一路的名次都记进日志，后面调试全靠它。</p>
<p>一句话小结：<strong>量纲不同别硬融合，名次是唯一通用货币。</strong> k 值为什么取 60、融合细节，同样在 <a href="/posts/rag-hybrid-search-4-steps/">混合检索四步</a> 里拆过。</p>
<h3 id="环节五query-改写用户不会提问系统替他补全">环节五：Query 改写——用户不会提问，系统替他补全</h3>
<p>真实用户的提问又短又含糊：「那个多少钱」「上次说的发货呢」。直接拿这种话去检索，神仙难救。</p>
<p>所以在粗召前面加了一步改写：规则 6 条、类型 6 种，输出 JSON，temperature 压到 0.1；两道成本短路（首条消息无历史直接跳过、启发式指代词判断），失败全部降级用原话。<strong>最关键的一条设计：改写只用于检索，生成时模型看到的还是用户原话</strong>——改写错了不污染答案。</p>
<p>一句话小结：<strong>改写是给检索器用的，不是给用户看的。</strong> 四步流程看 <a href="/posts/rag-query-rewrite-4-steps/">「RAG 怕问得含糊？Query 改写四步直接抄」</a>，规则和类型清单看 <a href="/posts/rag-rewrite-6rules-6types/">「RAG 改写没头绪？6 规则配 6 类型直接抄」</a>。</p>
<h3 id="环节六精排--生成最后五段决定答案质量">环节六：精排 + 生成——最后五段决定答案质量</h3>
<p>粗召是广撒网，top-20 里良莠不齐。最后一步用 <strong>bge-reranker-v2-m3</strong> 做 cross-encoder 精排：把问题和每条候选成对过一遍模型，逐条打分重排，<strong>只取 top-5</strong> 注入上下文，交给 DeepSeek 流式输出（SSE，打字机效果）。rerank 服务不可用时自动降级：按 RRF 分或召回原始序截断——精度掉一点，服务不断。</p>
<p>生成侧的 system prompt 上了硬约束：<strong>只根据资料回答，找不到就说找不到，不许编。</strong> 配合前端把命中片段的名次、来源全部展示出来，答错了能追责到具体文档。</p>
<p>一句话小结：<strong>精排是唯一「问题+资料同场打分」的环节，值得为它留降级通道。</strong></p>
<h2 id="全链路可观测效果差的时候先知道差在哪">全链路可观测：效果差的时候，先知道差在哪</h2>
<p>最后一环不是某个算法，是工程习惯：</p>
<ul>
<li>混合 / 纯关键词 / 纯向量<strong>三种模式页面一键切换</strong>，同一个问答抽屉按 mode 切换，切模式时强制重建组件清掉历史答案，防止新旧混着看；</li>
<li>单独的关键词检索、向量检索调试接口，支持单路对比；还有个 <code>retrieveOnly</code> 开关——只看检索结果，不触发生成；</li>
<li>每一路的名次、得分全部打日志。</li>
</ul>
<p>这套 observability 的回报巨大：用户反馈「答得烂」，我十分钟就能定位是检索没捞到、还是捞到了模型没用好。排查思路我写成了一篇 <a href="/posts/rag-debug-playbook/">「RAG 效果差怎么调试？面试官要听的排查思路」</a>，面试和干活都用得上。</p>
<h2 id="说点效果也说实话">说点效果，也说实话</h2>
<p>链路参数都在明处：双路各 top-20、RRF 融合取 top-20、精排取 top-5、k=60、k1=1.5、b=0.75。但你要问我「准确率提升多少个百分点」——<strong>说实话没有脸说，因为我没建标准评估集，上线前后的评测数字我没记下来</strong>，这是这期的欠账，补上评测再写。我唯一敢负责的话是：加精排和改写之前，专有名词类问题经常答不上来；之后这类问题基本能稳定命中——这是体感，不是数据。</p>
<p>手搓一圈最大的收获不在算法，在心态：<strong>RAG 系统 80% 的工程问题，最后都归结为「数据进库时没管好，查询进来时没接住」。</strong> 状态机、日志、降级通道，这些最土的工程实践，恰恰是 AI 应用最缺的东西。</p>
<p>这是「老朱的 RAG 系列」总开篇，六个环节各有一篇深拆，链接都在文中。下一篇不在 RAG 系列里，但和这套系统的账单直接相关：<strong>四个 AI API 平台的价格/速度/稳定性实测对比</strong>——手搓很爽，token 很贵，怎么选不花冤枉钱，下篇见。</p>
]]></content:encoded></item><item><title>AI 调 API 别乱花钱：4 个平台实测对比（价格/速度/稳定性）</title><link>https://blog.zjkun.top/posts/ai-api-platform-compare/</link><pubDate>Wed, 21 Oct 2026 16:44:00 +0800</pubDate><guid>https://blog.zjkun.top/posts/ai-api-platform-compare/</guid><description>手搓 RAG 知识库时真金白银用过（或仔细调研过）的 4 个 AI API 平台账本：硅基流动、阿里云百炼、DeepSeek、agicto。哪些能力免费、多模态每百万 token 多少钱、国内连海外模型会踩什么坑、平台挂了系统怎么自保——价格与稳定性按实测记录写，速度没测就明说没测。</description><content:encoded><![CDATA[<p>先说背景和底气：我是老朱，12 年前端转 AI 应用开发，手搓了一套企业知识库问答（RAG 那条链路可以看我上一篇 <a href="/posts/rag-fulllink-handcraft/">「前端老鸟手搓企业知识库：RAG 全链路落地实录」</a>）。这套系统要同时调用三类能力：<strong>embedding 向量化、reranker 精排、大模型生成</strong>——每一类都是按 token 计钱的 API。选型那段时间我把国内外几个平台挨个过了一遍，这份账本今天就摊开讲。</p>
<p>三个维度提前交底：<strong>价格</strong>按我调研和实际用过的口径写；<strong>稳定性</strong>写真实接入体验；<strong>速度</strong>要先坦白——我没做过正式压测 benchmark，四个平台的速度栏我全部写「没测到」，宁可缺一栏也不编数。标题叫实测，实的是价格和可用性测，速度那部分留给以后补。</p>
<h2 id="四个平台逐个过堂">四个平台逐个过堂</h2>
<h3 id="1-硅基流动白嫖起步的最佳一站">1. 硅基流动：白嫖起步的最佳一站</h3>
<p><strong>我用了什么</strong>：文本链路的全套起点。<code>BAAI/bge-m3</code> embedding——免费，1024 维，OpenAI 兼容接口；精排 <code>bge-reranker-v2-m3</code> 也是免费用的（免费档留了优化空间，但起步阶段完全够）。我那套 RAG 上线时向量库和精排一分钱没花，就是它在撑。</p>
<p><strong>价格记录</strong>：文本 embedding 免费（注册时有千万级 token 免费额度）；后来做多模态调研时它的 qwen3-vl 系模型报价是 embedding 约 ¥0.7/百万 token、rerank 约 ¥1.8/百万 token。</p>
<p><strong>稳定性</strong>：Key 长期可用，接口兼容 OpenAI 格式所以换模型几乎零改动。多模态选型定案后，它的角色从主力降为<strong>容灾备胎</strong>——不是它不好，是主平台统一之后的架构选择。</p>
<p><strong>一句话结论</strong>：个人项目和学习期，文本向量化和精排从它的免费档起步，没理由先花钱。</p>
<h3 id="2-阿里云百炼主力大模型和多模态都押这">2. 阿里云百炼：主力大模型和多模态都押这</h3>
<p><strong>我用了什么</strong>：项目的主大模型配置就是百炼，Key 确认可用（中间有过一次 401，查下来是旧配置记录的问题，不是平台毛病）。多模态升级选型最终定案：<code>qwen3-vl-embedding</code> + <code>qwen3-vl-rerank</code>，都是百炼。</p>
<p><strong>价格记录</strong>：<code>qwen3-vl-rerank</code> 约 <strong>¥0.5/百万 token</strong>，RPM 限流 600。这个数字是当时调研横向比价的核心依据——比硅基同系 VL 模型便宜（0.5 对 1.8），功能上是图/文统一打分，一个模型把两个模态的精排全包了。embedding 侧默认 2560 维可选 1024，32K 上下文。</p>
<p><strong>稳定性</strong>：国内直连，没有翻墙环节的拖累；RPM 600 对个人项目意味着限流风险主要在平台侧而不是我这侧。</p>
<p><strong>一句话结论</strong>：要用国产多模态（图文同空间向量 + 统一精排），百炼是目前把事办全的那家。</p>
<h3 id="3-deepseek生成层的性价比答案">3. DeepSeek：生成层的性价比答案</h3>
<p><strong>我用了什么</strong>：RAG 链路的最后一公里——候选片段注入后，<code>deepseek-chat</code> 流式输出（SSE），前端打字机效果。</p>
<p><strong>价格记录</strong>：<strong>没记下来。</strong> 选它的时候主要看生成质量和流式接口的顺滑度，账单没细算过，不编。</p>
<p><strong>稳定性</strong>：在我的问答链路里跑了几个月，配合 system prompt 的硬约束（只根据资料回答、找不到就说找不到）表现稳定。生成侧挂了整个答案链路就断，所以我在代码里把它当作「可能会死的一环」来设计——这话在下面稳定性专节展开。</p>
<p><strong>一句话结论</strong>：中文生成 + 流式问答场景，它是我这套系统的默认答案；价格敏感的项目自己去看实时价目，我给数会过时。</p>
<h3 id="4-agicto放了但说实话">4. agicto：放了，但说实话</h3>
<p>这个平台在我的调研清单里过过一遍，但<strong>没深度用过，以下是听说级别的印象</strong>：属于聚合/中转类服务，卖点是一站式接多家模型。价格、延迟、稳定性我一律没有实测数据——<strong>没测到</strong>。</p>
<p>为什么还写它？因为这类中转站是大模型 API 水最深的一层：价格看着香，链路中间多一个不透明的倒手方，Key 安全和数据合规都是暗坑。我自己的原则：<strong>没验证过的中转，主力链路绝不走，测试链路悠着走。</strong></p>
<h2 id="钱到底花在哪先看懂三类调用的消耗结构">钱到底花在哪：先看懂三类调用的消耗结构</h2>
<p>比价之前，有个比单价更重要的事：<strong>RAG 系统里三类 API 调用的消耗曲线完全不是一回事，搞混了就会错把省钱省到刀背上。</strong></p>
<p><strong>Embedding 是「一次性大头」。</strong> 全量文档切块后要逐块转向量，文档灌一遍库就付一遍的钱；以后只有新增和改版才再扣。所以它最适合白嫖免费档——我那套系统文本链路整个建库没花钱，就是靠硅基流动的 bge-m3 免费额度撑的。但注意一个隐藏税：<strong>向量空间换模型 = 全量重灌</strong>，新旧向量绝不能在一个库里混存（同维度也不行），正规做法是环境变量指向影子库先验证，通过后删掉正式库同名重建，把 published 文档逐篇重新向量化——这一步等于把 embedding 的钱再交一遍。选型时拍脑袋换模型，重灌时流泪。</p>
<p><strong>Rerank 是「每问必付」。</strong> 每次提问都要把粗召出来的 top-20 候选逐条过精排模型，它的 token 消耗和提问量成正比，是上线后会持续流血的口子。所以多模态定案时我盯着单价算账：百炼 <code>qwen3-vl-rerank</code> 约 ¥0.5/百万 token，比硅基同系的 ¥1.8 便宜太多，rerank 这种高频调用，每百万差 ¥1.3 看着不大，乘以「日提问量 × 候选条数」就不小了。这就是<strong>高频链路按厘定价，低频链路按爽定价</strong>（免费/够用就行）的原因。</p>
<p><strong>生成是「弹性最大」的。</strong> 每次问答只有 top-5 片段加 system prompt 进模型，单次不贵，但 prompt 写贪了（比如把整个文档塞进去）成本立刻翻几十倍。我的做法是上下文只进精排幸存者的原文，其余一律不进。另外百炼 <code>qwen3-vl-rerank</code> 那个 <strong>RPM 600</strong> 的限流也要提前算：个人项目够用，真上量了这就是你第一道天花板，平台「稳不稳定」有时其实是「限不限流」的问题。</p>
<h2 id="顺手记下海外模型的连不通">顺手记下海外模型的连不通</h2>
<p>比价过程中撞过两面墙，都不是模型本身的问题，是「从国内连」的问题：</p>
<ul>
<li><code>jina-reranker-m0</code>：多模态精排候选之一，国外服务，<strong>国内连通性存疑</strong>，直接出候选；</li>
<li>Tavily 搜索 API（首页 AI 助理的联网工具）：官方域名国内常被墙，改走国内镜像 <code>api.tavily.cn</code> 才通；免费额度 <strong>1000 credits/月</strong>——顺手把额度监控也挂上了，白嫖的额度也是要被管理预算的。</li>
</ul>
<p>结论粗暴但实用：<strong>选型清单里凡是国外服务，先测连通再测能力</strong>，不然文档写完第一天就卡在网络层。</p>
<h2 id="三张表--一条保命心得">三张表 + 一条保命心得</h2>
<p>价格和用途汇总（空栏 = 没测到/没记，不猜）：</p>
<table>
	<thead>
			<tr>
					<th>平台</th>
					<th>我实际用它的什么</th>
					<th>价格（素材记录）</th>
					<th>速度</th>
					<th>稳定性结论</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>硅基流动</td>
					<td>bge-m3 向量 + bge-reranker 精排</td>
					<td>文本档免费；VL 系 ¥0.7/¥1.8 每 M</td>
					<td>没测</td>
					<td>长期可用，现任容灾备胎</td>
			</tr>
			<tr>
					<td>阿里云百炼</td>
					<td>主大模型 + qwen3-vl 双模型</td>
					<td>vl-rerank 约 ¥0.5/M，RPM600</td>
					<td>没测</td>
					<td>国内直连，主力在任</td>
			</tr>
			<tr>
					<td>DeepSeek</td>
					<td>问答生成（流式）</td>
					<td>没记下来</td>
					<td>没测</td>
					<td>数月运行稳定</td>
			</tr>
			<tr>
					<td>agicto</td>
					<td>仅调研，未深用</td>
					<td>没测到</td>
					<td>没测到</td>
					<td>没测到</td>
			</tr>
	</tbody>
</table>
<p>最后那条保命心得比任何比价都值钱：<strong>平台级稳定性，与其指望供应商，不如指望自己的降级代码。</strong> 我那套 RAG 里每一条外部调用都有 Plan B——双路检索单路挂了另一路兜底、rerank 不可用自动退回融合分数排序、query 改写失败就用原话检索。接入任何 API 之前先问自己：它今天挂了，我的系统是报错还是变笨？答案是「变笨但不报错」，这钱花得才安心。</p>
<p>省钱的完整逻辑闭环是：<strong>先画消耗曲线再选平台（一次性白嫖、高频盯单价、弹性管 prompt）→ 免费档起步（硅基）→ 主力按能力定价选（百炼/DeepSeek）→ 中转站不碰主力链路 → 降级通道写好再省钱。</strong> 速度对比那栏的欠账，等我把 benchmark 脚本补上再写一篇续账。下一篇回到 RAG 系列内部：效果差的时候怎么十分钟定位是检索还是模型的锅，调试手册单开一页。</p>
]]></content:encoded></item><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 怕问得含糊？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><item><title>RAG 检索总跑偏？混合检索四步直接抄</title><link>https://blog.zjkun.top/posts/rag-hybrid-search-4-steps/</link><pubDate>Tue, 13 Oct 2026 18:53:00 +0800</pubDate><guid>https://blog.zjkun.top/posts/rag-hybrid-search-4-steps/</guid><description>RAG 效果差，九成不是模型的锅，是检索没捞对料。拆解我 Nuxt3 企业知识库 my-nuxt-app 的混合检索全链路：BM25 手写实现（k1=1.5、b=0.75）、RRF 融合（k=60）、精排 top5、三模式对照调试，参数和代码全给。</description><content:encoded><![CDATA[<p>RAG 效果差，九成不是大模型的锅，是检索没捞对料。模型只负责说好话，检索决定它能看到什么。</p>
<p>这篇拆我自己项目（Nuxt3 企业知识库，Nitro + TiDB + Chroma）里跑着的混合检索：BM25 怎么打分、RRF 的 k 为什么是 60、有向量召回了为什么还要精排。代码全部真实在跑，参数直接抄。</p>
<h2 id="为什么要两路">为什么要两路</h2>
<p>向量检索单干有毛病：换个说法问很准，但专有名词、条款号、缩写型号就拉胯——搜型号给你一堆「看着很像」的，偏偏不是原文。关键词检索反过来：字面命中稳，近义表达没辙。</p>
<p>所以两路都要：<strong>向量路（Chroma bge-m3，强在近义表达）+ 关键词路（手写 BM25，强在专有名词/术语）</strong>，各取所长。</p>
<h2 id="第-1-步并行粗召">第 1 步：并行粗召</h2>
<p>两路并行，各捞 20 条（<code>COARSE_TOP_K = 20</code>），用 <code>Promise.allSettled</code>——一路挂了另一路照常出，向量服务抖动不拖垮整个问答。向量路带 <code>status=published</code> 过滤，关键词路召回后回查 TiDB <code>document_chunks</code> 补全内容（<code>listChunksByIds</code> + Map 保序）。</p>
<h2 id="第-2-步bm25-这一路的两个旋钮">第 2 步：BM25 这一路的两个旋钮</h2>
<p>BM25 公式（对查询 q 中每个词项 qi 累加对文档 d 的得分）：</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>score(q, d) = Σ IDF(qi) · tf(qi,d)·(k1+1) / (tf(qi,d) + k1·(1 - b + b·|d|/avgdl))
</span></span><span style="display:flex;"><span>IDF(qi)     = ln((N - df(qi) + 0.5) / (df(qi) + 0.5) + 1)
</span></span></code></pre></div><ul>
<li><strong>k1=1.5 管词频饱和</strong>：一个词在文档里出现 10 次，不代表比出现 1 次相关 10 倍，得分要收敛</li>
<li><strong>b=0.75 管长文惩罚</strong>：长文档天然词多，不打折就被大段废话霸榜</li>
</ul>
<p>我的真实打分代码（<code>server/utils/bm25.ts</code>，零依赖手写，倒排索引 <code>token → (chunkId, tf)</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">const</span> <span style="color:#a6e22e">df</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">posting</span>.<span style="color:#a6e22e">size</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// IDF：用 +1 平滑避免负值；罕见词区分度高、IDF 大
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">idf</span> <span style="color:#f92672">=</span> Math.<span style="color:#a6e22e">log</span>((<span style="color:#a6e22e">N</span> <span style="color:#f92672">-</span> <span style="color:#a6e22e">df</span> <span style="color:#f92672">+</span> <span style="color:#ae81ff">0.5</span>) <span style="color:#f92672">/</span> (<span style="color:#a6e22e">df</span> <span style="color:#f92672">+</span> <span style="color:#ae81ff">0.5</span>) <span style="color:#f92672">+</span> <span style="color:#ae81ff">1</span>)
</span></span><span style="display:flex;"><span><span style="color:#75715e">// 词频饱和项：tf 越大得分越高，但受 k1 限制避免高频词刷分
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">// 文档长度归一化：长文档天然 tf 高，用 b 参数抑制
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">norm</span> <span style="color:#f92672">=</span> <span style="color:#ae81ff">1</span> <span style="color:#f92672">-</span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">b</span> <span style="color:#f92672">+</span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">b</span> <span style="color:#f92672">*</span> (<span style="color:#a6e22e">doc</span>.<span style="color:#a6e22e">length</span> <span style="color:#f92672">/</span> (<span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">avgdl</span> <span style="color:#f92672">||</span> <span style="color:#ae81ff">1</span>))
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">tfComponent</span> <span style="color:#f92672">=</span> (<span style="color:#a6e22e">tf</span> <span style="color:#f92672">*</span> (<span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">k1</span> <span style="color:#f92672">+</span> <span style="color:#ae81ff">1</span>)) <span style="color:#f92672">/</span> (<span style="color:#a6e22e">tf</span> <span style="color:#f92672">+</span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">k1</span> <span style="color:#f92672">*</span> <span style="color:#a6e22e">norm</span>)
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">score</span> <span style="color:#f92672">+=</span> <span style="color:#a6e22e">idf</span> <span style="color:#f92672">*</span> <span style="color:#a6e22e">tfComponent</span>
</span></span></code></pre></div><p><strong>为什么手写不上 ES</strong>：我这量级（全库 172 个 chunk）引 Elasticsearch 纯属找罪受。启动插件从 TiDB 全量捞 published 分块，分词建倒排一次预热，零冷启动；失败也不阻塞启动，BM25 降级空结果，向量路仍可用。avgdl 用增量公式维护（增删文档各一行数学），不为一个平均值遍历全库。</p>
<h2 id="第-3-步rrf-融合扔掉分数只排名次">第 3 步：RRF 融合，扔掉分数只排名次</h2>
<p>余弦分卡在 0~1，BM25 分没有上限（能到 30+），直接加权就是关公战秦琼。RRF（Reciprocal Rank Fusion）只看名次：</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>score(d) = Σ 1/(k + rank_i(d))    // k=60，某路未命中则不参与累加
</span></span></code></pre></div><p>直觉算例（k=60）：</p>
<ul>
<li>两路都排第 1：1/61 + 1/61 ≈ <strong>0.0328</strong></li>
<li>只有一路排第 1：1/61 ≈ <strong>0.0164</strong></li>
<li>一路第 1、另一路第 20：1/61 + 1/80 ≈ <strong>0.0289</strong></li>
</ul>
<p>→ <strong>两路都认的才浮上来。</strong> k 是平滑常数：k 越小头部名次优势越大，k=60 是论文经验值，把第 1 和第 2 名的差距抹平，不让某一路一票定生死；候选池小的话 k 可调低。真实实现（<code>server/utils/rrf.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">function</span> <span style="color:#a6e22e">rrf</span>(<span style="color:#a6e22e">rankedLists</span>: <span style="color:#66d9ef">string</span>[][], <span style="color:#a6e22e">k</span> <span style="color:#f92672">=</span> <span style="color:#ae81ff">60</span>)<span style="color:#f92672">:</span> <span style="color:#a6e22e">FusedResult</span>[] {
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">scores</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">Map</span>&lt;<span style="color:#f92672">string</span>, <span style="color:#a6e22e">number</span>&gt;()
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">ranksMap</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">Map</span>&lt;<span style="color:#f92672">string</span>, <span style="color:#a6e22e">Array</span>&lt;<span style="color:#f92672">number</span> <span style="color:#960050;background-color:#1e0010">|</span> <span style="color:#a6e22e">null</span>&gt;&gt;()
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">rankedLists</span>.<span style="color:#a6e22e">forEach</span>((<span style="color:#a6e22e">list</span>, <span style="color:#a6e22e">listIndex</span>) <span style="color:#f92672">=&gt;</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">list</span>.<span style="color:#a6e22e">forEach</span>((<span style="color:#a6e22e">id</span>, <span style="color:#a6e22e">idx</span>) <span style="color:#f92672">=&gt;</span> {
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">rank</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">idx</span> <span style="color:#f92672">+</span> <span style="color:#ae81ff">1</span> <span style="color:#75715e">// 名次从 1 开始
</span></span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">contribution</span> <span style="color:#f92672">=</span> <span style="color:#ae81ff">1</span> <span style="color:#f92672">/</span> (<span style="color:#a6e22e">k</span> <span style="color:#f92672">+</span> <span style="color:#a6e22e">rank</span>)
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">scores</span>.<span style="color:#66d9ef">set</span>(<span style="color:#a6e22e">id</span>, (<span style="color:#a6e22e">scores</span>.<span style="color:#66d9ef">get</span>(<span style="color:#a6e22e">id</span>) <span style="color:#f92672">??</span> <span style="color:#ae81ff">0</span>) <span style="color:#f92672">+</span> <span style="color:#a6e22e">contribution</span>)
</span></span><span style="display:flex;"><span>      <span style="color:#75715e">// ranks 记录每路名次，未出现记 null，便于调试观察
</span></span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">let</span> <span style="color:#a6e22e">ranks</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">ranksMap</span>.<span style="color:#66d9ef">get</span>(<span style="color:#a6e22e">id</span>)
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">if</span> (<span style="color:#f92672">!</span><span style="color:#a6e22e">ranks</span>) { <span style="color:#a6e22e">ranks</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> Array(<span style="color:#a6e22e">rankedLists</span>.<span style="color:#a6e22e">length</span>).<span style="color:#a6e22e">fill</span>(<span style="color:#66d9ef">null</span>); <span style="color:#a6e22e">ranksMap</span>.<span style="color:#66d9ef">set</span>(<span style="color:#a6e22e">id</span>, <span style="color:#a6e22e">ranks</span>) }
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">ranks</span>[<span style="color:#a6e22e">listIndex</span>] <span style="color:#f92672">=</span> <span style="color:#a6e22e">rank</span>
</span></span><span style="display:flex;"><span>    })
</span></span><span style="display:flex;"><span>  })
</span></span><span style="display:flex;"><span>  <span style="color:#75715e">// ...按融合分降序输出
</span></span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>融合取 top-20（<code>FUSE_TOP_K = 20</code>）；一路为空自动退化单路。</p>
<h2 id="第-4-步精排最不能省的一步">第 4 步：精排，最不能省的一步</h2>
<p>入库时分块是<strong>单独</strong>算向量的，跟你的问题从没同框比过——粗召本质是盲选。reranker（cross-encoder，硅基流动 bge-reranker-v2-m3）把「问题+每条候选」拼成一对喂进模型逐条打分，慢，但准。</p>
<p>只对融合后的 20 条精排（成本可控），取前 5 进上下文（<code>FINAL_TOP_K = 5</code>）。rerank 服务不可用就自动降级按 RRF 分排——链路永不整条挂掉。</p>
<p>然后生成：DeepSeek deepseek-chat SSE 流式，<strong>先推 sources（来源文档+页码）再推 delta</strong>，前端引用卡片比答案先渲染。</p>
<h2 id="调试方法三模式对照">调试方法：三模式对照</h2>
<p>这是整条链路我最推荐抄的东西——检索模式做成 <code>hybrid / bm25 / vector</code> 三档开关，<strong>召回策略是唯一变量</strong>，后面统一走精排+生成。同一个问题三档各跑一遍，控制台按模式打印两路名次/得分，谁拖后腿一眼看见。另有 <code>/api/rag/keyword-search</code>、<code>/api/rag/vector-search</code> 单路调试接口和 <code>retrieveOnly</code>（只看检索不生成）。</p>
<h2 id="速查表">速查表</h2>
<table>
	<thead>
			<tr>
					<th>项</th>
					<th>值</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>流程</td>
					<td>两路并行各召 20 → RRF 融合取 20 → 精排取 5 → 生成</td>
			</tr>
			<tr>
					<td>BM25</td>
					<td>k1=1.5，b=0.75，IDF=ln((N-df+0.5)/(df+0.5)+1)</td>
			</tr>
			<tr>
					<td>RRF</td>
					<td>score=Σ1/(60+rank)，只看名次不看分</td>
			</tr>
			<tr>
					<td>降级</td>
					<td>单路挂→另一路照常；rerank 挂→按 RRF 分</td>
			</tr>
			<tr>
					<td>自测</td>
					<td>同一问题跑纯向量/纯BM25/混合三遍</td>
			</tr>
	</tbody>
</table>
<p>RAG 的上限在检索，不在大模型。下一篇讲检索的第一站：Query 改写。</p>
<blockquote>
<p>本篇最早于 2026 年 10 月 13 日发布于小红书，此为含完整代码与数据的博客版。</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><item><title>RAG 还是微调？大模型应用 3 种模式别选错</title><link>https://blog.zjkun.top/posts/rag-vs-finetune-3-modes/</link><pubDate>Fri, 09 Oct 2026 09:30:00 +0800</pubDate><guid>https://blog.zjkun.top/posts/rag-vs-finetune-3-modes/</guid><description>给大模型加能力有三层：Prompt 上下文、RAG、微调。我用 Nuxt3 企业知识库的真实选型讲清边界——为什么知识类需求 RAG 完胜微调：SFT 教的是行为不是知识，知识更新频率、可溯源、成本三笔账怎么算。</description><content:encoded><![CDATA[<p>做 AI 应用绕不开这道选择题：让模型「懂你的业务」，到底靠提示词、RAG，还是微调？</p>
<p>我把自己的企业知识库项目（Nuxt3 my-nuxt-app：私有制度文档问答，检索走向量+BM25 混合，生成走 deepseek-chat API）从头选到尾，这篇文章把三层的边界和我的决策过程讲清楚——这也是面试被问最多的一题。</p>
<h2 id="先摆认知地基训练范式在干嘛">先摆认知地基：训练范式在干嘛</h2>
<p>一张表说人话（来自我系统补的《语言模型训练范式》课件，老师一句话总结：<strong>大模型 = 会接话茬的机器（Pretrain）+ 会做任务的机器（SFT）+ 会自我评判的机器（RM/PPO/DPO）</strong>）：</p>
<table>
	<thead>
			<tr>
					<th>阶段</th>
					<th>在干嘛</th>
					<th>人肉类比</th>
					<th>数据量</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Pretrain</td>
					<td>海量文字学「预测下一个字」</td>
					<td>儿童大量阅读</td>
					<td>Llama3 用了 15 万亿 tokens</td>
			</tr>
			<tr>
					<td>SFT</td>
					<td>带标注问答，从接话茬变做任务</td>
					<td>老师布置带答案的作业</td>
					<td>几十万条 ≈ 预训练的 1/5000</td>
			</tr>
			<tr>
					<td>RM + PPO/DPO</td>
					<td>学会「说人该说的话」</td>
					<td>按批改意见反复改写</td>
					<td>较小</td>
			</tr>
	</tbody>
</table>
<p>硬核数字记一个就够：Llama 3.1 405B 预训练 = 16384 块 H100 跑 54 天，亿级人民币。所以<strong>普通企业和个人根本不碰预训练</strong>，选择题只在「现成模型 + 什么方式喂能力」之间做。</p>
<p>面试加分项也在这：<strong>SFT/微调 ≠ 给模型喂知识，是教模型「做任务」。</strong> 知识靠 Pretrain（通用）+ RAG（私有/新）供给。很多人把微调当成「把公司文档灌进模型」，方向从根上就错了。</p>
<h2 id="三层模式按成本从低到高">三层模式，按成本从低到高</h2>
<h3 id="第一层prompt--上下文工程">第一层：Prompt / 上下文工程</h3>
<p>把关键信息直接写进 system prompt。适合：全局规则、输出格式、角色设定。</p>
<p>局限：窗口有限，塞不进几十份制度文档；每轮都重复计费。我的项目里它只负责「只用资料作答、不编造、标注来源页码」这类约束。</p>
<h3 id="第二层rag知识在模型外面">第二层：RAG——知识在模型外面</h3>
<p>文档入库切块 → 向量化（我用 Chroma + bge-m3）→ 提问先检索（混合检索：BM25+向量，RRF 融合，reranker 精排取 top5）→ 检索结果拼进 prompt → 生成。</p>
<p><strong>知识更新 = 重新入库，分钟级。</strong> 文档改了版本，知识库表里状态机走一遍（草稿→发布→下架），不动模型一根手指。回答能带来源文件和页码——<strong>可溯源</strong>，这是企业场景的硬需求，微调给不了。</p>
<h3 id="第三层微调把能力焊进权重">第三层：微调——把能力焊进权重</h3>
<p>拿几百到几万条「输入→期望输出」对模型做 SFT。适合什么？<strong>固定的输出格式、领域语气、术语习惯</strong>——也就是「行为方式」，不是「知识内容」。</p>
<h2 id="我的真实三笔账为什么-my-nuxt-app-选了-rag微调碰都不碰">我的真实三笔账（为什么 my-nuxt-app 选了 RAG，微调碰都不碰）</h2>
<p><strong>1. 知识更新频率。</strong> 部门制度文档是活的：新考勤规则、修订的报销流程。RAG 重新入库就完事；微调要攒数据、排队训练、验证回归——为「上周改的那条婚假规定」重训一次模型，纯属行为艺术。</p>
<p><strong>2. 机器现实。</strong> 我的生产环境是 99 元/年的 2核2G 阿里云 ECS，内存预算排完（nginx 64MB + node-app 384MB + 系统 + swap）只剩 1G 出头。<strong>这台机器连一个 7B 模型都推理不动</strong>，更别说训练。我架构里所有重活全在云 API：Embedding 走 bge-m3、精排走硅基流动 bge-reranker-v2-m3、生成走 deepseek-chat。微调需要训练数据+算力+评测集三件套，对个人项目是三重成本。</p>
<p><strong>3. 可回滚性。</strong> RAG 答错了，改文档、调分块、调检索参数，改完立刻生效，还能追溯「当时引用了哪一页」。微调把错误知识焊进权重后，你连它为什么胡说都查不出来，回滚要重训。</p>
<h2 id="什么时候我才会上微调">什么时候我才会上微调</h2>
<p>预判一下自己：等业务问答稳定，出现这类需求再考虑——「回答必须严格按公司工单模板的语气和结构输出」这种<strong>格式/风格</strong>问题。届时也是 LoRA 这类轻量方案 + 现成 API 模型，依旧不碰预训练。知识类需求，永远 RAG。</p>
<h2 id="速查表">速查表</h2>
<table>
	<thead>
			<tr>
					<th>需求</th>
					<th>选择</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>全局规则/格式约束</td>
					<td>Prompt</td>
			</tr>
			<tr>
					<td>私有知识、时效知识、要溯源</td>
					<td>RAG</td>
			</tr>
			<tr>
					<td>固定输出风格/任务行为</td>
					<td>微调（最后才考虑）</td>
			</tr>
			<tr>
					<td>「把文档灌进模型」的想法</td>
					<td>❌ 方向错了，那是 RAG 的活</td>
			</tr>
	</tbody>
</table>
<p>一句话收束：<strong>知识在模型外面解决（RAG），行为在模型权重里解决（微调）——分不清这两件事，选型必错。</strong></p>
<p>下一篇讲 RAG 效果差怎么调试。</p>
<blockquote>
<p>本篇最早于 2026 年 10 月 9 日发布于小红书，此为博客完整版。</p>
</blockquote>
]]></content:encoded></item></channel></rss>