<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>RAG on Renxin's Blog</title><link>https://zh.renxinblog.cn/tags/rag/</link><description>Recent content in RAG on Renxin's Blog</description><generator>Hugo -- gohugo.io</generator><language>zh</language><lastBuildDate>Sun, 30 Aug 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://zh.renxinblog.cn/tags/rag/index.xml" rel="self" type="application/rss+xml"/><item><title>第二话｜怎么根据用户的提问找到相关的退货政策？</title><link>https://zh.renxinblog.cn/post/gyr-c02-embedding-topk/</link><pubDate>Sun, 30 Aug 2026 00:00:00 +0000</pubDate><guid>https://zh.renxinblog.cn/post/gyr-c02-embedding-topk/</guid><description>&lt;img src="https://zh.renxinblog.cn/images/gyr-c02-embedding-topk-cover.png" alt="Featured image of post 第二话｜怎么根据用户的提问找到相关的退货政策？" /&gt;&lt;p&gt;“这双鞋七天内能退吗？”&lt;/p&gt;
&lt;p&gt;NovaShop 的资料里没有这句话。资料写的是：“NovaTrail X2 签收后 7 个自然日内可申请退货；商品需保持完好且配件齐全。”&lt;/p&gt;
&lt;p&gt;两句话说的是同一件事，字面上却对不上。如果助手只会查关键词，这个问题很可能直接被漏掉——用户不会觉得是“没搜到”，只会觉得这个助手不懂人话。&lt;/p&gt;
&lt;p&gt;这一话就把这次匹配拆开，看三件事：问题怎样变成数字，三条政策怎样被排出先后，以及“排第一”到底意味着什么。&lt;/p&gt;
&lt;h2 id="先别急着谈向量"&gt;先别急着谈向量
&lt;/h2&gt;&lt;p&gt;先准备三段极小的候选资料：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;标识&lt;/th&gt;
					&lt;th&gt;候选片段&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;return-policy&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;NovaTrail X2 签收后 7 个自然日内可申请退货；商品需保持完好且配件齐全。&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;shipping-policy&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;NovaTrail X2 标准配送通常在付款后 1 至 3 个工作日发货，偏远地区时效可能延长。&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;size-guide&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;NovaTrail X2 提供黑色 M 码与 L 码；下单前请参考尺码表确认脚长。&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;如果只做关键词匹配，“七天内能退”里的“退”，和资料里的“申请退货”，靠的是同一个意思，而不是同一个词。想接住这种说法，传统做法是在词表里补同义词规则——把“退”“寄回去”“不合适”都映射到“退货”上。&lt;/p&gt;
&lt;p&gt;规则不是没有价值。SKU、订单号这类精确标识，恰恰适合这种方式：用户原样报出“NS-X2-BLK-M”，关键词匹配又快又准。&lt;/p&gt;
&lt;p&gt;可客服问题远不止 SKU 和订单号。用户会说“能退吗”“不喜欢能不能寄回去”“尺码不合适怎么办”，把每种说法都写进词表，维护成本很快失控。&lt;/p&gt;
&lt;p&gt;Embedding 换了个思路：不比字符串，先把文本变成一串数字（向量），再比较两段文本在这个空间里离得近不近。阿里云百炼把 Embedding 定义为把文本等数据转换为数值向量，用于语义搜索等下游任务。&lt;a class="link" href="https://help.aliyun.com/zh/model-studio/embedding" target="_blank" rel="noopener"
 &gt;官方文档&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="系统比较的到底是什么"&gt;系统比较的到底是什么
&lt;/h2&gt;&lt;p&gt;候选政策可以提前编码、保存。用户发来问题时，系统只需要编码这一个问题，再拿它和候选向量逐个比较。&lt;/p&gt;
&lt;figure class="gallery-image"&gt;
 &lt;a class="image-link" href="https://zh.renxinblog.cn/images/gyr-c02-embedding-topk-question-to-topk.svg" data-pswp-width="1040" data-pswp-height="420" target="_blank"&gt;
 &lt;img src="https://zh.renxinblog.cn/images/gyr-c02-embedding-topk-question-to-topk.svg" width="1040" height="420" alt="用户提问、查询向量、候选片段向量、相似度计算、Top-K 到回答证据的直角流程图"&gt;
 &lt;/a&gt;
 &lt;figcaption&gt;检索先找“该看哪几段”，回答环节再决定怎样组织答案。&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;这里有三件事不能混：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;查询和候选片段必须用同一个 Embedding 模型编码，否则两者未必落在同一套表示空间里。&lt;/li&gt;
&lt;li&gt;参与比较的两条向量必须等长；维度不同，余弦相似度无从算起。&lt;/li&gt;
&lt;li&gt;得到的只是当前候选集合里的排序，不是“答案正确率”。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这很像 Java 里的 &lt;code&gt;Comparator&lt;/code&gt;：它能给对象排出先后，却不会替你判断“排第一的就是正确答案”。向量分数也一样——只负责排队，不负责下结论。&lt;/p&gt;
&lt;p&gt;密集检索的通行做法，就是分别编码问题和文本片段，再用相似度挑选候选上下文，DPR 论文用的正是这种双编码器思路。&lt;a class="link" href="https://aclanthology.org/2020.emnlp-main.550/" target="_blank" rel="noopener"
 &gt;Karpukhin 等，2020&lt;/a&gt; 我们选余弦相似度，只是为了把计算摊开看，不代表所有检索系统都这么算。&lt;/p&gt;
&lt;h2 id="相似度怎么算"&gt;相似度怎么算
&lt;/h2&gt;&lt;p&gt;候选片段和查询都变成了向量，“近不近”就变成了数值比较。那这个数值具体怎么算？这一节用余弦相似度回答。&lt;/p&gt;
&lt;p&gt;$$
\operatorname{cosine}(q,d)=\frac{q \cdot d}{\lVert q \rVert\lVert d \rVert}
$$&lt;/p&gt;
&lt;p&gt;&lt;code&gt;q&lt;/code&gt; 是查询向量，&lt;code&gt;d&lt;/code&gt; 是一条候选片段向量。分子把两个向量逐维相乘再相加（点积），衡量它们在每个维度上是不是朝同一个方向使劲；分母乘上两个向量的长度，避免“向量长分数就高”。结果有直观的含义：同方向的向量分数接近 1，垂直的接近 0，反方向接近 -1。排序器关心的，就是谁离 1 更近。&lt;/p&gt;
&lt;p&gt;余弦公式有两个必须预先处理的边界：两条向量要同维、非零。维度不一致，逐维相乘根本对不上；零向量没有方向，分母一除就崩。排序器在这里主动报错而不是硬算，是为了让配置错误早点暴露，而不是让一个坏分数混进排序。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;cosine_similarity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;left&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Sequence&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;right&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Sequence&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;&amp;#34;&amp;#34;计算两个同维、非零向量的余弦相似度。&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;left&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;right&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="ne"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;向量维度不一致，不能计算余弦相似度&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# 点积衡量两个向量在各维度上的同向程度。&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;dot_product&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;left&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;right&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;strict&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;left_norm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;left&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;right_norm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;right&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# 零向量没有方向；继续计算会产生除零错误，也没有检索意义。&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;left_norm&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;right_norm&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="ne"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;零向量没有方向，不能计算余弦相似度&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;dot_product&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;left_norm&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;right_norm&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这一段代码和公式一一对应：先检查输入，再算点积，再算两个长度，最后相除。拒绝分支不是额外负担，它就是“余弦在不同输入上的定义边界”。&lt;/p&gt;
&lt;h2 id="只取前-k-名"&gt;只取前 K 名
&lt;/h2&gt;&lt;p&gt;三个候选都有分数了，接下来怎么挑？答案朴素：按分数从高到低排，取前 K 个。这就是 Top-K 里的“K”。&lt;/p&gt;
&lt;p&gt;排序里有一个容易忽略的小决策：分数并列时怎么办。&lt;code&gt;rank_candidates&lt;/code&gt; 用原始下标做第二排序键，保证同一份输入反复运行得到同一份顺序——方便测试和复现，但不代表两个并列候选在业务上有高低。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;rank_candidates&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;query_vector&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Vector&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;candidates&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Sequence&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Candidate&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;candidate_vectors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Sequence&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Vector&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;top_k&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;RankedCandidate&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;&amp;#34;&amp;#34;按余弦相似度降序返回前 ``top_k`` 个候选。&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;candidates&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;candidate_vectors&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="ne"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;候选片段数量与候选向量数量必须一致&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;top_k&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="ne"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;top_k 必须大于 0&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;ranked_with_index&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;RankedCandidate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;candidate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cosine_similarity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query_vector&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;candidate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;candidates&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;candidate_vectors&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;ranked_with_index&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ranked_with_index&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="n"&gt;top_k&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;RankedCandidate&lt;/code&gt; 只是把候选片段和分数绑在一起的容器。真正的逻辑只有三步：逐条算分数、按分数降序排（分数打平看原始下标）、切片取前 &lt;code&gt;top_k&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;用一个玩具例子看：查询向量 &lt;code&gt;[1.0, 0.0]&lt;/code&gt;，三个候选向量是 &lt;code&gt;[1.0, 0.0]&lt;/code&gt;、&lt;code&gt;[2.0, 0.0]&lt;/code&gt;、&lt;code&gt;[0.0, 1.0]&lt;/code&gt;，分数分别是 1.0、1.0、0.0。取 &lt;code&gt;top_k=2&lt;/code&gt;，结果是前两个候选——同分时保住原始顺序，第三个出局。&lt;/p&gt;
&lt;h2 id="先把本地验证跑起来"&gt;先把本地验证跑起来
&lt;/h2&gt;&lt;p&gt;排序逻辑不依赖云端，验证也先离线做。运行：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; /Users/renxin/ai_brain/GYR
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;python3.11 -m unittest discover -s c02-embedding-topk/tests -v
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;测试喂的全是固定小向量，把排序器的不变量直接写进断言：同方向向量 &lt;code&gt;[3.0, 4.0]&lt;/code&gt; 和 &lt;code&gt;[6.0, 8.0]&lt;/code&gt; 的余弦必须是 1.0；并列分数的三个候选取前两名必须稳定得到 &lt;code&gt;first&lt;/code&gt;、&lt;code&gt;second&lt;/code&gt;。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assertAlmostEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cosine_similarity&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="mf"&gt;3.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;4.0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;6.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;8.0&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rank_candidates&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;candidates&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;2.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;]],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;top_k&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assertEqual&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;candidate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chunk_id&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;first&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;second&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这次实际运行 4/4 通过：同向向量、零向量拒绝、维度不一致拒绝、并列分数稳定排序。&lt;/p&gt;
&lt;p&gt;为什么坚持先离线验证？因为云端调用不可重放——每次可能在小数后几位漂移，还要消耗配额；而排序逻辑是确定性的，它该在本地一次测对。云端只负责一件事：把文本变成向量。&lt;/p&gt;
&lt;h2 id="接上真实模型"&gt;接上真实模型
&lt;/h2&gt;&lt;p&gt;本地排序器验证完毕，现在把玩具向量换成百炼 Embedding 的真实输出。接云端要做三件事：设环境变量、跑 &lt;code&gt;main.py&lt;/code&gt;、看输出。&lt;/p&gt;
&lt;p&gt;请求走的是 OpenAI 兼容接口的 &lt;code&gt;/embeddings&lt;/code&gt;：把模型名和输入文本发过去，拿回一串浮点数。Base URL 和 API Key 从环境变量读取，不要写进代码、文章或 Git。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;export&lt;/span&gt; &lt;span class="nv"&gt;DASHSCOPE_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;你的密钥&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;export&lt;/span&gt; &lt;span class="nv"&gt;DASHSCOPE_COMPATIBLE_BASE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;https://你的业务空间域名/compatible-mode/v1&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;python3.11 c02-embedding-topk/main.py --top-k &lt;span class="m"&gt;3&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这次真实运行（模型 &lt;code&gt;qwen3.7-text-embedding&lt;/code&gt;，请求 1024 维、实际返回 1024 维）的输出是这个形状，以第一个问题为例（候选正文为排版截断，分数与排序未改）：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;model=qwen3.7-text-embedding
requested_dimension=1024
actual_dimension=1024

