三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

从0搭建本地向量数据库:RAG技术原理与实战指南

从0搭建本地向量数据库:RAG技术原理与实战指南

1. 从“大海捞针”到“精准定位”:为什么我们需要RAG?

如果你最近在折腾大语言模型,或者关注AI应用开发,大概率已经不止一次听到“RAG”这个词了。它听起来像是个神秘的黑科技,但实际上,它的核心思想非常朴素:让AI在回答问题时,能先翻翻自己的“参考书”

想象一下,你是一个知识渊博但记忆力有限的专家。当有人问你一个非常具体、细节的问题时,比如“你们公司去年第三季度发布的XX产品,其用户手册第15页提到的那个技术参数是多少?”,你不可能把所有文档都背下来。最靠谱的做法是什么?你会说:“稍等,我查一下产品手册。” 然后快速翻到相关章节,找到那个精确的数字,再给出回答。RAG(Retrieval-Augmented Generation,检索增强生成)干的就是这个事。

传统的大语言模型,比如我们熟悉的ChatGPT,它的知识全部来自于训练时“吃”进去的海量数据。这带来了两个核心问题:知识陈旧幻觉(胡编乱造)。模型不知道训练截止日期之后发生的事情,也可能会为了“完成句子”而自信地编造一个看似合理但完全错误的答案。RAG的引入,就是为了给模型一个“外挂大脑”——一个可以实时查询、精准定位的向量数据库。

所以,当项目标题说“从0搭建本地向量数据库”时,它瞄准的正是RAG技术栈中最关键、最核心的一环。没有这个高效、精准的“参考书库”,RAG就成了无源之水。本地搭建意味着数据自主、成本可控、隐私安全,这对于企业私有知识库、个人学习助理等场景至关重要。接下来,我不会空谈概念,而是带你一步步拆解原理,并用最实用的工具,真正从零构建一个跑在你自己电脑上的向量数据库,为你的AI应用注入“精准记忆”。

2. 拆解RAG:检索、增强、生成的三步舞曲

理解RAG,不能停留在“检索+生成”这个简单的加法上。它是一个精心设计的流水线,每一步的选择都直接影响最终答案的准确性和可靠性。我们可以把它看作一场三步舞曲。

2.1 第一步:检索——把文档变成可搜索的“记忆碎片”

检索的核心目标,是把非结构化的文本(你的PDF、Word、TXT文件、网页内容),变成一种模型能够快速理解和匹配的格式。这里的关键技术是“嵌入”

你可以把“嵌入”想象成一个“语义翻译机”。它把一句话、一个段落,甚至一个词,转换成一个固定长度的数字列表(比如1024个数字),这个列表就是向量。神奇之处在于,语义相似的文本,它们的向量在数学空间里的“距离”也会很近。比如“猫”和“猫咪”的向量距离,会比“猫”和“汽车”的距离近得多。

这个过程具体如何操作?

  1. 文档加载与切分:首先,你需要一个文档加载器(比如LangChainPyPDFLoader,UnstructuredFileLoader)。它把你的PDF文件读进来,变成纯文本。但一整本书直接转换成一个巨大的向量是低效的,因为你可能只想问其中某一章的内容。所以,我们需要文本切分器。常见的策略是按固定字符数切分(如500字一段),或者按语义切分(确保一个段落或一个章节的完整性)。切分的大小是个艺术活:太短,丢失上下文;太长,检索精度下降。
  2. 向量化:接着,使用嵌入模型(Embedding Model)为每一段文本生成对应的向量。这个模型本身也是一个小型神经网络(如text-embedding-ada-002,BGE,或本地模型all-MiniLM-L6-v2)。它读取文本,输出那串有魔力的数字。这里的选择至关重要。不同的嵌入模型在不同语言、不同领域的表现差异很大。对于中文场景,BGE-zhm3e通常是比OpenAI的通用模型更好的选择。
  3. 存储:最后,将“文本片段”和它对应的“向量”一起,存入向量数据库。数据库的职责就是高效存储这些向量,并能根据一个“问题向量”,快速找出最相似的几个“文本向量”。

注意:文本切分是第一个容易踩坑的地方。直接按固定字符切分,可能会把一个完整的表格或一句话从中间切断,导致后续检索到的是语义不完整的“碎片”。在实际操作中,我通常会采用“重叠切分”策略。比如,每段500字符,但让后一段的前100字符与前一段的后100字符重叠。这样能有效减少边界信息丢失,虽然增加了少量存储和计算开销,但对检索质量提升明显。

