LLM 很聪明,但它有一个硬伤:知识截止到训练那天。你今天问它"公司上周更新的报销流程",它只能瞎编。RAG(Retrieval-Augmented Generation,检索增强生成)就是为了解决这个问题:先检索外部知识,再让 LLM 基于检索结果生成答案。而 RAG 的检索环节,核心靠的就是向量检索。
这篇文章从零讲清楚:什么是向量检索、Embedding 干了什么、相似度怎么算、大规模检索怎么加速,以及它在 Agent 里到底长什么样。
一、传统搜索 vs 向量检索:为什么"同义词"是个大麻烦
传统搜索引擎(比如数据库里 LIKE '%关键词%' 或倒排索引的 BM25)的核心逻辑是字面匹配。你用"调薪"去搜,文档里写的是"薪酬调整流程"——一个词对不上,直接搜不到。
这叫词汇鸿沟(lexical gap):你说的话和文档里写的话用的是不同词,但意思一样。传统搜索对此毫无办法——它没有"语义理解"能力,只有"字符串匹配"能力。
向量检索换了一个思路:不找"字",找"意思"。它把查询和文档都转成一串数字(向量),然后在数学空间里算距离。语义相近 → 距离近 → 命中。你说"怎么调薪",文档里有"薪酬调整流程",它们在向量空间里紧挨着——哪怕字面上完全不同。
| 匹配依据 | 字面命中(TF-IDF/BM25) | 语义距离(余弦相似度等) |
| “调薪"搜"薪酬调整” | 搜不到 | 能搜到,向量距离近 |
| 搜人名、型号、编号 | 精准 | 可能漂移 |
| 搜"大概意思" | 无能为力 | 强项 |
一句话:传统搜索像在找字,向量检索像在找意。成熟的系统都会做混合检索(Hybrid Search)——两路结果融合,互补短板。
二、Embedding:把文字变成"语义坐标"
为什么需要 Embedding
计算机只认识数字。想让计算机判断"猫在沙发上睡觉"和"猫咪在沙发上打盹"意思相近,得先把这两句话都变成数字。Embedding 就是这个转换过程——把文本映射到一个高维向量空间里的一个点。
输入一段文字,Embedding 模型输出一个固定长度的浮点数数组,比如 OpenAI 的 text-embedding-3-small 吐出 1536 个浮点数:
[0.012, -0.453, 0.891, -0.023, …, 0.147] # 共 1536 维
你没法想象 1536 维空间长什么样(人类大脑超过三维就抓瞎了),但数学上完全合理。Embedding 模型在训练时就是靠海量语料学到了一个规律:把语义相近的文字放在高维空间中相邻的位置。
经典类比:向量算术
Embedding 的妙处还不止于此。经典实验:"国王" – "男人" + "女人" ≈ "女王"。向量可以做加减法,而且结果还符合人类直觉——这说明向量确实编码了语义信息,不只是随机数字。
主流 Embedding 模型一览
| OpenAI text-embedding-3-small | 1536 | 通用性强,性价比高,中文支持不错 |
| OpenAI text-embedding-3-large | 3072 | 精度更高,维度更大,适合高要求场景 |
| BGE (BAAI) | 768/1024 | 开源,中文优化,自部署免 API 费用 |
| Jina Embeddings v3 | 1024 | 多语言,支持 8192 token 长文本输入 |
| Cohere Embed v3 | 1024 | 支持多种输入类型(搜索文档/搜索查询等) |
选模型的核心原则:入库和检索必须用同一个 Embedding 模型。不同模型产出的向量空间不一致,换模型 = 推倒重来。
三、相似度:怎么判断两个向量"靠得近"
向量检索最后一步,就是拿用户 query 的向量,去向量库里找距离最近的 K 个文档向量。这里"距离"有三种常用度量:
余弦相似度(Cosine Similarity)
最通用。计算两个向量夹角的余弦值,范围 [-1, 1]。只看方向不看长度,所以长的文档和短的 query 也能公平比较。
余弦相似度 = (A · B) / (||A|| × ||B||)
欧氏距离(Euclidean Distance)
两点之间直线距离。当向量已经 L2 归一化后,欧氏距离和余弦相似度等价。
点积(Dot Product)
两个向量逐元素相乘再求和。向量归一化后 = 余弦相似度。适合需要加权排序的场景。
实际工程里 90% 的场景用余弦相似度就对了。推理开销小,语义表达力足够。
四、ANN 加速:百万向量怎么毫秒级搜完
为什么要近似
如果暴力搜索(kNN)——拿 query 向量和库里每个向量都算一遍距离再排序——复杂度是 O(N)。N = 十万的时候还行,N = 百万、千万就崩了:每次查询要跑几百万次浮点运算,延迟秒级起步。
所以实际用的不是精确搜索,而是 ANN(Approximate Nearest Neighbor,近似最近邻)——牺牲一丁点精度(比如从"100% 召回"降到 “99.5% 召回”),换回几十上百倍的加速。
两大主流算法
IVF(Inverted File,倒排文件):先把所有向量用 K-Means 聚成 N 个桶(簇)。查询时只搜离 query 最近的几个桶,桶内做暴力搜索。核心思想是"先粗筛再细查"。
关键参数:
- nlist:分多少个桶。越大越细,但建索引越慢。
- nprobe:查几个桶。越大召回越高,但越慢。
HNSW(Hierarchical Navigable Small World,分层可导航小世界图):目前精度和速度的标杆。构建一个分层图结构——上层稀疏用于"导航到大致区域",下层稠密用于"精确定位"。搜索时从顶层逐层下降,类似在多层停车场里从顶层俯瞰逐步找到车位。
关键参数:
- M:每个节点的最大连接数(16~64)。越大越精确,但内存也越大。
- efSearch:搜索时考察的候选数(50~200)。越大召回越高,但越慢。
| 召回精度 | 高,但依赖 nprobe 调参 | 极高,通常优于 IVF |
| 检索速度 | 快 | 极快,接近 O(log N) |
| 构建速度 | 快(只聚类) | 慢(逐点建图) |
| 内存占用 | 高(存全部原始向量) | 高(存图结构 + 向量) |
| 适合场景 | 平衡速度与精度的通用场景 | 高要求场景(RAG、推荐系统) |
代表向量数据库
| FAISS (Meta) | IVF, HNSW, PQ | 库而非数据库,极高性能,GPU 加速 |
| Milvus | IVF, HNSW | 分布式架构,支持十亿级 |
| Qdrant | HNSW | 过滤功能丰富,Rust 实现性能好 |
| Chroma | HNSW | 轻量易用,适合原型和中小规模 |
| pgvector | IVF, HNSW | PostgreSQL 扩展,和业务库同存 |
五、分块策略:切多大才合适
Embedding 模型再好,分块没搞对,检索照样烂。这是工程中最容易被低估的环节。
核心矛盾
- 切太大(比如 2048 token):一段里塞了三个不同话题,Embedding 向量变成一个"杂糅语义点",搜什么都能命中一点,但精准度极差。
- 切太小(比如 128 token):一句完整的话被腰斩,上下文丢失,LLM 拿到碎片答不出完整答案。
工程上的经验值
- 通用场景:512 token 为一块,相邻块重叠 10%~15%(overlap)。这样关键句落在边界处也不被切开。
- 技术文档/代码:按自然边界切——函数级、段落级、章节级。不要机械按字符数切。
- QA 场景:每个 Q&A 对独立成块。
# chunking_config.py — 典型分块配置
from langchain.text_splitter import RecursiveCharacterTextSplitter
splitter = RecursiveCharacterTextSplitter(
chunk_size=500, # 每块 500 字符
chunk_overlap=50, # 相邻块重叠 50 字符(10%)
separators=["\\n\\n", "\\n", "。", ".", " "] # 优先按自然边界切
)
chunks = splitter.create_documents([document])
分块同时要带上元数据:来源文档名、章节标题、时间戳、版本号。检索时可以用元数据做过滤(“只搜 2024 年的文档”),精度大幅提升。
六、Rerank:少花钱、多办事
向量检索拉回 top-20 个候选块,直接全塞给 LLM?token 浪费不说,后面十几条可能是噪音。
两阶段检索是性价比最优解:
Rerank 模型比 Embedding 模型更"贵"但更准——它会把 query 和候选文档逐对精细比对,而不是只算一次向量距离。Cohere Rerank、BGE-Reranker 是常用选择。
# retrieval.py — 两阶段检索伪代码
def retrieve_and_rerank(query: str, top_k: int = 20, final_k: int = 3) –> list[str]:
# 阶段1:向量粗筛
query_vec = embed(query)
candidates = vector_db.search(query_vec, top_k=top_k)
# 阶段2:Rerank 精排
scores = reranker.compute_scores(query, [c.text for c in candidates])
ranked = sorted(zip(candidates, scores), key=lambda x: x[1], reverse=True)
return [c.text for c, _ in ranked[:final_k]]
七、在 Agent 中长什么样
Agent 用 RAG 不需要把每个细节都自己手写,成熟的框架已经封装好了。以你正在学的 LangChain 为例:
# agent_rag_pipeline.py — Agent 中 RAG 的最小闭环
from langchain.document_loaders import TextLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.embeddings import OpenAIEmbeddings
from langchain.vectorstores import Chroma
# 1. 加载文档 + 分块
loader = TextLoader("company_policy.txt")
docs = loader.load()
chunks = RecursiveCharacterTextSplitter(
chunk_size=500, chunk_overlap=50
).split_documents(docs)
# 2. 向量化 + 存入向量库
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
vectorstore = Chroma.from_documents(chunks, embeddings)
# 3. 作为检索器接入 Agent
retriever = vectorstore.as_retriever(search_kwargs={"k": 5})
# 4. Agent 调用时,retriever 自动搜相关文档注入上下文
完整链路:用户问 → Agent 决策 → 调 retriever 搜向量库 → 拿 top-k 文档 → 拼 prompt → LLM 生成 → 返回答案。
关键设计点:
- retriever 对 Agent 来说是一个工具(Tool),和其他工具(API 调用、数据库查询、代码执行)并列。Agent 自己决定什么时候该用检索、什么时候不该。
- 检索到的内容注入到 LLM 的 system prompt 或上下文里,而不是替换用户原始问题。
八、常见坑
| 向量检索结果完全不对 | 入库和检索用了不同的 Embedding 模型,向量空间不统一 |
| 换了个相似问法就搜不到了 | Embedding 模型对领域术语没训练过,考虑微调或换领域模型 |
| 明明文档有答案,检索就是命中不了 | 分块太大,语义被稀释;或分块太小,关键上下文被切断 |
| 向量检索快但内存爆炸 | 没做向量压缩(PQ),1536 维 × 百万条 = 几个 GB 起 |
| Rerank 加了反而变慢 | Rerank 模型和 Embedding 模型有"排序偏好冲突"——粗筛选出的候选集本身不行,精排救不回来 |
| 混合检索分数无法对齐 | 向量余弦值(-1~1)和 BM25 得分量纲不同,直接加权无效;用 RRF(倒数排名融合)代替 |
九、总结
向量检索用一句话概括:把文本映射到高维向量的语义空间,靠距离度量"意思有多近",用 ANN 算法在大规模数据中毫秒级找出最相关的文档。它在 RAG 链路里扮演"外部大脑的记忆索引"——不是取代 LLM,而是让 LLM 有据可依。
工程上的优先级:分块策略 > Embedding 模型选择 > 向量数据库选型。很多项目在向量数据库上纠结半天,结果分块一塌糊涂——方向跑偏了。
最后记住一个核心判断:向量检索擅长"语义模糊匹配",不擅长"精确条件查找"。人名、型号、编号、日期范围、法条编号——这些老老实实用过滤条件或关键词搜索,别硬往向量上套。
网硕互联帮助中心

评论前必须登录!
注册