小小博客Garden
Notes

Article

RAG 检索质量:分块策略与向量相似度

检索质量决定 RAG 的上限:聊聊为什么不能整篇向量化、如何做结构感知分块,以及余弦相似度与 top-K 的取舍。

很多人以为 RAG 的难点在生成,其实检索质量才是决定 RAG 上限的部分。如果检索回来的是无关片段,再强的模型也只能「一本正经地胡说」。这篇文章讲检索侧最基础也最重要的两个问题:怎么切、怎么比。

为什么不能整篇向量化

把一整篇文章压成一个向量,等于把文章里所有主题「平均」在一起。一篇同时讲「SSR」和「向量化」的文章,它的整体向量可能既不擅长回答 SSR 的问题,也不擅长回答向量化的问题。所以要先分块(chunking),让每个块聚焦一个相对单一的主题。

结构感知分块

最简单的是固定长度分块,但它会粗暴地把一句话从中间切断。我采用结构感知的方式:

  1. 按 Markdown 标题(#######)把文章切成章节;
  2. 章节内按句子合并成约 480 字符的窗口;
  3. 相邻窗口保留 80 字符重叠。

重叠的意义在于:一个关键句如果正好卡在两个窗口的边界,没有重叠就会被两边各切一半,检索时语义受损。

分块大小是个权衡:块太大,语义稀释、检索精度下降;块太小,上下文不完整、回答单薄。480 字符对中文技术文章是个比较舒服的区间。

余弦相似度

向量化之后,检索就变成了「找与问题向量最接近的块」。最常用的是余弦相似度:

function cosineSimilarity(a: number[], b: number[]): number {
  let dot = 0, normA = 0, normB = 0;
  for (let i = 0; i < a.length; i++) {
    dot += a[i] * b[i];
    normA += a[i] * a[i];
    normB += b[i] * b[i];
  }
  return dot / (Math.sqrt(normA) * Math.sqrt(normB));
}

它衡量的是两个向量的方向一致性,跟文本长度无关。如果 embedding 已经做了 L2 归一化(我用的 bge 模型就是这么配的),余弦相似度退化成点积,计算还能更快。

top-K 取多少

top-K 是「喂给模型多少个片段」。K 太小,可能漏掉关键信息;K 太大,噪声进来、还浪费 token。我的经验是 4 左右比较合适——既给了模型足够的上下文,又不会让它迷失。多个来源也能互相印证,回答更可靠。

内存里线性扫描够用吗

对于个人博客这种量级(几十到几百个 chunk),把向量存进数组、每次全量线性扫描算余弦,完全够用,单次检索毫秒级。真正的向量数据库(如 Qdrant、Milvus)解决的是百万级向量、高并发的场景,在这个项目里属于过度设计。

小结

RAG 检索侧没有银弹,但几个关键决策——结构感知分块、合适的块大小、重叠窗口、余弦相似度、克制的 top-K——叠加起来就能让检索质量上一个台阶。先把这些基本功做扎实,再考虑更花哨的召回重排也不迟。

Ask AI