Skip to content

Embedding 与向量检索

这一章在第二阶段的位置

做完第一个聊天助手,很多人的直觉是"接下来学 Agent"。但有一个常见陷阱:跳过检索直接追 Agent,遇到"模型拿不到外部资料"或"搜出来的东西和问题对不上"时,完全不知道从哪里排查。

第二阶段先学检索,不是因为它"更基础",而是因为它是后续所有"模型需要外部知识"场景的共同底层。RAG 要靠它找资料,Agent 里的工具调用经常要靠它找上下文,多轮对话的记忆系统有时也会借助它。不理解检索,后面的系统很容易都变成黑盒。

这一章的任务:在进入 RAG 之前,先搞清楚"系统是怎么找到相关资料的"。

本章目标

  • 理解 Embedding 在 AI 应用里解决的是什么问题
  • 理解 chunk、top-k、rerank 这些高频检索概念
  • 明白向量检索不是万能搜索,也需要调参和评测

什么是 Embedding

Embedding 是把文本、图片或其他内容转换成一组数字,使"语义相近"变成"可计算的距离关系"。

只看定义会有点虚。直白地说,它在教系统认"意思",而不只是认字面。

例如:

  • "怎么学习 React"
  • "React 入门路线"

两句话字面不同,语义接近。Embedding 的作用,就是让这种接近在向量空间里也体现出来。这样系统才能靠"意思相近"而不只是"字面匹配"来找内容。

Embedding 不是把知识塞进一个向量

这里有一个很常见的误解:很多人第一次接触 Embedding,会下意识觉得"向量里装着这段文本的全部知识"。这不太对。

Embedding 存的是一种压缩后的语义表示,大概能反映"这段内容在说什么、和哪些问题更接近",但不会把每个事实细节原封不动地保留。

向量检索很适合做召回,但不适合直接替代原文:

  • 它能帮你找到"可能相关"的片段
  • 但它不能代替原始片段本身去充当证据
  • 它尤其不擅长保留精确数字、编号、表格关系和局部措辞

你可以把 Embedding 想成一张语义地图上的坐标。坐标能告诉你"这个点离哪个区域更近",但不能替代那片区域里的原始内容。

为什么关键词检索不够

传统关键词检索擅长精确命中、字段过滤和明确词项查询,但它对同义表达、口语化问题和长文档里的隐含相关段落没那么敏感。

AI 问答场景里,很多问题本质上是在找"意思最接近的那几段",而不只是精确搜词。比如用户问"怎么把服务关掉再开",文档里写的可能是"重启服务";用户问"会员能退吗",文档里写的可能是"激活后不支持退款"。这类问题只靠关键词,经常要么漏召回,要么召回一堆不相关的结果。

但这也不意味着"有了向量检索,关键词就没用了"。后面你会看到,真实系统里两者往往是搭配使用的。

向量检索的基本流程

建库阶段

  1. 收集资料
  2. 清洗文本(去掉页眉、页脚、乱码等噪音)
  3. 切成 chunk(把长文档分成小块)
  4. 为每个 chunk 生成 embedding
  5. 存入向量库

查询阶段

  1. 用户提问
  2. 将问题转成 embedding
  3. 在向量库中查最相近的 chunk
  4. 取 top-k 结果
  5. 交给后续生成模块使用

注意一点:向量检索解决的不是"最后答案",而是"先把候选范围缩小"。检索层负责召回,目标是别漏掉可能有用的证据;生成层负责组织和表达,基于证据把话说明白。这两层职责不同,混在一起理解,后面排查问题会很痛苦。

什么是 Chunk

Chunk 就是把长文档切成更小的检索单元。全文太长不适合直接检索,用户问题通常也只对应文档里的局部内容,把命中片段拼回 prompt 时也更灵活。

很多初学者会把 chunk 理解成"随便切一切"。其实 chunk 的切法会直接影响检索质量,"切在哪里、切多长、要不要重叠"都很关键。

换一种方式看:切 chunk 其实是在定义系统的"最小证据单位"。

如果一个 chunk 太大,系统每次召回的就是一大块混杂内容,真正和问题相关的句子反而被埋掉;如果 chunk 太小,系统召回到的可能只是半句话,证据不完整。你最后比较的是"哪一个证据片段最可能支撑回答",粒度远比"文章 A 还是文章 B"细得多。

chunk 策略其实是在提前决定:你的 RAG 系统以后拿什么粒度的证据来说话。

Chunk Size 和 Overlap

Chunk Size

每个块的长度。太小会碎(上下文不完整),太大会引入太多噪音。没有绝对最优值,需要根据文档类型和问答场景实际测试。

