在 Cherry Studio 里配置本地文本嵌入模型:一篇踩坑到通关的实录
目录
省流:如果你只打算用远端嵌入模型(比如 OpenAI 的
text-embedding-3-small),那 Cherry Studio 开箱即用,本文可以直接划走。本文写给那些执意要把嵌入模型也跑在本地的人。我踩了两轮坑,试了三套方案,最后用一个 Python 脚本收了尾。全程记录,供后来者避雷。
一、背景:我到底想干什么
需求很简单:我想在 Cherry Studio 里建一个知识库,把项目文档、API 接口说明这些东西喂进去,做本地 RAG 检索。
聊天模型我已经在本地跑起来了,问题卡在文本嵌入模型(Embedding Model)这一环。我不想让文档内容走网络出去,所以坚持要用本地嵌入。
于是就有了下面这段血泪史。
二、第一轮失败:Ollama + bge-m3
这是最"标准"的方案,网上教程一抓一大把。
2.1 部署过程
先在 Ollama 里拉模型:
ollama pull bge-m3
确认模型在位:
ollama list

然后在 Cherry Studio 里配置 Ollama 服务:
设置 → 模型服务 → Ollama
API 地址:http://localhost:11434
添加嵌入模型:bge-m3

2.2 翻车现场
在知识库里上传文本,点击嵌入,界面一直没有响应,然后弹出报错:
Error invoking remote method 'knowledge-base:search':
TypeError: Cannot read properties of undefined (reading '0')

2.3 排查:到底是谁的锅
一开始我怀疑是 Ollama 版本太老(早期版本 ollama embed 命令都没有)。查了一下版本:
ollama --version
# 0.30.11
版本很新,没问题。那就直接测接口。
测旧接口 /api/embeddings:
Invoke-RestMethod -Uri "http://localhost:11434/api/embeddings" `
-Method Post `
-ContentType "application/json" `
-Body (@{
model = "bge-m3"
prompt = "测试文本"
} | ConvertTo-Json)
返回了正常的向量数组。
测新接口 /api/embed:
Invoke-RestMethod -Uri "http://localhost:11434/api/embed" `
-Method Post `
-ContentType "application/json" `
-Body (@{
model = "bge-m3"
input = "测试文本"
} | ConvertTo-Json)
也正常返回 embeddings 字段。

关键对照实验:我把嵌入模型换成远端的 text-embedding-3-small,知识库一切正常。
结论浮出水面:
- ✅ Ollama 本身正常
- ✅ bge-m3 正常
- ✅ 两个嵌入接口都能正确返回
- ✅ 远端 OpenAI 嵌入在 Cherry Studio 里正常
- ❌ 唯独 Cherry Studio 调本地 Ollama 嵌入就崩
2.4 根因:返回格式对不上
Cannot read properties of undefined (reading '0') 这个错误本质是:Cherry Studio 拿到返回体后,按某个路径去取 [0],结果那个路径是 undefined。
对比三种返回格式就明白了:
OpenAI / text-embedding-3-small 的格式:
{
"data": [
{ "embedding": [0.1, 0.2, ...], "index": 0 }
]
}
Ollama /api/embeddings:
{ "embedding": [0.1, 0.2, ...] }
Ollama /api/embed:
{ "embeddings": [[0.1, 0.2, ...]] }
Cherry Studio 按 OpenAI 的 data[0].embedding 去解析,而 Ollama 原生接口根本没有 data 字段,取 data[0] 自然就是 undefined,再取 [0] 直接抛异常。
我换了 nomic-embed-text,一样的错误——这就坐实了:跟具体模型无关,是 Cherry Studio 对 Ollama 原生嵌入返回格式的解析问题。
三、第二轮失败:LM Studio
既然怀疑是 Ollama 接口格式问题,那我换个能提供 OpenAI 兼容接口的后端总行了吧。于是我上了 LM Studio。
3.1 部署过程
- LM Studio 里下载一个文本嵌入模型
- 配置好之后,开启本地服务(Local Server)
- LM Studio 默认就暴露 OpenAI 兼容接口,地址类似
http://localhost:1234/v1
然后在 Cherry Studio 里以 OpenAI 类型接入:
API 地址:http://localhost:1234/v1
API Key:随便填
嵌入模型:填 LM Studio 里的模型名
3.2 翻车现场
这次没有报格式错误,但更糟心——Cherry Studio 的知识库那边一直显示 “等待中”,模型压根没连通的样子。