query=这双鞋七天内能退吗？
1. return-policy	score=0.650210	NovaTrail X2 签收后 7 个自然日…
2. shipping-policy	score=0.445476	NovaTrail X2 标准配送通常在付款后…
3. size-guide	score=0.418887	NovaTrail X2 提供黑色 M 码与 L 码…
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;模型、地域和业务空间配置都可能变化，接入时以你的控制台和&lt;a class="link" href="https://help.aliyun.com/zh/model-studio/text-embedding-synchronous-api/" target="_blank" rel="noopener"
 &gt;官方接口说明&lt;/a&gt;为准。&lt;/p&gt;
&lt;p&gt;这里有一个容易忽略的边界。&lt;code&gt;main.py&lt;/code&gt; 把“候选片段”和“用户问题”分成两批请求，代码里保留了 &lt;code&gt;document&lt;/code&gt; 与 &lt;code&gt;query&lt;/code&gt; 两种角色，这是为了对齐百炼“检索任务建议区分查询和文档”的惯例；但我们这次用的是 OpenAI 兼容模式，接口里没有对应的角色字段——本地变量叫 &lt;code&gt;query&lt;/code&gt;，不代表服务端真的按“查询模式”去编码。&lt;/p&gt;
&lt;h2 id="跑出来的结果直觉对了边界也露出来了"&gt;跑出来的结果：直觉对了，边界也露出来了
&lt;/h2&gt;&lt;p&gt;三个问题都跑完，Top-1 汇总如下：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;用户提问&lt;/th&gt;
					&lt;th&gt;Top-1 候选&lt;/th&gt;
					&lt;th style="text-align: right"&gt;分数&lt;/th&gt;
					&lt;th&gt;能说明什么&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;这双鞋七天内能退吗？&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;return-policy&lt;/code&gt;&lt;/td&gt;
					&lt;td style="text-align: right"&gt;0.650210&lt;/td&gt;
					&lt;td&gt;这次口语化退货问题把退货片段排在第一。&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;下单后多久能发货？&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;shipping-policy&lt;/code&gt;&lt;/td&gt;
					&lt;td style="text-align: right"&gt;0.614013&lt;/td&gt;
					&lt;td&gt;换成物流问题后，同一候选集的第一名随之变化。&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;NS-X2-BLK-M 什么时候发货？&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;shipping-policy&lt;/code&gt;&lt;/td&gt;
					&lt;td style="text-align: right"&gt;0.686929&lt;/td&gt;
					&lt;td&gt;“发货”语义占了主导；不能据此证明 SKU 精确检索可靠。&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;顺带说明：同一模型、同一候选集，分数会在第四位小数附近轻微漂移，这正是“分数不能当阈值”的一部分。以上面这次运行为准。&lt;/p&gt;
&lt;p&gt;第三行最容易让人误判。“NS-X2-BLK-M 什么时候发货”确实把“发货政策”排到了第一，看着像答对了方向；可候选资料里根本没有这件 SKU 的发货记录。这个结果只能说明“发货”两个字在语义上占了上风，证明不了 SKU 检索可用。&lt;/p&gt;
&lt;h2 id="分数能做什么不能做什么"&gt;分数能做什么、不能做什么
&lt;/h2&gt;&lt;p&gt;开篇承诺的三件事，到这里都能回答了：问题是怎样变成数字的（Embedding），三条政策是怎样排出先后的（余弦相似度 + Top-K），排第一意味着什么——它意味着“在当前候选集合里最靠前”，仅此而已。&lt;/p&gt;
&lt;p&gt;Top-K 的作用很朴素：把后续要看的资料缩小到几段。至于分数本身，别指望它替你判断下面这些事：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;不能直接据此判断&lt;/th&gt;
					&lt;th&gt;原因&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;最终答案正确&lt;/td&gt;
					&lt;td&gt;回答仍可能误读、遗漏或编造候选内容。&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;证据足够&lt;/td&gt;
					&lt;td&gt;高分只表示相对靠前，不代表覆盖了回答所需条件。&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;可以忽略精确词&lt;/td&gt;
					&lt;td&gt;SKU、订单号、政策版本等字段常需要过滤或关键词能力补充。&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;一个分数阈值通吃&lt;/td&gt;
					&lt;td&gt;分数依赖模型、候选集合、切分方式和相似度定义。&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;一句话收束：&lt;strong&gt;排第一不等于答案可信。&lt;/strong&gt; 0.65 只说明“退货片段排在最前”，不说明“这条退货规则就是答案”。所以 Agent 的检索工具到这里就该停下——它交回候选证据和来源，而不是把分数包装成“答案可信”。之后的回答、过滤、引用、拒答，仍然是编排层要承担的事。&lt;/p&gt;
&lt;h2 id="结尾一整篇政策只有一个分数"&gt;结尾：一整篇政策，只有一个分数
&lt;/h2&gt;&lt;p&gt;前面用到的候选都是精心切成一段的小文本。如果候选本身就是一整篇长政策呢？我们把退货政策扩成六个条款：&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;NovaTrail X2 退货政策：签收后 7 个自然日内可申请退货；商品需保持未使用状态、包装完整，并保留全部配件与购买凭证；因商品质量问题退货时，运费由 NovaShop 承担，退款将在收货后 3 个工作日内原路退回；因个人原因（尺码、喜好等）退货时，运费由买家承担；特价清仓商品与定制商品不支持无理由退货；运输途中损坏的商品可以申请换货。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;问它：“鞋收到是坏的，退货运费谁出？”（环境变量沿用上一节，运行 &lt;code&gt;main_long_policy.py&lt;/code&gt;）&lt;/p&gt;
&lt;p&gt;这次的真实结果：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;-- 整篇政策作为一个候选（整篇只有一个分数）--
1. return-policy-full	score=0.585282	NovaTrail X2 退货政策：…

-- 同一篇政策拆成条款逐条打分（预告第三话的切分视角）--
1. clause-4	score=0.708807	因个人原因（尺码、喜好等）退货时，运费由买家承担；
2. clause-3	score=0.697954	因商品质量问题退货时，运费由 NovaShop 承担，退款将在收货后 3 个工作日内原路退回；
3. clause-6	score=0.675624	运输途中损坏的商品可以申请换货。
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;整篇政策只有一个分数 0.585282——我们只知道“这篇政策相关”，却不知道是哪个条款把它顶上去的。拆成条款后，三条与“运费/损坏”相关的条款全部浮到前面，定位明显变细。&lt;/p&gt;
&lt;p&gt;这里还有一层诚实的噪声：排第一的是条款 4（“个人原因，买家承担”），而不是条款 3（“质量问题，NovaShop 承担”）。“坏的”按语义应该指向质量问题，但“运费谁出”的表层表达把条款 4 顶了上来。也就是说：分块让“哪一段相关”变得可见，但并没有让排序变成正确——检索找到候选是一回事，候选能不能支撑回答是另一回事，那要等后面的章节处理。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第二话到这里，检索单元的粒度问题就摆上台面了：一整篇政策只算一个候选片段，粒度还是太粗。长文档到底该切成多大一块，才能既找得到规则，又不丢掉规则的上下文？&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="参考资料"&gt;参考资料
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://help.aliyun.com/zh/model-studio/embedding" target="_blank" rel="noopener"
 &gt;阿里云百炼：向量化（Embedding）&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://help.aliyun.com/zh/model-studio/text-embedding-synchronous-api/" target="_blank" rel="noopener"
 &gt;阿里云百炼：通用文本向量同步接口 API 详情&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://aclanthology.org/2020.emnlp-main.550/" target="_blank" rel="noopener"
 &gt;Dense Passage Retrieval for Open-Domain Question Answering&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>第一话｜大模型会聊天，但怎么让它知道你的退货政策？</title><link>https://zh.renxinblog.cn/post/gyr-c01-enterprise-rag-start/</link><pubDate>Fri, 21 Aug 2026 00:00:00 +0000</pubDate><guid>https://zh.renxinblog.cn/post/gyr-c01-enterprise-rag-start/</guid><description>&lt;img src="https://zh.renxinblog.cn/images/gyr-c01-enterprise-rag-start-cover.png" alt="Featured image of post 第一话｜大模型会聊天，但怎么让它知道你的退货政策？" /&gt;&lt;!--
