Chroma向量数据库入门:从核心概念到本地知识库实战 1. 从零开始为什么我们需要一个向量数据库如果你最近在折腾大语言模型LLM应用比如想自己部署一个本地知识库问答系统或者给聊天机器人增加长期记忆那你大概率会遇到一个词向量数据库。听起来很高大上但它的核心任务其实很朴素——帮你快速找到“相似”的东西。想象一下你有一百万篇文档用户问了一个问题你怎么从这一百万篇里找出最相关的那几篇来回答传统的关键词匹配比如用CtrlF搜索“苹果”在语义层面是失效的。用户问“如何保养水果苹果”和一篇讲“苹果公司市值”的文档虽然都有“苹果”这个词但意思天差地别。这时候向量数据库的价值就体现出来了。它的工作流程通常是这样的先把你的文档或问题通过一个嵌入模型Embedding Model转换成一组高维度的数字也就是向量。这个向量就像文档的“数学指纹”语义相近的文档其向量在数学空间里的“距离”也会很近。然后当你提出一个新问题时同样把它转换成向量去数据库里快速找出和它“距离”最近的几个文档向量这些对应的文档就是最相关的答案。这就是向量数据库的核心高效的相似性搜索。而Chroma就是目前最受开发者欢迎的轻量级、开源向量数据库之一。它设计简洁API直观特别适合快速原型验证和中小规模的应用场景。今天我们就抛开那些复杂的概念直接上手通过它的两个最核心的概念——集合Collection和文档Document来一次实实在在的初体验。你会发现给LLM应用装上“记忆”和“知识库”并没有想象中那么难。2. 环境搭建与Chroma的“Hello World”在深入集合和文档之前我们得先把场子搭起来。Chroma的一大优势就是易于安装和集成它既可以作为独立的服务器运行也可以直接嵌入到你的Python应用中。对于初体验和学习来说嵌入式Embedded模式是最佳选择它无需额外启动服务所有数据默认存储在本地的一个目录下。2.1 安装与瞬间启动首先确保你有一个Python环境3.7以上。打开你的终端或命令行创建一个新的虚拟环境是个好习惯可以避免包依赖冲突。# 创建并激活虚拟环境以venv为例 python -m venv chroma_env source chroma_env/bin/activate # Linux/macOS # 或者 chroma_env\Scripts\activate # Windows # 安装ChromaDB核心库 pip install chromadb安装通常很快。完成后我们不需要任何配置直接就可以在Python代码里引入并使用。让我们写一个最简单的脚本感受一下Chroma的“零配置”魅力。import chromadb # 创建一个临时的、内存中的客户端。数据不会持久化关闭程序就消失。 # 这对于快速测试来说非常方便。 client chromadb.Client() # 尝试创建一个集合Collection。集合是Chroma中组织数据的基本单位可以理解为一张表或一个命名空间。 # 这里我们创建一个名为“my_first_collection”的集合。 collection client.create_collection(namemy_first_collection) print(f集合创建成功: {collection.name})运行这段代码如果没有报错恭喜你你的第一个向量数据库集合已经诞生了这个过程简单到令人怀疑但这就是Chroma的设计哲学让开发者专注于应用逻辑而不是基础设施。注意chromadb.Client()默认创建的是内存中的临时客户端。这意味着所有数据都保存在程序运行时的内存里一旦程序退出数据就丢失了。这非常适合做单元测试或一次性实验。当我们后续需要持久化数据时会使用PersistentClient。2.2 理解“嵌入模型”数据的翻译官在我们向集合里添加文档之前必须理解一个关键环节嵌入Embedding。Chroma本身不生产向量它只是向量的搬运工和检索工。你需要告诉它用什么“翻译官”嵌入模型把文本转换成向量。Chroma非常灵活支持多种方式默认模型如果你不指定Chroma会使用一个内置的轻量级句子转换器模型all-MiniLM-L6-v2。这是开箱即用最方便的选择但可能不是效果最好的。自定义开源模型你可以通过sentence-transformers或HuggingFace等库指定任何你喜欢的模型比如text-embedding-3-small的开源实现。调用API直接使用OpenAI、Cohere、百度千帆等商业模型的API来生成向量。这种方式效果通常很好但会产生费用。对于初体验我们使用默认模型就够了。但为了让你明白背后的机制我们显式地指定一下。在创建集合时我们可以通过embedding_function参数来设置。import chromadb from chromadb.utils import embedding_functions # 使用Chroma默认的句子转换器嵌入函数 default_ef embedding_functions.DefaultEmbeddingFunction() # 创建持久化客户端数据会保存在本地目录 ./chroma_data 中 client chromadb.PersistentClient(path./chroma_data) # 创建集合时指定嵌入函数 collection client.create_collection( namemy_knowledge_base, embedding_functiondefault_ef # 明确指定使用默认嵌入函数 )这里我们做了两个重要改变将客户端换成了PersistentClient并指定了数据存储路径./chroma_data。运行后你会发现当前目录下多了一个chroma_data文件夹里面存放着数据库文件。在create_collection时传入了embedding_function参数。这样之后所有添加到这个集合的文本都会通过这个函数自动转换为向量。现在舞台已经搭好演员嵌入模型也已就位接下来就该主角——文档登场了。3. 核心概念拆解Collection与Document到底是什么很多教程一上来就扔代码但如果不理解这两个核心概念的关系后面很容易迷糊。我们可以用一个简单的类比来理解数据库Chroma DB- 整个图书馆集合Collection- 图书馆里的一个特定书架比如“计算机科学”书架文档Document- 书架上的一本本书向量Embedding- 每本书的内容摘要卡片基于内容生成相似内容的卡片会放在附近元数据Metadata- 书脊上的标签作者、出版年份、分类号等ID- 每本书独一无二的索书号你绝不会把烹饪书和编程书混在同一个书架对吧同样在你的应用中你可能需要一个集合来存放“公司产品手册”另一个集合存放“用户反馈记录”。集合是进行相似性搜索的范围边界。当你搜索时你总是在某个特定的集合内搜索不会跨集合。而文档就是你要存储和搜索的基本单元。它可以是一段文字、一个句子、一个段落甚至是一篇文章。在Chroma中添加一个文档通常需要提供以下几个要素documents: 文本内容本身一个字符串列表。ids: 每个文档对应的唯一标识符一个字符串列表。如果你不提供Chroma会帮你生成UUID。metadatas(可选): 每个文档的附加信息一个字典列表。用于基于条件的过滤比如只搜索某个作者的文章。embeddings(可选): 如果你已经自己生成了向量可以直接提供避免Chroma重复计算。对于初学者我们让Chroma自动处理。理解了这些我们就可以开始“藏书”了。4. 实战向集合中添加与管理文档让我们用一个小型的“AI知识库”作为例子向刚刚创建的my_knowledge_base集合中添加一些文档。4.1 基础添加一次添加多个文档最常见的操作是批量添加文档。注意add方法的所有参数基本都是列表它们之间按索引一一对应。# 准备要添加的文档数据 documents [ Chroma是一个开源嵌入向量数据库用于构建AI应用。, 它简化了文档的存储、嵌入和查询。, 集合Collection是Chroma中组织文档的主要方式。, 你可以通过相似性搜索快速找到相关文档。, LangChain和LlamaIndex等框架与Chroma有很好的集成。 ] # 为每个文档指定一个唯一ID ids [doc_1, doc_2, doc_3, doc_4, doc_5] # 可选为每个文档添加元数据比如类别和来源 metadatas [ {category: introduction, source: official_doc}, {category: feature, source: official_doc}, {category: core_concept, source: tutorial}, {category: core_concept, source: tutorial}, {category: ecosystem, source: community} ] # 执行添加操作 collection.add( documentsdocuments, idsids, metadatasmetadatas ) print(成功添加了5个文档到集合中。)执行这段代码后五段文本就被转换成了向量并连同它们的ID、元数据一起存储在了本地的./chroma_data目录下。Chroma在背后默默完成了文本到向量的转换调用我们指定的default_ef。4.2 进阶操作更新、删除与查看数据库不可能只加不删不改。Chroma提供了简洁的API。更新文档使用update方法。你需要指定要更新的文档ID并提供新的文档内容、元数据或向量。注意如果你只更新了documents文本Chroma会自动用集合的嵌入函数重新计算向量。这是一个非常贴心的设计。# 更新id为“doc_1”的文档内容 collection.update( ids[doc_1], documents[Chroma是一个轻量级、开源嵌入向量数据库专为AI应用设计。], metadatas[{category: introduction, source: official_doc, updated: True}] )删除文档使用delete方法。你可以按ID删除也可以按元数据条件删除。# 1. 按ID删除 collection.delete(ids[doc_5]) # 2. 按元数据条件删除删除所有source为“tutorial”的文档 # collection.delete(where{source: tutorial})查看集合内容使用get方法。它可以获取集合中的所有文档或者指定ID的文档。# 获取集合中的所有数据 all_data collection.get() print(f集合中共有 {len(all_data[ids])} 个文档。) print(文档ID列表:, all_data[ids]) print(文档内容预览:, all_data[documents][:2]) # 只看前两个 # 也可以根据ID获取特定文档 specific_data collection.get(ids[doc_1, doc_2]) print(\n获取特定文档的元数据:, specific_data[metadatas])通过这些基本的CRUD操作你已经可以管理集合内的数据了。但向量数据库的真正威力在于接下来的查询。5. 灵魂功能基于语义的相似性搜索添加文档只是铺垫查询才是高潮。Chroma的query方法是其核心。5.1 基础文本查询最简单的查询方式就是直接扔一段文本进去让Chroma在集合里找最相似的文档。# 用户提出一个问题 query_texts [Chroma是什么] # 执行查询返回最相似的2个结果 results collection.query( query_textsquery_texts, n_results2 ) print(查询问题, query_texts[0]) print(\n最相似的文档) for i, doc in enumerate(results[documents][0]): print(f{i1}. {doc}) print(f 文档ID: {results[ids][0][i]}) print(f 相似度距离: {results[distances][0][i]:.4f}) print(- * 50)运行后你会看到返回了与“Chroma是什么”最相关的文档以及一个“距离”值。这个距离通常是余弦距离或欧氏距离值越小表示相似度越高。你会发现我们之前更新的“doc_1”内容为“Chroma是一个轻量级、开源嵌入向量数据库...”应该排在第一位并且距离值很小。这就是语义搜索的魅力——即使没有完全匹配的关键词也能找到正确答案。5.2 使用元数据进行过滤在实际应用中我们经常需要在某个子集内搜索。比如只想在“核心概念”类别的文档中搜索。# 查询“如何组织文档”但只从“core_concept”类别的文档中找 results collection.query( query_texts[如何组织文档], n_results3, where{category: core_concept} # 过滤条件 ) print(在‘core_concept’类别中查询‘如何组织文档’) for doc in results[documents][0]: print(f- {doc})where参数支持丰富的查询语法比如$and,$or,$in,$gt(大于) 等让你能进行复杂的过滤。5.3 同时查询多个问题与返回元数据query方法非常灵活可以一次查询多个问题并指定返回哪些字段。# 一次提出两个相关问题 my_queries [什么是向量数据库, Chroma和LangChain有什么关系] results collection.query( query_textsmy_queries, n_results2, include[documents, metadatas, distances] # 指定返回内容embeddings向量默认不返回 ) print(批量查询结果) for q_idx, query in enumerate(my_queries): print(f\n问题: {query}) for r_idx in range(len(results[documents][q_idx])): print(f 结果{r_idx1}: {results[documents][q_idx][r_idx]}) print(f 元数据: {results[metadatas][q_idx][r_idx]})6. 避坑指南与性能初探第一次使用难免会遇到一些“坑”。这里分享几个最常见的注意事项和优化思路。6.1 嵌入模型的一致性陷阱坑点你创建集合A时使用了嵌入函数E1后来不小心又用默认设置或另一个函数E2获取了同一个集合A。当你尝试添加或查询文档时可能会遇到维度错误或结果完全不对的情况。根因每个嵌入模型生成的向量维度是固定的比如384维、768维、1536维。一个集合里存储的所有向量必须是同一维度。如果你用E1384维创建了集合并添加了数据然后又用E2768维的客户端去打开它Chroma会困惑因为它期望找到384维的向量但你提供的查询向量是768维。解决方案显式指定并保持一致在创建集合时始终明确指定embedding_function。在后续获取这个集合时也最好使用同样的客户端设置。使用命名嵌入函数chromadb.utils.embedding_functions模块提供了几种预定义的函数如OpenAIEmbeddingFunction,SentenceTransformerEmbeddingFunction。使用它们可以避免混淆。检查维度如果遇到维度错误首先检查集合创建时用的函数和当前操作时用的函数是否一致。# 正确的做法始终使用同一个嵌入函数实例或同一种配置 from chromadb.utils.embedding_functions import SentenceTransformerEmbeddingFunction # 定义嵌入函数 sentence_transformer_ef SentenceTransformerEmbeddingFunction(model_nameall-MiniLM-L6-v2) # 创建集合时使用 collection client.create_collection(nameconsistent_collection, embedding_functionsentence_transformer_ef) # 后续获取集合时客户端已经‘记住’了这个函数配置无需再次指定 same_collection client.get_collection(nameconsistent_collection) # 此时 same_collection 使用的嵌入函数就是之前定义的 sentence_transformer_ef6.2 文档块Chunking的大小与重叠坑点直接把一整篇PDF论文几十页作为一个文档存进去然后查询某个具体概念效果很差。根因嵌入模型尤其是像all-MiniLM-L6-v2这类模型对输入文本长度有限制通常512个token左右超长的文本会被截断。更重要的是长文档包含多个主题生成的向量是整篇文章信息的“平均”会丢失细节导致搜索精度下降。解决方案在将文本存入Chroma前必须进行文档分割Text Chunking。这是构建高效知识库的关键预处理步骤。分割策略按段落、按句子、按固定长度如200个字符滑动窗口。重叠Overlap在滑动窗口分割时让相邻的两个块有一小部分内容重叠比如50个字符。这可以防止一个完整的句子或概念被生硬地切到两个块里保证检索的连贯性。你可以使用LangChain的RecursiveCharacterTextSplitter或LlamaIndex的SentenceSplitter等工具轻松完成这个工作。这是一个独立于Chroma的预处理流程。# 伪代码示例使用LangChain进行文本分割 from langchain.text_splitter import RecursiveCharacterTextSplitter text_splitter RecursiveCharacterTextSplitter( chunk_size200, # 每个块的最大字符数 chunk_overlap50, # 块之间的重叠字符数 length_functionlen, separators[\n\n, \n, 。, , , ] # 分割优先级 ) long_text 这里是一篇非常长的文档内容... chunks text_splitter.split_text(long_text) # 然后将 chunks 列表添加到Chroma的集合中6.3 数据持久化的路径管理坑点在Jupyter Notebook或脚本中多次运行PersistentClient(path./chroma_data)和create_collection有时感觉数据没保存上或者出现了奇怪的重复。根因PersistentClient在指定路径下维护数据库状态。如果你在同一个路径下用同一个集合名重复执行create_collectionChroma的行为是如果集合不存在则创建如果已存在则获取这个已存在的集合。这通常是你期望的行为。但如果你修改了代码比如换了嵌入函数然后直接运行旧的集合带着旧函数和旧数据会被加载可能导致冲突。解决方案清理旧数据在开发阶段如果想从头开始最直接的方法是删除本地的数据文件夹如./chroma_data。使用get_or_create_collection这是一个更安全的方法。它尝试获取集合如果不存在则创建。你可以在创建时传入所有参数如嵌入函数但如果集合已存在这些参数可能被忽略取决于客户端实现。所以最保险的做法还是先清理。程序化删除集合在脚本开始时可以先尝试删除旧集合client.delete_collection(namemy_collection)然后再创建。6.4 初探性能当数据量变大时我们体验的是小规模数据几个文档查询是瞬间完成的。但当你有数万甚至数十万个文档时就需要考虑性能了。索引IndexChroma默认使用HNSWHierarchical Navigable Small World算法来构建向量索引。这是一种近似最近邻搜索算法在精度和速度之间取得了很好的平衡。你通常不需要手动调整索引参数。客户端选择对于生产环境嵌入式客户端可能遇到资源限制。可以考虑部署Chroma Server然后用HttpClient远程连接。这样可以将向量数据库作为独立服务运行供多个应用调用。批量操作无论是add还是query尽量批量进行而不是在循环中单条操作这能显著减少开销。7. 整合实践构建一个极简本地问答引擎现在我们把所有知识点串联起来做一个能跑起来的简单应用一个命令行下的本地知识问答引擎。假设我们有一些关于Chroma的文本资料保存在knowledge.txt文件中我们要把它灌入Chroma然后允许用户提问。import chromadb from chromadb.utils import embedding_functions import os # 1. 初始化客户端和集合 PERSIST_DIRECTORY ./my_chroma_db EMBED_MODEL all-MiniLM-L6-v2 # 如果数据库目录已存在可以决定是否删除这里选择删除以重新开始 if os.path.exists(PERSIST_DIRECTORY): # 注意这里为了演示简单直接删除文件夹。生产环境请谨慎操作。 import shutil shutil.rmtree(PERSIST_DIRECTORY) client chromadb.PersistentClient(pathPERSIST_DIRECTORY) sentence_transformer_ef embedding_functions.SentenceTransformerEmbeddingFunction(model_nameEMBED_MODEL) # 获取或创建集合 collection client.get_or_create_collection( nameqa_knowledge, embedding_functionsentence_transformer_ef ) # 2. 读取并处理知识文本这里模拟从文件读取 def load_and_chunk_knowledge(file_path): # 假设文件里每行是一个知识段落 with open(file_path, r, encodingutf-8) as f: lines [line.strip() for line in f if line.strip()] # 这里简化处理直接将每行作为一个chunk。实际应用请使用更合理的分割器。 chunks lines return chunks # 模拟的知识文件内容 knowledge_texts [ ChromaDB是一个开源的向量数据库专注于存储和检索向量嵌入。, 它提供了简单的API用于创建集合、添加文档和执行相似性搜索。, 集合是Chroma中组织文档的主要单元类似于数据库中的表。, 每个文档都会被一个嵌入模型转换为高维向量进行存储。, 查询时将查询文本也转换为向量并计算与库中向量的相似度。, HNSW是Chroma默认使用的近似最近邻搜索算法兼顾速度和精度。, 元数据可以附加到文档上用于对搜索结果进行过滤。, Chroma支持持久化存储可以将数据保存到磁盘。 ] # 3. 将知识添加到集合 doc_ids [fdoc_{i} for i in range(len(knowledge_texts))] collection.add( documentsknowledge_texts, idsdoc_ids, metadatas[{source: simulated_knowledge_file}] * len(knowledge_texts) # 为每个文档添加相同元数据 ) print(f知识库已加载共 {len(knowledge_texts)} 个文档片段。) # 4. 简单的问答循环 print(\n 极简知识问答引擎输入‘退出’结束) while True: query input(\n请输入你的问题: ).strip() if query.lower() in [退出, exit, quit]: print(再见) break if not query: continue # 执行查询 results collection.query( query_texts[query], n_results3, # 返回最相关的3个片段 include[documents, distances] ) # 展示结果 print(f\n以下是相关度最高的 {len(results[documents][0])} 个知识片段) for i, (doc, dist) in enumerate(zip(results[documents][0], results[distances][0])): print(f\n[{i1}] (相似度距离: {dist:.3f})) print(f {doc}) print(- * 60)这个脚本虽然简单但完整走通了从数据准备、入库到查询的整个流程。你可以把knowledge_texts替换为从真实文档TXT、PDF、Word中提取并分割的文本块就能构建一个属于你自己的本地知识库。通过这次从环境搭建、核心概念理解、到增删改查、再到避坑和简单集成的初体验你应该已经对Chroma这个工具不再陌生。它就像乐高积木提供了构建AI记忆模块所需的基础组件。接下来你可以探索如何与LangChain等框架深度集成如何处理更复杂的文档格式以及如何优化查询结果的质量这些都是通往更强大应用的必经之路。记住动手试一遍远比读十遍教程要有效。