Overlap

相邻 chunk 之间保留一部分重叠,避免重要信息正好被切断。例如一句话跨了两个 chunk 边界,没有 overlap 就会让两边都读不完整。

这两个参数直接影响召回质量、上下文完整性和 token 成本。

相似度到底在算什么

Embedding 做完之后,系统要算"问题向量和文档向量有多接近"。最常见的度量方式是余弦相似度(cosine similarity),看两个向量方向是否接近。

为什么关心方向而不是绝对长度?语义检索里在意的是"意思是否接近",跟文本多长、数值多大没关系。方向相近,就意味着语义空间里的位置更接近。

这就引出一个常见现象:检索结果"看起来沾边",不代表它能回答问题。相似度衡量的是接近程度,不是答案充分性。片段可能和问题主题接近,但缺少用户真正要的那一句关键信息。

什么是 Top-K

Top-K 是从所有候选内容中取最相关的前 K 条,交给后续模块处理。

K 太小,可能漏掉关键资料;K 太大,噪音太多,模型注意力被分散,成本也变高。没有固定最优值,需要结合场景调优。

什么是 Rerank

Rerank 是二次排序。常见做法是:

  1. 先用向量检索召回一批候选片段(比如前 20 条)
  2. 再用更细的排序方法重新排一遍(比如交叉编码器或精排模型)
  3. 最终把最相关的 3 到 5 条交给模型

它的目标是减少"向量相似但其实不够回答这个问题"的片段,让最后进入 prompt 的内容质量更高。

两者对应的是不同能力:

  • 向量检索擅长大规模粗召回,先把范围缩小
  • rerank 擅长细粒度判断,决定"哪些片段最值得留下"

可以想成两阶段筛选:先问"哪些内容可能相关",再问"如果只能留 3 段,该留哪 3 段"。很多系统做了 Embedding 之后还是会再加一层 rerank,就是因为粗召回和精排各管各的。

向量数据库是什么

向量数据库是专门用来存储向量并做相似度搜索的系统。它擅长相似度搜索、最近邻查找和语义召回,但不能替代传统数据库做所有事情。

向量数据库的核心难点不是存储,而是在海量向量里快速找到近邻。

当你的文档只有几十条时,暴力比对也能跑。但文档一旦上万、几十万,就不能每次都和所有向量逐一比较了。向量库真正提供的价值,是近似最近邻搜索(ANN)、索引结构、过滤能力和工程上的稳定性,让语义召回在规模上仍然可用。

选向量库时,关键问题就是"谁能在我的数据规模、过滤需求、混合检索需求下,把近邻搜索做得够快、够稳"。

常用的向量库:

  • Chroma:本地可用,Python 友好,入门首选
  • FAISS:Facebook 开源,速度快,适合百万级向量
  • Qdrant:支持混合检索,有托管云版本
  • Pinecone:全托管,适合不想运维的团队

入门先用 Chroma,不需要起额外服务,直接在 Python 里本地运行。

一个最小可运行示例

用 Chroma 和 OpenAI Embedding 实现一个最小的向量检索流程:

python
import chromadb
from openai import OpenAI

openai_client = OpenAI()
chroma_client = chromadb.Client()

# 创建集合
collection = chroma_client.create_collection("knowledge_base")

# 准备文档(实际场景里这些是切好的 chunk)
documents = [
    "退款申请需在购买后 7 个工作日内提交,逾期不予受理。",
    "VIP 用户享有专属客服通道,工作日 9:00-18:00 在线。",
    "产品激活码在订单确认邮件中,请查收邮件后手动激活。",
]
ids = ["doc_0", "doc_1", "doc_2"]

# 生成 embedding 并存入向量库
def get_embedding(text: str) -> list[float]:
    response = openai_client.embeddings.create(
        model="text-embedding-3-small",
        input=text
    )
    return response.data[0].embedding

embeddings = [get_embedding(doc) for doc in documents]
collection.add(documents=documents, embeddings=embeddings, ids=ids)

# 查询
query = "申请退款需要多少天?"
query_embedding = get_embedding(query)

results = collection.query(
    query_embeddings=[query_embedding],
    n_results=2
)

print("检索到的相关片段:")
for doc in results["documents"][0]:
    print(f"- {doc}")
# - 退款申请需在购买后 7 个工作日内提交,逾期不予受理。

注意这里展示的只是检索部分,不是完整的 RAG 问答。拿到检索结果之后,还需要拼进 prompt 再让模型生成回答,那是 RAG 原理一章的内容。

混合检索:向量 + 关键词

