Skip to content

第三阶段: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(本教程实际使用) ​

  1. 在 LM Studio 搜索 bge-m3,选择 gpustack/bge-m3-GGUF 的 Q8_0 版本(约 634MB)。
  2. ⚠️ 不要下载 bge-reranker-v2-m3——它是重排序(reranker)模型,后面的课程才用。
  3. 进入 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 模型把一段文字变成一串数字(向量)。意思越接近,向量越接近。衡量“接近”最常用的是余弦相似度:

cos⁡(a,b)=a⋅b∥a∥∥b∥
  • 分子 a⋅b 是两个向量的点积;分母是两个向量长度的乘积。
  • 结果在 -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.525Wi-Fi 0.497
钱花了怎么拿回来报销 0.593生日 0.509加班 0.474Wi-Fi 0.469年假 0.462
网络连不上Wi-Fi 0.538加班 0.496报销 0.438年假 0.398生日 0.380

“钱花了怎么拿回来”里一个“报销”都没有,照样找到了报销条款——这就是语义检索,不是关键词匹配。

五、观察与思考 ​

  1. 分数没有绝对意义,要看名次和差距:“休假”时年假领先第二名约 0.17,非常明确;“网络连不上”时 Wi-Fi 只领先约 0.04,算是勉强胜出。
  2. 因此实际应用中通常取 top 3~5 条交给 LLM,或者再用 reranker(重排序模型) 精排——这就是前面说的 bge-reranker-v2-m3 的用途。
  3. 每次都重算全部文档向量太慢:文档一多就不现实。应该把向量算一次、存起来,查询时直接检索——这就是向量数据库要做的事。

六、提交代码 ​

bash
git add .
git commit -m "03 embedding 向量检索"
git push

下一节预告 ​

用 Chroma 向量数据库存储文档向量,再把检索结果交给 LLM 生成答案,完成第一个完整的 RAG 问答程序。

👉 下一阶段:第一个 RAG(Chroma + LLM)