周五下午四点,办公室的空调吹得人昏昏欲睡。老李端着保温杯从茶水间溜达出来,路过小王工位时突然停住了。
屏幕上密密麻麻全是浮点数:[-0.0231, 0.0142, -0.0089, 0.0317, ...],一行接一行,像股票行情刷屏似的。
“你这干嘛呢?炒币啊?”老李凑过来,枸杞香直扑小王的后脑勺。
“我在看 embedding 的结果,”小王头也没回,“就是把一段文字转成向量。”
“向量?就这堆小数点儿?”老李指着屏幕,“这不就是随便打的数字吗?我看着跟温度传感器坏了的日志似的。我们以前做搜索,关键词匹配多精确啊,‘年会’俩字儿搜下去,有就有,没有就没有,哪需要什么向量不向量的?”
小王转过身来,脸上带着那种“终于等到你问”的表情:“老李你想啊,关键词匹配有一个致命问题。”
“能有什么问题?精确匹配还不够?”
“那你说,‘苹果手机’和‘iPhone’,关键词匹配能找到吗?”
老李愣了一下:“那……得建个同义词表。”
“‘好吃的水果’和‘苹果’呢?”
“……建个更长的同义词表。”
“‘库克发布的那款全面屏设备’和‘iPhone X’呢?”
老李不说话了,保温杯在手里转了两圈。
水果摊的启发:三个数描述一个苹果
“老李,我给你整个水果摊。”小王打开一个新文件,开始画起来。
“你想象你是卖水果的。怎么给摊位上的水果分类?你可以用三个维度——”
小王在白板(实际上是工位旁边的玻璃隔断)上写下:
- 甜度:0 到 1
- 脆度:0 到 1
- 颜色:红色系为 0,黄色系为 1
“一个红富士苹果,甜甜的、脆脆的、红色的,大概就是 [0.8, 0.9, 0.2]。一个香蕉呢?甜的、但不脆、黄色,就是 [0.9, 0.1, 0.8]。”
老李盯着这三个数,若有所思:“这不就是把水果的属性转成数字嘛,三维坐标。”
“对!这就是向量的本质——用一个数字列表来描述一个东西的特征。三维你能在脑子里画出空间,苹果和香蕉各自占一个点,距离越近说明越像。”
图:三维空间里的水果——苹果离梨近,离香蕉远
“你看,如果我们想找‘像苹果一样的水果’,不用叫它苹果,只要找跟 [0.8, 0.9, 0.2] 距离最近的那个点就行。它可能是梨,可能是红富士的另一种叫法,但你不需要知道它叫啥名。”
老李举着保温杯:“所以你说的向量嵌入,就是把文字也变成这么一串坐标?”
“完全正确。只不过——”小王在玻璃上又画了一串长长的数字,“文字比水果复杂太多了,三个维度根本不够用。苹果可能跟‘红富士’、‘嘎啦果’、‘口感清脆的秋季水果’都有关系,还有‘iPhone’这种完全不是水果但字形相关的干扰。所以真正的 embedding 模型用的是 1536 维,甚至更多。”
“1536 维?!”老李的眉毛快飞出额头了,“谁能想象 1536 维空间长啥样?”
“没人能想象,也不需要想象。计算机处理高维空间就像你处理 Excel 表格,它不管这是几维的,只管算距离。”
那串数字不是随便打的
老李把保温杯往桌上一放,语气依然怀疑:“我现在信这玩意儿不是随便打的数字了。但这 1536 维是谁定的?每个维度代表什么?比如第一个维度是甜度?”
“这就是最妙的地方,”小王眼睛亮了,“没有人规定每个维度代表什么。这些维度是模型自己学出来的。”
“自己学出来的?这玩意儿靠谱吗?”
“我给你看个实在的。”小王打开终端,敲了几行代码。
# 用 OpenAI 的 embedding 模型把文字变成向量
from openai import OpenAI
client = OpenAI()
def get_embedding(text):
response = client.embeddings.create(
model="text-embedding-3-small", # 这个模型输出 1536 维
input=text
)
return response.data[0].embedding
# 把两句话变成向量
vec_apple = get_embedding("苹果手机非常好用")
vec_iphone = get_embedding("iPhone 用户评价很高")
# 只看前 5 个维度感受一下
print("苹果手机:", vec_apple[:5])
print("iPhone:", vec_iphone[:5])
# 输出类似:
# 苹果手机: [-0.021, 0.011, -0.009, 0.032, -0.004]
# iPhone: [-0.023, 0.014, -0.008, 0.031, -0.002]老李盯着那两行数字:“看着挺像,但怎么说明它们意思相近?肉眼能看出来这两串数字差别不大?”
“肉眼看不出来,但数学能算出来。这就轮到余弦相似度出场了。”
你比你想象的更懂余弦
小王又在玻璃上画起来。今天这玻璃已经快成黑板了。
“余弦相似度,名字听着吓人,其实就是看两个箭头的方向有多一致。”
他画了两个从原点出发的箭头。
“不管箭头长还是短,只要方向差不多,就说明它们‘意思’差不多。就像两个人往同一个方向走,一个人步子大一个人步子小,但目的地是一样的。”
“我们以前学的那个余弦定理?”老李眯起眼,高中几何的记忆正在缓慢加载。
“对,就是它!俩向量的点积除以它们长度的乘积,结果在 -1 到 1 之间。1 表示方向完全一致,0 表示垂直不相关,-1 表示完全相反。”
小王在代码里补了一段:
import numpy as np
def cosine_similarity(vec_a, vec_b):
# 点积除以模长的乘积,衡量方向一致性
dot = np.dot(vec_a, vec_b)
norm_a = np.linalg.norm(vec_a)
norm_b = np.linalg.norm(vec_b)
return dot / (norm_a * norm_b)
# 计算三组相似度
apple_phone = get_embedding("苹果手机")
iphone = get_embedding("iPhone 15")
banana = get_embedding("香蕉的营养价值很高")
print("苹果手机 vs iPhone:", cosine_similarity(apple_phone, iphone))
# 输出 0.91,方向很接近
print("苹果手机 vs 香蕉:", cosine_similarity(apple_phone, banana))
# 输出 0.23,方向差很远“看到没有?根本没有‘手机’这两个字出现在‘iPhone’里,但余弦相似度 0.91。而‘苹果’和‘香蕉’虽然都是水果,但这里‘苹果手机’的语义跟香蕉完全不搭,只有 0.23。”
老李把代码要来在自己电脑上跑了一遍,沉默了片刻。
“这不就是把模糊匹配变成了数学运算吗?我们以前写规则也能做到一部分……”
“老李,你写规则能覆盖多少种说法?‘水果手机’、‘那个被咬了一口的牌子’、‘库克的公司出的’——你同义词表写到下辈子也写不完。”
图:关键词匹配只有“命中/未命中”二选一,向量相似度能捕获语义关系
1536 维的意义:不是人能命名的
“我再问一个,”老李放下保温杯,“你刚才说那些维度是模型自己学的。那它能保证学出来的东西靠谱吗?”
“这就像你教一个小孩认识水果,你不用告诉他‘甜度’、‘脆度’这些专业词,你只要给他看足够多的苹果和香蕉,他自己脑子里会形成一套区分它们的维度。那套维度可能跟你的‘甜度’不完全对应,但效果是一样的。”
小王接着解释:“Embedding 模型就是这么训练的。给它看海量的文本——维基百科、新闻、书籍,让它根据上下文预测词语。训练完之后,模型的中间层就自然形成了 1536 个数字,这些数字组成的空间里,意思相近的词会聚集在一起。我们不需要理解每个维度的含义,只需要信任这个空间。”
“这么说,”老李拿起保温杯又放下,“你们上次那个 RAG 里的向量数据库,就是先把公司文档都变成向量存起来,提问的时候把问题也变成向量,然后在向量空间里找最接近的文档?”
“老李,你这总结能力,下周技术分享你来讲得了。”
“少废话。”老李摆摆手,但眼角有了一丝得意。
小王又调出一个图:“给你看看什么叫‘语义空间’。”他指着屏幕上用 t-SNE 降维后的散点图,几个点聚在一起,标签分别是“年会”、“年终总结大会”、“annual meeting”、“公司周年庆”。
“你看,明明是四个完全不同的词,但在语义空间里它们挤在一起。这就是为什么 RAG 里用向量检索比关键词检索强——你问‘去年年终大会’,它能找到‘2025年会于11月8日举行’这条记录,即便字面上一个都对不上。”
老李盯着屏幕看了好一会儿,最后吐出几个字:“嗯……还有点意思。”
“老李,你这是又认可了?”
“我没说认可。我是说……下不为例啊。”他把保温杯端起来,却没喝,又放下来,“那什么,刚才那个余弦,高中课本上那个公式我有点忘了,你再给我推导一下?”
小王笑着拿起笔,在玻璃上画下了一个直角三角形。周五午后的阳光透过玻璃照进来,那些浮点数的意义,终于在老李的脑海里找到了一个方向。