纯向量检索擅长语义相似,但有时会漏掉精确命中。比如用户搜索"退款 7 天",如果你的文档用的是"七个工作日",向量检索可能找得到;但如果用户搜索的是一个产品型号(SKU12345),关键词检索就比向量检索更准。

真实系统里,往往会组合使用:

关键词检索(BM25) + 向量检索 → 混合排序 → rerank → top-k

混合检索的标准流程:

  1. 向量检索召回一批候选(比如 top-20)
  2. 关键词检索也召回一批候选
  3. 两批结果做倒数排名融合(RRF,Reciprocal Rank Fusion)
  4. 对合并结果做 rerank,最终取 top-3 到 top-5

如果你用 Qdrant 或 Elasticsearch,原生就支持混合检索,不需要自己手动合并。如果用 Chroma,就需要结合传统搜索库(如 rank_bm25)自己实现。

什么时候需要混合检索:当你发现向量检索对精确词组(专有名词、产品编号、缩写)的召回效果差时,加入关键词检索通常能明显改善。

检索系统常见问题

这些问题在"能跑"之后非常容易遇到:

  • chunk 切得太碎:信息不完整,每条命中都只有半句话
  • chunk 太大:结果太噪,模型拿到一大段但只有一句有用
  • top-k 设置不合理:要么漏掉关键资料,要么带入太多干扰
  • 文档清洗差:页眉页脚、目录、乱码都被当作正文内容
  • 召回看起来相关,但并不能真正回答问题:向量相似度只能衡量语义接近,不代表能直接作答
  • 只用向量检索,精确词汇召回差:专有名词、数字、产品编号靠语义检索经常漏掉

这类问题不会在第一次运行时明显暴露,需要你真正拿着问题去测,看召回内容里有没有真的能回答问题的片段。

一个更底层的视角:检索到底在优化什么

表面上看,检索在做"找相似文本"。但它真正优化的是另一件事:在有限上下文里,把最有可能支撑回答的证据塞进去。

这句话很重要,因为它会直接改变你调系统的方式。

如果你把检索目标理解成"相似度越高越好",就容易陷在分数调参里;如果你把它理解成"让回答所需证据进入上下文",你就会开始关注:

  • 召回的片段能不能独立支撑回答
  • 这些片段放进 prompt 后会不会互相干扰
  • top-k 到底是在补召回,还是在制造噪音
  • 当前问题到底更依赖语义匹配,还是更依赖关键词精确命中

视角一转,你会意识到检索不是附属模块。它直接决定了生成层能看到什么。

最小检索评测

检索质量不能靠"感觉",必须真测。很多系统 demo 能答几道题,一上真实数据就露馅。原因往往不在模型,而是检索层压根没把对的片段拿回来。

最朴素的评测方式就够用:

准备 10-20 个测试问题,覆盖这几类:

  • 资料里有明确答案的问题
  • 需要综合多段资料才能回答的问题
  • 资料里完全没有相关内容的问题

对每个问题,手动检查召回的 chunk

  1. top-k 里有没有包含正确答案的片段?(如果没有,检索层就有问题)
  2. 有答案的片段排在第几位?(排在 top-3 外面的,模型可能看不到)
  3. 召回的内容和问题的语义真的相关,还是只是字面相似?

这套流程不需要任何自动化框架。你甚至不用先写评测代码,直接把问题和召回结果打印出来,用眼睛看,10 分钟就能发现很多明显问题。

几个容易踩的坑

先别急着接生成。单独看召回结果,比一上来就看最终答案更容易发现问题。

把 chunk 当成证据单位,不要当成纯技术切片。你怎么切,决定了系统以后拿什么说话。

相似度分高不等于答案一定对。"最像"和"能答"是两码事,检索的目标是找最能支撑回答的材料。

另外,纯向量检索很少是终局方案。业务里只要有编号、专有名词、精确条款,混合检索迟早会上。

你现在最适合做的练习

还没准备好做完整 RAG 也没关系,这一章的内容可以单独练。以下练习都能独立完成:

  • 给几篇文章生成 embedding,然后用一个问题查最相近的几段,看看命中情况
  • 把一份 PDF 切成不同大小的 chunk(200字、500字、1000字),比较哪种切法召回的结果更完整
  • 展示 top-k 命中片段列表,而不是直接生成答案,先看系统到底拿到了什么
  • 设计 5 个测试问题,手动检查哪些问题的召回是对的、哪些不对,分析原因

先把检索层看清楚,再进 RAG。后面很多看起来像 prompt 问题、模型问题的故障,根源其实在这里。

下一章:RAG 原理——从"检索到资料"推进到"基于资料生成回答"。

面向开发者的 AI 实战路线——Vibe Coding 与 AI 应用开发