2.2 第二步:增强——为问题配上“参考上下文”

当用户提出一个问题时(例如:“我们公司的年假政策是怎样的?”),RAG系统不会直接让大模型回答。

  1. 问题向量化:系统会用同样的嵌入模型,把用户的问题也转换成一个向量。
  2. 相似度搜索:拿着这个“问题向量”,去向量数据库里进行相似度搜索(最常见的是余弦相似度计算)。数据库会返回与问题向量最相似的K个文本片段(比如前3个)。这K个片段,就是模型回答问题所需的“参考上下文”。
  3. 组装提示词:系统会精心设计一个提示词模板,把“用户问题”和“检索到的上下文”组装在一起。一个经典的模板如下:
    请基于以下上下文信息回答问题。如果上下文信息不足以回答问题,请直接说“根据提供的信息,我无法回答此问题”。 上下文: {context} 问题: {question} 答案:

这里的核心技巧在于提示词工程。模板的指令必须清晰、强硬,以最大程度抑制模型的“幻觉”,强迫它基于给定上下文生成答案。我经常会在模板中加入“严禁使用上下文之外的知识”这样的强约束语句。

2.3 第三步:生成——基于上下文的“精准作答”

最后,这个组装好的、包含了上下文和问题的提示词,被送入大语言模型(如GPT-4、ChatGLM、Qwen等)。模型的工作,就是像一个阅读了参考资料的学生,综合这些上下文信息,组织语言,生成最终答案。

因为答案的素材直接来源于你提供的文档,所以其准确性时效性得到了极大保障。模型更像一个强大的“信息理解与重组专家”,而非一个全知全能的“预言家”。

至此,RAG的完整流程就清晰了:文档 -> 切分 -> 向量化 -> 存储 -> 问题向量化 -> 检索 -> 组装提示 -> 生成答案。接下来,我们要聚焦其中最工程化的部分:搭建本地向量数据库。

3. 向量数据库选型:轻量、免运维、本地优先

向量数据库是RAG的“记忆中枢”。市面上选择很多,从云服务(Pinecone, Weaviate)到开源自建(Milvus, Qdrant)。但对于“从0搭建本地”这个目标,我们的选型原则非常明确:轻量、免运维、易于集成、本地文件存储

基于这些原则,ChromaDBFAISS是两个最理想的候选。

ChromaDB是一个专门为AI应用设计的嵌入式向量数据库。它的最大优点就是“开箱即用”,无需启动额外的服务器进程,像一个Python库一样直接集成到你的代码中,数据默认保存在本地磁盘。它提供了简单的API,内置了常见的嵌入函数,对于快速原型开发和中小规模项目极其友好。

FAISS是Meta(Facebook)开源的一个向量相似性搜索库,严格来说它不是数据库,而是一个高性能的索引库。它更底层,性能极高,尤其是在处理千万级以上向量时优势明显。但它需要你手动管理向量和元数据的存储与加载。

为了平衡易用性和学习价值,我推荐以下路径:

  • 入门首选(本次实践)ChromaDB。它能让我们最快地看到效果,理解整个流程。
  • 性能与规模考量FAISS + 轻量级存储(如SQLite)。当你需要更极致的检索速度或处理更大数据量时,这是下一步的进化方向。

这里有一个简单的对比表格,帮助你理解:

特性ChromaDBFAISS (作为库)云向量数据库 (如Pinecone)
部署模式嵌入式,本地文件嵌入式,需搭配存储方案云端SaaS服务
运维复杂度极低,无需运维低,需处理数据持久化无需运维,由服务商负责
上手速度极快,API简单直观中等,需理解索引概念快,但依赖网络和API
本地/隐私数据完全本地,隐私安全数据完全本地,隐私安全数据上传至云端
适用场景原型开发,中小规模知识库,个人项目大规模向量检索,对性能有极致要求企业级应用,不愿管理基础设施
成本免费免费按使用量付费

对于我们的“从0搭建”目标,ChromaDB的免运维和本地化特性完美契合。它让我们能专注于RAG流程本身,而不是数据库的配置和维护。

4. 实战:手把手构建本地知识库系统

理论说再多,不如动手跑一遍。我们假设一个场景:你手头有几份公司内部的Markdown格式的产品文档,现在要搭建一个本地问答机器人。我们将使用LangChain(一个流行的AI应用框架)来串联整个流程,因为它封装了许多繁琐的步骤。

