尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

RAG技术实战:从零搭建基于LangChain与向量数据库的智能问答系统

RAG技术实战:从零搭建基于LangChain与向量数据库的智能问答系统 1. 先搞清楚 RAG 到底解决了什么问题以及它为什么现在这么火如果你正在处理一个需要从大量文档、PDF、网页或内部资料中快速找到答案的任务比如搭建一个智能客服、一个企业知识库问答系统或者一个能帮你从技术手册里找解决方案的工具那么 RAG 是你绕不开的技术。它的核心价值非常直接让大语言模型LLM在回答问题时能够“参考”你指定的、最新的、准确的外部知识而不是仅仅依赖它训练时学到的、可能过时或泛泛的内部知识。简单来说RAG 就是给 LLM 装上一个“外部记忆库”和“搜索引擎”。当用户提问时系统不是让 LLM 凭空想象而是先从这个记忆库里快速找到最相关的几段资料检索然后把问题和这些资料一起交给 LLM让它基于这些“证据”来组织答案生成。这直接解决了 LLM 的几个核心痛点幻觉胡编乱造、知识过时、无法处理私有或领域特定数据。现在很多人在谈 RAG从热搜词也能看出大家关心的点很分散有想快速上手的rag实战、windows电脑搭建rag有研究具体组件的向量数据库、召回与重排序有探索前沿的agentic rag、多模态rag也有面临实际问题的rag检索结果冲突怎么办。这篇文章不会只讲概念我会以一个从业者的角度带你从“这个东西到底怎么跑起来”开始一直拆解到“怎么让它跑得稳、答得准”。我会重点讲清楚环境准备、每一步的操作意图、参数背后的考量以及那些新手最容易踩进去的坑。2. 动手之前理解 RAG 的标准流程与核心组件在打开代码编辑器之前我们必须对 RAG 的完整流水线有个清晰的蓝图。一个典型的 RAG 系统其工作流程可以清晰地分为“线下”和“线上”两个阶段。线下阶段知识库构建这是准备“外部记忆库”的过程目标是把你的一堆原始文档如 PDF、Word、TXT、网页变成可以被高效检索的结构化数据。文档接入与加载从各种来源本地文件系统、数据库、网络爬虫读取原始文档。这里第一个坑就来了不同格式的文档需要不同的解析器Parser一个解析不好的 PDF 会导致文本错乱、丢失表格或图片中的文字。文档清洗与切片Chunking原始文档可能很长比如一本几百页的手册直接扔给检索器效率低且不精准。需要将它们切割成大小合适的“片段”Chunks。切片是影响效果的关键步骤之一。切得太碎上下文信息丢失切得太大检索会引入无关信息。常见的策略有按固定长度重叠切分、按段落/标题切分等。向量化与索引构建这是核心。使用一个嵌入模型Embedding Model将每一个文本片段转换成一个高维度的向量一组数字。这个向量在数学上代表了这段文本的“语义”。然后把所有向量存入一个专门的数据库——向量数据库如 Milvus, Pinecone, Qdrant, Weaviate。这个过程称为“建索引”。线上阶段问答查询这是用户实际使用的过程。问题向量化当用户提出一个问题时用同样的嵌入模型将问题也转换为一个向量。语义检索召回在向量数据库中寻找与“问题向量”最相似通常使用余弦相似度等度量方法的 K 个文本片段向量。这一步就是“召回”Top-K 个相关片段。重排序可选但重要初步召回的结果可能包含一些相似但实际不相关的片段。可以使用一个更精细的但通常也更耗资源的重排序模型对 Top-K 个结果进行再次打分和排序选出最相关的几个。提示构建与答案生成将用户原始问题和筛选后的相关文本片段按照一定的模板Prompt Template组装成一个完整的提示Prompt。例如“请基于以下上下文回答问题。上下文{检索到的文本}。问题{用户问题}。答案”。最后将这个提示发送给 LLM如 GPT-4, Claude, 或本地部署的 Llama 2/3 等生成最终答案。理解了这套流程我们就能明白那些热搜词分别对应哪个环节向量数据库对应索引构建召回与重排序对应检索环节rag框架如 LangChain, LlamaIndex就是帮我们串起整个流程的工具箱。3. 环境与工具选型从零搭建一个可运行的 RAG 原型理论清楚了我们立刻进入实战。我会选择一条对开发者友好、资源要求相对平易的路径来搭建一个最小可行原型。我们的目标是在本地用 Python 快速实现一个能问答的 RAG 系统。3.1 基础环境准备首先确保你的开发环境就绪。我推荐使用 Python 3.9 和venv或conda创建独立的虚拟环境避免包冲突。# 创建并激活虚拟环境 (以 venv 为例) python -m venv rag_env source rag_env/bin/activate # Linux/macOS # 或 rag_env\Scripts\activate # Windows3.2 核心库安装我们将使用LangChain这个流行的框架来简化流程用Chroma一个轻量级、可嵌入的向量数据库来存储向量用Sentence Transformers来获取开源的嵌入模型。pip install langchain langchain-community langchain-chroma pip install sentence-transformers pip install pypdf # 用于解析PDF pip install tiktoken # 用于文本切分可选但推荐为什么选这些库LangChain提供了文档加载、文本分割、链Chain编排等高层抽象让我们能快速组装流程而不是从头写每一行胶水代码。对于原型验证和快速迭代非常高效。Chroma它可以直接运行在内存中或持久化到磁盘无需像 Milvus 那样部署一个独立的服务极大降低了初学者的上手门槛。等你的数据量变大、对性能要求更高时再迁移到 Milvus、Qdrant 等生产级向量数据库。Sentence Transformers提供了大量高质量的开源嵌入模型如all-MiniLM-L6-v2可以在 CPU 上运行虽然速度不如 GPU但对于学习和测试完全足够避免了配置 CUDA 环境的麻烦。3.3 准备你的知识文档在项目目录下创建一个docs文件夹放入你想让系统学习的文档。例如放几篇关于 Python 编程的 PDF 或 TXT 文件。这是你的“知识源”。4. 核心流程代码实现与逐行解析下面我们一步步用代码实现 RAG 的线下和线上流程。我会在代码中加入大量注释解释每一步“为什么”要这么做。4.1 线下阶段构建向量知识库# rag_build_index.py import os from langchain_community.document_loaders import PyPDFLoader, TextLoader from langchain.text_splitter import RecursiveCharacterTextSplitter from langchain.embeddings import HuggingFaceEmbeddings from langchain.vectorstores import Chroma # 1. 文档加载 - 处理不同类型的文档 documents [] for file in os.listdir(./docs): file_path os.path.join(./docs, file) if file.endswith(.pdf): loader PyPDFLoader(file_path) documents.extend(loader.load()) # load() 返回 Document 对象列表 elif file.endswith(.txt): loader TextLoader(file_path, encodingutf-8) documents.extend(loader.load()) # 可以继续添加对 .docx, .md 等格式的支持 print(f已加载 {len(documents)} 个文档片段原始) # 2. 文本分割 - 这是效果的关键调节点之一 text_splitter RecursiveCharacterTextSplitter( chunk_size500, # 每个片段的字符数约 chunk_overlap50, # 片段之间的重叠字符数用于保持上下文连贯 length_functionlen, separators[\n\n, \n, 。, , , , , , ] # 中文环境可调整分隔符 ) split_docs text_splitter.split_documents(documents) print(f分割后得到 {len(split_docs)} 个文本块) # 3. 嵌入模型初始化 - 选择一个小而快的开源模型 # 模型会从 Hugging Face 下载第一次运行需要时间 embedding_model HuggingFaceEmbeddings( model_nameall-MiniLM-L6-v2, # 一个通用的英文小模型对中文也有效果 model_kwargs{device: cpu}, # 指定使用 CPU encode_kwargs{normalize_embeddings: True} # 归一化便于相似度计算 ) # 4. 向量化并存入向量数据库构建索引 # persist_directory 指定索引持久化到磁盘的路径下次启动可直接加载无需重新计算 vectorstore Chroma.from_documents( documentssplit_docs, embeddingembedding_model, persist_directory./chroma_db # 索引保存目录 ) vectorstore.persist() # 显式持久化 print(向量知识库构建完成已保存至 ./chroma_db)关键参数解析与避坑点chunk_size和chunk_overlap这是“艺术”所在。500是一个常见的起始值适合一般性问答。如果您的文档段落很长或问题需要更广泛的上下文可以增加到800或1000。overlap设为chunk_size的 10%-20% 有助于防止在分割点丢失重要信息。嵌入模型选择all-MiniLM-L6-v2是一个很好的起点。如果您处理的是中文可以考虑paraphrase-multilingual-MiniLM-L12-v2或专门的中文模型如BAAI/bge-small-zh。更换模型只需修改model_name但要注意不同模型的向量维度可能不同构建的索引不能混用。persist_directory一定要指定。这样你的索引数据会保存到磁盘。下次运行时你可以直接加载这个数据库而无需重新处理所有文档这对于迭代开发至关重要。4.2 线上阶段实现问答链知识库建好后我们来实现问答功能。# rag_query.py from langchain.embeddings import HuggingFaceEmbeddings from langchain.vectorstores import Chroma from langchain.chains import RetrievalQA from langchain.llms import Ollama # 假设使用本地运行的 Ollama Llama3 # 或者使用 OpenAI API # from langchain.chat_models import ChatOpenAI # 1. 加载已构建的向量数据库 embedding_model HuggingFaceEmbeddings(model_nameall-MiniLM-L6-v2) vectorstore Chroma( persist_directory./chroma_db, embedding_functionembedding_model ) print(向量知识库加载成功。) # 2. 定义检索器 (Retriever) # search_kwargs 中的 k 是最重要的参数之一它决定检索出多少相关片段提供给 LLM。 retriever vectorstore.as_retriever(search_kwargs{k: 4}) # 你可以尝试不同的检索策略比如 # retriever vectorstore.as_retriever(search_typemmr, search_kwargs{k: 4, fetch_k: 10}) # MMR (最大边际相关性) 可以在保证相关性的同时增加结果的多样性。 # 3. 定义大语言模型 (LLM) # 方案A使用本地模型 (例如通过 Ollama) llm Ollama(modelllama3:8b, temperature0.1) # temperature 控制创造性0.1 更倾向于确定性和事实性答案适合 RAG。 # 方案B使用 OpenAI API (需设置环境变量 OPENAI_API_KEY) # llm ChatOpenAI(model_namegpt-3.5-turbo, temperature0) # 4. 创建检索问答链 (RetrievalQA Chain) # chain_type 可选 stuff, map_reduce, refine, map_rerank。对于初学者stuff 最简单它把所有检索到的上下文塞进一个 Prompt。 qa_chain RetrievalQA.from_chain_type( llmllm, chain_typestuff, retrieverretriever, return_source_documentsTrue, # 非常有用返回检索到的源文档便于调试和溯源。 verboseTrue # 调试时打开可以看到链的详细执行过程 ) # 5. 进行问答 while True: query input(\n请输入您的问题 (输入 quit 退出): ) if query.lower() quit: break result qa_chain.invoke({query: query}) print(f\n答案: {result[result]}) print(\n--- 参考来源 ---) for i, doc in enumerate(result[source_documents]): print(f[{i1}] {doc.page_content[:200]}...) # 打印源文档片段的前200字符 print(f 来源: {doc.metadata.get(source, N/A)}, 页码: {doc.metadata.get(page, N/A)}\n)核心环节解析检索器 (retriever)k4意味着每次检索返回4个最相似的片段。这个数字需要权衡太少可能信息不足太多可能引入噪声并增加 LLM 的上下文长度负担。这是需要根据你的文档内容和问题复杂度来调整的关键参数。LLM 选择这里给出了本地Ollama和云端OpenAI两种方案。本地方案隐私性好、无网络成本但需要足够的机器资源内存、CPU/GPU。云端方案简单可靠但会产生费用且数据需出境。选择哪种取决于你的数据敏感性、预算和硬件条件。return_source_documentsTrue这是 RAG 系统可解释性的生命线。它让你能看到答案是基于哪几段原文生成的这对于验证答案准确性、调试检索效果比如检出的片段是否真的相关至关重要。没有这个功能的 RAG 就是一个黑盒。chain_type“stuff”是最直接的方式但如果你的k很大检索到的总文本长度可能超过 LLM 的上下文限制。对于超长文档需要考虑“map_reduce”或“refine”等更复杂的链类型它们会对片段进行分组合并或迭代精炼。5. 从原型到可用效果调优与生产化考量一个能跑起来的原型只是第一步。要让 RAG 真正“好用”我们需要关注效果、性能和稳定性。5.1 检索效果优化解决“答非所问”和“找不到”这是 RAG 最常见的痛点。答案不准首先要排查的是检索环节。问题1检索到的片段不相关。检查切片策略你的chunk_size是否合适对于技术文档按章节或标题切分可能比固定长度更好。可以尝试用MarkdownHeaderTextSplitter等更智能的分割器。检查嵌入模型你用的嵌入模型是否适合你的文本领域如中文、医学、法律尝试更换更专业的模型如BAAI/bge系列并对比效果。尝试混合检索除了语义检索向量搜索可以加入关键词检索如 BM25进行混合。这就是热搜词里的混合检索。LangChain 支持将两者结果融合取长补短。引入重排序这就是rag 重排。先用向量检索出较多的候选如k20再用一个更精细的交叉编码器模型如BAAI/bge-reranker对这20个结果重新打分排序只取前3个给 LLM。这能显著提升 Top 结果的相关性。问题2检索结果冲突或信息分散。当多个检索片段给出的信息矛盾时LLM 可能会混淆。rag检索结果冲突怎么办的解决思路是提升检索精度通过上述的重排序、更好的切片和模型确保 Top 片段高度相关且一致。在 Prompt 中明确指令在给 LLM 的 Prompt 模板中加入“如果上下文信息之间存在矛盾请指出矛盾所在或根据信息的可信度如来源页码进行判断”。后处理在 LLM 生成答案后可以设计一个验证步骤检查答案中的关键事实是否都能在源片段中找到支持。5.2 生成效果优化让答案更精准、格式更佳检索到好材料还要靠 LLM 加工出好答案。精心设计 Prompt 模板不要用默认的简单模板。一个结构良好的 Prompt 能极大提升效果。例如from langchain.prompts import PromptTemplate custom_prompt PromptTemplate( template你是一个专业的助手请严格根据以下提供的上下文信息来回答问题。如果上下文没有提供足够信息来回答问题请直接说“根据已知信息无法回答此问题”不要编造信息。 上下文 {context} 问题{question} 基于上下文的答案, input_variables[context, question] ) # 然后在创建 qa_chain 时指定 custom_prompt qa_chain RetrievalQA.from_chain_type( ..., chain_type_kwargs{prompt: custom_prompt} )控制 LLM 参数temperature设为较低值如0.1减少随机性。对于事实性问答甚至可以设为0。5.3 性能与生产化部署当你的知识库文档成千上万时需要考虑以下问题向量数据库选型从轻量级的 Chroma 迁移到支持分布式、持久化、高性能的 Milvus、Qdrant 或 Weaviate。它们支持海量向量数据的快速检索。索引更新知识库不是一成不变的。需要设计增量更新策略新文档切片、向量化后插入向量数据库旧文档修改或删除时需要能定位并更新或删除对应的向量。这是一个复杂的工程问题。服务化与 API将你的 RAG 系统封装成 REST API 或 gRPC 服务如使用 FastAPI供其他应用调用。这就是spring boot milvus langchain4j 实现 rag 问答这类 Java 技术栈在做的事情。日志、监控与评估记录每一次问答的查询、检索到的源、生成的答案。定期评估系统的准确率、召回率。设立监控关注响应延迟和错误率。6. 进阶方向与常见问题排查清单6.1 热门进阶方向Agentic RAG让 RAG 系统具备“思考”和“行动”能力。例如系统可以判断用户问题是否需要检索或者先进行多步推理再决定检索什么甚至根据检索结果自主调用工具如计算器、搜索API来完善答案。这代表了 RAG 从“检索-生成”向“智能体”的演进。多模态 RAG不仅处理文本还能处理图像、表格、音频中的信息。例如从产品手册的图片中提取文字和图表信息进行检索和问答。这需要多模态嵌入模型和能解析多种格式的加载器。Query 转换/扩展在检索前对用户原始查询进行优化。例如进行同义词扩展、纠错或将复杂问题分解成多个子问题分别检索。这能提升检索的召回率。6.2 实战问题排查清单当你遇到问题时按以下顺序排查输入阶段✅ 我的原始文档成功加载了吗用print(documents[0].page_content)检查一下内容。✅ 文本分割后片段长度和重叠是否合理检查几个split_docs的内容。✅ 嵌入模型下载成功了吗第一次运行可能需要联网。检索阶段✅ 向量数据库成功构建/加载了吗检查./chroma_db目录下是否有文件。✅ 检索器返回结果了吗在查询时打开verboseTrue或手动测试retriever.get_relevant_documents(“你的问题”)看返回的片段是否相关。✅ 检索数量k设置是否合适尝试调整k的值3, 5, 8观察答案变化。生成阶段✅ LLM 能正常连接和响应吗先测试一个不依赖检索的简单问题比如llm.invoke(“你好”)。✅ Prompt 模板是否正确组装了上下文和问题打开verboseTrue查看最终发送给 LLM 的完整 Prompt 是什么。✅ 答案是否基于上下文务必开启return_source_documentsTrue对比答案和源片段。性能问题✅ 查询速度慢可能是嵌入模型在 CPU 上运行慢或向量数据库未优化。考虑使用 GPU 运行嵌入模型或换用更高效的向量数据库。✅ 内存/显存不足降低chunk_size减少检索数量k或使用更小的嵌入模型和 LLM。最后也是最关键的建议RAG 项目的成功一半靠技术选型和代码另一半靠对业务数据的深入理解。花时间分析你的文档特性反复调整切片策略、测试不同的嵌入模型、精心设计 Prompt这些“脏活累活”带来的效果提升往往比单纯追求更复杂的架构更显著。先从一个小而准的原型开始确保核心流程畅通再逐步迭代优化是应对rag实战项目复杂性的最稳妥路径。
返回列表