一个 Embedding embedding 与向量数据库 的渐进式学习项目:从「怎么把文本变成向量」开始,到「向量存哪、怎么查」,最终落到 * 图文跨模态检索*(用 Qwen3-VL-Embedding 把图片和文本映射到同一语义空间,实现以文搜图、以图搜文、图文混合检索)。
所有模型全部本地离线运行,不依赖任何在线 Embedding API。
| 层面 | 选型 |
|---|---|
| 框架 | LangChain 1.3.18 / langchain-core 1.6.1 |
| 文本 Embedding | Qwen3-Embedding-0.6B、BAAI/bge-small-zh-v1.5 |
| 多模态 Embedding | Qwen3-VL-Embedding-2B(2048 维,图文同空间) |
| 推理 | PyTorch 2.14.0 · transformers 5.16.1 · Apple Silicon 走 MPS + fp16 |
| 向量库 | Milvus(pymilvus 3.0.1)· FAISS · Chroma |
| 语言 / 环境 | Python 3.12.14(conda 环境 langchain-embedding) |
LangChainEmbedding/
├── env_utils.py # 统一读取 .env(系统环境变量优先)
├── models/
│ └── init_chat_model_llm.py # 共享的 DeepSeek / GLM 聊天模型实例
│
├── embedding_demo/ # ① Embedding 基础
│ ├── download_model_embedding.py # 模型下载(hf-mirror 镜像 + 断点重试)
│ ├── 01langchain本地Qwen3的嵌入模型.py # LangChain 封装 Qwen3,含 Query 指令前缀
│ ├── 02私有化的Qwen3的嵌入模型.py # 纯 SentenceTransformer 离线加载
│ ├── 03加载bge的嵌入模型.py # BGE 系列 + 归一化检索
│ ├── 04Qwen3的嵌入模型和Langchain整合.py # 手写 Embeddings 子类
│ ├── 05Embedding案例.py # 完整案例:CSV 批量向量化 + 语义搜索
│ └── custom_embedding.py # 自定义 Embeddings 类(被 chroma/faiss 复用)
│
├── vector_db/ # ② 向量数据库
│ ├── chroma/01Chroma案例.py # Chroma 持久化 + 元数据过滤
│ ├── faiss/ # FAISS 三连:建库 / 落盘 / 加载删除
│ │ ├── 01faiss_案例.py
│ │ ├── 02faiss_案例.py
│ │ └── 03faiss_案例.py
│ └── milvus/
│ ├── milvus_basics/ # Milvus 原生 API 三步走
│ │ ├── collection_create.py
│ │ ├── collection_insert.py
│ │ └── collection_search.py
│ └── multi_model/ # ③ 多模态检索(本项目重点)
│ ├── qwen3_vl_embedding.py # Qwen3-VL 推理封装(含 chat template 补齐)
│ ├── multimodel_embeddings.py # LangChain Embeddings 适配层
│ ├── milvus_manager.py # 双向量 Collection 管理 + 三种检索
│ ├── multimodel_search_demo.py# 可运行入口
│ └── images/ # 5 张示例图片
│
├── datas/ # 06 案例的输入 / 输出 CSV
├── chroma_db/ faiss_db/ # 落盘数据(运行时生成,不入库)
├── requirements.txt # 精简依赖清单(只列直接依赖)
├── .env.example # 环境变量模板(复制为 .env 后填值)
└── README.md
conda create -n langchain-embedding python=3.12 -y
conda activate langchain-embedding
pip install -r requirements.txt
⚠️ 解释器别选错:项目根目录有个.venv,里面只装了一个 pip(空壳)。 在 IDE 里请把解释器指向/opt/miniconda3/envs/langchain-embedding/bin/python, 否则会报ModuleNotFoundError: No module named 'pymilvus'。
磁盘空间:PyTorch + transformers + chromadb 全套约 3~4 GB。
在项目根目录创建 .env(已被 .gitignore 忽略):
DEEPSEEK_API_KEY=sk-xxx
DEEPSEEK_BASE_URL=https://api.deepseek.com
GLM_API_KEY=xxx
GLM_BASE_URL=https://open.bigmodel.cn/api/paas/v4/
LANGSMITH_API_KEY= # 可选,留空即可只有 models/init_chat_model_llm.py 和 embedding_demo/01openai.py 需要 API Key,
向量库和多模态部分完全离线,不配也能跑。
配了
LANGSMITH_API_KEY会自动开启链路追踪上报。不想上报就加一行LANGCHAIN_TRACING_V2=false。
本项目共用到 3 个模型,本地均已缓存:
| 模型 | 用途 | 大小 |
|---|---|---|
Qwen/Qwen3-Embedding-0.6B |
文本向量(02/03/05、chroma、faiss、milvus_basics) | ~1.2 GB |
BAAI/bge-small-zh-v1.5 |
文本向量(04/06) | ~90 MB |
Qwen/Qwen3-VL-Embedding-2B |
图文多模态向量(multi_model) | ~4 GB |
下载脚本默认下载 Qwen3-VL-Embedding-2B。要下其他模型,改 download_model_embedding.py
顶部的 REPO_ID 再运行:
python embedding_demo/download_model_embedding.py脚本已做国内适配:走 hf-mirror.com 镜像、关闭 Xet 协议(否则镜像返回 401)、
带 3 次重试、只下推理必需文件(跳过 *.bin 原始权重)。
只有 vector_db/milvus/ 下的代码需要。用 Docker 起一个单机版:
curl -sfL https://raw.githubusercontent.com/milvus-io/milvus/master/scripts/standalone_embed.sh -o standalone_embed.sh
bash standalone_embed.sh start默认连 http://127.0.0.1:19530,可用环境变量覆盖:
export MILVUS_URI="http://127.0.0.1:19530"
export MILVUS_COLLECTION="multi_model_search_demo"按下面的顺序跑,就是一条完整的学习路径。
python embedding_demo/02langchain本地Qwen3的嵌入模型.py # LangChain 封装 + Query 指令前缀
python embedding_demo/03私有化的Qwen3的嵌入模型.py # 纯 sentence-transformers
python embedding_demo/04加载bge的嵌入模型.py # BGE + 归一化
python embedding_demo/05Qwen3的嵌入模型和Langchain整合.py # 手写 Embeddings 子类
python embedding_demo/06Embedding案例.py # CSV 批量向量化 + 语义搜索02 里有个容易忽略的细节:Qwen3 是指令感知模型,查询侧必须拼 Instruct: 前缀,
否则检索效果会明显下降。代码里通过 query_encode_kwargs={"prompt": QUERY_PROMPT} 处理。
# Chroma:在 vector_db/ 目录下执行(脚本里 persist_directory='../chroma_db')
cd vector_db && python chroma/01Chroma案例.py
# FAISS:先建库检索,再落盘
cd vector_db/faiss && python 01faiss_案例.py
cd vector_db && python faiss/02faiss_案例.py # 落盘到根目录 faiss_db/
# 03 用的是 '../../faiss_db',需在 faiss/ 目录下执行
cd vector_db/faiss && python 03faiss_案例.py # 加载、删除、带过滤检索
⚠️ FAISS 02 和 03 的相对路径基准不一致('../faiss_db'vs'../../faiss_db'), 必须按上面各自的 cwd 执行。这属于历史遗留,建议后续统一改成基于Path(__file__).resolve().parents[2]的绝对路径。详见下方踩坑记录。
Milvus 三步走,必须按顺序执行:
python vector_db/milvus/milvus_basics/collection_create.py # 建 Collection(dim=1024)
python vector_db/milvus/milvus_basics/collection_insert.py # 插入 8 条文档
python vector_db/milvus/milvus_basics/collection_search.py # ANN 搜索 / 标量过滤 / 标量查询cd vector_db/milvus/multi_model
python multimodel_search_demo.py首次运行会加载 2B 参数模型(约 4 GB 权重),约 40 秒 (Apple Silicon 自动走 MPS + fp16;无 MPS 时回退 CPU,会慢很多)。 输出三个场景的结果:
======================== 以文搜图:猫在窗边晒太阳 ========================
id=1, score=0.6630 图片: cat_sunbath.jpg 描述: 一只橘色的猫在窗台上晒太阳...
======================== 以图搜文:coffee_latte.jpg ========================
id=3, score=0.5420 图片: coffee_latte.jpg 描述: 一杯拿铁咖啡,表面有精致的拉花...
======================== 图文混合检索 ========================
id=4, rrf_score=0.0328 图片: milvus_architecture.jpg
id=5, rrf_score=0.0323 图片: qwen_multimodal.jpg
三层结构,职责清晰:
multimodel_search_demo.py → 编排:建表 / 插数据 / 跑三个场景
↓
milvus_manager.py → Milvus:双向量 Collection + 三种检索实现
↓
multimodel_embeddings.py → LangChain Embeddings 适配层(设备选择、图片解码)
↓
qwen3_vl_embedding.py → Qwen3-VL 推理:processor + last-token pooling + L2 归一化
每条数据在 Milvus 里存两个向量,都在同一个 2048 维语义空间:
| 字段 | 来源 |
|---|---|
image_vector |
图片过模型得到的向量 |
text_vector |
描述文本过模型得到的向量 |
因为图文同空间,所以可以交叉检索:
| 检索方式 | 查询向量 | 搜索字段 | 实现 |
|---|---|---|---|
| 以文搜图 | 文本 | image_vector |
search_by_text() |
| 以图搜文 | 图片 | text_vector |
search_by_image() |
| 图文混合 | 文本 + 图片 | 两路召回 | hybrid_search_multimodal() |
两路召回得到的 COSINE 分数量纲不可比(文本↔文本 和 图片↔图片 的分数分布不同), 直接加权相加没有意义。所以用 RRF(Reciprocal Rank Fusion) 按名次融合:
score = Σ
1 / (rrf_k + rank) # rrf_k 默认 60只关心排名、不关心原始分数,规避了量纲问题。rrf_k 越大,排名靠前的优势越小。
三个位置,各管一段:
| 位置 | 控制范围 | 能否改 |
|---|---|---|
| Milvus COSINE 检索 | 天然降序(分数越大越相似) | 不能翻转 |
milvus_manager.py RRF |
sorted(..., reverse=True) |
可改 reverse |
multimodel_search_demo.py |
最终输出顺序 | 改 RESULT_SORT_DESCENDING |
想让结果从低分到高分打印,把 RESULT_SORT_DESCENDING 改成 False 即可
(只影响已召回的 top_k 条,不改变召回集合)。
multimodel_embeddings.py 的 _load_image() 统一处理:
http:///https://URL- 本地文件路径
- Base64(
data:image/jpeg;base64,...或纯 Base64)
这些都是实际跑通过程中遇到并已修复的。
| 现象 | 原因 | 解决 |
|---|---|---|
ModuleNotFoundError: No module named 'pymilvus' |
解释器选了空壳 .venv |
切到 conda 环境解释器 |
ModuleNotFoundError: No module named 'env_utils' |
脚本运行时 sys.path[0] 是脚本所在目录 |
01openai.py 里显式补了项目根路径 |
Cannot use apply_chat_template because this processor does not have a chat template |
模型快照缺 .jinja 模板文件 |
qwen3_vl_embedding.py 内置 CHAT_TEMPLATE 并传入 processor |
| 插入后首次搜索偶发返回空 | Milvus 最终一致性,数据未落盘 | insert_data() 里加了 client.flush() |
torch_dtype 弃用警告 |
transformers 新版 API 变更 | 改用 dtype=torch.float16 |
| HF 下载走镜像报 401 | Xet 协议会请求 xethub.hf.co |
设 HF_HUB_DISABLE_XET=1 降级为 HTTP |
03faiss_案例.py 加载失败 |
相对路径相对当前工作目录,且 02/03 基准不一致:02 用 '../faiss_db'(cwd 需为 vector_db/),03 用 '../../faiss_db'(cwd 需为 vector_db/faiss/) |
按各自 cwd 执行;或把两处都改成 Path(__file__).resolve().parents[2] / "faiss_db" |
| Chroma 数据落在奇怪位置 | persist_directory='../chroma_db' 同样相对 cwd |
在 vector_db/ 下执行,数据落到根目录 chroma_db/ |
requirements.txt 只列直接依赖(项目代码真正 import 的包),传递依赖交给 pip 解析。
审计方式:AST 扫描全项目 import + importlib.metadata 反向依赖闭包复核。
conda activate langchain-embedding
pip uninstall -y dashscope zhipuai langchain-faiss pandas-stubs| 包 | 说明 |
|---|---|
dashscope |
通义 SDK。所有 Qwen 都走本地 transformers,删它收益最大(会带走 openai/uvicorn/tiktoken 一串) |
zhipuai |
智谱 SDK。GLM 走的是 OpenAI 兼容接口 |
langchain-faiss |
代码用的是 langchain_community.vectorstores.FAISS |
pandas-stubs |
纯类型存根,项目没配 mypy |
它们在 pip list --not-required 里看着像孤儿,实际删了直接崩:
| 包 | 真实身份 | 谁需要 |
|---|---|---|
langchain-deepseek |
langchain[deepseek] 的 extra |
init_chat_model(model_provider="deepseek") |
torchvision |
transformers[vision] 的 extra |
Qwen3VLProcessor 图像预处理 |
av |
qwen-vl-utils 的硬性依赖 |
import qwen_vl_utils 就会失败 |
httptools / uvloop / watchfiles 是 uvicorn[standard] 的可选件(chromadb 引入),
删了不报错但会让 Chroma server 模式降级,体积极小,建议留着。
想删掉某个目录时,照这张表卸载:
| 目录 | 专属依赖 |
|---|---|
embedding_demo/ |
langchain-huggingface、sentence-transformers、huggingface-hub、openai、pandas、numpy |
models/ |
langchain、langchain-deepseek、python-dotenv |
vector_db/chroma/ |
langchain-chroma(→ chromadb,本机最大的间接依赖源) |
vector_db/faiss/ |
faiss-cpu、langchain-community |
vector_db/milvus/milvus_basics/ |
pymilvus、sentence-transformers |
vector_db/milvus/multi_model/ |
pymilvus、torch、transformers、torchvision、qwen-vl-utils、pillow |
-
milvus_basics加上混合检索(BM25 + 向量) - 多模态 demo 支持视频输入(
Qwen3VLVideoProcessor已就绪) - 接 RAG:把检索结果喂给
models/init_chat_model_llm.py的 LLM 生成答案 - 换成更大的
Qwen3-VL-Embedding-8B对比效果 - 尝试向量降维(MRL 截断 / PCA)在召回率与速度之间找平衡点