4.1 环境准备与工具安装

首先,确保你的Python环境是3.8以上。然后,我们安装核心的库。

# 创建虚拟环境(推荐) python -m venv rag_env source rag_env/bin/activate # Linux/Mac # rag_env\Scripts\activate # Windows # 安装核心依赖 pip install langchain langchain-community chromadb # 安装文本加载器(以Markdown和PDF为例) pip install unstructured pymupdf # 用于PDF解析,也可用其他库 # 安装一个开源的嵌入模型(这里用HuggingFace上的轻量级模型) pip install sentence-transformers # 安装一个本地运行的大语言模型(这里用ChatGLM3为例,需根据自己选择安装) # pip install modelscope # 如果你用魔搭社区模型 # 我们也可以先用一个模拟的LLM来测试流程,后续替换 pip install fakeyou # 用于模拟LLM响应,仅测试用

注意unstructured库的安装可能因为系统依赖而有些麻烦,特别是处理PDF时。在Linux上,你可能需要apt-get install poppler-utils;在Mac上,brew install poppler。如果遇到问题,可以考虑使用pymupdf(fitz) 作为PDF解析的备选,它通常更简单。

4.2 文档加载与智能切分

假设你的产品文档放在./docs目录下。我们使用 LangChain 的文档加载器。

from langchain_community.document_loaders import DirectoryLoader, TextLoader from langchain.text_splitter import RecursiveCharacterTextSplitter # 1. 加载文档(以Markdown为例) loader = DirectoryLoader('./docs', glob="**/*.md", loader_cls=TextLoader) documents = loader.load() print(f"成功加载 {len(documents)} 个文档") # 2. 创建文本切分器 text_splitter = RecursiveCharacterTextSplitter( chunk_size=500, # 每个文本块的最大字符数 chunk_overlap=50, # 块之间的重叠字符数 length_function=len, # 计算长度的方法 separators=["\n\n", "\n", "。", "!", "?", ";", ",", " ", ""] # 优先按这些符号切分 ) # 3. 执行切分 all_splits = text_splitter.split_documents(documents) print(f"切分后得到 {len(all_splits)} 个文本块")

关键参数解析

  • chunk_size=500:这是一个经验值。对于通用问答,200-1000都是常见范围。太小会丢失上下文,太大会引入噪声。你可以根据你的文档平均段落长度调整。
  • chunk_overlap=50强烈推荐设置重叠。这能防止一个完整的句子或概念被硬生生切断,确保检索到的片段语义更完整。
  • separators:这个列表定义了切分的优先级。它会先尝试用“\n\n”(空行)切,不行再用“\n”(换行),以此类推。这种递归切分方式比简单按固定长度切分智能得多。

4.3 向量化与存入ChromaDB

现在,我们需要一个模型把文本变成向量,并存入数据库。

from langchain_community.embeddings import HuggingFaceEmbeddings from langchain.vectorstores import Chroma # 1. 初始化嵌入模型(使用开源模型) # 这里选用 all-MiniLM-L6-v2,它是一个小型但效果不错的英文模型。中文场景请换成 `BAAI/bge-small-zh` 等。 model_name = "sentence-transformers/all-MiniLM-L6-v2" embeddings = HuggingFaceEmbeddings(model_name=model_name) # 如果你是中文文档,强烈建议使用: # model_name = "BAAI/bge-small-zh" # embeddings = HuggingFaceEmbeddings(model_name=model_name, # model_kwargs={'device': 'cpu'}, # 指定设备 # encode_kwargs={'normalize_embeddings': True}) # 标准化向量,提升效果 # 2. 创建向量数据库(ChromaDB) # persist_directory 指定数据持久化到本地的目录 persist_directory = './chroma_db' vectordb = Chroma.from_documents( documents=all_splits, embedding=embeddings, persist_directory=persist_directory ) vectordb.persist() # 显式持久化到磁盘 print(f"向量数据库已创建并保存至 {persist_directory}")

第一次运行这段代码时,它会自动从HuggingFace下载对应的模型,这可能需要一些时间和网络。完成后,你的本地./chroma_db文件夹里就会保存所有的向量和索引。这就是你的本地向量数据库,它不依赖任何外部服务。

4.4 构建检索链并进行问答测试

数据库建好了,我们来测试检索功能,并模拟一个完整的RAG流程。

