尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

RAG系统实战:从零搭建与Embedding模型微调指南

RAG系统实战:从零搭建与Embedding模型微调指南 在构建企业级知识问答系统时你是否遇到过这样的困境通用大模型对内部文档一问三不知而微调整个大模型又成本高昂、数据需求量大RAG检索增强生成技术正是解决这一痛点的利器。它通过“外部知识检索 大模型生成”的模式让模型能够基于你的私有数据给出精准回答。然而一个高性能的RAG系统远不止调用API那么简单其核心在于高效的检索与精准的语义理解这直接依赖于向量数据库的选型与Embedding模型的质量。本文将为你提供一套从零到一的RAG系统实战指南。我们将不仅搭建一个基础的RAG架构更会深入核心探讨如何为特定领域数据微调专属的Embedding模型从而大幅提升检索准确率。内容涵盖RAG核心流程、主流量化数据库对比与实战、以及使用LoRA对Embedding模型进行轻量化微调的完整过程并附带可运行的项目代码。1. RAG技术核心概念与架构解析在深入实战之前我们有必要厘清RAG是什么、为什么需要它以及一个典型的RAG系统是如何工作的。1.1 什么是RAG它解决了什么问题RAG全称Retrieval-Augmented Generation即检索增强生成。它是一种将信息检索技术与大语言模型LLM生成能力相结合的技术范式。核心思想当用户提出一个问题Query时系统并非直接让LLM凭空生成答案而是先从外部知识库如文档、数据库中检索出与问题最相关的文档片段Chunks。然后将这些检索到的片段作为上下文Context连同原始问题一起提交给LLM让LLM基于给定的上下文生成最终答案。它主要解决两大问题LLM的“幻觉”与知识滞后LLM的训练数据有截止日期且其参数化知识可能不准确或产生“幻觉”编造事实。RAG通过引入可验证的外部知识源让答案有据可依。低成本接入私有数据相比于耗费巨资微调整个大模型RAG只需将私有数据向量化并存储即可让LLM“掌握”这些知识实现成本与效果的平衡。1.2 典型RAG系统工作流程一个完整的RAG流程可分为“索引构建”和“查询执行”两个阶段。阶段一索引构建离线文档加载与清洗从各种来源PDF、Word、网页、数据库加载文档并进行文本清洗去除无关字符、标准化格式。文本分割将长文档切割成大小适中、语义相对完整的片段Chunks。这是关键步骤分割策略直接影响检索效果。向量化使用Embedding模型将每个文本片段转换为一个高维向量Vector。这个向量捕获了文本的深层语义信息。向量存储将生成的向量及其对应的原始文本元数据存入向量数据库建立索引。阶段二查询执行在线问题向量化将用户的问题Query使用同样的Embedding模型转换为向量。语义检索在向量数据库中计算问题向量与所有存储向量之间的相似度如余弦相似度返回最相似的K个文本片段。提示构建将检索到的文本片段作为上下文与原始问题一起按照预设的提示模板Prompt Template组装成最终的提示。答案生成将组装好的提示发送给LLM生成最终答案。1.3 为什么Embedding模型微调如此重要Embedding模型是将文本转换为向量的核心组件。通用的Embedding模型如text-embedding-ada-002、bge-large-zh在通用领域表现良好但在特定垂直领域如医疗、法律、金融或处理特殊文档格式如代码、表格时其语义表示能力可能不足。微调Embedding模型的目标是让模型学会为你的领域数据生成更具区分度的向量。例如在医疗领域“高血压”和“糖尿病”虽然都是慢性病但在通用模型中向量可能相对接近。通过微调我们可以让模型学到它们是与不同器官、治疗方案相关的独立概念从而在检索时更精准地区分关于这两种疾病的文档。2. 环境准备与工具选型在开始搭建之前我们需要准备好开发环境并选择合适的技术栈。本项目将采用Python作为主要开发语言。2.1 基础环境与Python包确保你的Python版本在3.8以上。我们将使用conda或venv创建独立的虚拟环境。# 创建并激活虚拟环境 (以conda为例) conda create -n rag_tutorial python3.10 conda activate rag_tutorial # 安装核心依赖 pip install langchain langchain-community langchain-chroma # RAG框架与向量库客户端 pip install sentence-transformers # 用于加载和微调Embedding模型 pip install chromadb # 向量数据库 pip install pypdf python-docx markdown # 文档加载器支持 pip install transformers datasets accelerate peft # 用于模型微调 pip install jupyter # 可选用于实验2.2 向量数据库选型Chroma vs FAISS vs 其他选择合适的向量数据库是构建高效RAG系统的基石。下面从几个维度对比主流选择特性ChromaFAISSQdrantMilvus / PGVector部署模式内存/客户端-服务器库嵌入应用客户端-服务器客户端-服务器持久化支持需手动保存/加载支持支持易用性极高API简单中等需理解索引高中等功能强大但较复杂分布式不支持不支持支持支持生产就绪适合轻量级、原型适合研究、嵌入应用适合生产适合大规模生产语言Python/JSC/PythonRust/多语言Go/多语言核心优势开箱即用与LangChain集成好性能极致Meta开源功能丰富过滤强云服务功能全面生态成熟选型建议快速原型/学习/轻量应用首选Chroma。它极其简单无需额外服务适合本教程。需要极致性能的嵌入应用选择FAISS。你可以将它作为一个库集成到你的Python应用中。中大型生产环境需要过滤、分布式考虑Qdrant或Milvus。它们提供了更丰富的查询功能、可扩展性和运维工具。已有PostgreSQL想快速集成向量搜索PGVector是完美选择。本教程为简化部署将使用Chroma作为向量数据库。2.3 Embedding模型与LLM选择Embedding模型我们将使用BAAI/bge-small-zh-v1.5这是一个优秀的中文开源Embedding模型参数量小适合微调实验。后续微调也基于此模型。大语言模型为了本地运行和演示我们可以使用通过Ollama部署的轻量模型如qwen2.5:7b或llama3.2:3b。也可以使用OpenAI、DeepSeek等云API。3. 基础RAG架构搭建实战现在我们开始动手搭建一个最基础的RAG系统。我们将创建一个处理PDF文档的问答应用。3.1 项目结构初始化创建如下项目目录结构rag_project/ ├── data/ # 存放原始文档 │ └── example.pdf ├── docs_processed/ # 处理后的文本片段可选 ├── vector_db/ # Chroma数据库存储路径 ├── src/ │ ├── __init__.py │ ├── document_processor.py # 文档加载与处理 │ ├── embedding_client.py # 向量化客户端 │ ├── retriever_qa.py # 检索与问答链 │ └── config.py # 配置文件 ├── requirements.txt └── main.py # 主程序入口3.2 文档加载与处理首先实现文档的加载和智能分割。我们使用LangChain提供的丰富文档加载器和文本分割器。# file: src/document_processor.py from langchain_community.document_loaders import PyPDFLoader, TextLoader, Docx2txtLoader from langchain.text_splitter import RecursiveCharacterTextSplitter from langchain.schema import Document import os from typing import List class DocumentProcessor: def __init__(self, chunk_size500, chunk_overlap50): 初始化文档处理器 :param chunk_size: 每个文本块的最大字符数 :param chunk_overlap: 块之间的重叠字符数用于保持上下文连贯 self.text_splitter RecursiveCharacterTextSplitter( chunk_sizechunk_size, chunk_overlapchunk_overlap, separators[\n\n, \n, 。, , , , , , ] ) def load_document(self, file_path: str) - List[Document]: 根据文件后缀选择加载器加载文档 loader None if file_path.endswith(.pdf): loader PyPDFLoader(file_path) elif file_path.endswith(.txt): loader TextLoader(file_path, encodingutf-8) elif file_path.endswith(.docx): loader Docx2txtLoader(file_path) else: raise ValueError(fUnsupported file type: {file_path}) documents loader.load() print(fLoaded {len(documents)} pages from {file_path}) return documents def split_documents(self, documents: List[Document]) - List[Document]: 将文档分割成小块 chunks self.text_splitter.split_documents(documents) print(fSplit into {len(chunks)} chunks.) return chunks def process_directory(self, data_dir: str) - List[Document]: 处理整个目录下的所有支持文档 all_chunks [] supported_ext [.pdf, .txt, .docx] for filename in os.listdir(data_dir): file_path os.path.join(data_dir, filename) if os.path.isfile(file_path) and any(file_path.endswith(ext) for ext in supported_ext): try: docs self.load_document(file_path) chunks self.split_documents(docs) all_chunks.extend(chunks) except Exception as e: print(fError processing {filename}: {e}) print(fTotal chunks processed: {len(all_chunks)}) return all_chunks3.3 向量化与存储使用Chroma接下来我们将处理好的文本块向量化并存入Chroma数据库。# file: src/embedding_client.py from langchain_chroma import Chroma from langchain_huggingface import HuggingFaceEmbeddings import os class VectorStoreManager: def __init__(self, persist_directory: str ./vector_db): 初始化向量存储管理器 :param persist_directory: Chroma数据库持久化目录 self.persist_directory persist_directory # 使用开源的BGE小型中文模型 self.embedding_model HuggingFaceEmbeddings( model_nameBAAI/bge-small-zh-v1.5, model_kwargs{device: cpu}, # 使用GPU可改为 cuda encode_kwargs{normalize_embeddings: True} # 归一化便于余弦相似度计算 ) def create_vector_store(self, documents, collection_namerag_demo): 创建或加载向量存储并添加文档 # 清空持久化目录确保重新创建生产环境应判断是否存在 if os.path.exists(self.persist_directory): # 注意这里为了演示简单删除生产环境应更谨慎 import shutil shutil.rmtree(self.persist_directory) vectordb Chroma.from_documents( documentsdocuments, embeddingself.embedding_model, persist_directoryself.persist_directory, collection_namecollection_name ) print(fVector store created and persisted to {self.persist_directory}) return vectordb def load_vector_store(self, collection_namerag_demo): 从磁盘加载已有的向量存储 vectordb Chroma( persist_directoryself.persist_directory, embedding_functionself.embedding_model, collection_namecollection_name ) print(fVector store loaded from {self.persist_directory}) return vectordb3.4 构建检索与问答链现在我们将向量数据库与LLM连接起来构建完整的问答链。# file: src/retriever_qa.py from langchain.chains import RetrievalQA from langchain.prompts import PromptTemplate from langchain_community.llms import Ollama # 使用本地Ollama模型 # 或者使用OpenAI # from langchain_openai import ChatOpenAI class QASystem: def __init__(self, vector_store, llm_model_nameqwen2.5:7b): 初始化问答系统 :param vector_store: 已加载的向量数据库对象 :param llm_model_name: Ollama模型名称 self.vector_store vector_store # 初始化检索器设置返回最相似的4个片段 self.retriever self.vector_store.as_retriever(search_kwargs{k: 4}) # 初始化LLM (Ollama本地模型) self.llm Ollama(modelllm_model_name, temperature0.1) # 若使用OpenAI替换为 # self.llm ChatOpenAI(model_namegpt-3.5-turbo, temperature0) # 定义提示模板指导LLM基于上下文回答 self.prompt_template 请根据以下上下文信息回答问题。如果上下文没有提供足够信息请直接回答“根据已知信息无法回答该问题”不要编造信息。 上下文 {context} 问题{question} 答案 self.PROMPT PromptTemplate( templateself.prompt_template, input_variables[context, question] ) # 构建检索增强生成链 self.qa_chain RetrievalQA.from_chain_type( llmself.llm, chain_typestuff, # 将所有检索到的上下文“塞”进提示 retrieverself.retriever, chain_type_kwargs{prompt: self.PROMPT}, return_source_documentsTrue # 返回源文档用于追溯 ) def ask(self, question: str): 提出问题并获取答案 result self.qa_chain.invoke({query: question}) answer result[result] source_docs result[source_documents] print(f\n问题{question}) print(f答案{answer}) print(\n参考来源) for i, doc in enumerate(source_docs[:2]): # 显示前两个来源 print(f[{i1}] {doc.page_content[:150]}...) # 截取片段 return answer, source_docs3.5 主程序与运行演示最后我们编写主程序将以上模块串联起来。# file: main.py import sys import os sys.path.append(os.path.dirname(os.path.abspath(__file__))) from src.document_processor import DocumentProcessor from src.embedding_client import VectorStoreManager from src.retriever_qa import QASystem def build_knowledge_base(): 构建知识库处理文档并存入向量数据库 print( 开始构建知识库 ) processor DocumentProcessor(chunk_size400, chunk_overlap80) # 假设你的PDF文档放在 ./data/ 目录下 chunks processor.process_directory(./data) if not chunks: print(未找到或处理任何文档请检查./data目录。) return None vector_mgr VectorStoreManager(persist_directory./vector_db) vectordb vector_mgr.create_vector_store(chunks) print(知识库构建完成) return vectordb def interactive_qa(vectordb): 交互式问答 if not vectordb: print(向量数据库未初始化请先构建知识库。) return qa_system QASystem(vectordb, llm_model_nameqwen2.5:7b) # 确保已用Ollama拉取该模型 print(\n 知识库问答系统已就绪输入 quit 退出 ) while True: question input(\n请输入你的问题).strip() if question.lower() in [quit, exit, q]: print(再见) break if question: qa_system.ask(question) if __name__ __main__: # 首次运行需要构建知识库 # db build_knowledge_base() # 之后可以直接加载 vector_mgr VectorStoreManager(persist_directory./vector_db) db vector_mgr.load_vector_store() interactive_qa(db)运行步骤将你的PDF文档放入./data文件夹。确保Ollama服务已运行并已拉取模型ollama pull qwen2.5:7b。首次运行取消main.py中build_knowledge_base()的注释运行python main.py。后续运行注释掉构建部分直接加载数据库进行问答。至此一个基础的RAG系统已经搭建完成。你可以通过问答测试其效果。4. 进阶微调Embedding模型提升检索精度如果你的领域数据特殊如大量专业术语、特定句式通用Embedding模型可能表现不佳。这时微调Fine-tuning就派上用场了。我们将使用LoRA这种参数高效微调方法在少量数据上对BAAI/bge-small-zh模型进行微调。4.1 微调数据准备微调Embedding模型通常需要文本对数据格式为(query, positive_doc)即一个问题和一个与之相关的正例文档。对于RAG我们可以用“文档标题/核心句”作为query文档内容作为positive_doc或者从问答对中构造。我们创建一个简单的模拟数据集# file: prepare_finetune_data.py from datasets import Dataset import json # 模拟一个医疗领域的微调数据 finetune_data [ { query: 高血压的诊断标准是什么, positive: 在未使用降压药物的情况下非同日3次测量诊室血压收缩压≥140mmHg和/或舒张压≥90mmHg可诊断为高血压。 }, { query: 糖尿病患者应该如何控制饮食, positive: 糖尿病患者饮食控制应以低糖、低脂、高纤维为原则。建议定时定量均衡营养优先选择升糖指数低的食物如全麦面包、燕麦、绿叶蔬菜等。 }, { query: 什么是冠状动脉粥样硬化性心脏病, positive: 冠状动脉粥样硬化性心脏病简称冠心病是指冠状动脉发生粥样硬化引起管腔狭窄或闭塞导致心肌缺血、缺氧或坏死而引起的心脏病。 }, # ... 可以准备几十到几百条这样的数据 ] # 保存为json文件 with open(./finetune_data.json, w, encodingutf-8) as f: json.dump(finetune_data, f, ensure_asciiFalse, indent2) # 转换为HuggingFace Dataset格式 dataset Dataset.from_list(finetune_data) dataset dataset.train_test_split(test_size0.2, seed42) print(f训练集大小: {len(dataset[train])}, 测试集大小: {len(dataset[test])})4.2 使用Sentence Transformers与LoRA进行微调我们使用sentence-transformers库和peft库来实现LoRA微调。# file: finetune_embedding.py from sentence_transformers import SentenceTransformer, models, losses, InputExample from sentence_transformers.evaluation import InformationRetrievalEvaluator from torch.utils.data import DataLoader from peft import LoraConfig, get_peft_model import torch import json from datasets import Dataset import os # 1. 加载基础模型 model_name BAAI/bge-small-zh-v1.5 model SentenceTransformer(model_name) # 2. 为模型添加LoRA适配器仅作用于注意力层 # 定义LoRA配置 lora_config LoraConfig( r8, # LoRA的秩 lora_alpha32, target_modules[q_proj, v_proj], # 针对Transformer的query和value投影层 lora_dropout0.1, biasnone, task_typeFEATURE_EXTRACTION # 对于Embedding任务 ) # 获取模型的transformer部分并应用LoRA model[0].auto_model get_peft_model(model[0].auto_model, lora_config) model[0].auto_model.print_trainable_parameters() # 查看可训练参数量 # 3. 准备数据 def load_data(file_path): with open(file_path, r, encodingutf-8) as f: data json.load(f) train_examples [] for item in data: # Sentence Transformers 微调通常使用 MultipleNegativesRankingLoss # 它需要 (anchor, positive) 对。这里我们简单处理。 # 更复杂的训练可能需要构建难负例。 train_examples.append(InputExample(texts[item[query], item[positive]])) return train_examples train_examples load_data(./finetune_data.json) train_dataloader DataLoader(train_examples, shuffleTrue, batch_size8) # 4. 定义损失函数 # MultipleNegativesRankingLoss 适用于 (anchor, positive) 对假设batch内其他样本为负例 train_loss losses.MultipleNegativesRankingLoss(model) # 5. 配置训练参数并训练 num_epochs 3 warmup_steps int(len(train_dataloader) * num_epochs * 0.1) model.fit( train_objectives[(train_dataloader, train_loss)], epochsnum_epochs, warmup_stepswarmup_steps, output_path./output/finetuned_bge_lora, # 保存路径 show_progress_barTrue, checkpoint_path./output/checkpoints, # 可选保存检查点 checkpoint_save_steps50 ) print(微调完成模型已保存至 ./output/finetuned_bge_lora)4.3 在RAG中使用微调后的Embedding模型微调完成后只需在创建向量数据库和检索时使用我们微调好的模型路径即可。# 修改 embedding_client.py 中的初始化部分 self.embedding_model HuggingFaceEmbeddings( # model_nameBAAI/bge-small-zh-v1.5, # 原模型 model_name./output/finetuned_bge_lora, # 使用微调后的模型 model_kwargs{device: cpu}, encode_kwargs{normalize_embeddings: True} )然后重新构建向量数据库因为Embedding模型变了向量表示也变了你会发现针对你微调数据所在领域的查询检索准确率会有显著提升。5. 常见问题与排查思路在开发和部署RAG系统时你可能会遇到以下典型问题问题现象可能原因排查思路与解决方案检索结果不相关1. 文本分割不合理块太大或太小2. Embedding模型不匹配领域3. 相似度计算方式不合适1. 调整chunk_size和chunk_overlap尝试按段落、句子分割。2. 尝试不同的Embedding模型或进行领域微调如本文第4部分。3. 确保向量已归一化并使用余弦相似度。LLM回答未基于上下文1. 提示模板设计不佳2. 检索到的上下文过多或噪声大3. LLM本身“幻觉”倾向强1. 强化提示模板明确要求“基于上下文”。2. 调整检索数量k或使用MMR等算法对检索结果重排序去重。3. 降低LLM的temperature参数或更换更“听话”的模型。处理长文档时内存不足1. 一次性加载所有文档到内存2. 向量化过程占用大量内存1. 使用流式或分批处理文档。2. 对于极大文档考虑使用MapReduce或Refine等链式处理方式。向量数据库查询慢1. 向量维度高、数据量大2. 未建立高效索引1. 考虑使用更高效的向量数据库如FAISS的IVF索引。2. 在Chroma中尝试不同的距离函数或索引类型。对于生产环境评估Qdrant/Milvus。微调后效果反而变差1. 训练数据质量差或数量少2. 过拟合3. 超参数设置不当1. 确保训练数据query, positive对高质量、有代表性。2. 增加数据量使用数据增强或加入难负例挖掘。3. 减小LoRA的r值降低学习率使用更早的停止策略。Ollama模型无法连接1. Ollama服务未启动2. 模型未下载1. 终端运行ollama serve启动服务。2. 使用ollama pull model_name下载指定模型。6. 生产环境最佳实践与进阶优化当你准备将RAG系统投入生产时需要考虑以下方面6.1 架构优化异步处理文档索引构建读取、分割、向量化应设计为异步任务避免阻塞主API。缓存策略对频繁查询的问题及答案进行缓存减少对向量数据库和LLM的调用。服务解耦将Embedding服务、向量数据库、LLM服务拆分为独立微服务提高可扩展性和可靠性。6.2 检索质量提升混合检索结合语义检索向量搜索和关键词检索如BM25取长补短。LangChain的EnsembleRetriever可以轻松实现。重排序初步检索出较多结果如20个后使用一个更精细的重排序模型对结果进行精排选出最相关的3-5个。可以选用bge-reranker等模型。元数据过滤在存储时为每个文本块添加元数据如来源、章节、日期。检索时可先根据元数据过滤再进行语义搜索提高精度和速度。6.3 提示工程与答案生成优化动态上下文窗口根据检索片段的长度和数量动态调整提示确保不超出LLM的上下文限制。引用溯源在生成的答案中明确标注引用的来源文档和页码增强可信度。拒绝回答机制当检索到的上下文与问题相关性低于某个阈值或LLM置信度低时系统应主动拒绝回答而不是胡编乱造。6.4 可观测性与评估日志记录详细记录每次问答的Query、检索到的片段、LLM的输入提示和输出答案。这对于调试和效果分析至关重要。评估指标建立评估体系包括检索阶段命中率、平均排序位置。生成阶段答案相关性、事实准确性可通过与标准答案对比。端到端用户满意度反馈。A/B测试对比不同Embedding模型、不同分割策略、不同提示模板的效果用数据驱动优化。通过本文你不仅掌握了搭建一个基础RAG系统的全流程还深入到了通过微调Embedding模型来优化核心检索能力的进阶阶段。从环境准备、文档处理、向量数据库选型与使用到最后的模型微调实战我们覆盖了RAG落地的关键环节。记住一个优秀的RAG系统是迭代出来的持续在数据质量、检索策略和提示工程上进行打磨才能使其真正成为你业务的智能助手。
返回列表