排查了半天连接、端口、模型名,始终没能让它跑起来。考虑到时间成本,我放弃了 LM Studio 这条路。
顺便说一句,我在 Cherry Studio 里也试过把 Ollama 走 OpenAI 兼容接口
http://localhost:11434/v1,同样没成。看接口预览时我注意到一个细节:聊天走的是/api/chat,切成 OpenAI 后变成/v1/chat/completions——这俩都是对话接口,嵌入接口是另一套,配置时千万别把嵌入模型误加到聊天模型列表里。
四、转机:AnythingLLM 跑通了
两轮失败后,我换了个思路:不直接让 Cherry Studio 连后端,而是中间加一层。
我装了 AnythingLLM,配置嵌入模型时选 Ollama,本质上底层还是 Ollama 在跑 bge-m3,但 AnythingLLM 在客户端做了一层 API 中转,对外暴露标准的 OpenAI 兼容接口。
4.1 配置过程
- 提前打开ollama,然后打开 AnythingLLM,进初始化向导
- 嵌入后端选 Ollama,地址
http://localhost:11434,模型选bge-m3 - 向量数据库用内置的 LanceDB 即可

然后在 AnythingLLM 里生成 API Key,把它作为 OpenAI 服务接入 Cherry Studio:
API 地址:http://localhost:3001/api/v1/openai
API Key:AnythingLLM 生成的 Key