GYR 第一话：RAG 系列总览
演进主线：裸大模型 → Prompt → 最小 RAG → 企业级知识助手
下一篇：Embedding 与手写 Top-K 检索
--&gt;
&lt;p&gt;“NovaTrail X2 支持 7 天无理由退货吗？”&lt;/p&gt;
&lt;p&gt;把这个问题交给一个裸大模型，它大概率会给出一段很像客服的话：商品需要保持未使用、包装完整，并保留购买凭证。&lt;/p&gt;
&lt;p&gt;听起来没什么问题。&lt;/p&gt;
&lt;p&gt;但它不知道 NovaShop 的真实退货政策。它没有看过这份商品资料，不知道规则适用于哪个地区，也不知道政策是不是昨天刚刚改过。&lt;/p&gt;
&lt;p&gt;它只是生成了一段听起来合理的文字。&lt;/p&gt;
&lt;p&gt;这就是 RAG 系列要解决的起点：&lt;strong&gt;大模型虽然会聊天，但它不知道你的退货政策。&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="一裸大模型会生成答案但没有企业知识"&gt;一、裸大模型：会生成答案，但没有企业知识
&lt;/h2&gt;&lt;p&gt;先不谈 RAG，看看最原始的系统是什么样：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;用户问题 → Chat 模型 → 文本回答
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这类模型可以翻译、总结、写代码，也能把客服话术写得很自然。但它的回答依赖两类输入：训练时学到的参数知识，以及本次请求中传入的上下文。&lt;/p&gt;
&lt;p&gt;企业自己的商品政策、内部流程、最新价格和租户数据，通常不在它能够直接访问的上下文里。模型并不会因为“你是 NovaShop 客服”这句话，就自动获得 NovaShop 的资料。&lt;/p&gt;
&lt;p&gt;所以裸模型的第一个边界很清楚：&lt;strong&gt;它可以生成企业知识问答的语言，却没有企业知识问答的证据。&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="二第一种补救把资料塞进-prompt"&gt;二、第一种补救：把资料塞进 Prompt
&lt;/h2&gt;&lt;p&gt;最简单的办法，是把政策直接放进提示词：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;你是 NovaShop 客服。
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;以下是退货政策：
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;NovaTrail X2 支持签收后 7 天内无理由退货，商品须未使用、包装完整，并保留配件和购买凭证。
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;请根据这段政策回答用户的问题。
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这个办法确实有效。模型现在能看到资料，也能根据资料生成回答。&lt;/p&gt;
&lt;p&gt;问题是，企业资料不会永远只有一段。商品规格、物流规则、保修政策、不同地区的退货政策和新旧版本都会继续增加。最后，Prompt 会变成一份越来越长、越来越难维护的知识文档。&lt;/p&gt;
&lt;p&gt;当回答出错时，排查也很困难：资料没有放进去？放进去了但没有检索到？检索到了但模型没有使用？还是引用了已经过期的版本？&lt;/p&gt;
&lt;p&gt;Prompt 方案解决的是“把资料放到模型眼前”，却没有解决“资料如何管理、如何查找、如何证明回答有依据”。&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;&lt;strong&gt;关键洞察：&lt;/strong&gt; Prompt 可以临时装下知识，但不能替代一条可维护、可追溯的知识链路。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;h2 id="三第二种补救先检索再生成"&gt;三、第二种补救：先检索，再生成
&lt;/h2&gt;&lt;p&gt;RAG 在 Prompt 方案前面增加了一步：模型回答之前，系统先去资料中找相关内容。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;用户问题
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;检索相关资料
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;组装上下文
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Chat 模型生成回答
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;返回引用或拒答
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这条链路可以拆成五个动作：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;动作&lt;/th&gt;
					&lt;th&gt;要回答的问题&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;文档准备&lt;/td&gt;
					&lt;td&gt;企业资料从哪里来？&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;检索&lt;/td&gt;
					&lt;td&gt;哪些片段和当前问题有关？&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;上下文组装&lt;/td&gt;
					&lt;td&gt;哪些内容真正交给模型？&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;生成&lt;/td&gt;
					&lt;td&gt;模型是否只根据这些内容回答？&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;引用与拒答&lt;/td&gt;
					&lt;td&gt;能否指出依据，证据不足时能否不回答？&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;RAG 原始论文把外部检索到的知识接入生成过程，用来缓解参数知识难以更新、访问和追溯的问题。&lt;a class="link" href="https://arxiv.org/abs/2005.11401" target="_blank" rel="noopener"
 &gt;Lewis 等，&lt;em&gt;Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks&lt;/em&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;这里还要先纠正一个常见误解：&lt;strong&gt;RAG 不等于向量数据库。&lt;/strong&gt; Retriever 可以使用关键词检索、Elasticsearch 的分词器和倒排索引、向量检索、结构化查询，也可以把几种方式组合成混合检索。本系列会在后续文章里分别比较这些方式；本篇只把“检索”当作一个黑盒步骤，不展开它的内部算法。&lt;a class="link" href="https://www.elastic.co/docs/solutions/search/search-approaches" target="_blank" rel="noopener"
 &gt;Elasticsearch，&lt;em&gt;Search approaches&lt;/em&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="四从最小-rag-到企业级知识助手"&gt;四、从最小 RAG 到企业级知识助手
&lt;/h2&gt;&lt;p&gt;RAG 让模型“回答前先查资料”，但这还只是企业知识助手的中间一层。真正的系统需要沿着两条链路运行。&lt;/p&gt;
&lt;h3 id="1-文档入库链路"&gt;1. 文档入库链路
&lt;/h3&gt;&lt;p&gt;企业资料不是凭空出现在检索器里的，它要先经过入库：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;文档上传
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;→ 解析与清洗
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;→ 分块
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;→ 建立索引
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;→ 保存版本和来源
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这一条链路解决的是“系统手里到底有什么资料”。文档解析决定哪些内容能被看见，分块决定检索的最小单位，索引决定怎么找到它们，版本和来源决定答案能不能回溯。&lt;/p&gt;
&lt;h3 id="2-问答链路"&gt;2. 问答链路
&lt;/h3&gt;&lt;p&gt;用户提问时，系统再走另一条链路：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;用户问题
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;→ 检索候选片段
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;→ 权限与版本过滤
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;→ 组装上下文
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;→ 模型回答
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;→ 引用、拒答与记录
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;两条链路合起来，才是企业级 RAG 的基本轮廓：&lt;/p&gt;
&lt;pre class="mermaid" style="visibility:hidden"&gt;%%{init: {'theme':'neutral'}}%%
flowchart LR
 A["企业文档"] --&gt; B["解析与索引"]
 B --&gt; C["检索"]
 D["用户问题"] --&gt; C
 C --&gt; E["上下文与模型"]
 E --&gt; F["回答与引用"]&lt;/pre&gt;&lt;p&gt;这张图只画了主链路，没有画出所有生产组件。评测、权限、版本、任务恢复和观测会横向影响多个节点，后面分别展开。&lt;/p&gt;
