数据截至 (上游 commit 081bc084a5bc)
嵌入层:一套接口收编 11 种向量库
30 秒导读: 上一章(02)把文档切成了一颗颗 block 存进文本库。这一章讲 LLMware 怎么把这些 block 嵌入成向量、塞进你选的任意向量数据库(Milvus / FAISS / Pinecone / …),并且始终能凭一个
block_id把命中的向量倒查回原始文本。核心只有一个类:EmbeddingHandler。
本章聚焦 RAG 的「语义索引」这一环:如何把文本变成向量、如何统一管理十来种向量后端。不讲查询编排(那是 04 检索引擎)——这里只把 search_index 的返回约定交代清楚,作为 04 的输入。
1. 这是什么(零基础也能懂)
-
一句话定义:
EmbeddingHandler是一个向量后端的统一遥控器——你告诉它"用哪个模型、存到哪个向量库",它就把库里的文本块逐批变成向量、写进去,查询时再帮你从向量库找回最相近的文本块。 -
它解决什么问题: 向量数据库有一大堆(Milvus、FAISS、Pinecone、Redis、Qdrant、PGVector、Chroma…),每家 SDK 的建表、插入、检索 API 都长得不一样。如果每换一个库就要重写一遍嵌入逻辑,RAG 应用会被后端绑死。LLMware 把这层差异焊死在 10 个适配器类里,对上只暴露三个动作:建嵌入、搜、删。
-
给谁用: 想搭 RAG 但不想被某个向量库锁死的工程师。今天用本地 FAISS 做原型,明天换 Pinecone 上生产,业务代码一行不用改——只改一个
vector_db="..."字符串。 -
用起来什么样: 用户几乎从不直接碰
EmbeddingHandler,而是调Library.install_new_embedding(见 01):# 示意,非源码:一行给整个库装上语义索引library.install_new_embedding(embedding_model_name="mini-lm-sbert", # 用哪个嵌入模型vector_db="milvus", # 存到哪个向量后端batch_size=500,)# 之后就能做语义检索了(见第 04 章) -
一句话直觉: 把它想成快递分拣中心。包裹(block)都长一样,分拣中心(
EmbeddingHandler)按你写的地址(vector_db)把它送进对应的仓库(Milvus/FAISS…);每个包裹贴着一张回执单号(block_id),将来凭单号就能从仓库把原件调回来。
2. 顶层全景(它大概怎么转)
2.1 三层结构
嵌入层就三层:一个门面、一堆共用工具、十个适配器。
Library.install_new_embedding() ← 业务侧调用(library.py:783)
│ (vector_db="milvus", model=...)
▼
┌─────────────────────────────┐
│ EmbeddingHandler │ 门面 / 路由器 (embeddings.py:78)
│ create_new_embedding :104 │
│ search_index :129 │
│ delete_index :143 │
└─────────────┬───────────────┘
│ _load_embedding_db :155
│ 查 VectorDBRegistry,动态 import 对应类
┌──────────────┼───────────────────────────┐
▼ ▼ ▼
┌───────────┐ ┌───────────┐ …十个同构适配器… ┌───────────┐
│EmbedMilvus│ │EmbedFAISS │ │EmbedChroma│
│ :346 │ │ :656 │ │ :2544 │
└─────┬─────┘ └─────┬─────┘ └─────┬─────┘
│ │ 每个适配器都用同一套 _EmbeddingUtils
└──────────────┴──────────────┬─ ────────────────┘
▼
┌──────────────────────────┐
│ _EmbeddingUtils │ 共用逻辑 (:198)
│ 取块游标 get_blocks_cursor│
│ 倒查文本 lookup_text_index│
│ 汇总 generate_..summary│
└────────────┬─────────────┘
▼
文本集合库(Mongo/SQLite/Postgres,见 01)
怎么读这张图: 从上往下是一次"建嵌入"的调用;门面只做路由,真正干活的是某个适配器,而所有适配器把重复逻辑(取哪些块、怎么倒查文本、怎么汇总)都甩给中间那层 _EmbeddingUtils。
2.2 部件一句话职责
| 部件 | 干什么 | 位置 |
|---|---|---|
EmbeddingHandler | 门面:接收 vector_db 名,路由到对应适配器 | embeddings.py:78 |
_load_embedding_db | 查注册表、动态 import 并实例化适配器 | embeddings.py:155 |
VectorDBRegistry | 名字 → {module, class} 的路由表 | configs.py:474 |
_EmbeddingUtils | 所有适配器共用:命名、取块、倒查文本、汇总 | embeddings.py:198 |
Embedding{Milvus…Chroma} | 10 个适配器,把统一契约翻译成各家 SDK | embeddings.py:346 起 |
| 文本集合库 | 存原始 block 文本 + 嵌入标记 | 见 01 |
2.3 一句话主线
一次 install_new_embedding:业务侧传入模型 + 向量库名 → 门面据名加载对应适配器 → 适配器分批从文本库拉出 block、调模型算向量、连同 block_id 写进向量库,同时在文本库给每个 block 打上"已嵌入"标记 → 返回一份汇总字典(建了多少条、维度、时间戳)。查询时反过来:向量库返回命中的 block_id → _EmbeddingUtils 凭 id 从文本库取回整块文本 → 打包成 (block, distance) 交给上层。
3. 核心原理
3.1 路由:一个字符串决定用哪个后端
要解决的小问题: 上层只想说一句 vector_db="milvus",不想知道 Milvus 的类叫什么、在哪个模块。
思路: 用一张注册表把名字映射到"模块 + 类名",门面拿到名字后动态 import——所以没装 pymilvus 的用户也能正常 import embeddings 模块,只有真正用到 Milvus 时才去加载它的驱动。
注册表就是一个字典(configs.py:478,VectorDBRegistry.vector_db_list):
# 真实源码 configs.py:478 VectorDBRegistry.vector_db_list
vector_db_list = {"milvus": {"module": "llmware.embeddings", "class": "EmbeddingMilvus"},
"chromadb":{"module": "llmware.embeddings", "class": "EmbeddingChromaDB"},
...
"postgres":{"module": "llmware.embeddings", "class": "EmbeddingPGVector"},
"pg_vector":{"module": "llmware.embeddings", "class": "EmbeddingPGVector"},
...}
这里有个诚实的小账:表里有 11 个名字,但 postgres 和 pg_vector 指向同一个 EmbeddingPGVector 类——所以是 10 个适配器类、11 个可选后端名。"收编 11 种向量库"说的是这 11 个可选项。
门面的路由发生在 _load_embedding_db(embeddings.py:155):它查表拿到 module/class,importlib.import_module 后 getattr 取出类,直接实例化返回。找不到就抛 LLMWareException。三个公开方法(create_new_embedding:104、search_index:129、delete_index:143)全靠它拿到具体适配器,自己不含任何后端逻辑。
一个巧妙细节 —— query_vector 归一化(search_index:129,行 137-138):Transformer 出来的向量有时是 [[...]](多一层),有时是 [...]。门面在分发前统一剥成一维,让每个适配器不必各自处理这个坑。