Appearance
第三阶段:Embedding 与向量检索
本阶段是 RAG 的地基:把文字变成向量,再按“意思”找到最相关的文档。
一、为什么用本地 Embedding
我们用的中转平台(tokenrhythm.studio)没有 Embedding 模型,所以在本机跑一个免费的 bge-m3(中文效果好,输出 1024 维向量)。两种方式任选其一:
方式 A:Ollama
bash
ollama pull bge-m3 # 下载 bge-m3 模型OpenAI 兼容地址:http://localhost:11434/v1
方式 B:LM Studio(本教程实际使用)
- 在 LM Studio 搜索
bge-m3,选择 gpustack/bge-m3-GGUF 的 Q8_0 版本(约 634MB)。 - ⚠️ 不要下载
bge-reranker-v2-m3——它是重排序(reranker)模型,后面的课程才用。 - 进入 Developer 页,加载模型,点 Start Server,默认端口
1234。
OpenAI 兼容地址:http://localhost:1234/v1
安装依赖并验证
bash
pip install numpy # 向量计算库python
from openai import OpenAI
# 连接本地 LM Studio;本地服务不校验 Key,随便填一个即可
client = OpenAI(base_url="http://localhost:1234/v1", api_key="lm-studio")
# 用 Ollama 的话改成:base_url="http://localhost:11434/v1",model="bge-m3"
resp = client.embeddings.create(model="bge-m3", input=["你好"]) # model 以 LM Studio 中显示的名称为准
print("向量维度:", len(resp.data[0].embedding)) # 应打印 1024二、原理
Embedding 模型把一段文字变成一串数字(向量)。意思越接近,向量越接近。衡量“接近”最常用的是余弦相似度:
- 分子
是两个向量的点积;分母是两个向量长度的乘积。 - 结果在 -1 到 1 之间,越接近 1 表示方向越一致、意思越相近。
三、03_embedding.py 完整代码
python
import numpy as np # 做向量运算
from openai import OpenAI # 本地服务兼容 OpenAI 接口,同一个 SDK 就能用
# 连接本地 LM Studio 的 Embedding 服务
client = OpenAI(base_url="http://localhost:1234/v1", api_key="lm-studio")
def embed(texts):
"""把一组文字转成向量,返回形状为 (文字数, 1024) 的 numpy 数组"""
resp = client.embeddings.create(model="bge-m3", input=texts)
return np.array([item.embedding for item in resp.data])
def cosine(a, b):
"""计算两个向量的余弦相似度:点积 ÷ (长度 × 长度)"""
return float(np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b)))
# 模拟的“公司知识库”:5 条规章制度
docs = [
"公司年假为每年 15 天,入职满一年后生效",
"报销需在费用发生后 30 天内提交发票",
"办公室 Wi-Fi 密码每月更新一次",
"员工生日当月可领取 200 元购物卡",
"加班需提前在 OA 系统中申请",
]
doc_vecs = embed(docs) # 一次性把 5 条文档都转成向量
query = "我能休几天假?" # 想换问题就改这里
query_vec = embed([query])[0] # 问题也转成向量(取第一条)
# 计算问题与每条文档的相似度,组成 (分数, 文档) 列表
scores = [(cosine(query_vec, v), d) for v, d in zip(doc_vecs, docs)]
scores.sort(reverse=True) # 按分数从高到低排序
print("问题:", query)
for score, doc in scores:
print(f"{score:.3f} {doc}")四、实测结果(真实运行数据)
| 问题 | 第 1 | 第 2 | 第 3 | 第 4 | 第 5 |
|---|---|---|---|---|---|
| 我能休几天假? | 年假 0.728 | 报销 0.555 | 加班 0.532 | 生日 0.525 | Wi-Fi 0.497 |
| 钱花了怎么拿回来 | 报销 0.593 | 生日 0.509 | 加班 0.474 | Wi-Fi 0.469 | 年假 0.462 |
| 网络连不上 | Wi-Fi 0.538 | 加班 0.496 | 报销 0.438 | 年假 0.398 | 生日 0.380 |
“钱花了怎么拿回来”里一个“报销”都没有,照样找到了报销条款——这就是语义检索,不是关键词匹配。
五、观察与思考
- 分数没有绝对意义,要看名次和差距:“休假”时年假领先第二名约 0.17,非常明确;“网络连不上”时 Wi-Fi 只领先约 0.04,算是勉强胜出。
- 因此实际应用中通常取 top 3~5 条交给 LLM,或者再用 reranker(重排序模型) 精排——这就是前面说的
bge-reranker-v2-m3的用途。 - 每次都重算全部文档向量太慢:文档一多就不现实。应该把向量算一次、存起来,查询时直接检索——这就是向量数据库要做的事。
六、提交代码
bash
git add .
git commit -m "03 embedding 向量检索"
git push下一节预告
用 Chroma 向量数据库存储文档向量,再把检索结果交给 LLM 生成答案,完成第一个完整的 RAG 问答程序。
👉 下一阶段:第一个 RAG(Chroma + LLM)