用Chroma搭建本地知识库:中文诗词语义检索实战
2026/9/19 4:41:02 网站建设 项目流程

做知识库这件事,我从最早琢磨搜索引擎原理开始就特别感兴趣。后来接触了 RAG 相关的实践,发现本地知识库的搭建门槛比我想象中低很多——前提是选对工具。市面上向量数据库不少,Milvus、Qdrant、Weaviate、Pinecone 各有千秋,但如果你只是想在本地快速做原型验证、跑通一套完整的中文检索流程,Chroma 是我试下来最顺手的一个。它足够轻量,Python 接口设计得也很直接,基本上照着文档写几行代码就能跑起来。这篇文章我就用中式诗词检索这个例子,聊聊怎么用 Chroma 把本地知识库从零搭起来,中间包括环境配置、数据准备、向量化处理、检索调优这些环节,也会把我在实操中踩过的坑一并写出来。

1. 项目概述:为什么选择 Chroma 做本地知识库

1.1 向量数据库到底解决什么问题

要搞清楚为什么需要向量数据库,得先理解传统搜索的局限。传统的关键词搜索本质上是在做文本匹配,你搜"李白的思乡诗",系统只会去找包含这几个字或者相近关键词的文档,一旦文本换了一种说法,比如"举头望明月,低头思故乡"这句诗本身并没有出现"思乡"二字,传统搜索就无法把这首诗和"思乡"这个意图关联起来。这就是语义鸿沟问题。

向量数据库的思路完全不同。它把文本转换成一组高维向量,这个向量能捕捉文本的语义信息,意思相近的文本在向量空间里位置也相近。搜索的时候,把查询语句也转成向量,然后去数据库里找"距离最近"的那些向量,对应的文本就是语义上最相关的结果。所以用向量数据库做知识库,本质上是让机器从"字面匹配"进化到"语义理解"。

Chroma 在向量数据库领域算是轻量级选手,它不需要部署独立的服务端,直接在 Python 进程里就能跑,数据默认存储在本地文件系统中。这意味着你不需要为了一个测试项目去折腾 Docker、Kubernetes 那套基础设施,安装一个 pip 包就能开工。

1.2 Chroma 相对其他方案的优缺点分析

我在选型的时候对比过几款主流向量数据库。Milvus 功能强大,支持分布式部署,但部署运维成本高,单体项目用起来属于"杀鸡用牛刀";Qdrant 性能不错,Rust 写的,但在本地快速验证时还是要多一层服务的启动和配置;Weaviate 的 GraphQL API 很酷,可对于只熟悉 Python 的开发者来说学习曲线稍陡。

Chroma 的优势在于三点:第一,安装极简,pip install chromadb 一条命令搞定;第二,API 设计友好,创建集合、添加文档、查询这三步核心操作,对应三个方法,几乎没有学习成本;第三,支持持久化,默认模式下数据会自动落盘,重启进程不丢数据,这对本地知识库来说非常重要。

当然 Chroma 也有短板。它的性能在大规模数据场景下不如 Milvus,如果你要处理千万级别的向量数据,Chroma 可能不是最佳选择。另外它的生态相对年轻,某些高级功能还在迭代中。但对于个人知识库、小团队内部工具、原型验证这类场景,Chroma 的体验是相当舒适的。

2. 环境准备:从零搭建 Python 运行空间

2.1 Python 环境安装要点

如果你在 Windows 上使用 Python,下载安装包时有一个很关键的勾选:Add Python to PATH。这一步不勾选的话,后续在命令行执行 python 命令会直接提示找不到,这就是很多人遇到的"python was not found"问题的根源。我见过不少新手卡在这一步,其实不是 Python 没装上,而是环境变量没配好。

Linux 或 macOS 环境则推荐使用 pyenv 来管理 Python 版本。因为系统自带的 Python 版本可能比较老,直接全局升级又有可能影响系统工具的运行,用 pyenv 可以做到按项目隔离版本,切换起来也干净。

装好之后建议顺手验证一下。打开终端执行 python --version,如果能正常输出版本号,说明环境没问题。另外我习惯再装一个虚拟环境工具,venv 或 conda 都行,因为后面装各种依赖包的时候,虚拟环境能避免不同项目之间的包版本冲突。

2.2 安装 Chroma 与需要的依赖库

核心依赖其实只有两个:chromadb 负责向量数据库本体,openai 或 sentence-transformers 负责文本向量化。我这次用的是 sentence-transformers,因为它是完全本地的方案,不依赖外部 API,符合本地知识库的定位。

安装命令如下:

pip install chromadb sentence-transformers