&lt;h2 id="五跑一次全链路预览"&gt;五、跑一次全链路预览
&lt;/h2&gt;&lt;p&gt;理论地图有了，接着跑最小实现。配套代码目前保存在私有工作区的 &lt;code&gt;code/agent-tutorial/c01-rag-overview/&lt;/code&gt;，尚未作为公开代码仓库发布；实验只依赖 Python 3.10+ 标准库。云端运行需要百炼兼容模式的 &lt;code&gt;DASHSCOPE_API_KEY&lt;/code&gt;。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; code/agent-tutorial/c01-rag-overview
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;export&lt;/span&gt; &lt;span class="nv"&gt;DASHSCOPE_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;你的Key
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;python3 main.py
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;本次预览实验使用阿里云百炼：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;角色&lt;/th&gt;
					&lt;th&gt;模型&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;Chat&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;deepseek-v4-flash-0731&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Embedding&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;qwen3.7-text-embedding&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;没有 Key 时，可以先验证本地链路：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;python3 main.py --offline
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;python3 -m unittest discover -s tests -v
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;离线模式只验证这条链路的数据结构、上下文组装和引用格式，不代表云端模型的回答效果。云端跑通的判定标准是：看到模型配置、命中的来源、上下文字符数、回答，以及形如 &lt;code&gt;[source:returns-cn#chunk-001]&lt;/code&gt; 的引用。至于系统为什么会把某些片段排在前面，下一篇再拆开解释。&lt;/p&gt;
&lt;p&gt;2026-08-21 的一次真实运行结果如下：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-json" data-lang="json"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nt"&gt;&amp;#34;mode&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;dashscope&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nt"&gt;&amp;#34;provider&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;aliyun-bailian&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nt"&gt;&amp;#34;chunks&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nt"&gt;&amp;#34;chat_model&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;deepseek-v4-flash-0731&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nt"&gt;&amp;#34;embedding_model&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;qwen3.7-text-embedding&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nt"&gt;&amp;#34;question&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;NovaTrail X2 是否支持 7 天无理由退货？&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nt"&gt;&amp;#34;sources&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;returns-cn#chunk-001&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;novatrail-x2#chunk-001&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="nt"&gt;&amp;#34;context_chars&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;215&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nt"&gt;&amp;#34;answer&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;支持。根据退货政策，NovaTrail X2 可在签收后 7 天内无理由退货，但需满足商品未使用、包装完整、保留配件和购买凭证等条件。 [source:returns-cn#chunk-001]&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nt"&gt;&amp;#34;question&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;NovaShop 是否提供月球基地配送？&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nt"&gt;&amp;#34;sources&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;returns-cn#chunk-001&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;novatrail-x2#chunk-001&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="nt"&gt;&amp;#34;context_chars&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;215&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nt"&gt;&amp;#34;answer&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;无法确认。现有资料中未提及月球基地配送服务。&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;上面是从真实输出中保留本篇关心字段后的结果。第一个问题走通了完整链路：系统返回了相关来源，模型根据上下文回答，并附上了来源标记。至于这些来源如何被计算和排序，本篇先不展开。&lt;/p&gt;
&lt;p&gt;第二个问题暴露了当前 Demo 的边界。资料里没有月球基地配送，但系统还是返回了两个候选片段，因为程序现在固定返回若干候选。模型这次回答“无法确认”，但无关片段已经进入上下文；换一个问题，模型可能会被这些噪声影响。&lt;/p&gt;
&lt;p&gt;这说明“能检索、能生成”还不等于“知识助手可靠”。我们还需要阈值、评测、引用校验和拒答策略。&lt;/p&gt;
&lt;p&gt;如果运行失败，先看错误发生在哪一层：&lt;code&gt;DASHSCOPE_API_KEY is not set&lt;/code&gt; 表示当前 Shell 没有读到 Key；&lt;code&gt;401&lt;/code&gt; 或 &lt;code&gt;403&lt;/code&gt; 通常是 Key 无效、过期或没有权限；&lt;code&gt;404 model not found&lt;/code&gt; 则要检查模型名、地域和百炼工作空间。代码默认使用 &lt;code&gt;aliyun-bailian&lt;/code&gt;、&lt;code&gt;deepseek-v4-flash-0731&lt;/code&gt; 和 &lt;code&gt;qwen3.7-text-embedding&lt;/code&gt;，也可以通过 README 中的环境变量覆盖。&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;&lt;strong&gt;关键洞察：&lt;/strong&gt; 这个 Demo 的价值不是证明 RAG 已经可靠，而是把“资料从哪里来、系统找了什么、模型看到了什么、回答引用了什么”第一次暴露出来。下一篇，我们再追问“系统为什么找到了这些片段”。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;h2 id="六gyr-系列接下来会补齐什么"&gt;六、GYR 系列接下来会补齐什么
&lt;/h2&gt;&lt;p&gt;现在可以回头看整个系列的路线。每一篇不是凭空增加一个组件，而是在解决上一层暴露出来的具体问题：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;阶段&lt;/th&gt;
					&lt;th&gt;要解决的问题&lt;/th&gt;
					&lt;th&gt;主题&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;第一话&lt;/td&gt;
					&lt;td&gt;整条链路到底是什么&lt;/td&gt;
					&lt;td&gt;RAG 全景与演进路线&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;第二话&lt;/td&gt;
					&lt;td&gt;问题和文本为什么可以比较&lt;/td&gt;
					&lt;td&gt;Embedding、余弦相似度、Top-K&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;第三话&lt;/td&gt;
					&lt;td&gt;一篇长文应该如何检索&lt;/td&gt;
					&lt;td&gt;文档分块与 Chunk 设计&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;第四话&lt;/td&gt;
					&lt;td&gt;找到资料后如何可靠回答&lt;/td&gt;
					&lt;td&gt;上下文、引用与拒答&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;第五话–第六话&lt;/td&gt;
					&lt;td&gt;如何保存和评价检索结果&lt;/td&gt;
					&lt;td&gt;PGVector、版本与评测基线&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;第七话–第八话&lt;/td&gt;
					&lt;td&gt;关键词和向量如何配合&lt;/td&gt;
					&lt;td&gt;检索优化与混合检索&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;第九话–第十二话&lt;/td&gt;
					&lt;td&gt;企业资料如何进入系统并保持边界&lt;/td&gt;
					&lt;td&gt;解析、租户权限、任务恢复与观测&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;第十三话–第十四话&lt;/td&gt;
					&lt;td&gt;如何收敛为可运行的服务&lt;/td&gt;
					&lt;td&gt;API、模块化原型与框架对照&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;这条路线有一个刻意的约束：先手写最小机制，再引入数据库、服务和框架。否则读者很容易得到一个“能启动的 RAG”，却不知道每个组件为什么存在。&lt;/p&gt;
&lt;h2 id="下一篇embedding-到底做了什么"&gt;下一篇：Embedding 到底做了什么
&lt;/h2&gt;&lt;p&gt;现在我们知道 RAG 的整体链路，也看到一个最小版本确实可以把问题和资料交给模型。但“检索”仍然像一个黑盒：为什么退货政策排在前面？相似度分数是什么？SKU 这种精确编号也适合这样检索吗？&lt;/p&gt;
&lt;p&gt;下一篇只回答一个问题：&lt;strong&gt;一句用户问题，为什么可以和一段商品政策计算相似度？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;我们会检查 Embedding 的真实输出，手写余弦相似度和 Top-K 排序，把第一话图里的“检索”拆开来看。&lt;/p&gt;
&lt;p&gt;第一篇只需要记住一句话：&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;&lt;strong&gt;RAG 是裸大模型走向企业知识助手的第一条外部知识链路，但它远没有替企业完成评测、权限和数据治理。&lt;/strong&gt;&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;h2 id="参考资料"&gt;参考资料
&lt;/h2&gt;&lt;ol&gt;
&lt;li&gt;Patrick Lewis 等，&lt;a class="link" href="https://arxiv.org/abs/2005.11401" target="_blank" rel="noopener"
 &gt;&lt;em&gt;Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks&lt;/em&gt;&lt;/a&gt;，2020。&lt;/li&gt;
&lt;li&gt;Alibaba Cloud Model Studio，&lt;a class="link" href="https://help.aliyun.com/en/model-studio/list-models" target="_blank" rel="noopener"
 &gt;&lt;em&gt;List models&lt;/em&gt;&lt;/a&gt;。&lt;/li&gt;
&lt;li&gt;Alibaba Cloud Model Studio，&lt;a class="link" href="https://help.aliyun.com/zh/model-studio/deepseek-api" target="_blank" rel="noopener"
 &gt;&lt;em&gt;DeepSeek API&lt;/em&gt;&lt;/a&gt;。&lt;/li&gt;
&lt;li&gt;Elasticsearch，&lt;a class="link" href="https://www.elastic.co/docs/solutions/search/search-approaches" target="_blank" rel="noopener"
 &gt;&lt;em&gt;Search approaches&lt;/em&gt;&lt;/a&gt;。&lt;/li&gt;
&lt;/ol&gt;</description></item><item><title>RAG 分块策略：为什么你的知识库答非所问，问题多半出在这里</title><link>https://zh.renxinblog.cn/post/rag-chunking-strategies/</link><pubDate>Thu, 13 Aug 2026 00:00:00 +0000</pubDate><guid>https://zh.renxinblog.cn/post/rag-chunking-strategies/</guid><description>&lt;img src="https://zh.renxinblog.cn/images/rag-chunking-strategies-cover.png" alt="Featured image of post RAG 分块策略：为什么你的知识库答非所问，问题多半出在这里" /&gt;&lt;p&gt;你搭了一个知识库问答系统：上传文档、建好索引、跑通 demo——然后 LLM 开始答非所问。&lt;/p&gt;
&lt;p&gt;你以为是模型不够聪明。换更强的模型，还是不对。&lt;/p&gt;
&lt;p&gt;问题很可能出在一个不起眼的环节：&lt;strong&gt;文档入库时的分块&lt;/strong&gt;。LLM 根本没读到该读的内容，模型再强也白搭。&lt;/p&gt;
&lt;p&gt;这篇文章把分块这件事讲透：为什么要分块、五种分块策略分别解决什么问题、生产环境到底怎么选。&lt;/p&gt;
&lt;h2 id="为什么要分块三个绕不过去的原因"&gt;为什么要分块？三个绕不过去的原因
&lt;/h2&gt;&lt;p&gt;先把结论摆出来：解析完的文档，不能整篇扔进检索。&lt;/p&gt;
&lt;p&gt;原因有三个，每一个都独立成立。&lt;/p&gt;
&lt;h3 id="原因一上下文窗口是有限的"&gt;原因一：上下文窗口是有限的
&lt;/h3&gt;&lt;p&gt;LLM 的上下文窗口再大也有上限。就算现在有 128K、1M 上下文的模型，检索也不是把整个文档库塞进 Prompt——那样成本直接爆炸。&lt;/p&gt;
&lt;p&gt;更重要的是，窗口越大，模型越难从中找到准确的那一段。这就是著名的&amp;quot;大海捞针&amp;quot;问题：上下文拉长，检索精度反而下降。&lt;/p&gt;
&lt;h3 id="原因二检索信噪比"&gt;原因二：检索信噪比
&lt;/h3&gt;&lt;p&gt;用户问的是文档里的一个小问题，但整篇文档有几千字。&lt;/p&gt;
&lt;p&gt;整篇匹配会发生什么？不相关的内容把相关内容的信号稀释了。向量检索返回的 Top-K 结果里，噪音占了大多数，相关的那一小段被埋没。&lt;/p&gt;
&lt;p&gt;块太大 → 无关信息稀释信号。这是信噪比的第一面。&lt;/p&gt;
&lt;h3 id="原因三embedding-对长文本的平均化"&gt;原因三：Embedding 对长文本的&amp;quot;平均化&amp;quot;
&lt;/h3&gt;&lt;p&gt;Embedding 模型擅长表示短文本——一两句话的语义最清晰。&lt;/p&gt;
&lt;p&gt;文本一长，向量就被&amp;quot;平均&amp;quot;了：整段的语义混在一起，变成一个模糊的方向。检索时，这个模糊向量既匹配不上问题，也匹配不上文档里的任何精确信息。&lt;/p&gt;
&lt;p&gt;Embedding 模型对短文本的语义表示最好，长文本会被&amp;quot;平均&amp;quot;成模糊向量。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;三个原因叠加，结论只有一个：&lt;strong&gt;必须把文档切成合适大小的块&lt;/strong&gt;。&lt;/p&gt;
&lt;pre class="mermaid" style="visibility:hidden"&gt;graph LR
 A["① 上下文窗口有限"] --&gt; D["必须分块"]
 B["② 检索信噪比"] --&gt; D
 C["③ Embedding 平均化"] --&gt; D
 style A fill:#dbeafe,stroke:#2563eb,color:#1e3a5f
 style B fill:#dbeafe,stroke:#2563eb,color:#1e3a5f
 style C fill:#dbeafe,stroke:#2563eb,color:#1e3a5f
 style D fill:#d1fae5,stroke:#059669,color:#064e3b&lt;/pre&gt;&lt;p&gt;但怎么切，才是真正的问题。切小了语境不够，切大了检索不精——这就是分块的核心矛盾：&lt;strong&gt;检索精度 vs 上下文完整性&lt;/strong&gt;。&lt;/p&gt;
&lt;h2 id="分块的两个基础参数"&gt;分块的两个基础参数
&lt;/h2&gt;&lt;p&gt;在讨论策略之前，先理解所有分块策略都绕不开的两个参数。&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;参数&lt;/th&gt;
					&lt;th&gt;含义&lt;/th&gt;
					&lt;th&gt;经验值（中文场景）&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;chunk_size&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;每块的大小&lt;/td&gt;
					&lt;td&gt;300-800 字（约 500-1200 tokens）&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;chunk_overlap&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;相邻块之间的重叠量&lt;/td&gt;
					&lt;td&gt;chunk_size 的 10%-20%&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;为什么需要 overlap？看一个例子。&lt;/p&gt;
&lt;pre class="mermaid" style="visibility:hidden"&gt;graph LR
 A["文档&lt;br/&gt;AAAABBBBCCCCDDDD"] --&gt; B["chunk_size=8, overlap=2&lt;br/&gt;块1: AAAABBBB&lt;br/&gt;块2: BBCCCCDD&lt;br/&gt;BB 被两块共享"]
 A --&gt; C["chunk_size=8, overlap=0&lt;br/&gt;块1: AAAABBBB&lt;br/&gt;块2: CCCCDD&lt;br/&gt;BB-CC 之间语义断裂"]
 B --&gt; D["✅ 跨块内容仍可检索"]
 C --&gt; E["❌ 关键句恰好被切断时丢失"]
 style A fill:#dbeafe,stroke:#2563eb,color:#1e3a5f
 style B fill:#d1fae5,stroke:#059669,color:#064e3b
 style C fill:#fef9c3,stroke:#ca8a04,color:#713f12
 style D fill:#d1fae5,stroke:#059669,color:#064e3b
 style E fill:#fee2e2,stroke:#dc2626,color:#7f1d1d&lt;/pre&gt;&lt;p&gt;overlap=0 时，如果&amp;quot;BBCC&amp;quot;恰好是完整语义（比如一个概念的完整描述），它被拦腰切断，两块都不完整。检索时哪块都匹配不精准，LLM 拿到的都是残句。&lt;/p&gt;
&lt;p&gt;overlap 就是给跨块语义留的缓冲带。&lt;/p&gt;
&lt;h2 id="五种分块策略逐个拆解"&gt;五种分块策略，逐个拆解
&lt;/h2&gt;&lt;p&gt;理解了参数，进入正题。分块策略从简单到复杂，正好是一条问题驱动的问题链。&lt;/p&gt;
&lt;h3 id="策略一固定大小分块最简单也最粗暴"&gt;策略一：固定大小分块——最简单，也最粗暴
&lt;/h3&gt;&lt;p&gt;做法：按预设的字符数或 token 数直接切。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# LangChain&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;text_splitter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;CharacterTextSplitter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;chunk_size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;chunk_overlap&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;优点只有一个：实现最简单、速度最快。&lt;/p&gt;
&lt;p&gt;缺点却很致命：&lt;strong&gt;完全无视语义边界&lt;/strong&gt;。可能在句子中间、甚至词语中间切断。&lt;/p&gt;
&lt;p&gt;切出来的块，很多是&amp;quot;半个意思&amp;quot;。检索时匹配到半句话，LLM 看着上下文猜——这正是答非所问的来源。&lt;/p&gt;
&lt;p&gt;适合日志、代码等结构弱、语义依赖不强的文本。自然语言文档用它，基本等于摆烂。&lt;/p&gt;
&lt;h3 id="策略二递归分块默认策略性价比之王"&gt;策略二：递归分块——默认策略，性价比之王
&lt;/h3&gt;&lt;p&gt;做法：按优先级尝试分隔符，从大到小逐级切。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# LangChain&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;text_splitter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;RecursiveCharacterTextSplitter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;chunk_size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;chunk_overlap&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;separators&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;。&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;，&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34; &amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;pre class="mermaid" style="visibility:hidden"&gt;graph LR
 A["超长段落"] --&gt; B{"按优先级逐级切分&lt;br/&gt;段落 → 换行 → 句子 → 词"}
 B --&gt;|"每块 ≤ 上限"| C["✅ 在语义边界成块"]
 B --&gt;|"仍超限"| D["降级到更细的分隔符"]
 D --&gt; B
 style A fill:#dbeafe,stroke:#2563eb,color:#1e3a5f
 style B fill:#fef9c3,stroke:#ca8a04,color:#713f12
 style C fill:#d1fae5,stroke:#059669,color:#064e3b
 style D fill:#fef9c3,stroke:#ca8a04,color:#713f12&lt;/pre&gt;&lt;p&gt;思路：优先在语义最完整的边界（段落、换行、句号）切断，实在超限才降级到更细的分隔符。&lt;/p&gt;
&lt;p&gt;优点：&lt;strong&gt;在保持语义边界和控制块大小之间取得最佳平衡&lt;/strong&gt;，通用场景的默认选择。&lt;/p&gt;
&lt;p&gt;缺点：分隔符优先级需要按文档类型微调；对复杂结构文档（表格、代码）力不从心。&lt;/p&gt;
&lt;h3 id="策略三语义分块让-embedding-参与决策"&gt;策略三：语义分块——让 Embedding 参与决策
&lt;/h3&gt;&lt;p&gt;递归分块是按&amp;quot;形式&amp;quot;切（分隔符）。语义分块按&amp;quot;意思&amp;quot;切。&lt;/p&gt;
&lt;p&gt;做法：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;把文本拆成句子&lt;/li&gt;
&lt;li&gt;计算每个句子的 Embedding 向量&lt;/li&gt;
&lt;li&gt;计算相邻句子的语义相似度&lt;/li&gt;
&lt;li&gt;在相似度&amp;quot;断崖&amp;quot;处切分&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class="mermaid" style="visibility:hidden"&gt;graph LR
 A["句子1: 聚类算法把样本分组"] --&gt; B["句子2: K-Means 是经典算法"]
 B --&gt; C["句子3: 明天北京多云转晴"]
 C --&gt; D["语义断崖&lt;br/&gt;句子2→3 相似度骤降&lt;br/&gt;在这里切"]
 A -.-&gt;|"高相似"| B
 B -.-&gt;|"低相似"| C
 style A fill:#dbeafe,stroke:#2563eb,color:#1e3a5f
 style B fill:#dbeafe,stroke:#2563eb,color:#1e3a5f
 style C fill:#fee2e2,stroke:#dc2626,color:#7f1d1d
 style D fill:#fef9c3,stroke:#ca8a04,color:#713f12&lt;/pre&gt;&lt;p&gt;核心算法是新颖度检测：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;novelty(s_i) = 1 - cosine_sim(embed(s_i), embed(context_window))
当 novelty &amp;gt; 均值 + 0.8 * 标准差 → 切分
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;优点：语义连贯性最好，检索精度最高。这也是为什么生产环境在&amp;quot;疑难块&amp;quot;上会启用它。&lt;/p&gt;
&lt;p&gt;缺点：每个句子都要算一次 Embedding，&lt;strong&gt;计算成本翻倍&lt;/strong&gt;。全量用不划算，适合作为精修手段。&lt;/p&gt;
&lt;p&gt;工具：semchunk、LlamaIndex SemanticSplitterNodeParser。官方 benchmark 里，semchunk 的 AI 分块模式在 Legal RAG QA 上正确率 37.7%，比 LangChain 递归分块的 34.8% 高约 2.9 个百分点。&lt;/p&gt;
&lt;h3 id="策略四结构感知分块利用文档自身的结构"&gt;策略四：结构感知分块——利用文档自身的结构
&lt;/h3&gt;&lt;p&gt;如果文档本身有结构，为什么不直接用？&lt;/p&gt;
&lt;p&gt;做法：按文档的固有边界切——Markdown 按标题层级、HTML 按标签、代码按函数类定义、对话按轮次。&lt;/p&gt;
&lt;p&gt;关键技巧是&lt;strong&gt;注入结构元数据&lt;/strong&gt;：每个 chunk 附带它在文档中的位置路径。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-json" data-lang="json"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;content&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;聚类算法的核心思想是...&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;metadata&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;header_path&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;第三章 &amp;gt; 3.2 无监督学习 &amp;gt; 3.2.1 K-Means&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;section_level&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;好处很实在：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;逻辑清晰，切块天然语义完整&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;检索结果自带上下文&lt;/strong&gt;——LLM 不光看到这段内容，还知道它来自哪一章哪一节&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class="mermaid" style="visibility:hidden"&gt;graph LR
 A["第三章 无监督学习"] --&gt; B["3.2 无监督学习"]
 B --&gt; C["3.2.1 K-Means&lt;br/&gt;chunk 携带 header_path"]
 C --&gt; D["检索命中 3.2.1 时&lt;br/&gt;LLM 知道完整上下文位置"]
 style A fill:#dbeafe,stroke:#2563eb,color:#1e3a5f
 style B fill:#dbeafe,stroke:#2563eb,color:#1e3a5f
 style C fill:#d1fae5,stroke:#059669,color:#064e3b
 style D fill:#d1fae5,stroke:#059669,color:#064e3b&lt;/pre&gt;&lt;p&gt;缺点：依赖文档格式规范。纯文本、扫描件这类无结构文档，它无从下手。&lt;/p&gt;
&lt;h3 id="策略五父子分块生产环境的首选"&gt;策略五：父子分块——生产环境的首选
&lt;/h3&gt;&lt;p&gt;前面的策略都在解决一个问题：块大还是块小。父子分块跳出这个取舍——&lt;strong&gt;大小都要&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;做法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;父块&lt;/strong&gt;：段落/小节级别（500-1000 tokens），提供完整上下文&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;子块&lt;/strong&gt;：句子级别（100-200 tokens），负责精确检索&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;检索流程：用小的找，用大的答。&lt;/p&gt;
&lt;pre class="mermaid" style="visibility:hidden"&gt;graph LR
 A["用户提问"] --&gt; B["在子块中检索&lt;br/&gt;句子级别，高精度"]
 B --&gt; C["命中子块：&lt;br/&gt;K-Means 是经典算法"]
 C --&gt; D["通过 parent_id 找到父块"]
 D --&gt; E["返回父块完整内容&lt;br/&gt;上下文完整，LLM 能理解"]
 style A fill:#dbeafe,stroke:#2563eb,color:#1e3a5f
 style B fill:#d1fae5,stroke:#059669,color:#064e3b
 style C fill:#d1fae5,stroke:#059669,color:#064e3b
 style D fill:#fef9c3,stroke:#ca8a04,color:#713f12
 style E fill:#d1fae5,stroke:#059669,color:#064e3b&lt;/pre&gt;&lt;pre class="mermaid" style="visibility:hidden"&gt;graph LR
 subgraph P["父块：3.2.1 完整小节"]
 C1["子块1: 算法定义"]
 C2["子块2: 参数说明"]
 C3["子块3: 适用场景"]
 end
 Q["问题命中子块2"] --&gt; C2
 C2 --&gt; P
 P --&gt; LLM["LLM 收到父块全文"]
 style P fill:#dbeafe,stroke:#2563eb,color:#1e3a5f
 style C1 fill:#d1fae5,stroke:#059669,color:#064e3b
 style C2 fill:#fef9c3,stroke:#ca8a04,color:#713f12
 style C3 fill:#d1fae5,stroke:#059669,color:#064e3b
 style Q fill:#dbeafe,stroke:#2563eb,color:#1e3a5f
 style LLM fill:#d1fae5,stroke:#059669,color:#064e3b&lt;/pre&gt;&lt;p&gt;优点：&lt;strong&gt;检索精度与上下文完整性兼得&lt;/strong&gt;——化解了分块最核心的矛盾。小粒度保证命中精准，大粒度保证 LLM 拿到完整语境，避免&amp;quot;找到了关键句但缺前因后果&amp;quot;。&lt;/p&gt;
&lt;p&gt;缺点：存储开销翻倍（父子块都要存向量）、实现复杂度较高。&lt;/p&gt;
&lt;p&gt;工具：LlamaIndex 的 auto-merging retriever / recursive retriever 就是这种思路的官方实现。&lt;/p&gt;
&lt;h2 id="生产环境怎么选"&gt;生产环境怎么选
&lt;/h2&gt;&lt;p&gt;五种策略各有适用场景，先看总对比。&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;策略&lt;/th&gt;
					&lt;th&gt;原理&lt;/th&gt;
					&lt;th&gt;优点&lt;/th&gt;
					&lt;th&gt;缺点&lt;/th&gt;
					&lt;th&gt;适用场景&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;固定大小&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;按字符/token 数硬切&lt;/td&gt;
					&lt;td&gt;最简单、最快&lt;/td&gt;
					&lt;td&gt;无视语义，可能在句中切断&lt;/td&gt;
					&lt;td&gt;日志、代码等弱语义文本&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;递归分块&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;按分隔符优先级切&lt;/td&gt;
					&lt;td&gt;语义边界与块大小平衡，通用&lt;/td&gt;
					&lt;td&gt;需调分隔符；复杂结构无力&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;通用默认&lt;/strong&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;语义分块&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;用 Embedding 找语义断崖&lt;/td&gt;
					&lt;td&gt;语义最连贯、精度最高&lt;/td&gt;
					&lt;td&gt;计算成本翻倍&lt;/td&gt;
					&lt;td&gt;高质量问答、疑难块精修&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;结构感知&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;按文档标题/标签/轮次切&lt;/td&gt;
					&lt;td&gt;语义完整、自带上下文&lt;/td&gt;
					&lt;td&gt;依赖文档格式规范&lt;/td&gt;
					&lt;td&gt;Markdown/HTML/代码文档&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;父子分块&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;子块检索、父块作答&lt;/td&gt;
					&lt;td&gt;精度与上下文兼得&lt;/td&gt;
					&lt;td&gt;存储翻倍、实现复杂&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;生产环境首选&lt;/strong&gt;&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;单看这张表，你可能会问：到底用哪个？&lt;/p&gt;
&lt;p&gt;答案是：&lt;strong&gt;生产环境用混合分块&lt;/strong&gt;。单一策略覆盖不了所有文档类型——你的知识库里既有 Markdown 规范文档，也有扫描 PDF，还有代码片段。&lt;/p&gt;
&lt;pre class="mermaid" style="visibility:hidden"&gt;graph LR
 B["① 结构感知粗切&lt;br/&gt;按标题/章节"] --&gt; C{"块大小判断"}
 C --&gt;|"过小"| D["合并相邻块"]
 C --&gt;|"合适"| E["直接使用"]
 C --&gt;|"过大"| F["递归/语义细分"]
 D --&gt; G["② 父子分块映射&lt;br/&gt;+ 元数据注入"]
 E --&gt; G
 F --&gt; G
 style B fill:#dbeafe,stroke:#2563eb,color:#1e3a5f
 style C fill:#fef9c3,stroke:#ca8a04,color:#713f12
 style D fill:#d1fae5,stroke:#059669,color:#064e3b
 style E fill:#d1fae5,stroke:#059669,color:#064e3b
 style F fill:#d1fae5,stroke:#059669,color:#064e3b
 style G fill:#dbeafe,stroke:#2563eb,color:#1e3a5f&lt;/pre&gt;&lt;p&gt;混合分块的思路：结构感知先粗切出框架，块大小判断决定后续处理（过小合并、合适直接用、过大递归/语义细分），最后用父子分块建立层级，注入元数据。&lt;/p&gt;
&lt;h2 id="落地建议"&gt;落地建议
&lt;/h2&gt;&lt;p&gt;回到开头的场景——知识库答非所问，怎么排查？&lt;/p&gt;
&lt;p&gt;按这个顺序自查，90% 的问题能定位：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;先看分块&lt;/strong&gt;：打开一个 chunk 看看，句子是不是被拦腰切断？上下文是不是残缺？&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;默认从递归分块起步&lt;/strong&gt;：chunk_size 500-800、overlap 10%-20%，把基线跑通&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;结构化文档换结构感知&lt;/strong&gt;：你的文档有标题层级，就别浪费这个信息&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;检索精度不够再上父子分块&lt;/strong&gt;：让子块找、父块答，通常能显著提升&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;疑难块用语义分块精修&lt;/strong&gt;：只对检索差的高频问题块启用，控制成本&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;分块不是一个&amp;quot;选一次就完事&amp;quot;的环节。文档类型变了、检索效果变了，分块策略就要跟着调。这是 RAG 系统里最值得花时间的调优点之一——因为它是整个检索质量的起点，也是你最能控制的一环。&lt;/p&gt;
&lt;p&gt;&lt;em&gt;本文基于个人对 RAG 工程实践的梳理与官方文档验证（MinerU OmniDocBench v1.6 得分 95.39 来自其官方 README；semchunk 基准数据来自其官方 README 及 isaacus.com 博客）。&lt;/em&gt;&lt;/p&gt;</description></item><item><title>RAG 是什么？从一条文档的完整旅程说起</title><link>https://zh.renxinblog.cn/post/rag-overview/</link><pubDate>Fri, 10 Jul 2026 00:00:00 +0000</pubDate><guid>https://zh.renxinblog.cn/post/rag-overview/</guid><description>&lt;img src="https://zh.renxinblog.cn/images/rag-overview-cover.png" alt="Featured image of post RAG 是什么？从一条文档的完整旅程说起" /&gt;&lt;p&gt;你有没有想过一个问题——&lt;/p&gt;
&lt;p&gt;LLM 训练时读了几万亿字的互联网数据，几乎什么都知道。但它不知道你的业务数据——你的产品手册、售后政策、内部文档、客服对话记录。这些它没读过。&lt;/p&gt;
&lt;p&gt;怎么让它知道该知道的？&lt;/p&gt;
&lt;p&gt;这就是 RAG 要解决的问题。这篇文章从为什么需要 RAG 开始，一直讲到它的完整架构和最容易翻车的地方。&lt;/p&gt;
&lt;h2 id="llm-什么都知道但不知道你的业务"&gt;LLM 什么都知道，但不知道你的业务
&lt;/h2&gt;&lt;p&gt;LLM 有两个先天缺陷，在实际落地中很难绕过去。&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;缺陷&lt;/th&gt;
					&lt;th&gt;表现&lt;/th&gt;
					&lt;th&gt;影响&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;知识截止&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;训练数据停在某个时间点&lt;/td&gt;
					&lt;td&gt;问最新政策、实时数据，它不知道&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;不知道私有数据&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;没读过你的内部文档、产品手册、客服记录&lt;/td&gt;
					&lt;td&gt;业务场景下基本不可用&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;怎么解决？目前有三条路可以走。&lt;/p&gt;
&lt;h3 id="方案一微调fine-tuning"&gt;方案一：微调（Fine-tuning）
&lt;/h3&gt;&lt;p&gt;拿业务数据继续训练模型，让它把新知识学进去。&lt;/p&gt;
&lt;p&gt;听起来最直接，但实际操作成本很高。你需要准备高质量的训练数据、处理算力资源，而且每次业务文档更新了，又得重新训一遍。更麻烦的是，微调可能会破坏模型原有的通用能力——模型学会了你的业务术语，但写邮件的能力反而变差了。&lt;/p&gt;
&lt;h3 id="方案二塞长上下文long-context"&gt;方案二：塞长上下文（Long Context）
&lt;/h3&gt;&lt;p&gt;把所有资料都塞进 Prompt 里，让模型自己翻。&lt;/p&gt;
&lt;p&gt;实现最简单，不用动模型。现在有些模型支持 128K、1M 甚至更长的上下文，看起来够用。但实际效果没那么理想：一是 Token 成本会随着内容量线性增长；二是内容太多之后，模型很难从海量信息中找到准确的那一段——这就是著名的&amp;quot;大海捞针&amp;quot;问题。上下文再长，检索精度是会下降的。&lt;/p&gt;
&lt;h3 id="方案三rag检索增强生成"&gt;方案三：RAG（检索增强生成）
&lt;/h3&gt;&lt;p&gt;每次提问的时候，先去知识库里检索相关内容，拼到 Prompt 里，再让 LLM 基于这些材料回答。&lt;/p&gt;
&lt;p&gt;RAG 是目前最主流的方案。原因很实在：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;成本低&lt;/strong&gt;：不需要重新训练模型，省了算力&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;更新快&lt;/strong&gt;：知识库加个文档就行，不需要重训&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可追溯&lt;/strong&gt;：LLM 是看着你给的资料回答的，能知道它引用的是哪份文档&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;幻觉少&lt;/strong&gt;：基于具体材料生成，比凭空回答可靠得多&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;当然，RAG 也有自己的问题——它依赖检索质量。如果检索到的内容不对或者不完整，LLM 材料再好也回答不对。后面会详细讲这个。&lt;/p&gt;
&lt;h2 id="rag-不是搜索引擎它和搜索引擎的区别"&gt;RAG 不是搜索引擎——它和搜索引擎的区别
&lt;/h2&gt;&lt;p&gt;很多人第一次接触 RAG 的反应是：这不就是搜索引擎吗？用户搜关键词，系统返回匹配的文档——有什么新鲜的？&lt;/p&gt;
&lt;p&gt;这个理解对了一半，错的一半恰好是 RAG 的核心。&lt;/p&gt;
&lt;h3 id="搜索引擎esbm25怎么工作"&gt;搜索引擎（ES/BM25）怎么工作
&lt;/h3&gt;&lt;p&gt;你搜一个关键词，搜索引擎在你的文档库里找到包含这个词的所有文档，按匹配度排序返回。它做的是&lt;strong&gt;关键词精确匹配&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;比如说你搜&amp;quot;2025年Q3营收&amp;quot;，它能找到包含这几个词的那份财报。但如果你问&amp;quot;去年第三季度营收怎么样？&amp;quot;，它可能找不到——因为文档里写的是&amp;quot;2025年Q3营收同比增长15.3%&amp;quot;，关键词对不上。&lt;/p&gt;
&lt;pre class="mermaid" style="visibility:hidden"&gt;%%{init: {'theme':'neutral'}}%%
graph LR
 A["搜：去年第三季度营收怎么样？"] --&gt; B[ES 关键词匹配]
 B --&gt; C["找到「去年」「第三」「季度」「营收」"]
 C --&gt; D[❌ 文档写的是「Q3营收同比增长15.3%」&lt;br/&gt;关键词对不上，找不到]&lt;/pre&gt;&lt;p&gt;这是一直以来搜索引擎的运作方式。在&amp;quot;搜文档、找文件&amp;quot;的场景下非常好用，但在&amp;quot;回答问题&amp;quot;的场景下就显得力不从心。&lt;/p&gt;
&lt;h3 id="rag向量检索怎么工作"&gt;RAG（向量检索）怎么工作
&lt;/h3&gt;&lt;p&gt;RAG 不一样。它先把你的文档库转成一组向量（可以理解成一组数字坐标），用户提问时也把问题转成向量，然后计算问题向量和文档向量之间的距离。距离越近，说明内容越相关。&lt;/p&gt;
&lt;pre class="mermaid" style="visibility:hidden"&gt;%%{init: {'theme':'neutral'}}%%
graph LR
 A["同样的问题：去年第三季度营收怎么样？"] --&gt; B[RAG 向量检索]
 B --&gt; C[把问题和文档都转成向量&lt;br/&gt;计算语义距离]
 C --&gt; D[✅ 匹配到「Q3营收同比增长15.3%」&lt;br/&gt;语义相近，找到了]&lt;/pre&gt;&lt;p&gt;&amp;ldquo;去年第三季度营收怎么样？&amp;ldquo;和&amp;quot;2025年Q3营收同比增长15.3%&amp;ldquo;在字面上没有共同的关键词，但在语义上是同一件事。向量检索能捕捉到这种语义上的相近。&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;维度&lt;/th&gt;
					&lt;th&gt;搜索引擎（ES/BM25）&lt;/th&gt;
					&lt;th&gt;RAG（向量检索）&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;匹配方式&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;关键词精确匹配&lt;/td&gt;
					&lt;td&gt;语义匹配&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;搜&amp;quot;苹果&amp;rdquo;&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;返回含&amp;quot;苹果&amp;quot;二字的所有文档&lt;/td&gt;
					&lt;td&gt;能区分你在问水果还是手机品牌&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;输出&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;文档列表&lt;/td&gt;
					&lt;td&gt;相关内容 + LLM 生成的自然语言回答&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;适合场景&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;搜文档、找文件&lt;/td&gt;
					&lt;td&gt;回答问题、知识问答&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;局限性&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;同义词、近义词不识别&lt;/td&gt;
					&lt;td&gt;对数字/专有名词匹配不如精确搜索&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;所以搜索引擎和 RAG 不是替代关系，而是互补关系。生产环境最常用的方案是&lt;strong&gt;混合检索&lt;/strong&gt;：向量检索负责语义匹配，BM25 负责精确匹配，两者互补。&lt;/p&gt;
&lt;p&gt;但语义检索有一个前提条件：内容必须被正确地理解和切分。这就引出了下一个问题。&lt;/p&gt;
&lt;h2 id="一条文档在-rag-系统中的完整旅程"&gt;一条文档在 RAG 系统中的完整旅程
&lt;/h2&gt;&lt;p&gt;语义匹配的效果，很大程度上取决于文档在入库时被处理成了什么样子。一条文档从上传到最终被 LLM 用来回答问题，中间经历了一整套加工管道。&lt;/p&gt;
&lt;a href="https://zh.renxinblog.cn/images/rag-pipeline.svg" target="_blank"&gt;
 &lt;figure&gt;&lt;img src="https://zh.renxinblog.cn/images/rag-pipeline.svg"
 			alt="RAG系统完整管道图" width="100%"&gt;&lt;figcaption&gt;
 			&lt;p&gt;RAG 系统完整管道：入库阶段 → 检索阶段（点击图片查看大图）&lt;/p&gt;
 		&lt;/figcaption&gt;
 &lt;/figure&gt;

&lt;/a&gt;
&lt;h3 id="入库阶段让机器能读懂文档"&gt;入库阶段：让机器能&amp;quot;读懂&amp;quot;文档
&lt;/h3&gt;&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;环节&lt;/th&gt;
					&lt;th&gt;做什么&lt;/th&gt;
					&lt;th&gt;为什么重要&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;文档解析&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;把 PDF、Word、HTML 等格式转成纯文本&lt;/td&gt;
					&lt;td&gt;PDF 看着是文字，实际是一堆坐标指令。解析不好，后面的所有环节都白费&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;文本清洗&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;去噪、去重、格式归一化&lt;/td&gt;
					&lt;td&gt;把乱码、多余的空格、无关的页眉页脚清掉&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;分块&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;把长文本切成语义完整的片段&lt;/td&gt;
					&lt;td&gt;不能整篇检索——太大了检索不准，太小了语境不够&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;向量化&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;用 Embedding 模型把文本片段转成向量&lt;/td&gt;
					&lt;td&gt;向量是语义匹配的基础&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;存入向量库&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;把向量和原文存入向量数据库&lt;/td&gt;
					&lt;td&gt;供后续检索使用&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id="检索阶段找到相关内容并让-llm-回答"&gt;检索阶段：找到相关内容并让 LLM 回答
&lt;/h3&gt;&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;环节&lt;/th&gt;
					&lt;th&gt;做什么&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;问题向量化&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;把用户提问也转成向量&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;向量检索&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;在向量库里找距离最近的文档片段&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;重排序&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;对检索结果重新精排，提升 Top 结果的准确率&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;拼入 Prompt&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;把检索到的内容拼到 Prompt 里，加上指令让 LLM 基于材料回答&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id="为什么检索质量是关键"&gt;为什么检索质量是关键
&lt;/h3&gt;&lt;p&gt;整个管道里，最核心的瓶颈不是 LLM 本身，而是&lt;strong&gt;检索质量&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;Andrew Ng 在一个分享里提过一个数据：优化 RAG 系统时，检索质量的提升对最终答案准确率的影响远大于模型本身的升级。换句话说，用更强的模型不能弥补检索结果差的问题——模型再聪明，材料给错了也答不对。&lt;/p&gt;
&lt;p&gt;而检索质量又依赖于前面的入库环节：文档解析是否正确、分块是否合理、向量化是否准确。这是一个完整的链路，短板出在任何一环都会影响最终效果。&lt;/p&gt;
&lt;h2 id="最容易在哪里翻车"&gt;最容易在哪里翻车？
&lt;/h2&gt;&lt;p&gt;把各个环节按翻车概率排序，前三个是：&lt;/p&gt;
&lt;h3 id="1-文档解析最大的坑"&gt;1. 文档解析——最大的坑
&lt;/h3&gt;&lt;p&gt;你传上去的文档是 PDF。PDF 看着是文本，但本质上是一堆坐标指令的集合——&amp;ldquo;在坐标 (x,y) 处渲染字符 A，在 (x+5,y) 处渲染字符 B&amp;rdquo;。它并不知道什么是段落、什么是表格、什么是页眉。&lt;/p&gt;
&lt;p&gt;遇到以下几种 PDF，解析很容易翻车：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;PDF 类型&lt;/th&gt;
					&lt;th style="text-align: center"&gt;难度&lt;/th&gt;
					&lt;th&gt;翻车方式&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;数字原生 PDF&lt;/td&gt;
					&lt;td style="text-align: center"&gt;⭐ 简单&lt;/td&gt;
					&lt;td&gt;基本没问题&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;扫描件 PDF（图片）&lt;/td&gt;
					&lt;td style="text-align: center"&gt;⭐⭐⭐ 中等&lt;/td&gt;
					&lt;td&gt;需要 OCR，识别率取决于清晰度&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;双栏/多栏排版&lt;/td&gt;
					&lt;td style="text-align: center"&gt;⭐⭐⭐⭐ 困难&lt;/td&gt;
					&lt;td&gt;阅读顺序恢复错误，左右栏混在一起&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;含表格 PDF&lt;/td&gt;
					&lt;td style="text-align: center"&gt;⭐⭐⭐⭐⭐ 极难&lt;/td&gt;
					&lt;td&gt;无线框表格、合并单元格、跨页表格，很容易解析错位&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;含公式 PDF&lt;/td&gt;
					&lt;td style="text-align: center"&gt;⭐⭐⭐⭐⭐ 极难&lt;/td&gt;
					&lt;td&gt;LaTeX 公式、化学结构式，专用模型才能处理好&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id="2-分块切得不合适"&gt;2. 分块——切得不合适
&lt;/h3&gt;&lt;p&gt;解析完的文本不能整篇扔进去检索。整篇检索的问题：一篇文档可能几千字，但用户问的只是其中一小段。整篇匹配会让不相关的内容稀释相关内容的信号。&lt;/p&gt;
&lt;p&gt;但切得太细也不行。比如只切一两句话——检索时可能命中了，但上下文缺失，LLM 看了也不知道前因后果。&lt;/p&gt;
&lt;p&gt;这就是分块的核心矛盾：&lt;strong&gt;检索精度 vs 上下文完整性&lt;/strong&gt;。切小了检索精准但语境不足，切大了语境完整但检索不精准。&lt;/p&gt;
&lt;p&gt;实际生产中有好几种分块策略来解决这个矛盾——按固定大小切、按语义边界切、按文档结构切、父子分块等等。每种策略各有优缺点，选型取决于你的文档类型和场景。&lt;/p&gt;
&lt;h3 id="3-检索精确匹配-vs-语义匹配的盲区"&gt;3. 检索——精确匹配 vs 语义匹配的盲区
&lt;/h3&gt;&lt;p&gt;即使解析和分块都做好了，检索环节仍然可能翻车。典型场景：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;找不到相关内容&lt;/strong&gt;：你的文档里确实有答案，但向量检索没把它排到前面&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;找到太多噪音&lt;/strong&gt;：检索返回了大量不太相关的内容，稀释了有效信号&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数字/专有名词匹配弱&lt;/strong&gt;：向量检索对&amp;quot;Q3-2025营收同比增长率15.3%&amp;ldquo;这种精确表达不如关键词搜索&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这也是为什么生产环境通常用混合检索——语义 + 关键词组合，取长补短。&lt;/p&gt;
&lt;h2 id="回头看看"&gt;回头看看
&lt;/h2&gt;&lt;p&gt;把整篇文章的问题串起来：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;LLM 不知道你的业务数据
 → 三种方案对比，RAG 最实用（成本低、更新快、可追溯）
 → RAG 不是搜索引擎，是语义匹配
 → 但语义匹配的前提是内容被正确加工
 → 入库阶段：解析 → 清洗 → 分块 → 向量化 → 存储
 → 检索阶段：检索 → 重排序 → 拼入 Prompt → 生成
 → 最容易翻车的地方：解析、分块、检索
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;RAG 看起来简单——&amp;ldquo;查资料再回答&amp;rdquo;——但做了才发现，每一步都有坑。理解了这个全貌，后续不管是选型还是排查问题，都知道问题可能出在哪一环。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;参考资料&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Andrew Ng, &lt;em&gt;Building RAG Agents with LLMs&lt;/em&gt;, DeepLearning.AI, 2025 — 检索质量对 RAG 效果影响的分享&lt;/li&gt;
&lt;li&gt;Lewis et al., &lt;em&gt;Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks&lt;/em&gt;, arXiv:2005.11401, 2020 — RAG 论文原文&lt;/li&gt;
&lt;/ol&gt;</description></item><item><title>向量化原理——为什么向量能算语义相似度</title><link>https://zh.renxinblog.cn/post/embedding-principle/</link><pubDate>Fri, 10 Jul 2026 00:00:00 +0000</pubDate><guid>https://zh.renxinblog.cn/post/embedding-principle/</guid><description>&lt;img src="https://zh.renxinblog.cn/images/embedding-principle-cover.png" alt="Featured image of post 向量化原理——为什么向量能算语义相似度" /&gt;&lt;p&gt;你有没有想过一个问题——&lt;/p&gt;
&lt;p&gt;RAG 的核心是「语义匹配」：你说「去年第三季度营收怎么样」，它能找到文档里写的「Q3 营收同比增长 15.3%」。字面不一样，但意思一样。&lt;/p&gt;
&lt;p&gt;这到底是怎么做到的？一段文字怎么变成一个「向量」？为什么两个向量离得近就代表意思相近？&lt;/p&gt;
&lt;p&gt;这篇文章从最基础的概念讲起，不涉及复杂的数学公式。&lt;/p&gt;
&lt;h2 id="人能判断语义相似度机器也能"&gt;人能判断语义相似度，机器也能
&lt;/h2&gt;&lt;p&gt;先想一个简单的问题：人和人之间怎么判断两句话意思差不多？&lt;/p&gt;
&lt;p&gt;「今天天气真好」和「今天阳光明媚」——你一看就知道它们说的是一回事。&lt;/p&gt;
&lt;p&gt;背后的过程很复杂（你用了多年的语言经验、对词汇的理解、对语境的把握），但直觉上很简单：你在大脑里给这两句话各自建立了一个「语义位置」，发现它们靠得很近。&lt;/p&gt;
&lt;p&gt;机器做同样的事，只是用的方法不一样——它用&lt;strong&gt;向量&lt;/strong&gt;来表示语义位置。&lt;/p&gt;
&lt;h2 id="向量是什么一组数字而已"&gt;向量是什么？一组数字而已
&lt;/h2&gt;&lt;p&gt;向量听起来很高深，其实就是一组数字。&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;[0.52, -0.13, 0.87, -0.42, 0.11, ...]
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;你可以把向量想象成&lt;strong&gt;语义空间中的一个坐标点&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;比如我们想象一个二维的语义空间，每一段话都对应一个坐标点：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;「今天天气真好」→ 坐标 (0.9, 0.3)
「今天阳光明媚」→ 坐标 (0.8, 0.4)
「苹果很好吃」 → 坐标 (0.1, 0.2)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;在语义空间里，「今天天气真好」和「今天阳光明媚」靠得近（意思相近），和「苹果很好吃」离得远（不相关）。&lt;/p&gt;
&lt;figure&gt;&lt;img src="https://zh.renxinblog.cn/images/semantic-space.svg"
			alt="语义空间坐标示意图" width="80%"&gt;&lt;figcaption&gt;
			&lt;p&gt;二维语义空间示意：意思相近的文本在空间中靠得近&lt;/p&gt;
		&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;当然，现实中的语义向量不是两维，而是几百甚至几千维——但原理一样。两个向量在空间中的距离越近，代表它们的意思越相近。&lt;/p&gt;
&lt;h2 id="语义从哪来embedding-模型怎么训练的"&gt;语义从哪来：Embedding 模型怎么训练的
&lt;/h2&gt;&lt;p&gt;刚才的水果例子是我手动编的维度（甜度、大小）。但在真实场景中，不可能有人来给每个词标注「甜度 = 0.9」这种数据。&lt;/p&gt;
&lt;p&gt;那向量的数值从哪来？答案是 Embedding 模型自己学出来的。&lt;/p&gt;
&lt;p&gt;训练方法的核心思路很简单：&lt;strong&gt;出现在相似语境中的词，应该有相似的向量&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;一个经典的例子：&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;「国王」和「女王」经常出现在类似的句子里（「__ 统治着国家」「__ 戴着一顶王冠」）。所以它们的向量应该离得近。&lt;/p&gt;
&lt;p&gt;「国王」和「苹果」很少出现在同一个语境里。所以它们的向量应该离得远。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;训练时，模型会读海量的文本，对每个词做一个调整：如果两个词经常出现在相似的上下文里，就把它们的向量拉近一点；如果很少同时出现，就把它们推远一点。&lt;/p&gt;
&lt;p&gt;经过无数次这样的调整，模型最终学会了一个语义空间——意思相近的词，在空间里靠得近；意思不相关的词，离得远。&lt;/p&gt;
&lt;p&gt;这一步就是 Embedding 模型做的事。你给它输入一段文本，它返回一组向量。&lt;/p&gt;
&lt;pre class="mermaid" style="visibility:hidden"&gt;%%{init: {'theme':'neutral'}}%%
graph LR
 A["输入文本&lt;br/&gt;去年第三季度赚了多少"] --&gt; B[Embedding 模型]
 B --&gt; C["输出向量&lt;br/&gt;[0.23, -0.56, 0.91, ...]"]

 style A fill:#dbeafe,stroke:#3b82f6,color:#1e3a5f
 style B fill:#d1fae5,stroke:#10b981,color:#064e3b
 style C fill:#fce7f3,stroke:#ec4899,color:#831843&lt;/pre&gt;&lt;h2 id="如何判断两个向量离得近"&gt;如何判断两个向量离得近
&lt;/h2&gt;&lt;p&gt;有了向量之后，怎么计算两段文字的相似度？&lt;/p&gt;
&lt;p&gt;最常用的方法叫&lt;strong&gt;余弦相似度&lt;/strong&gt;（Cosine Similarity）。你不需要记住公式，只要理解它的直觉：&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;两个向量在空间中有一个夹角。夹角越小，说明方向越一致，内容越相似。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;完全相同的文本 → 夹角 0°，相似度 = 1&lt;/li&gt;
&lt;li&gt;完全不相关的文本 → 夹角 90°，相似度 ≈ 0&lt;/li&gt;
&lt;li&gt;意思相反的文本 → 夹角 180°，相似度 = -1&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class="mermaid" style="visibility:hidden"&gt;%%{init: {'theme':'neutral'}}%%
graph LR
 A["文本A: 今天天气真好"] --&gt; C[Embedding 模型]
 B["文本B: 今天阳光明媚"] --&gt; C
 C --&gt; D["向量A: [0.8, 0.5, ...]&lt;br/&gt;向量B: [0.7, 0.6, ...]"]
 D --&gt; E["余弦相似度 = 0.92&lt;br/&gt;✅ 意思相近"]

 style A fill:#dbeafe,stroke:#3b82f6,color:#1e3a5f
 style B fill:#dbeafe,stroke:#3b82f6,color:#1e3a5f
 style C fill:#d1fae5,stroke:#10b981,color:#064e3b
 style D fill:#fce7f3,stroke:#ec4899,color:#831843
 style E fill:#d1fae5,stroke:#10b981,color:#064e3b&lt;/pre&gt;&lt;h2 id="回到-rag向量检索就是这样工作的"&gt;回到 RAG：向量检索就是这样工作的
&lt;/h2&gt;&lt;p&gt;RAG 的检索过程，就是把上面的逻辑倒过来：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;入库时&lt;/strong&gt;：把每篇文档的每个片段都通过 Embedding 模型转成向量，存进向量数据库&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;查询时&lt;/strong&gt;：把用户的问题也转成向量&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;匹配时&lt;/strong&gt;：计算问题向量和所有文档向量的余弦相似度，找出最接近的那几个片段&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;用户问题：去年第三季度我们赚了多少钱？
 │
 ▼ Embedding 模型
 │
[0.23, -0.56, 0.91, ...]
 │
 ▼ 跟库里所有文档向量算相似度
 │
文档中匹配到的内容：Q3营收同比增长15.3%（相似度 0.87）
 去年全年营收1.2亿元（相似度 0.45）
 公司成立于2018年（相似度 0.12）
 │
 ▼ 取 Top-K，拼入 Prompt
 │
LLM 基于这些内容生成回答
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;向量检索不只是「找到包含关键词的文档」，而是「找到意思最相近的内容」。这也解释了为什么在 RAG 中，&lt;strong&gt;分块和向量化的质量决定了检索的天花板&lt;/strong&gt;——如果文档切得不好、向量化得不准确，后面用多好的 LLM 也弥补不了。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;参考资料&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Mikolov et al., &lt;em&gt;Efficient Estimation of Word Representations in Vector Space&lt;/em&gt;, arXiv:1301.3781, 2013 — Word2Vec，向量化技术的奠基论文&lt;/li&gt;
&lt;li&gt;Reimers &amp;amp; Gurevych, &lt;em&gt;Sentence-BERT: Sentence Embeddings using Siamese BERT-Networks&lt;/em&gt;, EMNLP 2019 — 现代 Embedding 模型的基础架构&lt;/li&gt;
&lt;li&gt;Datawhale, &lt;em&gt;hello-agents&lt;/em&gt;, 第八章 记忆与检索, &lt;a class="link" href="https://github.com/datawhalechina/hello-agents" target="_blank" rel="noopener"
 &gt;https://github.com/datawhalechina/hello-agents&lt;/a&gt; — 从认知科学到向量检索的实践教程&lt;/li&gt;
&lt;/ol&gt;</description></item></channel></rss>