from langchain.chains import RetrievalQA from langchain.llms import FakeListLLM # 先用一个模拟的LLM测试检索部分 # 1. 首先,测试纯检索功能 query = "我们产品的主要优势是什么?" docs = vectordb.similarity_search(query, k=3) # 检索最相似的3个片段 print(f"针对问题 '{query}',检索到的相关片段:") for i, doc in enumerate(docs): print(f"\n--- 片段 {i+1} ---") print(doc.page_content[:200]) # 打印前200个字符 # 2. 构建一个完整的RAG链(使用模拟LLM) # 模拟LLM会固定返回我们预设的答案,用于验证流程是否通畅 responses = ["根据文档,我们产品的主要优势在于其易用性和强大的集成能力。", "这是一个模拟回答。"] fake_llm = FakeListLLM(responses=responses) # 创建检索问答链 qa_chain = RetrievalQA.from_chain_type( llm=fake_llm, chain_type="stuff", # 最常用的类型,将所有检索到的上下文“塞”进提示词 retriever=vectordb.as_retriever(search_kwargs={"k": 3}), # 指定检索器,返回3个片段 return_source_documents=True # 返回源文档,便于调试 ) # 3. 运行问答链 result = qa_chain.invoke({"query": query}) print(f"\n=== 模拟RAG回答 ===") print(f"问题:{query}") print(f"答案:{result['result']}") print(f"\n答案来源(检索到的文档):") for doc in result['source_documents']: print(f"- {doc.metadata.get('source', 'N/A')}: {doc.page_content[:100]}...")

运行这段代码,你会看到系统首先检索出了与问题相关的文本片段,然后组装提示词,最后调用(模拟的)LLM生成了答案。虽然答案是预设的,但整个检索-增强-生成的管道已经完整跑通了。

4.5 接入真实的大语言模型

最后一步,我们把模拟的LLM换成真正有智能的大模型。这里以通过API调用开源模型(如DeepSeek)为例,你也可以部署本地模型(如ChatGLM3-6B, Qwen-7B)。

# 假设使用OpenAI兼容的API(例如,本地部署的Ollama或开源的API服务) from langchain_openai import ChatOpenAI import os # 设置API基础地址和密钥(以Ollama本地运行为例) os.environ["OPENAI_API_BASE"] = "http://localhost:11434/v1" # Ollama的API地址 os.environ["OPENAI_API_KEY"] = "ollama" # 可任意填写,但需要设置 # 初始化LLM,指定模型名称(对应你本地运行的模型) llm = ChatOpenAI(model="qwen:7b", temperature=0.1) # temperature控制创造性,0.1更倾向于事实性回答 # 重新创建QA链,使用真实的LLM real_qa_chain = RetrievalQA.from_chain_type( llm=llm, chain_type="stuff", retriever=vectordb.as_retriever(search_kwargs={"k": 3}), return_source_documents=True, chain_type_kwargs={ "prompt": YOUR_CUSTOM_PROMPT # 这里可以传入你精心设计的提示词模板对象,以更好地控制模型行为 } ) # 进行真实问答 real_result = real_qa_chain.invoke({"query": "请总结一下产品安装的必备条件。"}) print(f"问题:{real_result['query']}") print(f"答案:{real_result['result']}")

至此,一个完整的、本地的RAG系统就搭建完成了。你的数据在本地,向量数据库在本地,大模型也可以在本地运行,形成了一个完全私密、自主可控的知识问答系统。

5. 避坑指南与进阶优化:从“能用”到“好用”

搭建起来只是第一步,要让RAG系统真正可靠、好用,还需要避开很多坑,并做一些优化。以下是我在实际项目中总结的几个关键点。

5.1 检索质量不佳:问题可能出在切分和嵌入

症状:系统总是检索不到相关的文档片段,导致答案不准或回答“不知道”。

  • 根因1:文本切分不合理。这是最常见的问题。如果切分得太碎,一个完整的概念被分散在多个片段中,每个片段单独看都语义不全,自然无法被正确检索。
    • 解决方案:调整chunk_sizechunk_overlap。尝试更大的chunk_size(如800-1000),并确保有足够的重叠(如100-150)。对于结构清晰的文档,可以尝试按标题进行切分。
  • 根因2:嵌入模型不匹配。用针对英文优化的模型(如all-MiniLM-L6-v2)去处理中文文档,效果会大打折扣。
    • 解决方案务必使用与文档语言和领域匹配的嵌入模型。中文通用可选BAAI/bge-small-zh,金融领域可以看看m3e系列。在小范围数据上做相似度搜索的测试,对比不同模型的效果。
  • 根因3:检索策略单一。仅使用简单的相似度搜索(similarity_search),可能无法处理问题与文档表述差异较大的情况。
    • 解决方案:尝试max_marginal_relevance_search。它在考虑相似度的同时,还会在返回结果中引入多样性,避免返回多个高度重复的片段。有时,关键词检索(如BM25)与向量检索的混合搜索(Hybrid Search)能取得更好效果,LangChain也支持这种模式。