4.2 通了
这次知识库嵌入和检索都正常了。
为什么 AnythingLLM 能通,直连 Ollama 不行?
因为 AnythingLLM 做的那层中转,把 Ollama 原生的 { "embedding": [...] } 重新包装成了 OpenAI 标准的 { "data": [{ "embedding": [...] }] }。Cherry Studio 拿到的就是它认识的格式,data[0].embedding 能正确取到,问题自然消失。
这也反过来验证了第二章的根因判断:核心矛盾就是返回格式。
五、最终方案:一个 Python 脚本搞定
AnythingLLM 虽然能用,但它是个完整的桌面应用,为了一个"格式中转"的功能装这么重的东西,我觉得不划算。而且我本地已经有 Ollama,再叠一层 AnythingLLM,软件太多,维护心智负担重。
于是我把不需要的软件都卸了,只保留一个思路:
既然问题只是"返回格式要符合 OpenAI 规范",那我自己写个几十行的服务,直接提供一个 OpenAI 兼容的
/v1/embeddings接口不就行了?
5.1 核心思路
写一个 FastAPI 服务,做三件事:
- 本地加载 bge-m3 嵌入模型
- 对文本做 embedding + mean pooling + L2 归一化
- 按 OpenAI 标准格式返回
我用的是 OpenVINO 的 IR 格式模型,可以在 GPU/CPU(甚至 NPU)上跑,不依赖 Ollama。核心是那个 /v1/embeddings 接口,返回结构严格对齐 OpenAI。
5.2 关键代码
服务的核心是嵌入接口,返回体的结构是能不能被 Cherry Studio 接受的关键:
@app.post("/v1/embeddings")
def embeddings(req: EmbeddingRequest):
"""OpenAI 兼容的 Embedding 接口,使用 OpenVINO IR 格式的 bge-m3"""
if not embedding_loaded:
raise HTTPException(status_code=503, detail="Embedding model not loaded")
texts = [req.input] if isinstance(req.input, str) else req.input
inputs = embedding_tokenizer(
texts,
padding=True,
truncation=True,
max_length=8192,
return_tensors="np"
)
model_inputs = {
"input_ids": inputs["input_ids"],
"attention_mask": inputs["attention_mask"]
}
if "token_type_ids" in inputs:
model_inputs["token_type_ids"] = inputs["token_type_ids"]
outputs = embedding_model(model_inputs)
last_hidden_state = outputs[embedding_model.output(0)]
# mean pooling + L2 归一化
embeddings = mean_pooling(last_hidden_state, inputs["attention_mask"])
embeddings = normalize(embeddings)
# 关键:按 OpenAI 标准格式组织返回体
data = []
for i, emb in enumerate(embeddings):
data.append({
"object": "embedding",
"index": i,
"embedding": emb.tolist()
})
total_tokens = int(np.sum(inputs["attention_mask"]))
return {
"object": "list",
"data": data, # <- 就是这个 data 数组,Cherry Studio 认它
"model": req.model,
"usage": {
"prompt_tokens": total_tokens,
"total_tokens": total_tokens
}
}
两个辅助函数:
def mean_pooling(last_hidden_state, attention_mask):
"""对 token 级 embedding 按 attention mask 做 mean pooling"""
mask = np.expand_dims(attention_mask, axis=-1).astype(np.float32)
sum_embeddings = np.sum(last_hidden_state * mask, axis=1)
sum_mask = np.clip(mask.sum(axis=1), a_min=1e-9, a_max=None)
return sum_embeddings / sum_mask
def normalize(embeddings):
"""L2 归一化"""
norms = np.linalg.norm(embeddings, axis=1, keepdims=True)
return embeddings / np.clip(norms, a_min=1e-9, a_max=None)
模型加载部分按 GPU → CPU 的顺序尝试(NPU 对 bge-m3 的动态 shape 支持不好,就不折腾了):
EMBEDDING_MODEL_PATH = "D:/ollama_models/bge-m3-ov"
EMBEDDING_DEVICES = ["GPU", "CPU"]
def load_embedding_model():
global embedding_tokenizer, embedding_model, embedding_loaded, embedding_device
embedding_tokenizer = AutoTokenizer.from_pretrained(EMBEDDING_MODEL_PATH)
core = ov.Core()
model_xml = os.path.join(EMBEDDING_MODEL_PATH, "openvino_model.xml")
for dev in EMBEDDING_DEVICES:
try:
embedding_model = core.compile_model(model_xml, dev)
embedding_device = dev
logger.info(f"Embedding model loaded on {dev}")
embedding_loaded = True
return True
except Exception as e:
logger.warning(f"Embedding {dev} compile failed: {e}")
return False
再补一个 /v1/models 接口,方便 Cherry Studio 探测可用模型:
@app.get("/v1/models")
def list_models():
models = []
if embedding_loaded:
models.append({
"id": "bge-m3-npu",
"object": "model",
"created": 0,
"owned_by": "local-npu"
})
return {"object": "list", "data": models}
启动:
if __name__ == "__main__":
uvicorn.run(app, host="127.0.0.1", port=8000)
如果你没有 OpenVINO 环境,把加载和推理部分换成
sentence-transformers或transformers直接跑 bge-m3 也一样,关键是返回体保持 OpenAI 格式。
5.3 启动服务并自测
python npu_llm_server.py
看到日志里出现 Embedding model loaded on GPU(或 CPU)就说明模型加载成功。

先自己测一下接口,确认返回是 OpenAI 格式:
$ApiUri = "http://127.0.0.1:8000/v1/embeddings"
$ModelName = "bge-m3-npu"
$TestInput = "Hello World"
$body = @{ model = $ModelName; input = $TestInput } | ConvertTo-Json
try {
$response = Invoke-RestMethod -Uri $ApiUri -Method Post -ContentType "application/json" -Body $body -TimeoutSec 30
Write-Host "OK - Model: $($response.model) | Dim: $($response.data[0].embedding.Count) | Tokens: $($response.usage.total_tokens)"
Write-Host "`nResponse:" -ForegroundColor Cyan
$response | ConvertTo-Json -Depth 5
}
catch {
Write-Host "FAILED: $($_.Exception.Message)" -ForegroundColor Red
}
返回里应该能看到 object: list 和 data 数组,每个元素带 embedding。

