[控场AI]
· 14 分钟阅读· 7,406 字

从零构建RAG:用本地小模型打造领域专家系统

从零构建RAG:用本地小模型打造领域专家系统

如果能让一个大语言模型精通某个它从未接触过的领域——不靠重新训练,而是让它实时检索相关文档——会发生什么?这正是 RAG(Retrieval-Augmented Generation,检索增强生成) 的核心价值所在。

RAG 并非凭空出现,而是 NLP 领域「开放域问答」(Open-Domain QA)研究数十年积累的集大成者。早在 2017 年,Facebook AI Research 就已提出 DrQA 系统,将 TF-IDF 检索与阅读理解模型结合用于维基百科问答,奠定了「先检索、再阅读」的双阶段范式雏形。RAG 由 Meta AI 研究团队于 2020 年在论文《Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks》中正式提出,其背后是 NLP 领域长期存在的「参数记忆 vs. 非参数记忆」之争。「参数记忆」与「非参数记忆」的本质区别在于知识的存储方式:传统 LLM 将知识分布式编码在神经网络权重中(参数记忆),更新成本极高;而 RAG 引入的外部文档存储(非参数记忆)则以可编辑的形式存在,可随时增删改查——这一设计哲学与计算机科学中「计算与存储分离」的经典原则高度一致。这一思路受到了信息检索领域经典算法 TF-IDF 和 BM25 的启发,并与神经网络的强大语言理解能力相结合,形成了「检索-生成」的混合范式。其核心动机是解决大语言模型的两大固有缺陷:知识截止日期(Training Cutoff) 和 幻觉(Hallucination) 问题。传统 LLM 的知识被"冻结"在训练数据的时间节点,无法感知最新信息;而 RAG 通过在推理阶段动态注入外部文档,使模型能够访问任意时间点的专有知识,且无需承担重新训练数以百亿参数的巨额计算成本。本文以一个趣味横生的实战项目为例,带你从零搭建一条完整的 RAG 流水线,让一个仅有 15 亿参数的本地小模型,摇身变成能够"破案"的 AI 侦探。

项目概览:一场关于 Elrond 的"侦查"

本教程的场景设定颇为有趣:给模型准备 10 份自制"证据文档"(涵盖证人证词、法证报告等),让它调查"精灵王 Elrond 是否秘密身份是特工史密斯"。荒诞外壳之下,是一套严肃的技术栈:

  • Ollama:在本地免费运行大语言模型,数据完全自主掌控
  • LangChain:Python 调用 Ollama 的统一接口。LangChain 本质上是一个「胶水层」框架,其核心价值在于为大量异构组件(模型、向量库、文档加载器、记忆模块等)提供统一的抽象接口,使开发者能够以声明式方式组装复杂 AI 流水线。值得一提的是,LlamaIndex 作为另一个重要替代方案,其设计哲学更专注于数据索引与检索层,在处理复杂文档结构时各有所长
  • FAISS:高效存储与检索向量嵌入
  • Qwen 2.5(1.5B):负责最终对话与答案生成
  • BGE-M3:负责文本嵌入表示

整个流程在 CPU 上即可运行,无需昂贵的 GPU,非常适合初学者动手实践。

环境准备

通过 ollama.com 提供的 Linux 命令安装 Ollama 后,拉取两个轻量模型:ollama pull qwen2.5:1.5b(对话)和 ollama pull bge-m3(嵌入)。接着用 conda 创建 Python 3.12 环境(命名为 ragenv),安装 langchain、faiss、pypdf、jupyter 等依赖,在 Jupyter Lab 中开始编码。

Jupyter 环境中的依赖警告提示

文档加载与切分:RAG 流水线的起点

RAG 的第一步是把文档读入内存。教程使用 os.listdir 列出 data 文件夹中的所有 PDF,并以 sorted 排序确保顺序一致,随后通过 PyPDFLoader 逐个加载,用 loader.load() 提取每页内容,最终汇总到 all_pages 列表——本例共得到 49 页。

