在 Ollama 使用教程:本地大模型部署工具完全指南 中提到过 Embeddings,但写得比较简略。这篇文章单独展开说一下,因为我发现很多人在做 RAG(检索增强生成)时卡住的点其实并不在模型本身,而是对 Embeddings 的理解不够透,导致后面检索出来的东西质量很差。
什么是Embeddings(嵌入向量)
先讲一个简单概念。
计算机不认识「意思」这一说。你给它一句「猫趴在垫子上」,它看到的只是一堆字符。但如果你把这句话转成一个由几百个浮点数组成的数组(比如长度 384 或 768),两个句子的向量在空间里距离近,说明它们的语义相似。嵌入向量就是干这件事的。
Ollama 里跑 Embeddings 模型的时候,模型不生成自然语言回复,只吐出向量。输入「Man 爱吃 pizza」,输出就是类似 [0.023, -0.457, 0.112, ...] 这样的数组。后续你可以拿这个数组和别的句子向量做余弦相似度计算,找语义最接近的文本。
和普通的 LLM(重点是生成)不一样,Embeddings 模型只做「编码」这一半工作。
Ollama Embeddings的使用场景
1. 搜索引擎 / RAG(检索增强生成)
假设你有一堆文档(比如你的笔记、公司内部 Wiki、法律条款),用户问一个问题。你不能把整篇文档都塞进模型的上下文(太长了而且贵),但你可以:先把所有文档切块,每块生成一个 embedding → 存到向量数据库;用户问问题时,把问题也转成 embedding → 在数据库里找最相似的几块 → 把那几块原文 + 用户问题一起发给大模型,让模型基于资料回答。这个流程就是 RAG 的基础。
2. 去重 / 聚类
你想把一堆用户评论按主题分组,或者找重复的客服工单,靠人眼看不过来。可以把每条评论转成 embedding,然后用聚类算法(比如 k-means)自动分堆,或者两两算相似度,超过阈值就认为是重复。
3. 推荐系统 / 搜索排序
用户点击了一篇文章,你就用那篇文章的 embedding 去库里找「看过这篇文章的人还看了什么」——本质就是找最相似的向量。
Ollama 里怎么生成 Embeddings
命令行
最简单的用法:
ollama run embeddinggemma "Hello world"
这个会给出一长串浮点数,你可以直接重定向到文件或者用管道处理。
embeddinggemma 只是其中一种模型,也可以换成 nomic-embed-text、all-minilm 等。
REST API
批量生成(比如你要一次性给 100 段文字做向量):
curl -X POST http://localhost:11434/api/embed \
-H "Content-Type: application/json" \
-d '{
"model": "embeddinggemma",
"input": ["第一段文本", "第二段文本"]
}'
返回的 JSON 里 embeddings 字段是一个二维数组,每个元素对应一个输入的向量。
返回的 embeddings 是一个多维浮点数数组。注意不是所有模型都支持 embedding 功能,需要模型本身是专门训练来做 embedding 的,比如 all-minilm、nomic-embed-text、bge-m3 等。用普通的对话模型(像 qwen3.5)做 embedding 也不是不行,但效果一般,因为训练目标不同。
Python SDK
import ollama
response = ollama.embed(
model='embeddinggemma',
input=['天空是蓝色的', '太阳从东边升起']
)
vectors = response.embeddings
print(len(vectors[0])) # 向量的维度,比如 384
需要注意:embed 函数是同步的,输入太多文本会等很久。如果你的文档集合很大,建议分批调用,或者换用异步方式自己控制并发。
一个易混淆的点:Ollama 有两个 embedding 相关的 API:
- /api/embed(新版,推荐),支持批量输入和 dimensions 参数(截断维度)
- /api/embeddings(旧版,仅单条输入,后续可能弃用)
两个都能用,但新项目直接用 /api/embed 就行。
和推理模型的关系:embedding 模型往往比对话模型小很多(几十 MB 到一两 GB),因为它们的任务简单——不需要生成自然语言,只需要输出固定维度的向量。所以跑 embedding 的硬件门槛低得多,甚至树莓派都能跑。
选哪个 Embeddings 模型
Ollama 官方库( https://ollama.com/library?q=embed )里带 embedding 标签的模型不少。我自己试过几个,简单做个对比:
| 模型 | 维度 | 中文支持 | 速度 | 推荐场景 |
|---|---|---|---|---|
| embeddinggemma | 768 | 一般 | 较快 | 通用英文,小项目 |
| nomic-embed-text | 768 | 还凑合 | 中等 | 长文档(8192 token 上下文) |
| all-minilm | 384 | 差 | 极快 | 快速原型,对精度要求不高 |
| bge-m3 | 1024 | 好 | 较慢 | 多语言 + 稠密+稀疏混合检索 |
中文内容多的话,bge-m3 虽然慢但效果好。如果只处理技术文档(术语偏英文),nomic-embed-text 性价比不错。all-minilm 维度低,存储和计算都快,但语义区分度弱一些。
经典用法:通过Ollama Embedding实现一个最简单的语义搜索
假设你有一堆自己的笔记或者博客文章,想通过问题找到最相关的那几段。
步骤 1:预计算所有笔记内容的向量
import ollama
import json
notes = [
"今天用 Ollama 跑了下 qwen3,速度还行",
"买了新的机械键盘,青轴声音太大了",
"Embeddings 可以用来做相似度计算",
"RAG 需要先把文档切成块,再生成向量",
]
# 存储 (文本, 向量)
corpus = []
for text in notes:
resp = ollama.embed(model='nomic-embed-text', input=[text])
vec = resp.embeddings[0]
corpus.append((text, vec))
步骤 2:用户输入 query,转为向量
query = "怎么用 Ollama 做向量搜索"
query_vec = ollama.embed(model='nomic-embed-text', input=[query]).embeddings[0]
步骤 3:计算余弦相似度,排序
import numpy as np
def cosine_similarity(a, b):
return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))
results = []
for text, vec in corpus:
sim = cosine_similarity(query_vec, vec)
results.append((sim, text))
results.sort(reverse=True)
for sim, text in results[:2]:
print(f"{sim:.4f}: {text}")
你会看到和「向量搜索」语义最接近的那条笔记排在前面。这套东西跑通了,后面就能搭 RAG。
进阶:配合生成模型做 RAG
RAG 的基本流程:
- 用户提问
- 把问题转成向量,去向量库里检索相关文档片段
- 把检索到的片段 + 原始问题一起喂给普通 LLM(比如 qwen3)生成最终答案
伪代码:
# 假设已经有了 corpus 向量库(上面 notes 的例子)
def rag_answer(question):
q_vec = ollama.embed(model='nomic-embed-text', input=[question]).embeddings[0]
# 找到最相似的 top-2 文本
scores = [(cosine_similarity(q_vec, vec), text) for text, vec in corpus]
scores.sort(reverse=True)
context = "\n".join([text for _, text in scores[:2]])
prompt = f"基于以下信息回答用户的问题:\n{context}\n\n问题:{question}\n回答:"
response = ollama.generate(model='qwen3:4b', prompt=prompt)
return response['response']
注意:这里为了简单演示,每来一个问题都会重新算一次相似度。工程上应该把向量存到数据库里(如 Chroma、Qdrant、FAISS),然后建索引,才能支撑大量文档。
一些容易踩的坑
维度不匹配
不同模型输出的向量维度不一样。比如 all-minilm 是 384 维,bge-m3 是 1024 维。你把两种模型生成的向量混在一个库里,相似度计算会出错(即使强行 dot 也能算,但语义空间不一致)。要么统一用一个模型,要么做模型对齐(很麻烦,不推荐)。
上下文长度
Embeddings 模型也有输入长度限制。nomic-embed-text 支持 8192 token,embeddinggemma 只有 2048。如果某个文本块超过限制,要么截断,要么切得更碎。一般做法是把文档按句子或段落切块,每块控制在 500 token 以内比较安全。
归一化
余弦相似度计算前,很多实现会先把向量 L2 归一化。ollama 返回的原始向量已经是归一化过的吗?我没在文档里找到明确说明。保险的做法还是自己 normalise 一下,或者直接用 np.dot(vec1, vec2) / (norm1 * norm2)。
中文 token 计数
Ollama 的 token izer 对不同模型不同。一个中文字可能占 2-3 token,自己估算长度的时候别按字符数来,最好实际 ollama show <embedding-model> --parameters 看看详细信息。稳妥做法是写一小段代码测试超长输入会报什么错误。
什么时候用嵌入向量,什么时候不用
- 需要找「语义相似」的内容 → 用嵌入向量
- 需要找完全相同的关键词(比如搜索「ollama 报错 404」) → 用传统倒排索引(BM25)更好,Embeddings 对罕见词不敏感
- 需要做聚类、分类、异常检测 → 嵌入向量做特征输入到下游模型
- 只需要生成一句话 → 直接
ollama run就行,别绕一圈嵌入
实践中很多 RAG 系统会把 Embedding 检索和关键词检索混合(HyDE、RRF 等)。初期先把纯 Embedding 跑通,后面再慢慢优化。
总结
微信公众号后台自动回复中AI回复其实就是一种RAG,根据你的文章来回复粉丝的私信。相信你看完这篇文章之后也懂了它的实现原理。
如果是自己跑数据,要处理大规模数据(百万级以上),建议上专门的向量数据库 + GPU 加速版 FAISS。Ollama 这边负责生成向量,剩下的检索部分交给别的工具。
