Day 06 / 共 20 天 · 第 2 周 索引/嵌入/存储
嵌入 Embeddings(流水线第 ③ 步)
第 2 周开始,进入 RAG 最神奇的核心——嵌入。它是"语义检索"能成立的地基:把文字变成数字向量,让"意思相近"变成"位置相近"。今天用一张图讲透它,你就懂了 RAG 为什么能"问退款也能查到退货"。
📍 你在整条链的位置
① 读→
② 切→
③ 嵌入(语义地基)→
④ 存索引→
⑤检索 ⑥合成
L01
计算机不懂"意思相近"
🤔 用户问"怎么退款",文档里写的是"退货流程"——怎么匹配上?
如果用关键词匹配:"退款" 和 "退货" 不是同一个词,匹配不上,检索失败。但人一看就知道它们意思相近。计算机怎么知道两段文字"意思相近"?
💡 把"意思"变成"数字",就能算了
计算机不懂文字,但极擅长算数字。如果能把每段文字变成一串数字(向量),并且让"意思相近的文字→数字也相近",那"找意思相近"就变成"找数字相近"——纯数学问题,计算机秒算。这个"文字→数字"的转换,就是嵌入(embedding)。
L02
嵌入:把文字放进"意义空间"
💡 嵌入模型把每段文字变成高维空间里的一个点
向量(如 1536 个数字)= 高维空间里的一个坐标。嵌入模型经过海量训练,学会把"意思相近的文字放在相近的位置"。下图用 2D 简化(真实是上千维):
嵌入把文字变成空间里的点:退款/退货/返还挤在一起,汽车/发动机在另一头。"找相关" = "找附近的点"。
这就是 RAG 比关键词搜索强的根本原因
用户问"退款",它的向量落在退款簇附近,检索就能找到"退货流程"(同一簇),哪怕一个字都不同。关键词搜索做不到这点。嵌入让检索"懂语义"。
L03
余弦相似度:怎么算"多近"
💡 用夹角衡量两个向量有多"像"
最常用余弦相似度(cosine similarity)——算两个向量夹角的余弦,范围 -1~1,越接近 1 越相似。
utils.py 的 SimilarityMode 定义了几种模式(DEFAULT=余弦、DOT_PRODUCT 点积、EUCLIDEAN 欧氏距离)。📝 具体数值感受一下
→ 检索时:问题嵌入成向量,和库里每个 Node 向量算相似度,取最高的几个(Day 09 的向量检索)。
相似度(退款向量, 退货向量) = 0.88(很像)相似度(退款向量, 汽车向量) = 0.09(几乎无关)→ 检索时:问题嵌入成向量,和库里每个 Node 向量算相似度,取最高的几个(Day 09 的向量检索)。
L04
BaseEmbedding 接口
embeddings/ 的 BaseEmbedding(继承 TransformComponent,所以能进 Day 05 的摄入管道):
get_text_embedding(text) -> List[float] # 单条文本 → 向量
get_text_embedding_batch(texts) -> ... # 批量(省 API 请求)
get_query_embedding(query) -> List[float] # 查询 → 向量(L05)
similarity(emb1, emb2, mode) -> float # 算两向量相似度
def __call__(nodes) -> nodes # 作为转换:给每个 Node 填 embedding
读法:因为它是 TransformComponent,
__call__ 遍历 Node、给每个填 node.embedding(Day 02 那个字段)——所以能直接插进 Day 05 的管道。接口统一,OpenAI/HuggingFace/本地各种嵌入模型都能互换。L05
text 和 query 嵌入为什么分开
🤔 "一段文档"和"一个问题"能用一样的方式嵌入吗?
有些嵌入模型区别对待:文档加前缀
"search_document: "、查询加 "search_query: "——因为"陈述性文档"和"疑问句"语言风格不同,分开处理检索更准。💡 建库用 text 嵌入,检索用 query 嵌入
建库时用
get_text_embedding 嵌入每个 Node;用户提问时用 get_query_embedding 嵌入问题。大部分模型两者一样,但接口留了区分,让支持"非对称嵌入"的模型能发挥。这个细节直接影响检索质量。L06
批量与缓存:嵌入是要花钱的
🤔 建库嵌入几万个 Node,成本多少?
嵌入模型多是调 API(OpenAI 等),按 token 收费。几万个 Node 嵌一遍是笔真金白银。
💡 两招省钱:批量 + 缓存
① 批量(
get_text_embedding_batch,embed_batch_size 控制批大小):一次发多条,省请求开销。② 缓存(Day 05 的 ingestion 缓存):没变的 Node 不重嵌。读法:这就是为什么 Day 05 的管道缓存那么重要——它省的主要就是嵌入费用。本地嵌入模型(HuggingFace BGE)免费但慢/占 GPU——用钱换速度还是用机器换钱,看你权衡。
L07
怎么选嵌入模型
core 只有 MockEmbedding(测试);真实模型在 integrations:
- OpenAI text-embedding-3:效果好、联网付费。
- BGE / GTE(HuggingFace 本地):免费、数据不出本地、要 GPU/慢。中文选 BGE-zh。
- Cohere / Ollama 等。
一个关键坑:换模型必须重新嵌入
不同嵌入模型的向量空间"不通用"——OpenAI 的向量和 BGE 的向量不能混着比相似度(就像用不同语言写的坐标)。所以换嵌入模型,整个库要重新嵌入。选嵌入模型直接影响检索质量,且和你的语言、领域、预算都相关——这是建库前要定好的关键决策。
L08
今日小结 + 动手
🧠 今天你应该能回答
- 为什么关键词搜索匹配不了"退款/退货",嵌入能?
- 嵌入把文字变成什么?"意思近→向量近"怎么支撑语义检索?
- 余弦相似度怎么衡量两段文字有多像?
- 为什么 text/query 嵌入分开?为什么嵌入要批量+缓存?换模型为什么要重嵌?
✋ 动手
cd /Users/bitmart/work/codes/github/llama_index/llama-index-core/llama_index/core
ls embeddings/
grep -rn "SimilarityMode\|def similarity\|get_top_k_embeddings" embeddings/utils.py indices/query/embedding_utils.py 2>/dev/null | head
ls ../../../../llama-index-integrations/embeddings/ 2>/dev/null | head
明天预告 · Day 07:Node 都有向量了(第 ③ 步完成)——第 ④ 步建索引。精读 VectorStoreIndex:
from_documents 一行代码内部怎么串起切块+嵌入+入库(Day 01 那 5 行第二行的真相)。