六、在 Cherry Studio 里接入自建服务
- 打开 Cherry Studio,进入
设置 → 模型服务 - 添加一个 OpenAI 类型的服务
| 配置项 | 值 |
|---|---|
| 名称 | Local-Embedding(随便取) |
| API 地址 | http://127.0.0.1:8000/v1 |
| API Key | 随便填,比如 local |
| 嵌入模型 | bge-m3-npu |
- 新建知识库,嵌入模型选
bge-m3-npu - 上传文档,等待索引完成
- 提问检索
这次一切正常,嵌入和检索都跑通了,而且不依赖 Ollama、也不依赖 AnythingLLM,只有一个 Python 进程。
七、避坑清单
写给赶时间的人,这几条是本文的精华:
- 报
Cannot read properties of undefined (reading '0'),几乎可以断定是 Cherry Studio 按 OpenAI 格式data[0].embedding解析,而 Ollama 原生接口没有data字段。跟你用哪个嵌入模型无关。 - 验证后端是否正常,用 PowerShell / curl 直接打接口,别只在 Cherry Studio 里瞎猜。Ollama 的
/api/embeddings、/api/embed、/v1/embeddings三种返回格式各不相同。 - 远端
text-embedding-3-small能通、本地不通,说明问题在"本地后端的返回格式",不在知识库功能本身。 - LM Studio 那条路我没走通(一直"等待中"),如果你要试,重点排查端口、模型名和服务是否真的监听成功。
- AnythingLLM 能救急,本质是它帮你把 Ollama 的返回包装成了 OpenAI 格式。但为一个格式转换装个桌面应用,重了点。
- 最干净的方案是自己写个 OpenAI 兼容的
/v1/embeddings,几十行 FastAPI 搞定。核心只有一句话:返回体必须是{ "object": "list", "data": [{ "embedding": [...], "index": 0 }] }。
八、写在最后
这趟折腾下来,最大的体会是:遇到"客户端连不上本地模型"这类问题,先用命令行把每个后端接口单独打一遍,看返回结构,比在 GUI 里反复点重试高效太多。
Cherry Studio 对 Ollama 原生嵌入接口的兼容,希望官方后续能修(当前版本:v1.9.11)。但在那之前,如果你也执意要本地嵌入,一个几十行的 Python 中转服务就是最省心的答案。
关于本文脚本的适用范围(重要)
需要说明的是,本文分享的部分脚本是基于我当前的电脑环境构建的:
- 系统:Windows 11 x64
- CPU:Intel Ultra 7 155H(自带 NPU / Intel AI Boost)
- 推理框架:OpenVINO,模型用的是 bge-m3 的 OpenVINO IR 格式
所以脚本里用到了 openvino、openvino_genai,并且按 GPU → CPU 的顺序编译模型(NPU 对 bge-m3 的动态 shape 支持不好,我没走 NPU)。
如果你的机器没有 OpenVINO 环境,或者不想折腾 IR 格式模型,直接照抄这个脚本是跑不起来的。 你需要把模型加载和推理那部分改写成 sentence-transformers 版本,思路是:
- 去掉
import openvino、AutoTokenizer+ov.Core()那一整套加载逻辑,换成SentenceTransformer("BAAI/bge-m3")直接加载; /v1/embeddings里的 tokenizer + mean pooling + normalize 手动流程也可以省掉,sentence-transformers的model.encode(texts, normalize_embeddings=True)一步就把归一化后的向量给你了;- 唯一不能动的是返回体结构——不管底层怎么换,最后返回的 JSON 必须保持 OpenAI 格式:
{
"object": "list",
"data": [
{ "object": "embedding", "index": 0, "embedding": [0.1, 0.2, ...] }
],
"model": "bge-m3",
"usage": { "prompt_tokens": 5, "total_tokens": 5 }
}
只要这个格式对,Cherry Studio 那头就能正常解析。至于底层是 OpenVINO、sentence-transformers 还是别的什么,它并不关心。
更多推荐

所有评论(0)