Embedding 与向量数据库:给每段文字发一个坐标,然后找邻居
Embedding 与向量数据库:给每段文字发一个坐标,然后找邻居 - 朱瑞福
用 20 行 numpy 代码和 Chroma 复现 embedding 检索全流程:bge-m3 把 10 段文本编码成 1024 维向量,手写余弦相似度排序取 top-3,与 Chroma 检索分数逐位一致。
"把模型跑起来"只是本地大模型项目的第一步。真正让它对公司业务有用的,是一套给它"递资料"的系统——而这套系统的地基,就是 embedding 和向量数据库。这两个词听起来很玄,本文用一个 20 行代码的实验把它们拆开看清楚。
一、先看全景:它们在一个本地大模型项目里的位置
一个典型的本地大模型项目,不是"把模型跑起来"就完事的。假设一家公司把 qwen 部署在了自己的服务器上,很快就会发现:问它通用问题头头是道,问它"我们公司的报销制度是什么"就一本正经地胡编。模型训练时没见过这家公司的任何文档,它不可能知道。
解决办法是把公司的资料作为上下文,随问题一起喂给模型。那么问题来了:用户提问的时候,系统怎么知道该把公司几千份文档里的哪几份递给模型?
这就是 embedding 和向量数据库登场的位置。整个系统大概长这样:
【入库:一条自动化管道】
公司文档(PDF/Word/网页/扫描件…)
→ 解析提取文字 → 清洗 → 切成分块
→ 每块文字做 embedding(变成一个向量)
→ 存入向量数据库
【查询:每次提问时】
用户提问
→ 提问也做 embedding(变成一个向量)
→ 拿这个向量去向量数据库做语义搜索
→ 取回语义最接近的前 N 条文档块
→ 拼进 prompt,连同问题一起交给大模型
→ 模型基于资料作答
注意入库那条管道:公司的文档是各种格式、持续更新的混合体,"把文档变成可检索的知识库"本身就是一个需要 pipeline 系统来自动化的工程(这也正是做过数据管道的后端工程师熟悉的味道——它就是一条 ETL,终点从数仓换成了向量库)。
所以一个完整的本地大模型项目是:用户通过搭好的系统上传各种格式的文件,管道自动处理入库;用户在问答界面提问,系统检索出最相关的资料递给模型,得到有公司自己味道的回答。 embedding 和向量数据库,就是这条链路里"找资料"的那一环——本文只讲它们。
二、embedding 到底是个啥:把对象落到语义空间的一个位置
"Embedding"是个一直没有正经中文翻译的词,直译"嵌入",有人译"向量化"——但对没接触过的人来说,哪个都是黑话。它实际干的事情其实一句话能说清:
把一段文字(或图片、视频),按照模型定义好的维度,落到一个坐标系里。embedding 的结果,就是它在这个坐标系里的位置。
二维、三维的坐标我们还能在脑子里画出来:北京在上海北边、深圳在上海南边,位置关系一目了然。而 embedding 模型输出的通常是 1024 维(本文实验用的模型就是)——1024 维的空间人脑当然无法想象,但这不妨碍理解,因为重要的不是"想象出这个空间",而是它的一条性质:
语义相近的对象,落点也相近。
"如何缓解大模型的幻觉"和"怎么减少 AI 一本正经地编造",这两句话没有一个共同的实词,但意思几乎一样——在语义空间里,它们的落点就挨得很近。于是:
- embedding 模型:负责"发坐标"的模型,输入文字,输出一串数字(向量)
- 向量数据库:保存这些坐标的数据库,核心能力是回答"离这个点最近的 K 个点是哪些"
有了这两样东西,语义搜索就成为可能:提问和文档不用共享任何关键词,只要意思近,就能互相找到。这是传统的关键词检索(LIKE / 倒排索引)做不到的,也是它存在的全部理由。
三、工程视角:模型都是现成的包,选型看什么
好消息是,embedding 这一步对开发者来说是"开箱即用"的——业界训练好的 embedding 模型都是现成的包,主要看三个地方:
- Hugging Face:模型的源头仓库,几乎所有 embedding 模型都发布在这里,模型卡片上写着维度、支持语言、license
- MTEB 榜单(huggingface.co/spaces/mteb/leaderboard):embedding 模型的公开评测榜,按检索、分类、聚类等任务分别打分,选型先看这里
- ollama 模型库:
ollama pull bge-m3一条命令拉下来就能用,和我在《使用 ollama 部署本地大模型 qwen2.5 3b/7b》里部署 qwen 的体验完全一致
不同的模型针对不同的语言、不同的业务场景训练,维度也各不相同(从几百维到几千维都有)——维度越高,理论上能编码越细的语义差别,但也越贵(存储和计算都翻涨)。维度是模型训练时就定死的属性,使用者不能调。
本文的实验选了 bge-m3:中文表现好、1024 维、支持 8K token 输入、约 1.2GB 的体积 Mac 上轻松跑。它不是榜单第一——embedding 模型迭代很快,任何"头部"名单几个月就过期——但作为学习实验足够称职。向量数据库则选了 Chroma——它是"嵌入式"的:作为 Python 库直接 pip install chromadb 跑在进程里,不需要起 Docker 服务,是做实验和 demo 最轻量的选择。
四、实验:20 行裸 numpy,再换 Chroma
实验分两步,刻意为之:先用裸 numpy 手写一遍,再换向量数据库重做。这样第二步时你能清楚看到工具到底替你干了什么。
环境:Apple M2 / 16GB / ollama 常驻服务 / bge-m3。
第一步:裸 numpy 手写
准备 10 段文本作为"知识库",故意排成语义簇:
DOCS = [
"RAG 通过先检索知识库再生成回答,缓解大模型的幻觉问题。", 核心代码就是三步:调 ollama 的 embedding API 把 10 段文本变成 [10, 1024] 的矩阵;手写余弦相似度(三行 numpy);拿查询向量算一遍相似度、排序、取前三。向量数据库的全部秘密就在这三步里:存矩阵、算相似度、排序。
def embed(texts):
查询准备了三组对照:一个主题相关的问题(应命中 RAG 簇)、一个概念改写的问题(应命中 embedding 簇)、一个纯无关的问题("香蕉多少钱一斤",应全部低分)。
第二步:换 Chroma 重做
同样的 10 段文本、同样的 bge-m3、甚至向量都是同一个函数算好再喂进去的——只有"存和查"这一环从手写 numpy 换成 Chroma,保证两边严格可比。入库后用同样的三个查询检索,对照分数。
五、结果分析
相似度矩阵:语义簇真的亮了
10×10 余弦相似度矩阵(下三角):
#0 #1 #2 #3 #4 #5 #6 #7 #8 #9
#0 1.00
#1 0.65 1.00
#2 0.55 0.55 1.00
#3 0.54 0.66 0.65 1.00
#4 0.52 0.54 0.51 0.51 1.00
#5 0.58 0.59 0.48 0.48 0.64 1.00
#6 0.49 0.46 0.50 0.47 0.52 0.52 1.00
#7 0.50 0.55 0.47 0.53 0.47 0.53 0.44 1.00
#8 0.44 0.42 0.46 0.51 0.46 0.41 0.52 0.43 1.00
#9 0.34 0.34 0.30 0.37 0.39 0.38 0.26 0.34 0.39 1.00
先看设计好的三个簇:#0↔#1 为 0.65、#2↔#3 为 0.65、#4↔#5 为 0.64,都站在各自行列的高位。最值得单独一提的是 #0↔#1:这两句一个说"幻觉"一个说"编造",零个共同关键词,照样被认出来是近邻——这就是第二节那句"语义相近 = 落点相近"的实证。
但要诚实地说,这个矩阵并没有想象中干净。眼尖的话会注意到 #1↔#3 也有 0.66,比簇内的 #0↔#1 还高——RAG 簇和 embedding 簇之间的墙没砌住。回头想想这其实不是模型判错了:#1 说的是"检索增强生成",#3 说的是"向量数据库语义检索",RAG 的核心构件恰恰就是向量检索,这两个话题本来就近。是我在设计语料时高估了簇与簇之间的独立性。这也是 10 句话的玩具语料的局限:区分度有限,"相关"和"同簇"搅在一起。真正的分界线看 #9(黄金,全场唯一的金融文本)那一行更清楚——整行 0.26–0.39,和所有技术文本拉开了实打实的距离。领域差距大时分得很开,领域内细分时差距收窄到 0.15 左右,这个梯度本身是有用的认知。
查询改写的 A/B 对照:一个"失败"引出的发现
第一版查询我用的是口语:"如何减少大模型胡说八道?"。结果 Top-1 是错的:
0.6751 #5 在消费级硬件上运行大模型,通常需要压缩模型权重。 ← 答非所问
0.6150 #1 检索增强生成让模型基于参考资料作答……
0.6098 #0 RAG 通过先检索知识库再生成回答……
正确的 #1/#0 都在 top-3,但榜首被 #5 抢走。原因不难找:查询里有"大模型",#5 里也有"大模型"——bge-m3 这类模型仍然保留着相当的词法敏感性。所谓"语义检索"并不是纯语义,是语义加词法的混合体,只是天平从"纯字面"移向了"偏语义"。
把查询改写成术语版"如何缓解大模型的幻觉问题?"再跑一次:
0.7472 #0 RAG 通过先检索知识库再生成回答,缓解大模型的幻觉问题。 ← 正确登顶
0.6407 #5 在消费级硬件上运行大模型……
0.5382 #4 量化技术把模型参数从 16 位浮点压到 4 位……
正确答案从第 3 跳到第 1,分数涨了 0.14。查询怎么写,直接影响检索质量——这叫查询改写(query rewriting),是 RAG 优化的正统手段之一。用户不会说"幻觉"这种术语,所以成熟的 RAG 系统常在检索前加一步自动改写。
一个诚实的补充:改写后 #1 掉出了前三——因为"幻觉"这个词恰好和 #0 共享、不再和 #1 的"编造"共享,词法加成跟着词汇走了。但这无伤大局:检索不需要召回"所有"正确段落,正确信息进了 top-K,任务就完成了(#0 和 #1 说的是同一件事)。
绝对分数:别设死阈值
"香蕉多少钱一斤"的最高分有 0.46——看起来不低?注意 bge-m3 的余弦相似度天然挤在 0.3–0.7 这一小段里,无关对照的 0.46 在它的量表里已经是"无关区"。所以生产系统从不写"分数大于 0.8 才算相关"这种死规则,而是看相对排序(top-K),或者交给下一道工序——rerank 精排——那个是另一个主题,不在这里展开。
Chroma 对照:分数逐位一致
第二步 Chroma 的检索分数和裸 numpy 版完全一致(同一模型、同一距离度量)。这个"一致"本身就是结论:向量数据库没有魔法,它就是你手写那三步的工程化——替你管了持久化(数据落在本地目录,重启不丢)、索引(数据量大了以后近似最近邻搜索不退化)和元数据过滤(真实场景里挂来源、页码、权限)。这些在 10 条数据上看不出差距,但到了百万条 + 权限过滤时,就是自己写 numpy 撑不住的部分。
结语
回到开头的全景图:本地大模型项目的"递资料"环节,拆开看就是"发坐标 + 找邻居"。20 行 numpy 证明它没有魔法,三次查询证明它也不是万能——词法干扰、口语查询、分数量表,每个坑都有对应的解法(改写、top-K、rerank)。把这两件事想清楚,向量数据库在选型时该问什么问题(维度、语言、license、和现有系统的取舍),也就自然有了答案。
2026-11
声明:本文的实验、数据和结论均为本人实际操作所得;写作过程中 AI 参与了文字润色与结构整理,技术观点经本人核对确认。
来源:博客园 · cnblogs.com