5.2 模型“幻觉”依旧:提示词工程是关键

症状:即使检索到了正确上下文,模型还是会编造一些上下文里没有的信息。

  • 根因:提示词的指令不够强硬,模型仍然倾向于动用其内部知识。
  • 解决方案:设计更强约束力的提示词。例如:
    请你严格扮演一个专业文档分析员的角色。你的任务是根据用户提供的<上下文>来回答问题。 <上下文> {context} </上下文> <问题> {question} </问题> 请遵循以下规则: 1. 你的答案必须完全且仅基于提供的<上下文>。 2. 如果<上下文>中没有足够信息来回答问题,请明确说“根据上下文,我无法回答这个问题”。 3. 禁止推断、禁止添加任何<上下文>之外的知识。 4. 答案要简洁、准确。 开始回答:
    通过角色设定、XML标签分隔、明确列举规则等方式,可以极大地约束模型行为。

5.3 回答冗长或跑题:优化生成环节

症状:答案啰嗦,或者包含了一些与问题弱相关的内容。

  • 根因1temperature参数过高。这个参数控制随机性,值越高答案越创造性也越不稳定。对于知识问答,通常应该设置较低(如0.1)。
  • 根因2:检索到的上下文片段过多或质量参差不齐,模型试图综合所有信息。
    • 解决方案:减少k(检索数量),比如从4降到2。或者在检索后增加一个“重排序”步骤,用一个更小的模型对检索结果进行相关性评分,只保留最相关的1-2个片段送入生成模型。
  • 进阶技巧——查询转换:有时用户的问题很模糊或很长,直接用于检索效果不好。可以先让LLM对原问题进行改写或扩展。例如,将“它怎么用?”在检索前转换成“[产品名]的使用方法是什么?”。这能显著提升检索命中率。

5.4 系统扩展与性能考量

当你的文档库越来越大,从几百个片段增长到几十万时,你会面临新的挑战:

  • 检索速度变慢:ChromaDB的默认索引在小数据量时很快,但数据量大后需要调整。可以考虑切换到性能更强的后端(如使用ClickHousePostgreSQL作为Chroma的存储后端),或者直接评估QdrantWeaviate等支持分布式的中型向量数据库。
  • 嵌入模型效率:在本地CPU上运行嵌入模型,处理大量文档会非常慢。解决方案是:1) 使用更轻量的模型(如all-MiniLM-L6-v2已经很小);2) 使用GPU加速;3) 对于批量处理,可以使用异步并行编码。
  • 数据更新:如何增量更新知识库?ChromaDB支持add_documents。你需要一个机制来监控源文档变化,计算新旧文档的哈希值,只对变化的文档重新进行切分、嵌入和更新。注意,简单的添加新文档容易,但删除或修改已有文档在向量数据库中是比较棘手的操作,通常需要根据元数据过滤后重建部分索引。

从0搭建一个本地向量数据库驱动的RAG系统,就像为自己的AI应用安装了一个精准的“外部记忆体”。这个过程从理解“检索增强生成”为何能解决大模型的幻觉与时效性问题开始,到亲手实践文档加载、智能切分、向量化存储,再到最终接入大模型完成闭环。其中,文本切分的策略、嵌入模型的选择、提示词的设计是决定系统好坏的三大支柱,每一个都需要根据你的具体数据和场景进行仔细调优。

我个人的体会是,RAG项目初期,快速迭代验证比追求完美架构更重要。先用ChromaDB和一份小规模文档把整个流程跑通,看到它确实能基于你的文档回答问题。然后,再针对遇到的具体问题——是检索不准,还是回答不好——去深入优化相应的模块。这个从“能用”到“好用”的过程,才是真正掌握RAG精髓的关键。最后,别忘了给你的系统加上日志和评估环节,记录下用户的每一个问题和系统的每一次检索与生成,这是你持续优化最宝贵的燃料。

← 返回列表