最近在补 RAG 的基础,绕不开一个词:cosine similarity,中文一般叫 余弦相似度。
说实话,一开始我对这个词挺抗拒的。它看起来像是那种“你要先补线性代数才能继续往下看”的概念。尤其是看到 cosine、vector、embedding、dot product 这些词堆在一起时,很容易有一种感觉:我只是想知道 RAG 怎么查资料,怎么突然开始上数学课了。
但看了一圈之后,我发现如果只是为了理解 RAG 里的检索,余弦相似度没有那么吓人。它背后的直觉其实很朴素:
把两段文字都变成向量,然后看这两个向量是不是朝着差不多的方向。
方向越像,通常说明它们语义越接近。
这篇不是严格数学推导,更像是我自己的理解笔记。如果你也刚开始看 RAG、embedding、向量数据库,希望这篇能帮你少绕一点路。
我最开始卡住的地方
我之前一直有个误解:既然是“相似度”,那是不是就是看两个东西离得近不近?
比如两个点在图上,距离越短,就越相似。这个想法当然也没错,很多算法确实会用距离来衡量接近程度。但余弦相似度稍微不一样,它更关心的是“方向”,不是单纯的距离,也不是长度。
可以想象两个人从同一个地方出发。
一个人往东北走得很远,另一个人也往东北走,只是走得短一点。虽然他们走的距离不一样,但方向是一致的。
如果另一个人往西南走,那就算他走的距离差不多,也明显不是一回事。
余弦相似度看的就是这种感觉。
它想问的不是:
谁更长?谁数值更大?
而是:
它们是不是大概指向同一个方向?
这个直觉一旦建立起来,后面的公式就没那么难受了。公式只是把这个“方向像不像”变成一个分数。
文字为什么能比较方向?
这里还有一个前提:文字要先变成向量。
计算机没法直接理解一句话的意思。它看到的是字符、token、数字。为了让机器能比较“这句话和那句话是不是意思接近”,我们通常会用 embedding 模型把文本变成一串数字。
比如一句话:
RAG 是什么?
模型会把它变成一串数字,大概长这样:
[0.12, -0.35, 0.88, 0.04, ...]
这串数字不是给人看的,是给机器算的。你可以把它理解成这句话在“语义空间”里的一个位置,或者一个方向。
另一句话:
大模型怎么先查资料再回答?
它字面上和“RAG 是什么”不太一样,但意思可能很接近。embedding 模型如果做得不错,这两句话对应的向量方向就会比较接近。
这也是向量检索比纯关键词搜索有意思的地方:它不只看有没有同一个词,还试图看背后的意思是不是接近。
比如你搜索“怎么让 AI 看文档回答问题”,文档里可能写的是“Retrieval Augmented Generation”。关键词完全对不上,但语义上是在说同一个东西。向量检索就有机会把它找出来。
当然,这里我也不想把它说得太神。embedding 模型不是永远懂你,它只是把文本压成了一组数字表示。这个表示好不好,直接决定后面相似度算得准不准。
如果 embedding 本身就把意思表示歪了,余弦相似度算得再认真,也只是认真地算错。
一个小例子:口味相似
为了让这个更具体一点,可以先不用文本,想一个简单的口味例子。
假设我们只用两个维度来描述一个人的口味:
[喜欢甜的程度, 喜欢辣的程度]
小明是:
[9, 1]
小红是:
[8, 2]
小刚是:
[1, 9]
你大概一眼就能看出来,小明和小红更像。他们都偏甜口。小刚就不一样,他明显偏辣口。
如果把这些点从原点画成箭头,小明和小红的箭头方向会很接近,小刚的方向就偏得多。
这就是余弦相似度想捕捉的东西。
真实的文本向量当然不会只有两个维度。它可能有几百维、几千维。每一维也不是“甜”或者“辣”这么好解释。但直觉类似:模型把文本放进一个高维空间里,语义接近的文本会有相似的方向。
我们看不懂那个空间,但机器可以在里面计算。
公式不用背,但可以看一眼
余弦相似度的公式是:
cosine_similarity(A, B) = A · B / (||A|| × ||B||)
第一次看会觉得烦。
但如果只保留直觉,它就是在算两个向量夹角的 cos 值。
大概可以这样记:
接近 1:方向很像
接近 0:关系不大
接近 -1:方向相反
其中 A · B 是点积,||A|| 和 ||B|| 是两个向量的长度。分母把长度的影响除掉了,所以最后更关注方向。
这也是为什么它叫“余弦”相似度。它不是凭空来的,而是来自几何里的夹角。
在文本检索里,我们多数时候不用特别纠结每个分数的绝对意义,只要比较谁更高。
比如用户问:
RAG 的三个阶段是什么?
资料库里有三段:
chunk 1:RAG 包括 ingestion、retrieval 和 synthesis。
chunk 2:Prompt engineering 是设计提示词的方法。
chunk 3:模型微调可以让模型适应特定任务。
算出来可能是:
问题 vs chunk 1 = 0.91
问题 vs chunk 2 = 0.35
问题 vs chunk 3 = 0.28
那系统就会优先拿 chunk 1。
放到 RAG 里,它其实只是一个“找资料”的环节
RAG 这件事,如果说得朴素一点,就是让大模型回答之前先查资料。
但资料库可能很大,不可能每次把所有文档都塞给模型。比如一个公司的内部知识库,可能有产品文档、接口说明、会议记录、FAQ、故障复盘。用户问一句话,你不可能把全部内容都丢进 prompt。
所以中间需要一个检索步骤:先从一堆资料里挑出几段最可能有用的。
流程大概是:
文档切块
每个块变成向量
用户问题也变成向量
用余弦相似度找方向最接近的几个块
把这些块交给大模型回答
所以余弦相似度不是那个“生成答案”的东西。
它更像一个检索员。你问了一个问题,它先去资料库里翻一翻,挑几段它觉得可能相关的内容出来。后面怎么组织语言、怎么回答,那是大模型的事。
这个区分对我还挺重要的。因为一开始学 RAG 的时候,很容易把所有能力都归到 LLM 身上,好像模型什么都懂。其实很多时候,答案好不好,前面检索有没有找对资料,占了很大一部分。
如果检索拿错了材料,大模型写得再流畅,也只是在错误材料上发挥。
这也是为什么很多 RAG 问题,不一定是“模型不够强”,而是“资料没找对”。
关键词搜索和向量搜索不是敌人
还有一点我后来才慢慢理解:向量搜索不是用来完全取代关键词搜索的。
关键词搜索很直接,也很可靠。你搜一个错误码、函数名、配置项、产品编号,它往往比向量搜索更准。因为这些东西需要精确匹配,不是语义差不多就行。
但向量搜索擅长处理表达方式不一样、意思差不多的情况。
比如:
用户:为什么模型会胡说八道?
文档:LLM hallucination refers to generated content that is factually incorrect...
关键词不一定能命中,但向量可能觉得它们很接近。
所以真实系统里经常会做 hybrid search,也就是关键词搜索和向量搜索一起用。先从两个角度各找一批候选,再合并、重排。
这件事也提醒我:很多工程问题不是选一个“最先进”的方法,而是把几个朴素的方法组合好。
它好用,但别迷信
我觉得学习这类概念时,很容易走到两个极端。
一个极端是觉得公式太难,干脆不碰。
另一个极端是觉得只要用了向量、用了 cosine similarity,系统就智能了。
这两个都不太对。
余弦相似度确实很实用,但它只是一个相似度度量。它不会真正理解你的业务,也不会判断资料是否权威,更不会自动保证答案正确。
它至少有几个限制。
第一,embedding 表示不好,相似度也会跟着错。比如模型不擅长中文,或者对某个专业领域不熟,生成出来的向量可能就不靠谱。
第二,相似不等于有用。某段内容可能和问题主题相关,但并没有回答问题。用户问“RAG 常见失败点有哪些”,系统可能找到一段“RAG 是什么”。它相关,但不够有用。
第三,它只是在找候选资料,不是在做完整推理。真正要判断答案是否充分、是否引用正确、是否覆盖问题,还需要后面的模型、prompt、rerank、评估一起配合。
所以真实的 RAG 系统里,后面往往还会有 rerank、过滤、引用、评估这些步骤。余弦相似度只是入口,不是终点。
我现在会怎么记它
如果让我用一句话给自己记笔记,我会写:
余弦相似度是在比较两个向量的方向,方向越接近,通常表示语义越相近。
如果放到 RAG 里,再补一句:
它负责帮系统从资料库里先挑出几段可能相关的内容。
就这样。
不需要把它神化,也不需要被公式吓住。
它只是 AI 系统里一个很基础的零件。理解它之后,再去看 embedding、向量数据库、RAG 检索,就会踏实很多。至少你会知道,所谓“语义搜索”,中间有一部分其实就是在做这样一件朴素的事:
把问题和资料都变成向量,然后看看谁和谁方向更像。
我觉得这也是学习 AI 工程里比较有意思的地方。很多听起来很抽象的词,拆开以后并不神秘。它们不是魔法,只是一层一层的小机制叠起来。
余弦相似度就是其中一个小机制。它不炫技,但很常用。懂了它,再看 RAG,就不会只停留在“模型会查资料”这种模糊印象上,而是能看到中间那一步具体是怎么发生的。