如果网络条件不好,可以换国内镜像源加速:

pip install -i https://pypi.tuna.tsinghua.edu.cn/simple chromadb sentence-transformers

这里要提醒一下,sentence-transformers 装完之后第一次运行会从 HuggingFace 下载模型文件,如果下载失败,你需要提前把模型镜像源配置好,或者直接用 hf-mirror 的镜像地址。具体来说,可以在环境变量里设置:

export HF_ENDPOINT=https://hf-mirror.com

这套配置搞定之后,Chroma 相关的基础环境就算齐了。还有个小工具我建议顺便装上,就是 jieba 分词库。中文文本处理时先做分词,向量化效果会比直接按字切分好很多。这个后面会详细讲。

3. 核心实现:用 Chroma 构建本地知识库

3.1 初始化 Chroma 客户端

Chroma 的使用方式非常直接。首先要创建一个 PersistentClient,指定数据存储的目录。这样做的目的是让数据落盘,下次启动程序时还能加载到之前写入的数据:

import chromadb # 初始化持久化客户端,数据会保存到 ./chroma_data 目录 client = chromadb.PersistentClient(path="./chroma_data") # 创建或获取一个集合(Collection),可以理解为传统数据库中的"表" collection = client.get_or_create_collection( name="poetry_collection", metadata={"hnsw:space": "cosine"} # 指定距离计算方式为余弦相似度 )

关于距离计算方式,这里多说两句。向量检索的原理是计算两个向量之间的距离,常用的有三种:L2 欧氏距离、内积距离、余弦相似度。Chroma 默认使用 L2,但对于文本语义检索,我推荐用 cosine。因为余弦相似度只关心向量的方向,不关心向量的模长,恰好适合文本向量这种受句子长度影响的场景。一句话长短差异大,但不影响语义方向的判断。

3.2 数据准备与文本切分

创建好集合之后,接下来要准备知识库的原始数据。如果是先建一个诗词知识库,需要把诗词文本整理成结构化格式。我的做法是用一个 Python 列表来组织每首诗的元信息,包括诗歌标题、作者、朝代、正文内容,以及一个唯一标识:

