作为坐标的含义:从嵌入模型到向量数据库与RAG
用四句简短的话,以狗、猫和巴黎为例解释嵌入、余弦相似度、向量数据库以及RAG循环,从而提升语义搜索的点击率。
来源: Meaning as Coordinates: Semantic Search, Vector Databases, and RAG
尽管知识库中满是关于养狗的建议,但搜索框对“适合活跃人士的宠物”这一查询却没有任何结果,这恰恰暴露了关键词匹配机制的不足。要解决这个问题,就需要将语义视为空间中的位置。这一理念是语义搜索、向量数据库以及检索增强生成技术的基础,只需一个简单的例子就能帮助理解这一切。
贯穿全文的示例
想象一个包含四句话的微型知识库:
1. Dogs are loyal and need daily walks and exercise
2. Cats are independent pets that groom themselves
3. Golden retrievers are friendly family dogs
4. The capital of France is Paris
有用户提问:
"What's a good pet for an active person who exercises a lot?"
该问题中从未出现“dog”或“walks”这样的词。简单的关键词搜索根本找不到有用信息。然而机器依然能给出正确答案,因为它比较的是文本在语义空间中的位置,而非它们共有的词汇。
相似的意义,相近的点
向量其实就是一列数字,用于编码文本的含义。一旦相似的意义会生成相似的向量,寻找相关文本就变成了寻找相近的点。这便将语言问题转化为几何问题,而这也是现代搜索、推荐系统以及RAG技术的基础——RAG技术能让语言模型参考外部数据库,从而避免凭空编造内容。
当意义被视为一组坐标时,处理意义就意味着处理距离和方向,而这正是计算机极其擅长的领域。
整段文本的嵌入表示
仅靠词级向量不足以用于文档检索。句子、段落或整页内容都需要对应的向量。Transformer模型能够生成此类向量:输入一段文本后,会得到一个通常被称为嵌入向量的结果,它能够概括该文本的含义。
"Dogs need daily walks" ──► [ 0.91, 0.08, -0.30, ... ] (e.g. 384 numbers)
"The capital is Paris" ──► [-0.05, 0.62, 0.44, ... ]
句子嵌入将一段文字压缩成固定长度的数字列表。主题相同的文本会产生相似的列表,而无关的文本则会产生截然不同的列表。这些单独的数字对人类而言没有任何意义,重要的是它们之间的相互关系,而且没人会直接阅读这些数字,只是对它们进行比较而已。
需牢记的意象
把每段文字想象成空间中的一个点,含义相似的文字会彼此靠近,而无关的文字则相距甚远。
(a simplified 2-D "meaning space")
dog walks • • golden retriever ← the "dogs" neighborhood
\ /
(pets)
cat grooming • • Paris ← far away, different topic
在那个简化的地图中,两个关于狗的句子聚集在一起,关于猫的句子也位于它们附近,因为它同样涉及宠物,而关于巴黎的句子则处于完全不同的区域。真实的嵌入空间拥有数百个维度,但原理是一样的:含义相关的内容意味着点会彼此靠近。接下来的内容都是寻找最接近的点的方法。
关键词搜索与语义搜索
关键词搜索是按文字匹配的。如果搜索“锻炼”,那么包含“每日散步”这样的文档就会被忽略,因为两者没有共同的词汇。这种方法速度快但仅限于字面匹配,无法理解含义。
语义搜索则是按含义匹配的。查询会被转换成向量,然后返回与之最接近的文档向量,而不管这些文档使用了哪些词汇。以下是使用实际相似度得分对该示例生成的排序结果:
Query: "a good pet for an active person who exercises"
0.999 ############################# Golden retrievers are friendly family dogs
0.995 ############################# Dogs are loyal and need daily walks and exercise
0.398 ########### Cats are independent pets that groom themselves
0.059 # The capital of France is Paris
该查询与关于狗的句子没有共同关键词,但两者的得分都接近1.0,为最高值。关于猫的句子因为仍属于宠物而处于中间位置,而关于巴黎的句子得分则接近零。如果使用关键词搜索,将不会得到任何结果。这就是为什么语义搜索显得如此智能:它能理解经常锻炼的人和需要每天散步的狗其实指的是同一类事物。关键词搜索与语义搜索之间的差异,就在于“找不到结果”与“得到真正想要的答案”之间的区别。
利用余弦相似度衡量相似度
衡量两个向量之间相似程度的常用工具是余弦相似度。它关注的是两个向量之间的夹角,而忽略其长度。
- 方向相同的向量,其相似度接近1,表示含义极为相似。
- 垂直方向的向量,其相似度接近0,表示毫无关联。
- 指向相反方向的箭头表示相似度接近-1,意味着含义相反。
查询词与相关文档之间的小角度如下所示:
^ query
│ /
│/ small angle → cosine ≈ 1 (similar)
└──────► dog doc
以下是关于巴黎文件的广角视角:
^ query
│
└──────► Paris doc big angle → cosine ≈ 0 (unrelated)
使用角度而非长度,以便让长文本和短文本能够公平比较。相关计算可在一行内完成:将向量相乘后再进行归一化处理。
import numpy as np
def cosine(a, b):
return a @ b / (np.linalg.norm(a) * np.linalg.norm(b))
上述排名中的每个分数背后都是那个小型函数在起作用。分数范围在-1到1之间,数值越接近1表示这两段文本的含义越相同。
用于大规模搜索的向量数据库
该示例包含四份文档。实际系统中存储的文档数量可达数百万甚至数十亿,比如所有的支持工单、产品页面以及公司文档中的各个段落。如果在每次搜索时都逐一将查询向量与数十亿个向量进行比较,速度会慢得无法接受。
向量数据库正是为了解决这个问题而存在的。它是一种专为高效完成某项任务而设计的存储系统:接收查询向量后能迅速返回最接近的存储向量。它依赖于近似最近邻搜索技术,这类智能索引可以通过跳过大部分比较来找到几乎最接近的匹配项。虽然精度会有所牺牲,但速度却能得到极大提升。
documents ─► embed ─► [ vector database ]
│ stores millions of vectors
│ + a smart index (ANN)
query ─► embed ─────────► │
▼
top-k nearest matches (in milliseconds)
这些文档会经过嵌入模型处理一次,然后与索引一起保存。当有查询出现时,查询内容也会被嵌入,无论数据库中存储了多少向量,它都能瞬间返回匹配度最高的k个结果。该领域的产品包括FAISS、Pinecone、Weaviate、Chroma和Milvus,以及pgvector——这是一种将向量搜索功能引入普通PostgreSQL的扩展。虽然具体实现有所不同,但它们都能快速存储嵌入向量并查找相似项。简而言之,向量数据库就是用于语义搜索的引擎。
普通语言模型存在的缺陷
大型语言模型固然令人印象深刻,但它们本身存在三个难以克服的弱点:
- 幻觉现象。在不确定时,它们会给出流畅、自信却错误的答案。
- 知识截止日期。它仅知晓在某个较早时间点就已固定的训练数据中的内容。
- 不了解你的数据。内部文档、私人笔记以及上周的会议内容,模型均不知情。
You: "What's our refund policy?"
Plain LLM: "Typically, refunds are offered within 30 days..." ← a guess, not YOUR policy
当被问及退款政策时,普通模型会凭记忆给出看似合理的猜测,而非公司的实际政策。对于任何涉及事实或隐私且可能带来风险的内容,都必须在模型回答时直接提供准确的事实,而无需重新训练,RAG正是实现了这一点。
检索增强生成
RAG将上述所有步骤整合到一个循环中。它不会让模型凭记忆回答,而是首先通过语义搜索获取相关文档,然后再让模型基于这些文档进行回答。其名称恰好描述了这一过程:检索用于找到相关文本,增强意味着将其加入提示词中,而生成则让模型写出答案。
1. Question ─► embed ─► search the vector DB ─► top relevant chunks
│
2. Build a prompt: "Using ONLY the context below, answer the question.
Context: [retrieved chunks]
Question: [user's question]"
│
3. LLM generates an answer grounded in the retrieved text ─► with sources
通过该工具运行示例:
Question: "What's a good pet for an active person?"
→ retrieves docs 1 & 3 (the dog sentences, from Figure 3)
→ prompt: "Using this context: [Dogs need daily walks and exercise;
Golden retrievers are friendly family dogs] — answer the question."
→ LLM: "A dog — especially a golden retriever — is a great fit, since
they need daily walks and exercise. (Source: docs 1, 3)"
现在的答案有了依据。它依据的是所有者掌控的真实文档,并且能够明确指出事实的来源。虚假答案的出现几率大大降低。RAG是语义搜索与语言模型的结合:首先检索准确的事实,再由模型来组织回复内容。
实际应用流程及其主要陷阱
RAG包含两个阶段。设置只需进行一次,而每次有人提问时都需要执行查询操作。
── SETUP (once) ── ── QUERY (every question) ──
documents user question
│ split into chunks │ embed
│ embed each chunk │ search vector DB
│ store in vector DB │ get top-k chunks
▼ │ build prompt with chunks
[ vector database ] ◄───── search ──────┤ send to LLM
▼
grounded answer
文档会被分割成多个块,嵌入后仅存储一次。每个问题都会被嵌入并与存储的块进行匹配,最优的块会被传递给模型以生成最终答案。
关键问题在于文档分割的方式,即嵌入前的分块处理。过大的块会将多个主题混为一个向量,导致匹配结果模糊不清;而过小的块则会丢失上下文,使得答案被截断。选择合适的块大小是构建实用RAG系统时最重要的实际决策之一,这也提醒我们,看似不起眼的数据准备工作实际上决定了后续所有环节的质量。
代码实现
RAG中的检索部分其实是一种简单的语义搜索,仅需几行NumPy代码即可实现。它接收若干文档的向量以及查询向量,然后根据余弦相似度对文档进行排序:
import numpy as np
def cosine(a, b):
return a @ b / (np.linalg.norm(a) * np.linalg.norm(b))def search(query_vec, doc_vecs, docs, k=2):
scores = [(cosine(query_vec, v), d) for v, d in zip(doc_vecs, docs)]
return sorted(scores, reverse=True)[:k] # top-k most similar
输入四只宠物的向量、巴黎的向量以及关于活跃用户的查询后,系统会首先返回两条与狗相关的文档,其排序结果与之前展示的一致。这就是几行代码实现的语义搜索。
在实际项目中,并不会手动构建向量,而是调用嵌入模型和向量存储,相关代码大致如下:
# Illustrative real-world usage (libraries download a model, so not run here)
from sentence_transformers import SentenceTransformer
import chromadb
model = SentenceTransformer("all-MiniLM-L6-v2") # turns text into vectors
db = chromadb.Client().create_collection("pets")db.add(documents=my_docs, embeddings=model.encode(my_docs).tolist(),
ids=[str(i) for i in range(len(my_docs))])q = "a good pet for an active person"
hits = db.query(query_embeddings=model.encode([q]).tolist(), n_results=2)
# hits now holds the top-2 most semantically similar documents
这些库承担了大部分工作,但本质上它们执行的是相同的余弦相似度搜索。语义搜索不过是针对向量的一些数学运算,而真正的工具则通过提升速度和扩展能力来实现这一功能。
完整流程
文本最初以字符和标记的形式存在,随后转化为嵌入向量,再通过变换器中的注意力机制获得上下文信息,最终形成可搜索的知识图谱,从而使语言模型更加准确且实用。
raw text
→ characters / tokens / embeddings [article 1: From Characters to Meaning]
→ attention turns them into context-aware
vectors inside a transformer [article 2: Attention, Explained Simply]
→ those vectors become coordinates we can
store, search, and retrieve [article 3: this one]
→ semantic search (find by meaning)
→ vector databases (search at scale)
→ RAG (ground an LLM in real data)
贯穿这一切的核心理念是随着成长而逐渐形成的:意义即空间中的一个点。最初它适用于单词,接着扩展到上下文中的句子,最终则涵盖可被检索和推理的整个文档库。
还有另一个区别值得牢记。RAG能让模型获取知识,而无需改变模型本身,就好比在回答问题时递给它笔记让其阅读。微调则是另一条路径,通过用示例对模型内部权重进行训练来改变模型。RAG提供的是可供阅读的知识,而微调则改变模型本身的结构。知道何时该使用哪种方法本身也是一项实用技能。
相关阅读
- 实用笔记:RAG中向量搜索的局限——余弦相似度及其应用 — 该实用笔记的详细操作指南:针对采用此模式的团队提供的模板、检查项及可直接插入的代码片段。
- 实用笔记:用于生产环境RAG的向量数据库——索引构建与混合搜索 — 该实用笔记的详细操作指南:针对采用此模式的团队提供的模板、检查项及可直接插入的代码片段。