切分(Chunking):为什么 49 页变成了 147 块

加载完成后进入 Chunking(文本切分) 阶段。这一步将页面拆成更小的文本单元,并在相邻块之间保留一定重叠,以防语义在边界处断裂。

文本切分是 RAG 流水线中被严重低估的关键环节,切分策略直接决定检索质量的上限——块太大会导致嵌入向量"稀释",检索精度下降;块太小则可能丢失完整的语义单元。除了固定字符数切分(RecursiveCharacterTextSplitter),工程实践中还发展出多种高级策略:语义切分(Semantic Chunking,依据句子嵌入相似度动态确定切分点,能更好地保留段落的完整语义边界)、按文档结构切分(如 Markdown 标题层级、HTML 标签,适合有明确层次结构的技术文档)、以及父子块检索(Parent Document Retriever,存储小块用于精确检索,返回对应的大块父文档以保留完整语境)。不同策略的选择取决于文档类型、领域特性和下游任务需求,通常需要通过评估框架量化验证效果。

作者用一个生动的比喻解释"重叠"的必要性:把"this is not my first rodeo"切成两块,得到"this is not my"和"not my first rodeo",重叠的"not my"就像一张安全网,让相邻块共享部分上下文。chunk_overlap 参数的设计借鉴了滑动窗口思想,确保跨块的关键信息不因边界切割而丢失。

切分阶段:将页面拆分为更小的文本单元

一个值得关注的实战细节:初次切分后,块数仍是 49,与页数持平。原因是这批 PDF 字体大、留白多,每页字符数本就低于默认的 2000 字符上限,切分器"无从下手"。解决方案是显式调整参数:将 chunk_size 设为 500、chunk_overlap 设为 150,重新运行后得到 147 个块。这说明切分参数必须根据实际数据特征调整,而非照搬默认值。

嵌入与向量数据库:让语义检索成为可能

接下来是 Embeddings(嵌入) 环节。文本嵌入技术历经三代演进:第一代是基于词频统计的稀疏表示(TF-IDF、BM25),无法捕捉同义词关系;第二代是 Word2Vec、GloVe 等静态词向量,首次将语义关系编码为几何距离,但无法区分多义词在不同语境中的含义;第三代是以 BERT 为代表的上下文动态嵌入,同一词在不同句子中得到不同向量表示,极大提升了语义理解精度。嵌入(Embedding)正是将离散的文本转化为连续高维向量的过程,其本质是将语义关系编码为几何距离关系——语义相近的文本,在高维向量空间中的距离也更近。值得注意的是,嵌入模型的选择对 RAG 系统性能的影响甚至超过生成模型——「垃圾进、垃圾出」的道理同样适用于向量检索。

本项目采用的 BGE-M3(BAAI General Embedding - Multi-Functionality, Multi-Linguality, Multi-Granularity)是北京智源研究院发布的多功能嵌入模型,支持超过 100 种语言,代表了当前嵌入技术的前沿水平。其架构独特之处在于将三种检索范式统一于单一模型之中:密集检索(Dense Retrieval,通过余弦相似度匹配语义)、稀疏检索(Sparse Retrieval,类似 BM25 的词频权重匹配,擅长精确关键词对齐)和多向量检索(Multi-Vector Retrieval,即 ColBERT 风格的 token 级细粒度交互)。这种多模式融合使其在语义模糊查询和精确术语查询场景下均能保持高性能。不同于 OpenAI 的 text-embedding-ada-002 等闭源方案,BGE-M3 完全开源且可本地部署,向量维度达 1024,在 MTEB 等标准基准上与商业模型并驾齐驱,是本地化 RAG 方案的理想选择。

嵌入模型的作用好比"乐高整理师":先按颜色把积木分类归位,等到真正拼装时效率倍增。加载嵌入模型只需一行:OllamaEmbeddings(model='bge-m3')。存储处理后的文本块,则依赖向量数据库 FAISS。