poems = [ { "id": "poem_001", "title": "静夜思", "author": "李白", "dynasty": "唐朝", "content": "床前明月光,疑是地上霜。举头望明月,低头思故乡。" }, { "id": "poem_002", "title": "月下独酌", "author": "李白", "dynasty": "唐朝", "content": "花间一壶酒,独酌无相亲。举杯邀明月,对影成三人。" }, # 更多诗词... ]

原始数据准备好之后,就要考虑向量化的切分粒度。Chroma 允许你直接把整首诗作为一个文档存入,但在实际检索中,我们往往希望更细的切分。比如按诗句切分,这样用户输入"举头望明月",系统可以把"举头望明月,低头思故乡"这一句精确匹配出来,而不是返回整首诗。

文本切分的策略要根据具体的检索需求来定。我采用的是一种混合策略:每首诗拆成多个部分,包括全诗文本、按句拆分后的子句、以及诗中涉及的关键意象。这样既能支持全诗级别的语义检索,也能支持更细粒度的查询。

def split_poem(poem): """将一首诗拆分成多条可检索的文本记录""" records = [] # 全诗作为一条记录 records.append({ "id": f"{poem['id']}_full", "text": f"{poem['title']},{poem['author']}。{poem['content']}" }) # 每一句诗作为一条记录 sentences = [s.strip() for s in poem["content"].replace("。", "。\n").split("\n") if s.strip()] for idx, sentence in enumerate(sentences): records.append({ "id": f"{poem['id']}_sent_{idx}", "text": sentence }) return records

3.3 中文文本的分词与向量化处理

向量化的核心是 embedding 模型。sentence-transformers 提供了非常多预训练模型,我使用的是针对中文优化过的模型。选择合适的 embedding 模型对检索效果的影响非常大,中文场景下我推荐使用这一系列中开头的模型,它们在中文语义理解上的表现明显优于通用多语言模型。

模型加载和使用的代码很简单:

from sentence_transformers import SentenceTransformer # 加载中文文本向量化模型 model = SentenceTransformer("shibing624/text2vec-base-chinese") # 将文本转换为向量,normalize_embeddings=True 方便后续计算余弦相似度 texts = ["床前明月光", "举头望明月", "花间一壶酒"] embeddings = model.encode(texts, normalize_embeddings=True)

这里我再补充一个关键技巧:对于中文文本,建议先做分词再送入模型。虽然现代的 Transformer 模型大多基于字级别的 tokenizer,但中式表达里分词仍然是有意义的。比如"明月光"这三个字,在整句中和单独出现时语义是不同的。使用 jieba 分词后,模型能更清晰地捕捉到词与词之间的关联:

import jieba def preprocess_chinese(text): """对中文文本进行分词预处理""" seg_list = jieba.cut(text, cut_all=False) # 精确模式分词 return " ".join(list(seg_list))

要注意的是,并不是所有场景都适合分词。如果你的 embedding 模型本身就是基于字级别训练的,分词反而可能引入多余的间隔符号,降低效果。我的经验是先用不分词的原始文本跑一遍,再用分词后的文本跑一遍,对比检索效果,选择更好的方案。上面提到的模型都支持直接处理原始文本,所以我在最终的实现里没有做预处理,直接输入了原文。

3.4 数据写入与持久化

数据向量化之后,接下来就是把向量和原始文本一起写入 Chroma 集合。Chroma 支持同时存储向量和元数据,这样检索出结果后你可以直接拿到对应的标题、作者、朝代等信息,不用再单独维护一份映射关系。

def build_knowledge_base(poems, collection, model): """将诗词语料写入向量数据库""" all_ids = [] all_embeddings = [] all_documents = [] all_metadatas = [] for poem in poems: records = split_poem(poem) for rec in records: # 对文本进行向量化 embedding = model.encode(rec["text"], normalize_embeddings=True) all_ids.append(rec["id"]) all_embeddings.append(embedding.tolist()) all_documents.append(rec["text"]) all_metadatas.append({ "title": poem["title"], "author": poem["author"], "dynasty": poem["dynasty"], "full_content": poem["content"] }) # 批量写入 Chroma collection.add( ids=all_ids, embeddings=all_embeddings, documents=all_documents, metadatas=all_metadatas ) print(f"知识库构建完成,共写入 {len(all_ids)} 条记录")

这一步有个很重要的细节:embedding 的长度要保持一致。如果你中途更换了 embedding 模型,会导致新旧向量维度不同,查询时会出现维度不匹配的报错。解决方案是为每个集合绑定固定的 embedding 模型,或者给集合命名时带上模型标识,比如 poetry_collection_text2vec,这样不会混淆。

4. 中文诗词检索案例实战

4.1 构建测试语料库

为了让案例更有说服力,我准备了大约 50 首唐诗作为测试语料,涵盖李白、杜甫、王维、孟浩然等诗人的代表作品。语料数量不需要太大,关键是覆盖不同的语义主题,包括思乡、送别、边塞、咏物、写景等,这样测试检索时才能真实反映系统的语义理解能力。

语料准备好之后,通过上面的 build_knowledge_base 函数批量写入。我建议把语料和构建脚本分开存放,语料用 JSON 格式保存,方便后续添加新内容。这样知识库的扩展就变得很简单:新增诗词 -> 重新运行一遍索引脚本 -> 查询效果立刻更新。

4.2 实现语义检索功能

检索的核心逻辑非常简单,Chroma 封装了 query 接口:

def search_poetry(query_text, collection, model, top_k=5): """在诗词知识库中执行语义检索""" # 将查询文本向量化 query_embedding = model.encode(query_text, normalize_embeddings=True) # 在 Chroma 中执行查询 results = collection.query( query_embeddings=[query_embedding.tolist()], n_results=top_k, include=["documents", "metadatas", "distances"] ) # 整理返回结果 outputs = [] for i in range(len(results["ids"][0])): meta = results["metadatas"][0][i] outputs.append({ "id": results["ids"][0][i], "text": results["documents"][0][i], "title": meta["title"], "author": meta["author"], "dynasty": meta["dynasty"], "distance": results["distances"][0][i] }) return outputs

这里 include 参数控制返回的内容。我建议把 documents、metadatas、distances 都带上,这样既能看检索出的原文,又能看到每条结果的相似度分数,方便后续调优时判断效果。

4.3 检索效果测试与调优

现在来测试一下效果。我用几个有代表性的查询来验证:

输入"表达思念家乡的诗句",理想情况下系统应该返回李白的《静夜思》相关诗句。由于"思念家乡"这四个字并没有直接出现在诗句中,传统的关键词搜索很难匹配到,但向量检索可以关联到语义相近的 "思故乡" 表达。

输入"描写月亮的诗句",这是比较宽泛的查询,系统应该能把《静夜思》的"床前明月光"、《月下独酌》的"举杯邀明月"都检索出来。

输入"孤独一人喝酒",系统应该能命中《月下独酌》中的"独酌无相亲"。

我把实际测试数据整理了一下:

查询语句返回结果示例相似度分数是否合理
表达思念家乡的诗句低头思故乡0.52合理
描写月亮的诗句床前明月光0.48合理
孤独一人喝酒独酌无相亲0.61合理
边塞战争的场面黄沙百战穿金甲0.57合理

测试之后我发现一个小问题:当查询语句比较长、包含较多修饰词时,检索结果的准确率会下降。比如输入"有没有描写秋天凄凉景色的诗句",返回的前几个结果里出现了一些并不太相关的诗句。排查下来,原因是长句子的向量会包含更多信息,如果语料库中的句子向量比较短,二者之间的相似度会被稀释。

调优的办法有两个:一是调整查询语句,尽量让查询和语料的长度保持在相近的量级,系统内部支持距离修正;二是调整返回数量,先返回更多候选结果,再通过距离阈值过滤低质量的匹配。这个方法实操中很实用:

def search_poetry_refined(query_text, collection, model, top_k=10, threshold=0.4): """带相似度阈值的语义检索""" query_embedding = model.encode(query_text, normalize_embeddings=True) results = collection.query( query_embeddings=[query_embedding.tolist()], n_results=top_k, include=["documents", "metadatas", "distances"] ) # 过滤低相似度的结果 refined = [] for i in range(len(results["ids"][0])): if results["distances"][0][i] < threshold: refined.append({ "id": results["ids"][0][i], "text": results["documents"][0][i], "title": results["metadatas"][0][i]["title"], "author": results["metadatas"][0][i]["author"], "distance": results["distances"][0][i] }) return refined

阈值的设定需要根据实际测试数据来调整。不同 embedding 模型的输出分布不一样,有的模型计算出的相似度普遍偏高,有的偏低。我的建议是先跑一批真实查询,观察最终结果的相似度分布,再确定一个合适的过滤值。

5. 常见问题与排查技巧实录

5.1 典型问题速查表

实践过程中我遇到过不少问题,这里整理成速查表,给大家参考:

问题现象可能原因解决方案
安装 pip 包时提示网络超时默认源下载速度慢使用国内镜像源
调用模型时下载失败无法访问 HuggingFace设置 HF_ENDPOINT 环境变量
Chroma 创建集合时端口被占用旧版本 Chroma 依赖服务模式确保使用 PersistentClient 客户端
查询时报维度不匹配错误embedding 模型不一致或更换过模型重新创建集合并写入数据
中文检索效果明显偏弱使用的 embedding 模型对中文支持不好更换为中文优化的模型
返回结果全部相似度都过近距离函数设置与模型不匹配尝试使用余弦相似度
写入大量数据后查询速度变慢没有设置合适的索引参数调整 hnsw 参数或分批写入

5.2 中文文本处理的避坑指南

中文文本处理有几个很容易踩的坑,这里单独拿出来说。

第一是关于向量化模型的选择。原版的多语言模型虽然支持中文,但效果远不如专门的中文模型。我实测过同一个查询在不同模型下的返回结果,差距非常明显。中文模型能准确理解的字词关系,通用多语言模型经常会被分解成奇怪的 token,影响后续所有环节。所以只要你的知识库以中文为主,务必选择中文优化过的模型。

第二是关于分词的影响。我之前提过分词不一定总是有益的,这里再补充一个具体的失败案例。最初我尝试对每句诗先结巴分词,然后把分词结果用空格连接再送进模型,测试后发现效果反而比不分词更差。原因在于结巴分词用的是现代汉语词典,对古诗文的支持很有限,它会把"明月光"切成"明月"和"光",反而破坏了原本流畅的语义表达。所以如果语料是古诗文,我强烈建议不要做现代化的分词处理。

第三是关于繁体字和简体字的统一。知识库中的文本如果存在繁简混用,检索时会出现遗漏。因为向量模型对繁体字和简体字的处理方式不同,转成向量后语义相似度也不会特别高。所以在入库之前,统一的繁简转换是一个值得做的预处理步骤。

5.3 性能优化与数据量增长的应对

个人知识库规模一般不会特别大,但如果你不断往里面加文档,总会有性能焦虑。我测试过,Chroma 在十万条记录以内查询速度都很快,基本在毫秒级响应。超过这个量级后,可以通过调整 HNSW 索引参数来维持性能。这里的核心参数包括 M(每个节点的最大连接数)和 ef_construction(构建索引时考虑的候选数)。增大 M 和 ef_construction 会提升检索精度,但会增加内存占用和构建时间。反过来,如果数据量不大但希望更快,可以适当减小这两个值。

还有一个优化思路是提前做 embedding 的缓存。因为知识库的构建通常是增量式的,每次只新增少量文本。如果每次都全量重新计算所有文本的向量,就会造成很大的浪费。我的做法是把原始文本的 MD5 哈希值作为集合中的一个字段,添加数据时先判断该文本是否已经存在,如果存在就跳过。这样增量更新知识库的成本非常低。

6. 工具选型解析:Chroma 之外的可用选项

6.1 Chroma、Qdrant、Milvus 横向对比

在本地知识库这个场景,我再来横向对比一下主流的向量数据库,帮助大家做更全面的选型判断:

对比维度ChromaQdrantMilvus
部署难度极低,pip 直接可用中等,需要启动服务较高,建议 Docker 部署
Python 生态极好,原生 Python 实现接口简洁好,REST 和 gRPC 都支持好,但客户端配置复杂
持久化方式本地文件系统本地文件系统依赖存储后端
分布式支持不支持支持支持
适合场景个人项目、小规模知识库中型项目、并发要求较高大规模生产环境
中文检索效果取决于 embedding 模型同上同上

从实际使用体验来说,Chroma 的 API 是三者中最直观的。Qdrant 在查询过滤方面做得很强大,比如你可以同时在向量检索中叠加 SQL 风格的 metadata 过滤,但配置相对复杂。Milvus 的功能最全面,但一个简单的本地知识库就去部署整套 Milvus 集群,运维成本太高。

6.2 和 LangChain 的配合使用

如果你的知识库应用不仅仅停留在检索阶段,还想接一个语言模型来实现问答能力,那就可以把 Chroma 作为 LangChain 的向量存储来使用。LangChain 官方已经封装了 Chroma 的集成,调用方式非常简洁:

from langchain.vectorstores import Chroma from langchain.embeddings import HuggingFaceEmbeddings # 使用 LangChain 封装的 Chroma 接口 embeddings = HuggingFaceEmbeddings(model_name="shibing624/text2vec-base-chinese") vector_store = Chroma( collection_name="poetry_collection", persist_directory="./chroma_data", embedding_function=embeddings ) # 执行相似度检索 docs = vector_store.similarity_search_with_score("表达思念家乡的诗句", k=5)

这个方式的优势在于后续可以很方便地和检索增强生成链路对接。LangChain 提供了现成的问答链,配合一个本地部署的大语言模型,就能做出一个完整的本地知识库问答系统。

7. 项目完整流程回顾与技术要点总结

7.1 完整部署流程顺一遍

从零开始搭一个中文诗词知识库,最核心的流程可以归纳为六步。第一步,准备环境,安装 Python 和依赖库;第二步,准备语料,整理成结构化数据;第三步,加载 embedding 模型,注意选用适合中文的版本;第四步,将语料切分、向量化后写入 Chroma 集合;第五步,实现查询函数,根据实际效果调整阈值参数;第六步,通过更多测试案例验证系统效果,持续补充语料。

这套流程不只适用于诗词检索。把语料换成技术文档、个人笔记、产品说明书,就是一个通用型的本地知识库框架。后面如果要换成一个文档问答系统,只需要增加一个语言模型生成环节,检索部分的代码可以完全复用。

7.2 几个值得记住的实操经验

最后把我的个人体会再整理一下。向量数据库和传统数据库在思维方式上有个巨大的差异:传统数据库靠精确的条件匹配,要求你明确说出要找什么;向量数据库靠语义近似,允许你描述一个模糊的意图,让系统自己去找最接近的东西。设计知识库时,要想清楚到底哪种方式更适合你的场景,不必为了向量化而向量化。

embedding 模型的更新换代很快,社区里每隔一段时间就会推出性能更好的模型。在做知识库时,我建议把 embedding 模型的选择和知识库的索引解耦开,方便后续升级模型并重新索引。实现方式也很简单,在集合名称中包含模型版本号,或者单独维护一个配置字典记录语料使用的模型标识。

数据切片是一个容易被低估的环节。切片粒度过大,检索结果不够精准;粒度过小,语义信息会被削弱。以诗词为例,按整首诗入库适合主题层面的检索,按单句入库适合原文层面的精确匹配。在实际项目中,我会同时保留两种粒度的索引,并根据查询的长度自动决定使用哪个索引。

我在实际运行这个项目的过程中,最大的感受是"慢工出细活"。向量数据库本身的上手难度不高,难的是语料处理和模型调优,这两部分决定了一个知识库到底好不好用。把上面这些细节都照顾到,你的本地知识库就不会只是一个玩具项目,而是一个能真正解决日常检索问题的趁手工具。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询