Langchain-Chatchat 基于 ChromaDB 的知识库向量服务:ChromaKBService 实现解析与接入指南
导读
本文面向在 Langchain-Chatchat 项目中需要选用 ChromaDB 作为本地知识库向量存储的开发者,系统梳理 ChromaKBService 的完整实现:包括本地持久化路径的组织规则、与 LangChain Chroma 封装与 Embedding 模型的对接方式、文档增删查清(CRUD)与相似度检索的底层调用链,以及如何通过配置和工厂机制把默认向量库切换到 ChromaDB。读完本文,你将掌握 ChromaKBService 每个关键方法的作用与数据流转,能够在自己的知识库工程中直接复用这套基于 ChromaDB 的服务抽象,并具备阅读其余向量库实现(FAISS、Milvus、PG 等)的迁移能力。
1. ChromaKBService 在项目中的定位
在 Langchain-Chatchat 中,知识库功能被抽象为 KBService 基类(见 kb_service/base.py),不同后端通过子类实现统一接口。其中面向 ChromaDB 的实现即为 ChromaKBService,源码位于 chromadb_kb_service.py。
该服务类通过 SupportedVSType.CHROMADB(值为字符串 "chromadb")声明自己的后端类型,SupportedVSType 中还定义了 FAISS、MILVUS、ZILLIZ、PG、RELYT、ES、DEFAULT 等其他可选后端。KBServiceFactory.get_service(见 base.py)根据传入的 vector_store_type 动态实例化对应子类——当类型为 chromadb 时即返回 ChromaKBService 实例,因此上层代码(知识库 API、WebUI、初始化迁移脚本)无需关心底层存储细节。
从类结构看,ChromaKBService 声明了以下实例属性:
| 属性 | 含义 |
|---|---|
vs_path |
向量库持久化目录路径 |
kb_path |
知识库根目录路径 |
chroma |
LangChain 的 langchain_chroma.Chroma 封装对象(负责 embedding 计算与检索 API 封装) |
client |
原生 chromadb.PersistentClient 客户端实例(负责本地持久化与集合管理) |
collection |
当前知识库对应的 Chroma 集合(部分方法直接使用 chroma._collection) |
kb_name、embed_model、kb_info 等则继承自 KBService.__init__(base.py),其中 kb_name 为知识库名称,embed_model 为构建该知识库时使用的向量化模型名。
2. 路径规划:知识库根目录、文档目录与向量目录
ChromaKBService 的两个路径方法直接委托给知识库模块的公共工具函数(见 knowledge_base/utils.py):
get_kb_path()返回KB_ROOT_PATH/<kb_name>,即该知识库的根目录;get_vs_path()返回get_vs_path(self.kb_name, self.embed_model),解析后为KB_ROOT_PATH/<kb_name>/vector_store/<embed_model>。
两个关键点值得注意:
- 同一个知识库可存在多份向量索引:向量目录中嵌入了
embed_model名,因此切换/升级 Embedding 模型时,不同模型的向量空间会落在不同子目录,互不覆盖。这一点与 FAISS 实现一致——FaissKBService.get_vs_path同样以self.vector_name(默认是替换:为_后的模型名)组织子目录,见 faiss_kb_service.py。 - 知识库内容目录另有分工:原始文档(
content)目录由get_doc_path返回(<kb_path>/content),Chroma 持久化目录则承载切分后文档的向量数据,二者分离,便于文件管理层面的独立操作。
文中部分描述方法原文以
/path/to/...示意路径,实际取值取决于运行时配置的KB_ROOT_PATH(属于 basic_settings),不同部署环境结果不同。
3. 初始化:do_init 与 Chroma 本地持久化
do_init 是知识库服务启动时由 KBService.__init__ 自动调用的钩子(base.py),ChromaKBService 的实现流程如下(chromadb_kb_service.py):
- 通过
get_kb_path()/get_vs_path()计算并缓存self.kb_path、self.vs_path; - 用
chromadb.PersistentClient(path=self.vs_path)创建本地持久化客户端——ChromaDB 无需独立部署服务端,数据落盘在指定目录中,这是它相比 Milvus/Zilliz/PG 方案部署成本低的核心原因; - 调用
get_or_create_collection(self.kb_name)按知识库名获取或创建集合; - 调用私有方法
_load_chroma(),用Chroma(client=self.client, collection_name=self.kb_name, embedding_function=get_Embeddings(self.embed_model))构造 LangChain 层封装。其中get_Embeddings依据self.embed_model返回对应的 Embedding 模型适配器,真正在写入与查询时把文本转成向量。
需要强调的是,初始化期间会校验 kb_name 与 embed_model 两个属性:前者决定集合名与路径,后者决定向量维度与 embedding 来源。若二者与知识库建库时不一致,会得到空结果或维度不匹配的异常。
3.1 do_create_kb:空实现背后的语义
创建 Chroma 集合的操作实际发生在 do_init 的 get_or_create_collection 中,因此 ChromaKBService.do_create_kb 直接 pass(chromadb_kb_service.py)。这与基类的调用约定并不冲突:基类 create_kb() 会先在文件系统创建 content 文档目录、把知识库记录写入数据库(add_kb_to_db),再回调子类 do_create_kb()(base.py);Chroma 场景下"建库"已被初始化集合的动作覆盖,故无需额外逻辑。对照 FAISS 实现(需要 os.makedirs(vs_path) 并加载向量池)可以看出不同后端的建库动作差异。
4. 文档写入:do_add_doc 的向量化与落库
do_add_doc(docs, **kwargs) 接收 List[Document],返回每个入库文档的 {"id", "metadata"} 列表。其内部实现(chromadb_kb_service.py)共四步:
- 通过
get_Embeddings(self.embed_model)获得 embedding 函数; - 从
docs中拆出texts(page_content)与metadatas,并调用embed_func.embed_documents(texts=texts)批量生成向量; - 用
uuid.uuid1()为每篇文档生成基于时间戳的唯一 ID(保证分布式并发下基本不冲突); - 逐个调用
self.chroma._collection.add(ids=..., embeddings=..., metadatas=..., documents=...)写入集合,并把id与metadata收集进doc_infos返回。
4.1 上层封装:从文件到向量的完整链路
do_add_doc 只是"向量化 + 写库"的最底层一环。真正从知识库文件入口走起的是基类的 add_doc(kb_file, docs, **kwargs)(base.py):
- 先校验 Embedding 模型可用性(
check_embed_model); - 若未显式传入
docs,则调用kb_file.file2text()完成文档加载与文本切分(此过程还会给每篇文档的metadata["source"]打上相对路径的文件名标记); - 先
delete_doc(kb_file)清掉该文件旧版本数据,再调用do_add_doc入库,最后通过add_file_to_db把文件记录、文档数与doc_infos写入数据库元信息表。
由此形成 "KBService.add_doc -> do_add_doc -> Chroma collection.add" 的调用链;知识库 API 层(如 kb_doc_api.py)与初始化脚本只需面对 add_doc 一个入口。
5. 相似度检索:do_search、阈值过滤与辅助转换函数
5.1 do_search 的检索参数
do_search(query, top_k, score_threshold) 是知识库检索的核心方法,签名(chromadb_kb_service.py)三个参数分别为:
query:查询文本(字符串);top_k:最多返回的文档条数(整数);score_threshold:相关度阈值(浮点数),默认取Settings.kb_settings.SCORE_THRESHOLD。
在 KBSettings 中,SCORE_THRESHOLD 默认值为 2.0,注释明确说明:"取值范围在 0-2 之间,SCORE 越小相关度越高,取到 2 相当于不筛选,建议设置在 0.5 左右";同时 VECTOR_SEARCH_TOP_K 默认 3,即默认返回 3 条结果。这些默认值决定了不传参时知识库问答检索的命中数量与严格程度。
5.2 检索实现:LangChain 层封装而非裸 query
需要说明:早期/文档注释中所描绘的实现是用 Embedding 适配器先把 query 编码,再直接调用 collection.query(n_results=top_k) 获得 QueryResult,最后由结果转换函数打包成 (Document, score) 元组列表。当前仓库代码则统一改走 file_rag.utils.get_Retriever("vectorstore") 通道:
retriever = get_Retriever("vectorstore").from_vectorstore(
self.chroma,
top_k=top_k,
score_threshold=score_threshold,
)
docs = retriever.get_relevant_documents(query)
return docs
即先通过 Chroma 封装建立 retriever,将 top_k、score_threshold 注入检索器(BaseRetrieverService 抽象定义见 file_rag/retrievers/base.py,其 from_vectorstore 统一接收 vectorstore/top_k/score_threshold 三个参数),再一次性完成 "query 编码 -> 向量相似度检索 -> 距离阈值过滤 -> 结果裁剪"。score_threshold 的过滤语义沿用 score_threshold_process(base.py)中对 "越小越相关、score <= threshold 保留" 的约定:对 Chroma 返回的 L2 距离类分数而言,阈值越小保留越严格。这也解释了为什么在 Knowledge Base 对话/检索 API 中"相关度阈值调低更精准"。
5.3 结果转换辅助函数
ChromaKBService 模块级还保留了两个纯函数,用于把 Chroma 原生结果结构解包为 LangChain 对象:
_get_result_to_documents(get_result: GetResult) -> List[Document](chromadb_kb_service.py):
- 若
get_result["documents"]为空则直接返回[]; - 若
metadatas缺失或为空,则以[{}] * len(documents)补齐同长度的空元数据,保证"一文一 metadata"一一对应; - 遍历
zip(documents, metadatas),以page_content与metadata关键字构造Document列表。
典型输出形态:
[
Document(page_content="文档内容1", metadata={"作者": "张三"}),
Document(page_content="文档内容2", metadata={"作者": "李四"})
]
该函数当前被 get_doc_by_ids 调用,用于把 Chroma collection.get(ids=...) 的原始 GetResult 还原成业务可读的 Document 列表。
_results_to_docs_and_scores(results) -> List[Tuple[Document, float]](chromadb_kb_service.py):
- 函数注释标明其逻辑参考自
langchain_community.vectorstores.chroma.Chroma; - 入参结构包含
documents[0]、metadatas[0]、distances[0]三层(批查询结果的第一个 batch),通过zip并行解包,构造(Document, distance)元组列表,metadata 缺失时兜底为空字典。
典型输出形态:
[
(Document(page_content="文档内容1", metadata={"作者": "张三"}), 0.95),
(Document(page_content="文档内容2", metadata={"作者": "李四"}), 0.89)
]
从源码结构看,
_results_to_docs_and_scores当前已不被do_search直接调用(检索改走 retriever 通道),但在依赖原始collection.query的扩展或调试场景中,它依然是把QueryResult快速转成带分文档列表的实用工具。
6. 文档按 ID 查询与删除
知识库中每篇文档都有一个 Chroma 集合内的稳定 ID(入库时由 do_add_doc 生成)。针对 ID 的两个方法构成配套:
get_doc_by_ids(ids: List[str]) -> List[Document](chromadb_kb_service.py):调用 self.chroma._collection.get(ids=ids) 获得 GetResult,再交给 _get_result_to_documents 转换后返回。基类 list_docs(base.py)正是借助 get_doc_by_ids 实现按文件名/元数据批量回捞全文内容的,因此它是"数据库记录 -> 文档实体"的桥接点。
del_doc_by_ids(ids: List[str]) -> bool(chromadb_kb_service.py):调用 _collection.delete(ids=ids) 并固定返回 True。注意该方法不校验 ID 是否存在,对不存在的 ID 亦不报错,文档注释提醒调用方不要依赖返回布尔值来判断实际删除数量。此外 Chroma 只按 ID 批量删除,KBService.update_doc_by_ids(base.py)即通过 "先按 ID 删除、再对非空文档重新 do_add_doc" 的方式实现单篇更新。
7. 整库级操作:删库与清空向量空间
do_drop_kb(chromadb_kb_service.py):Chroma 场景下"删除知识库"等价于删除对应集合,即 self.client.delete_collection(self.kb_name)。代码捕获 ValueError,且仅在异常信息不是 Collection {kb_name} does not exist. 时才重新抛出——也就是说当集合本就不存在时静默放行,避免误报,这与 Milvus 等后端在 drop 阶段的"存在性容错"思路一致。基类 drop_kb 会随后删除数据库中的知识库元信息记录(base.py),保证文件系统与元数据库同步。
do_clear_vs(chromadb_kb_service.py):清空向量空间被实现为直接调用 do_drop_kb(),即"删集合"而非逐条删除向量。相比 collection.delete 全量扫描,drop 集合在 Chroma 的持久化语义下是更高效且幂等的做法。需要注意的是该操作不可逆,业务层调用 clear_vs 前应确认确实需要重建(例如 migrate.py 的 recreate_vs 模式正是在入库前调用 clear_vs + create_kb,见 knowledge_base/migrate.py)。
do_delete_doc(kb_file: KnowledgeFile, **kwargs)(chromadb_kb_service.py):按"文件"粒度删除,条件是 where={"source": kb_file.filepath},即通过 metadata 中的 source 字段精确定位该文件全部切分文档后删除。这里的 source 在被基类 add_doc 处理时会统一改写为相对 content 目录的相对路径(见 base.py),确保入库与按文件删除时匹配的是同一取值。KnowledgeFile 对象封装文件路径与加载器信息(knowledge_base/utils.py),上层 delete_doc 还负责同步删除数据库中的文件记录。
8. 从服务方法到对外 API:一次知识库检索的完整链路
把上文各方法串起来,Langchain-Chatchat 中一次"知识库对话检索"的调用路径大致如下:
- WebUI/API 层发起
kb_chat/ 知识库检索请求; KBServiceFactory.get_service_by_name(kb_name)依据数据库记录还原出正确的vs_type(如chromadb)与embed_model,并构造对应服务实例(构造过程中触发do_init,连接 Chroma 持久化目录);- 上层调用
KBService.search_docs(query, top_k, score_threshold),其先校验 embedding 模型可用性,再转发到子类do_search(base.py); do_search经get_Retriever("vectorstore")构建检索器并返回命中文档,作为 LLM 的上下文素材进入对话拼接。
类似的,KBService.add_doc / update_doc / delete_doc / clear_vs / drop_kb 分别被知识库文档管理 API(上传、删除、重建向量库等)与 migrate.py 初始化脚本复用。因此,掌握 ChromaKBService 一个类,即掌握了该后端在"建库 -> 入库 -> 检索 -> 维护"四个阶段的所有行为。
9. 如何让 Langchain-Chatchat 使用 ChromaDB
9.1 后端能力与配置声明
ChromaDB 方案的优势是无需单独起服务、无外部依赖,向量数据以文件形式保存在 vs_path。在配置上,Chroma 无需连接参数,kbs_config 中 "chromadb": {} 即为空配置(settings.py);与 Milvus(需要 host/port)、PG(需要 connection_uri)、ES(需要 host/index)形成鲜明对比。
要使知识库默认走 ChromaDB,需将默认向量库类型切换为 chromadb:
KBSettings.DEFAULT_VS_TYPE的取值域为"faiss" | "milvus" | "zilliz" | "pg" | "es" | "relyt" | "chromadb",默认"faiss"(settings.py)。运行时该配置由kb_settings配置文件覆盖;- 知识库的
vs_type一经入库,后续通过get_service_by_name恢复服务时会沿用建库时的类型与模型,因此"建库时用什么类型,之后访问就应保持什么类型"。
9.2 命令行/脚本视角的接入点
从代码结构看,接入 ChromaDB 的入口有两处:
- 初始化迁移:
folder2db接受vs_type参数(类型约束中显式包含"chromadb"),对知识库目录中已有文件执行recreate_vs/update_in_db/increment三种模式之一的向量化(knowledge_base/migrate.py),并在脚本内通过KBServiceFactory.get_service(kb_name, vs_type, embed_model)组装出 Chroma 服务后调用kb.add_doc逐文件入库; - 业务 API:知识库管理 API 在创建知识库时接受向量库类型参数,类型为
chromadb时即持久化为 Chroma 后端。
因此,把 DEFAULT_VS_TYPE 指到 chromadb 并重新执行知识库初始化(重建向量空间),即可在 Langchain-Chatchat 中完整切换到本地持久化的 ChromaDB 检索方案。
9.3 仓库内的测试参考
仓库的向量库测试集中在 tests/kb_vector_db 目录(含 test_faiss_kb.py、test_milvus_db.py、test_pg_db.py、test_relyt_db.py),尽管当前仓库未看到 chromadb 专项测试文件,但这些测试共用同一套 KBService 接口约定。若需为 Chroma 后端编写冒烟测试,可以参照上述测试的组织方式,覆盖 create_kb / add_doc / search_docs / get_doc_by_ids / delete_doc / drop_kb 六个核心动作的往返一致性。
10. 使用注意事项与易错点汇总
结合 ChromaKBService 与基类的实现,实际接入时有以下注意事项值得牢记:
- 删除语义偏"乐观":
del_doc_by_ids恒返回True,不会因 ID 不存在而失败;do_drop_kb对"集合不存在"的ValueError静默吞掉。调用方不应以返回值断言数据量变化,必要时自行用get_doc_by_ids复查。 - 不可逆操作需谨慎:
do_drop_kb、do_clear_vs、do_delete_doc均直接作用于 Chroma 数据,删除后无法从向量库恢复;业务层应保留content源文件并善用recreate_vs重建。 - embed_model 一致性:
vs_path目录与集合内向量都依赖embed_model,更换 Embedding 模型后旧向量不可直接复用,必须重建向量空间,否则会出现维度不匹配或空命中。 - 阈值方向:Chroma 场景沿用
score_threshold_process的score <= threshold语义(分数为距离,越小越相似),SCORE_THRESHOLD默认2.0等效于不过滤,需要更严格召回时建议调到0.5量级。 - 入参格式:
do_add_doc依赖get_Embeddings(embed_model)与有效Document;_get_result_to_documents、_results_to_docs_and_scores对 Chroma 原生结果的documents/metadatas/distances结构有强依赖,手工构造结果时需保证三键齐全。 - 并发与 ID:入库 ID 使用
uuid.uuid1()(含时间戳),多进程并发写入时仍需在业务层保证集合访问安全,避免同一 ID 重复。
结语
ChromaKBService 是 Langchain-Chatchat 多向量库抽象体系中最"轻量"的实现之一:它用 PersistentClient 换掉了独立向量数据库的部署负担,用 get_or_create_collection 让"建库即初始化"天然幂等,又通过 get_Retriever 统一到项目共用的阈值过滤检索链路上。把握住它继承自 KBService 的 "do_ 方法 + 业务封装方法"双层设计,再结合 KBServiceFactory 的装配逻辑,你就能在 FAISS、Milvus、PG、ES 等后端之间快速迁移同样的知识库运维经验。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00