随着 RAG 技术的普及,向量数据库市场形成了分层生态:本地轻量级方案中,FAISS 以无服务器、零网络依赖见长;ChromaDB 提供了更友好的 Python API,是原型开发的热门选择。云原生方案中,Pinecone 率先商业化,Weaviate 和 Qdrant 则以开源可自托管、支持混合搜索著称。传统数据库也在追赶——PostgreSQL 通过 pgvector 扩展插件支持向量索引。选择哪种方案需综合考量数据规模、更新频率、延迟要求和团队运维能力。

vectordb = FAISS.from_documents(chunks, embeddings)
vectordb.save_local("faiss_index")

FAISS(Facebook AI Similarity Search)是 Meta 开源的高效相似向量检索库,专为十亿级别的向量检索场景设计。其核心思想是将精确最近邻搜索(Exact NN)替换为近似最近邻搜索(ANN),并提供多种索引类型以适应不同规模:小规模场景使用 Flat 索引(暴力精确搜索,保证 100% 召回率);中大规模场景则借助 IVF(倒排文件索引) 将向量空间划分为多个 Voronoi 单元,查询时只搜索最近的几个单元而非全库,大幅降低计算量;PQ(乘积量化) 则通过将高维向量分段压缩来减少内存占用,代价是引入少量精度损失。工程师需要在召回率、查询延迟和内存成本三者之间做出权衡。在本教程的小规模场景中,FAISS 使用 Flat 索引足以应对数百个向量块的检索需求。作为本地库,FAISS 无需网络请求,数据不出本机,与 Ollama 本地化运行的理念高度契合。

from_documents 一次性完成"切块 → 嵌入 → 存储",save_local 将结果持久化到磁盘,后续无需重复预处理,直接加载即用。

检索器:赋予模型"翻查证据"的能力

有了向量数据库,还需要一个 Retriever(检索器) 根据问题找出相关文本块。通过 vectordb.as_retriever() 创建检索器后,调用 retriever.invoke("why is Elrond under investigation") 即可返回相关内容。

使用 as_retriever 创建检索器并检索相关文本块

默认返回 4 个块,可通过 search_kwargs={'k': 5} 精确控制数量——这再次体现了 RAG 中"一切参数皆可调控"的理念。检索数量 k 是影响系统性能的重要超参数:k 值过小可能遗漏关键证据,k 值过大则会向模型注入噪声信息,增加上下文窗口压力。此外,LangChain 还支持 MMR(Maximal Marginal Relevance,最大边际相关性) 检索策略——在保证相关性的同时,主动惩罚内容高度重复的候选块,从而提升检索结果的多样性,避免将同质化信息反复注入上下文。更前沿的方向还包括查询改写(Query Rewriting)技术,即在检索前先用 LLM 将用户的模糊问题扩展或改写为多个更精确的检索查询,再通过多路召回融合提升整体召回率。

检索得到的是块列表,而对话模型需要的是连续字符串。因此要遍历所有块,将每个块的 page_content 用 \ \ 拼接成一段 context,作为稍后喂给对话模型的"案件材料"。

有无 RAG 的对比实验:差距一目了然

为直观展示 RAG 的价值,作者先进行了对照测试:直接调用 llm.invoke 提问,不提供任何上下文。结果模型要么以"涉及政治"为由拒绝回答,要么凭空捏造"区块链""洗钱"等内容——典型的**幻觉(Hallucination)**现象。

幻觉的根源在于 LLM 的自回归生成机制——模型本质上是在预测"下一个最可能的 Token",而非"最真实的信息"。幻觉可细分为两类:事实性幻觉(Factual Hallucination,编造训练数据之外的具体事实,如伪造不存在的论文引用或人物经历)和忠实性幻觉(Faithfulness Hallucination,生成内容偏离或矛盾于给定上下文,即模型"没有忠实地遵循"输入材料)。RAG 主要针对第一类:通过在 Prompt 中注入真实文档片段并要求模型「仅基于以下材料回答」,将开放式生成约束为有据可查的信息提取任务,从而大幅降低模型"无中生有"的概率。然而 RAG 并不能完全消除幻觉——模型仍可能对检索到的证据进行错误推理,或在上下文不足时凭借参数记忆"补全"信息。生产系统中通常还需配合答案可信度评估、来源引用标注机制,以及对生成内容进行事后验证(Grounding Check)等多层防护手段。

