ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Chroma中文支持实战:换Embedding模型搭建本地知识库

Chroma中文支持实战:换Embedding模型搭建本地知识库 “让 Chroma 支持中文”这个说法其实有点误导——Chroma 本身是向量数据库理论上它不挑语言文本进来都会变成向量存进去查询的时候再变成向量算相似度。真正的问题是Chroma 默认带的那套 embedding 模型是英文模型中文文本经过它转换之后语义信息丢得七七八八所以中文检索效果惨不忍睹。我见过太多人兴冲冲用 Chroma 搭本地知识库结果中文一问一个不准第一反应就是“这数据库不支持中文”其实病根在 embedding不在数据库。这篇文章把我做中文知识库时踩过的坑、改过的配置、验证过有效的方法都写清楚怎么换中文 embedding 模型、中文文档怎么切分、查询时需要注意什么以及一条完整的 Ollama LangChain Chroma 本地知识库链路。适合准备搭中文 RAG 应用、本地知识库的开发者不管新手老手都能直接照着做。1. 先搞清楚Chroma 中文效果差病根到底在哪1.1 向量数据库的原理与“不支持中文”的真相很多人第一次接触 Chroma是照着英文教程搭了一个问答机器人换成中文语料后检索结果完全没法看。于是得出结论Chroma 不支持中文。这个结论是错的。Chroma 内部根本没有“分词器”也没有“语言检测”这种东西它的核心逻辑只有三步把文本通过 embedding 模型变成向量。把向量和元数据一起存进 HNSW 索引。查询时把你的问题也变成向量在索引里做最近邻搜索。换句话说Chroma 本身是语言无关的。真正决定中文检索质量的地方在第一步文本变成向量的过程中embedding 模型到底懂不懂中文。Chroma 默认的 embedding 函数来自 ONNX 版的all-MiniLM-L6-v2这是一个在英文语料上训练的小型模型。把中文句子丢进去它并不是“不能处理”而是处理得很粗糙中文里的同义词、语序变化、口语表达在这个模型看来和随机噪声没太大区别。打个比方这就像让一个完全不懂中文的人给中文图书馆做索引他能记住每个字的长相但完全不知道这些字组合起来是什么意思。查询的时候只能靠“字面重合”去碰运气语义检索自然无从谈起。所以让 Chroma 支持中文本质上不是改 Chroma而是换掉它默认的 embedding 模型换成在中文语料上训练过的模型。这一步能做到位后面切分和检索的很多问题都会迎刃而解。1.2 三分钟定位做一个 embedding 效果小实验我建议你在动手改造之前先花三分钟做一个验证实验亲眼看看默认模型对中文有多“瞎”。这个实验不需要 LangChain只要装一个sentence-transformers就够了。from sentence_transformers import SentenceTransformer from sklearn.metrics.pairwise import cosine_similarity texts [ 今天天气怎么样, 明天会下雨吗, The weather is nice today, ] # 默认模型Chroma 默认同源和中文模型各跑一次 default_model SentenceTransformer(all-MiniLM-L6-v2) zh_model SentenceTransformer(BAAI/bge-small-zh-v1.5) vecs_default default_model.encode(texts) vecs_zh zh_model.encode(texts) print(默认模型相似度矩阵) print(cosine_similarity(vecs_default)) print(\n中文模型相似度矩阵) print(cosine_similarity(vecs_zh))在我自己的测试里默认模型对“今天天气怎么样”和“明天会下雨吗”这两句中文的相似度可能在 0.6 左右而对“The weather is nice today”这句英文反而能给到 0.7 以上。这已经很不合理了明明都是天气话题中文近义句的相似度居然不如跨语言的句子。换成bge-small-zh-v1.5之后前两句中文的相似度能到 0.8 左右和英文句子的相似度则明显下降。这个实验能直观地告诉你两件事第一你的知识库检索不准大概率是 embedding 的问题第二换成中文模型之后效果提升是肉眼可见的。所谓“让 Chroma 支持中文”核心就是这一步。注意不同版本模型的相似度数值会有浮动但趋势是一致的。如果默认模型对中文句对和英文句对的相似度差异不大说明这个模型基本没学到中文的语义结构该换了。2. 中文支持的关键改造Embedding、切分、检索三件套2.1 换 Embedding 模型选型与接入方式目前中文场景下常用的 embedding 模型大致有以下几类我按“无脑可用”到“效果好但更重”排个序模型体积中文效果特点与适用场景BAAI/bge-small-zh-v1.5约 100MB中上体积小、CPU 可跑、效果稳定个人知识库首选shibing624/text2vec-base-chinese约 400MB中上中文语义相似度任务表现好查询时不需要特殊前缀BAAI/bge-large-zh-v1.5约 400MB高效果更好CPU 推理明显更慢适合离线批处理BAAI/bge-m3约 2GB高多语言、支持长文本能处理 8192 token重但全面nomic-embed-textOllama约 500MB中下英文为主Ollama 直接可用中文不如专门中文模型我的建议很简单如果你用 LangChain 或者直接操作 Chroma优先选bge-small-zh-v1.5它是我目前用过“性价比”最高的中文 embedding 模型。如果电脑配置比较好、检索精度要求高可以上bge-large-zh-v1.5或者bge-m3。如果整个链路都在 Ollama 里那么bge-m3是目前 Ollama 官方模型库中中文支持最好的向量模型。接入 LangChain 时新版用法是走langchain-huggingface包from langchain_huggingface import HuggingFaceEmbeddings embedding_model HuggingFaceEmbeddings( model_nameBAAI/bge-small-zh-v1.5, model_kwargs{device: cpu}, encode_kwargs{normalize_embeddings: True}, )如果你没有用 LangChain也可以直接给 Chroma 传一个自定义 embedding functionfrom chromadb.utils import embedding_functions ef embedding_functions.SentenceTransformerEmbeddingFunction( model_nameBAAI/bge-small-zh-v1.5, devicecpu, normalize_embeddingsTrue, )这里有两个细节值得注意。第一normalize_embeddingsTrue会把向量归一化成单位向量让余弦相似度和内积等价在很多检索任务里都能提升稳定性。第二首次运行会自动从 HuggingFace 下载模型如果网络环境不稳定建议先手动跑一次下载后面就不会中断了。2.2 中文文档切分让语义单元保持完整Embedding 模型换好了第二个大坑就是文档切分。LangChain 默认的RecursiveCharacterTextSplitter是按[\n\n, \n, , ]来切分的这套规则对英文很友好对中文却有点水土不服中文没有空格分词默认切分器经常会把一句完整的话从中间截断导致一个 chunk 里装了一半语义检索时自然找不对上下文。我常用的做法是自定义 separators把中文标点加进切分规则from langchain_text_splitters import RecursiveCharacterTextSplitter splitter RecursiveCharacterTextSplitter( chunk_size300, chunk_overlap50, separators[\n\n, \n, 。, , , , , , ], )原理不复杂切分器会优先按长分隔符切如果某个 chunk 仍然超过chunk_size就依次尝试下一个分隔符。加了中文标点之后一个 chunk 能在句子边界附近停下来而不是毫无预兆地把句子腰斩。chunk_size的选择也很关键。我见过有人直接把整篇文档丢进 Chroma结果检索到的几乎都是整个文档的向量一问就答非所问。对中文知识库来说chunk_size300到500是一个比较稳妥的区间既能保证每个片段有足够的语境又不至于超过大部分 embedding 模型 512 token 的输入上限。chunk_overlap建议设成 30 到 60让相邻片段有少量重叠避免一个完整知识点刚好卡在切分缝里。一个小技巧切分完成后可以先随机打印几个 chunk 检查一遍。如果每个 chunk 都能读通、语义完整说明切分参数基本靠谱如果经常出现“半个标题”“半句话”就要继续调 separators。2.3 查询侧也要对齐指令前缀与距离度量Embedding 模型选好、切分参数调好之后还有一个很容易被忽略的细节查询时怎么把问题也变成向量。bge系列模型官方建议在检索场景下需要给查询语句加上一个指令前缀文档入库时则不加。具体到bge-small-zh-v1.5前缀是query 为这个句子生成表示以用于检索相关文章 raw_query这是微软在发布 bge 模型时定下的规则原因是训练时查询侧的表示和文档侧的表示是分别优化的。如果忘了加这个前缀检索效果会打一些折扣但也不会完全崩。text2vec系列则没有这个要求直接在原始查询上计算即可。另一个容易被忽略的配置是向量距离度量。Chroma 底层用的 HNSW 索引默认的距离度量是 L2欧氏距离但 embedding 模型产出的向量空间更适合用余弦相似度来度量。建议在创建 collection 的时候显式指定vectorstore Chroma.from_documents( documentssplit_docs, embeddingembedding_model, persist_directory./chroma_db, collection_namezh_kb, collection_metadata{hnsw:space: cosine}, )注意一点collection_metadata里的空间度量在 collection 创建时生效之后不能修改。所以哪怕你只是建一个测试库也建议一开始就把cosine写进去不然后面想改只能删了重建。3. 实录Ollama LangChain Chroma 搭建中文本地知识库3.1 环境准备与模型选择接下来我们走一遍完整链路本地文档 → 切分 → 中文 embedding → 存入 Chroma → 用户提问 → 检索 → Ollama 大模型生成回答。首先确认环境。假设你已经安装了 Python 3.9 以上版本然后安装依赖pip install langchain langchain-huggingface langchain-ollama langchain-chroma chromadb sentence-transformersOllama 方面需要两个模型一个生成模型一个 embedding 模型。生成模型我用的是qwen2.5:7b中文对话能力强跑在本地完全够用。embedding 模型用bge-m3中文效果好、支持超长文本是 Ollama 里最省事的选项。ollama pull qwen2.5:7b ollama pull bge-m3如果你不想折腾 Ollama 的 embedding也可以直接用前面说的HuggingFaceEmbeddings。两条路都能走通我这边的经验是如果 Ollama 已经跑起来了用bge-m3少一套模型加载逻辑链路更干净如果 Ollama 只用来跑大语言模型embedding 单独用 HuggingFace 模型也完全没问题。3.2 核心代码从入库到问答的完整链路下面是一段可以直接跑的完整代码注释我写得比较细from langchain_huggingface import HuggingFaceEmbeddings from langchain_ollama import ChatOllama from langchain_text_splitters import RecursiveCharacterTextSplitter from langchain_community.document_loaders import TextLoader from langchain_chroma import Chroma # 1. 初始化中文 embedding 模型 embedding_model HuggingFaceEmbeddings( model_nameBAAI/bge-small-zh-v1.5, model_kwargs{device: cpu}, encode_kwargs{normalize_embeddings: True}, ) # 2. 加载本地文档注意编码 loader TextLoader(knowledge_base.txt, encodingutf-8) docs loader.load() # 3. 按中文标点切分 splitter RecursiveCharacterTextSplitter( chunk_size300, chunk_overlap50, separators[\n\n, \n, 。, , , , , , ], ) split_docs splitter.split_documents(docs) # 4. 写入 Chroma持久化到本地目录 vectorstore Chroma.from_documents( documentssplit_docs, embeddingembedding_model, persist_directory./chroma_db, collection_namezh_kb, collection_metadata{hnsw:space: cosine}, ) # 5. 构造检索器 retriever vectorstore.as_retriever(search_kwargs{k: 5}) # 6. 初始化 Ollama 大模型 llm ChatOllama(modelqwen2.5:7b, temperature0.1) # 7. 检索 拼接 prompt 生成回答 def ask(question: str): docs retriever.invoke(question) context \n\n.join([d.page_content for d in docs]) prompt f你是一个知识库问答助手请根据下面的参考资料回答用户问题。 如果参考资料中没有相关内容请直接说明不知道不要编造。 参考资料 {context} 用户问题{question} resp llm.invoke(prompt) return resp.content, docs if __name__ __main__: for q in [退货流程是什么, 支持哪些支付方式, 怎么联系客服]: answer, refs ask(q) print(问题:, q) print(回答:, answer) print(参考片段:, len(refs)) print(- * 40)这里有一个容易踩的坑如果你用 Ollama 的bge-m3作为 embeddingLangChain 侧要用OllamaEmbeddings代码是这样的from langchain_ollama import OllamaEmbeddings embedding_model OllamaEmbeddings( modelbge-m3, )其他部分不用改。什么时候用哪个取决于你 Ollama 里到底拉了什么模型。不管选哪个核心原则不变入库和查询必须用同一个 embedding 模型不能混用。3.3 效果验证检索精度到底提升在哪跑通之后我建议你做一个 A/B 对比用同一份中文文档、同一个查询分别用默认模型和中文模型检索对比返回的片段。下面是我实际测试时的结果文档是一份电商客服知识库查询默认模型 top1 命中中文模型 top1 命中“退货流程是什么”商品介绍段落退货政策第一段“怎么申请发票”配送说明段落发票申请段落“会员积分怎么用”会员等级介绍段落积分使用说明段落差距就是这么明显。原因也不复杂默认模型把中文语义“混成一团”检索时只能靠字符重合度蒙所以经常返回一些看起来无关但字面相近的段落。换成中文模型后语义信息保留充分返回的段落才真正对得上问题。如果你不打算接大语言模型只做纯检索用vectorstore.similarity_search(query, k5)也能直接看到效果提升。后面接大模型只是让系统从“搜到相关片段”进一步变成“组织成自然回答”。4. 中文场景下的疑难杂症与进阶技巧4.1 常见问题速查表下面这些问题都是我实际见过、或者自己在项目里踩过的问题原因解决方法中文检索结果完全不相关还在用 Chroma 默认英文 embedding换成bge-small-zh-v1.5或text2vec报错说向量维度不匹配collection 已经用其他 embedding 建过向量维度不同删掉旧 collection 或新开 collection不要混用加载旧的持久化目录后检索变差没传同一个 embedding 模型加载时传相同的embedding_function中文文档全是乱码文件不是 UTF-8 编码加载时指定encodingutf-8必要时先转码CPU 推理慢批量入库要等很久模型太大或 chunk 太多换bge-small-zh或者分批 encode 后再写入Ollama 返回 404 / model not foundOllama 没拉模型或名字不一致先ollama pull bge-m3确认OllamaEmbeddings(model...)名称一致所有查询都返回同一个片段切分粒度过大、chunk 内容过于笼统缩小chunk_size增加中文标点切分加了 bge 指令前缀后效果反而差入库侧也加了前缀语义空间不对称只给查询侧加前缀文档侧保持不加4.2 进阶从“能搜到”到“搜得准”如果你的知识库规模变大单纯靠向量检索会遇到两个问题一是相似段落太多返回结果重复度高二是字面相近但语义不同的片段会干扰判断。这时候有几个比较实用的手段。第一个是 metadata 过滤。给每个 chunk 打上来源、章节、日期等标签查询时用where条件缩小范围vectorstore.similarity_search( 退货政策是什么, k5, where{source: return_policy.md}, )第二个是 MMR 检索。MMR 会在相关性和多样性之间做平衡避免返回的 5 个片段全是同一段话的重复改写vectorstore.max_marginal_relevance_search( 退货政策是什么, k5, fetch_k20, )第三个是混合检索。向量检索擅长语义但有时候关键词精确命中也很重要。可以把 BM25 的检索结果和向量检索结果做一个简单分数融合能显著提升长尾问题的召回率。这块如果要展开写会引入rank_bm25之类的额外依赖但工程上值得投入。第四个是重排。先用向量检索召回 20 条候选再用bge-reranker-large这类 CrossEncoder 对“查询候选片段”逐一打分取排序后的前 3 条。重排是效果提升最明显的一招代价是额外的推理时间适合离线把答案生成好、或者对延迟不敏感的场景。4.3 几个我踩过的坑最后分享几个真实的坑都是文档里不会写的东西。第一个坑在同一个持久化目录下反复实验不同的 embedding 模型。我一开始为了对比默认模型和 bge 模型直接在同一个persist_directory下新建了 collection结果向量维度不一致报错不说旧 collection 还残留在目录里。后来我每次换模型都新建一个目录或者在建 collection 之前先确认维度。这个习惯帮我省了很多事。第二个坑切分参数不是越大越好。我曾经把chunk_size调到 1000想着上下文越全越好结果很多 chunk 超过 embedding 模型输入上限被截断检索效果反而变差。后来压回 300 到 400配合中文标点切分效果立刻回升。第三个坑Ollama 的 embedding 模型不要选nomic-embed-text跑中文。不是说不能跑是效果和bge-m3差距明显。如果你已经用 Ollama 组织整个链路建议直接拉bge-m3别在这个问题上省事。第四个坑在团队协作或 CICD 环境里如果你把 Chroma 的持久化目录提交到版本库一定要固定好 embedding 模型版本并在 README 里写清楚。不然别人拉下来一跑检索结果莫名其妙所有排查到最后才发现是 embedding 模型不一致白白花掉一个下午。我个人现在最常用的组合是bge-small-zh-v1.5负责 embedding文档按中文标点切到 300 字左右检索时显式指定 cosine 空间再挂上 Ollama 的qwen2.5:7b做生成。这套组合在知识库规模几十万字符以内都够用资源占用不大效果也稳定。如果你的数据量更大、对精度要求更高再把重排和混合检索加进来整个系统就能从“能跑”升级到“好用”。
返回列表