在 Cherry Studio 里配置本地文本嵌入模型:一篇踩坑到通关的实录

📅 2026/7/4 3:51:38
在 Cherry Studio 里配置本地文本嵌入模型:一篇踩坑到通关的实录
目录一、背景我到底想干什么二、第一轮失败Ollama bge-m32.1 部署过程2.2 翻车现场2.3 排查到底是谁的锅2.4 根因返回格式对不上三、第二轮失败LM Studio3.1 部署过程3.2 翻车现场四、转机AnythingLLM 跑通了4.1 配置过程4.2 通了五、最终方案一个 Python 脚本搞定5.1 核心思路5.2 关键代码5.3 启动服务并自测六、在 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-m32.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/embeddingsInvoke-RestMethod-Urihttp://localhost:11434/api/embeddings-Method Post -ContentTypeapplication/json-Body({model bge-m3prompt 测试文本}|ConvertTo-Json)返回了正常的向量数组。测新接口/api/embedInvoke-RestMethod-Urihttp://localhost:11434/api/embed-Method Post -ContentTypeapplication/json-Body({model bge-m3input 测试文本}|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 ServerLM 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 StudioAPI 地址http://localhost:3001/api/v1/openai API KeyAnythingLLM 生成的 Key4.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)defembeddings(req:EmbeddingRequest):OpenAI 兼容的 Embedding 接口使用 OpenVINO IR 格式的 bge-m3ifnotembedding_loaded:raiseHTTPException(status_code503,detailEmbedding model not loaded)texts[req.input]ifisinstance(req.input,str)elsereq.inputinputsembedding_tokenizer(texts,paddingTrue,truncationTrue,max_length8192,return_tensorsnp)model_inputs{input_ids:inputs[input_ids],attention_mask:inputs[attention_mask]}iftoken_type_idsininputs:model_inputs[token_type_ids]inputs[token_type_ids]outputsembedding_model(model_inputs)last_hidden_stateoutputs[embedding_model.output(0)]# mean pooling L2 归一化embeddingsmean_pooling(last_hidden_state,inputs[attention_mask])embeddingsnormalize(embeddings)# 关键按 OpenAI 标准格式组织返回体data[]fori,embinenumerate(embeddings):data.append({object:embedding,index:i,embedding:emb.tolist()})total_tokensint(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}}两个辅助函数defmean_pooling(last_hidden_state,attention_mask):对 token 级 embedding 按 attention mask 做 mean poolingmasknp.expand_dims(attention_mask,axis-1).astype(np.float32)sum_embeddingsnp.sum(last_hidden_state*mask,axis1)sum_masknp.clip(mask.sum(axis1),a_min1e-9,a_maxNone)returnsum_embeddings/sum_maskdefnormalize(embeddings):L2 归一化normsnp.linalg.norm(embeddings,axis1,keepdimsTrue)returnembeddings/np.clip(norms,a_min1e-9,a_maxNone)模型加载部分按 GPU → CPU 的顺序尝试NPU 对 bge-m3 的动态 shape 支持不好就不折腾了EMBEDDING_MODEL_PATHD:/ollama_models/bge-m3-ovEMBEDDING_DEVICES[GPU,CPU]defload_embedding_model():globalembedding_tokenizer,embedding_model,embedding_loaded,embedding_device embedding_tokenizerAutoTokenizer.from_pretrained(EMBEDDING_MODEL_PATH)coreov.Core()model_xmlos.path.join(EMBEDDING_MODEL_PATH,openvino_model.xml)fordevinEMBEDDING_DEVICES:try:embedding_modelcore.compile_model(model_xml,dev)embedding_devicedev logger.info(fEmbedding model loaded on{dev})embedding_loadedTruereturnTrueexceptExceptionase:logger.warning(fEmbedding{dev}compile failed:{e})returnFalse再补一个/v1/models接口方便 Cherry Studio 探测可用模型app.get(/v1/models)deflist_models():models[]ifembedding_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,host127.0.0.1,port8000)如果你没有 OpenVINO 环境把加载和推理部分换成sentence-transformers或transformers直接跑 bge-m3 也一样关键是返回体保持 OpenAI 格式。5.3 启动服务并自测python npu_llm_server.py看到日志里出现Embedding model loaded on GPU或 CPU就说明模型加载成功。先自己测一下接口确认返回是 OpenAI 格式$ApiUrihttp://127.0.0.1:8000/v1/embeddings$ModelNamebge-m3-npu$TestInputHello World$body {model $ModelName;input $TestInput}|ConvertTo-Jsontry{$responseInvoke-RestMethod-Uri$ApiUri-Method Post-ContentTypeapplication/json-Body$body-TimeoutSec 30Write-HostOK - Model:$($response.model)| Dim:$($response.data[0].embedding.Count)| Tokens:$($response.usage.total_tokens)Write-HostnResponse:-ForegroundColor Cyan$response|ConvertTo-Json-Depth 5}catch{Write-HostFAILED:$($_.Exception.Message)-ForegroundColor Red}返回里应该能看到object: list和data数组每个元素带embedding。六、在 Cherry Studio 里接入自建服务打开 Cherry Studio进入设置 → 模型服务添加一个OpenAI类型的服务配置项值名称Local-Embedding随便取API 地址http://127.0.0.1:8000/v1API 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 x64CPUIntel 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、AutoTokenizerov.Core()那一整套加载逻辑换成SentenceTransformer(BAAI/bge-m3)直接加载/v1/embeddings里的 tokenizer mean pooling normalize 手动流程也可以省掉sentence-transformers的model.encode(texts, normalize_embeddingsTrue)一步就把归一化后的向量给你了唯一不能动的是返回体结构——不管底层怎么换最后返回的 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 还是别的什么它并不关心。