引入 RAG 后,作者封装了一个 ask(question) 函数,将检索到的 context 与用户问题一同通过 f-string 传入模型。再次提问"Elrond 为何被调查",模型给出了准确答案:因为他可能与一个名为"特工史密斯"的跨维度实体存在关联。

RAG 完成后模型返回准确答案

至此,完整的 RAG 流水线搭建完毕,Qwen 这个小模型也"正式成为本案专家"。值得一提的是,Qwen 2.5 1.5B 是阿里云广告通义千问团队于 2024 年发布的轻量级模型,支持 32K 上下文长度,并针对指令遵循和结构化输出进行了专项优化。在 RAG 场景中,模型无需"记住"知识,只需具备良好的阅读理解和信息提取能力,而这恰恰是经过指令微调的小模型所擅长的——这也解释了为何 1.5B 的模型能在特定领域输出超越其参数量的专业水准。

从演示到生产:关键最佳实践

要将 RAG 系统从"玩具级"提升到"生产可用",还需关注以下几个方向:

  • 对话历史管理:真实系统涉及多轮对话,模型本身没有记忆,需开发者自行存储并维护历史记录。LangChain 提供了 ConversationBufferMemory 等多种记忆管理方案,可在上下文窗口有限的情况下通过摘要压缩等策略保留关键历史信息。
  • 系统提示词与角色设定:通过 System Prompt 赋予模型明确身份(侦探、律师、顾问等),可显著提升输出的一致性与专业感。精心设计的系统提示词还能约束模型在上下文不足时主动声明"无法从给定材料中找到相关信息",而非凭空填补,进一步抑制幻觉。
  • 向量库持久化加载:利用 save_local 保存的索引,避免每次重启都重建数据库,大幅降低启动开销。
  • GPU 加速:若有 CUDA 支持的 GPU,只需将 faiss-cpu 替换为 faiss-gpu,代码无需其他改动;Ollama 会自动检测并启用 GPU。
  • 超参数系统化调优:chunk size、overlap、k 值等参数对检索质量影响显著。可借助 RAGAS(RAG Assessment)等评估框架进行量化评估。RAGAS 通过 LLM-as-Judge 范式解决了开放域问答难以量化评估的难题——用另一个强大的语言模型充当裁判,构建了四个核心评估维度:答案相关性(Answer Relevancy,生成答案与问题的契合程度)、上下文精确率(Context Precision,检索到的文档块中真正有用的比例,衡量检索的"准")、上下文召回率(Context Recall,所有需要的信息是否都被检索到,衡量检索的"全")以及忠实度(Faithfulness,答案是否完全基于检索内容而非模型参数)。这四个维度形成互相制衡的评估矩阵,使工程师能用数据驱动的方式替代凭感觉调参,在检索效率与答案质量之间找到最优平衡点。
  • 迈向 Agentic RAG:更前沿的方向是赋予系统自主决定「是否需要检索」「检索哪个知识库」「如何分解复杂问题」的推理能力,从静态流水线演进为动态推理系统。LangChain 推出的 LangGraph 正是为此类需要循环推理和条件分支的 Agent 系统而设计的。

总结

这个看似荒诞的"精灵王侦查案",清晰拆解了 RAG 的每一个核心环节:加载 → 切分 → 嵌入 → 存储 → 检索 → 生成。它证明了即使是 15 亿参数量级的本地小模型,只要配上合适的检索管线,也能在特定领域输出可靠、有据可查的答案。

对于希望以低成本、可控方式在本地构建领域知识库的开发者来说,Ollama + LangChain + FAISS + Qwen 这套组合是一个扎实的起点。掌握了 RAG 的核心逻辑,下一步自然就是探索能够自主规划、执行任务的 AI Agent 了。

分享:

相关推荐