ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

LangChain实战:RAG问答应用从搭建到调优

LangChain实战:RAG问答应用从搭建到调优 简介基于LangChain的RAG问答应用实战PDF面向大模型应用开发学习者和相关岗位面试准备者是一份以百度百科藜麦数据为样本的项目式参考资料。内容以RAG问答系统构建为主线覆盖CUDA 11.7、Python 3.10、PyTorch 1.13.1cu117、LangChain等环境版本梳理以及本地文档加载、固定长度文本分割、向量化与Chroma入库等完整实现步骤。资源为单个PDF文档包体约390KB内含可直接对照实验的项目代码片段、命令行指令、运行输出及笔记式说明便于离线学习与复现。目前已有283人学习或下载适合期望快速掌握大模型八股面试RAG链路知识的学习者。文档还针对OCR扫描产生的个别字识别错误、漏识别等问题给出人工修正和理解提醒并补充了藜麦种类、原产地、耐旱耐寒等背景知识有助于结合真实上下文完成问答系统部署与调试。1. 大模型面试必考RAG 问答应用到底怎么落地RAG检索增强生成现在是大模型应用面试里出现频率最高的题目之一但很多人背得住原理一问到“检索链路怎么搭、chunk 怎么切、向量库怎么选、多轮对话怎么处理”就露怯。这份基于 LangChain 的 RAG 问答应用实战资料用百度百科的藜麦词条模拟企业私域数据完整走了一遍从文本加载、分割、Embedding 入库到检索问答的闭环。它适合两类人一是准备大模型岗位面试、需要把 RAG 流程讲出细节的求职者二是刚接触 LangChain、想用最短路径跑通一个带界面的问答应用的开发者。读完你不仅能复现还能知道每个环节的选型理由和踩坑点。2. 环境搭建先把版本对齐再谈跑通2.1 为什么版本必须严格对齐这份资料在环境上给得很明确CUDA 11.7、Python 3.10、PyTorch 1.13.1cu117再加 LangChain 全家桶。很多人在 RAG 项目上翻车第一关就死在版本不一致——比如 Python 3.12 装不上某些旧版依赖或者 CUDA 版本和 PyTorch 不匹配导致 embedding 模型无法调用 GPU。我的建议是严格按照资料给的版本建虚拟环境不要用系统全局 Python。conda 环境的好处是隔离干净坏了直接删掉重建不会污染其他项目。macOS 用户尤其注意m3e-base 模型在 Apple Silicon 上跑 CPU 推理也能用但 PyTorch 版本要选对应的 arm64 版本否则会报 Illegal instruction 错误。# 创建独立环境Python 版本必须 3.10 conda create -n py310_chat python3.10 # 激活环境Windows 去掉 source直接用 activate py310_chat source activate py310_chat这里有个细节资料里用的是source activate这是 Linux/macOS 的写法。Windows 上直接conda activate py310_chat就行。创建完环境后检查一下 Python 版本确认无误再装依赖。2.2 依赖安装的坑与顺序依赖清单里有几个关键包langchain、langchain_wenxin、chromadb、sentence_transformers、datasets。注意langchain_wenxin是百度的文心 LangChain 扩展包不是官方核心包需要单独安装。pip install datasets langchain sentence_transformers tqdm chromadb langchain_wenxin我实际装的时候遇到过两个问题。第一个是chromadb在 Python 3.10 下会拉取onnxruntime如果网络不好容易超时建议用国内镜像源。第二个是langchain版本更新很快资料里用的是旧版 API比如from langchain.document_loaders import TextLoader如果装了最新的 0.1.x 以上版本部分接口已经迁移到langchain_community子包下代码需要适配。我的建议是装完依赖后锁定版本号# 锁定版本避免接口变化 pip install langchain0.0.350 langchain_wenxin0.0.6 chromadb0.4.22提示如果后面执行代码时出现ModuleNotFoundError: No module named langchain.embeddings之类的报错说明版本太新降级回 0.0.3xx 系列即可。3. 数据加载与分割RAG 效果的第一道关口3.1 文本加载从百度百科到本地文件资料选用的是百度百科的藜麦词条这一步的意义在于企业私域数据的原始形态通常就是网页、PDF、Word 文档RAG 的第一步就是把这些非结构化文本变成可处理的纯文本文件。先把百科内容保存为藜.txt然后用 LangChain 的TextLoader加载from langchain.document_loaders import TextLoader loader TextLoader(./藜.txt) documents loader.load() print(documents)TextLoader返回的是一个Document对象列表每个Document包含两个字段page_content文本内容和metadata元数据默认记录文件路径。这一步看起来简单但有个隐藏问题百科网页里包含大量 HTML 标签、参考文献标记如[1]、[5]和无意义字符如\xa0不间断空格。如果直接扔进 Embedding 模型这些噪声会影响向量质量。我的做法是加载后先做一次文本清洗import re # 清洗特殊字符和参考文献标记 cleaned_content re.sub(r\[\d\], , documents[0].page_content) cleaned_content cleaned_content.replace(\xa0, ) documents[0].page_content cleaned_content3.2 文档分割chunk_size128 是怎么定出来的CharacterTextSplitter是 LangChain 最基础的字符分割器按照固定字符长度切分文本。资料里设置chunk_size128, chunk_overlap0意思是每 128 个字符切一个 chunk块与块之间没有重叠。from langchain.text_splitter import CharacterTextSplitter text_splitter CharacterTextSplitter(chunk_size128, chunk_overlap0) documents text_splitter.split_documents(documents)这里要重点说三个参数的实际含义chunk_size128每个块的最大字符长度。128 算是偏小的值适合百科词条这种信息密集、段落间关联不强的文本。如果处理长文档128 会切得太碎语义被截断如果处理短文本chunk 太大又会让向量包含多余噪声。chunk_overlap0块与块之间不重叠。这意味着两个相邻块在边界处可能丢失上下文。比如一句“藜麦不宜重茬”被切成“藜麦不宜”和“重茬”两块检索时单独命中哪一块都得不到完整语义。分割粒度直接决定检索精度chunk 越小召回越精确但上下文越缺失chunk 越大上下文保留越好但噪声越多。实际做企业知识库时我更推荐先按段落切RecursiveCharacterTextSplitter再对超长段落按句子切chunk_overlap 设置在 2050 之间这样能兼顾语义完整性和检索精度。# 进阶分割方案递归字符分割器 from langchain.text_splitter import RecursiveCharacterTextSplitter text_splitter RecursiveCharacterTextSplitter( chunk_size200, chunk_overlap50, separators[\n\n, \n, 。, , ] ) documents text_splitter.split_documents(documents)RecursiveCharacterTextSplitter会优先按separators里的分隔符切如果切出来的块还太长再降级按下一级分隔符切。这样能最大程度保证一个 chunk 内部是语义完整的句子或段落。4. 向量化与入库m3e-base 和 Chroma 的选型逻辑4.1 Embedding 模型选择为什么用 m3e-base资料里选的 embedding 模型是moka-ai/m3e-base这是一个开源的中文文本向量模型最大输入长度 512 token输出 768 维向量。用它而不是 OpenAI 的 text-embedding-ada-002 的原因很简单中文效果好、免费、数据不出域。RAG 场景里embedding 模型决定了“语义相似”的上限检索阶段只是在这个向量空间里做近邻查找。from langchain.embeddings import HuggingFaceBgeEmbeddings model_name moka-ai/m3e-base model_kwargs {device: cpu} encode_kwargs {normalize_embeddings: True} embedding HuggingFaceBgeEmbeddings( model_namemodel_name, model_kwargsmodel_kwargs, encode_kwargsencode_kwargs, query_instruction为文本生成向量表示用于文本检索 )几个参数说清楚devicecpu强制在 CPU 上跑向量化。CUDA 11.7 环境虽然支持 GPU 推理但 m3e-base 模型很小约 400MBCPU 跑批量向量化也就几十秒不值得占显存。normalize_embeddingsTrue对向量做 L2 归一化。归一化后向量内积等价于余弦相似度这在检索时是必要操作不然相似度分数会受向量长度干扰。query_instruction为文本生成向量表示用于文本检索这是 m3e 系列模型的查询指令。核心逻辑是文档入库和查询检索要用同一个指令否则 query 向量和 document 向量不在同一语义空间检索效果会明显下降。这是最容易踩的坑之一。4.2 Chroma 入库与相似度检索Chroma 是一个轻量级向量数据库支持嵌入 Python 进程运行不需要单独起服务。对于 demo 和中小规模知识库几万条 chunk完全够用企业级场景再换 Milvus 或 Elasticsearch 也不迟。from langchain.vectorstores import Chroma # 将文档向量化并写入 Chroma db Chroma.from_documents(documents, embedding) # 执行相似度检索 results db.similarity_search(藜一般在几月播种, k4) for doc in results: print(doc.page_content)s similarity_search默认返回最相似的 4 个文档块k4。这一步的本质是把 query 向量化然后在 Chroma 里做暴力最近邻搜索。k值是个调参点k 太小可能漏掉相关文档k 太大则会把不相关内容塞进 prompt稀释大模型的注意力。我的经验是初始 k4根据回答质量逐步调最多不超过 6。这里要提醒一个关键点Chroma.from_documents默认在内存里建库进程退出后数据就没了。如果需要持久化要指定persist_directorydb Chroma.from_documents( documents, embedding, persist_directory./chroma_db ) # 下次启动时直接从磁盘加载 db Chroma(persist_directory./chroma_db, embedding_functionembedding)注意重新加载时embedding_function参数名和第一次调用时不同第一次是embedding之后是embedding_function。这就是 LangChain 里典型的“同一个东西两个叫法”的坑。5. 构建问答链Prompt 设计是 RAG 的灵魂5.1 用 Wenxin 作为大模型底座资料里的大模型用的是百度文心ernie-bot通过langchain_wenxin扩展包接入。这里需要先到百度千帆平台申请 API Key 和 Secret Key。from langchain_wenxin.llms import Wenxin llm Wenxin( modelernie-bot, baidu_api_key你的API Key, baidu_secret_key你的Secret Key )选择文心而不是 OpenAI 的原因通常有两个一是国内网络环境访问 OpenAI 不稳定二是企业合规和数据安全要求数据不出域。LangChain 的抽象层设计让 LLM 可以自由替换上面的代码换成ChatOpenAI或ChatGLM只需改动几行。5.2 多轮对话的记忆链ConversationalRetrievalChain资料用了ConversationalRetrievalChain而不是基本的RetrievalQA核心差异在于它支持多轮对话记忆。流程是先利用 memory 里的历史对话把用户当前提问改写成独立问题再做检索最后注入 prompt 让大模型回答。from langchain.memory import ConversationBufferMemory from langchain.chains import ConversationalRetrievalChain retriever db.as_retriever() memory ConversationBufferMemory( memory_keychat_history, return_messagesTrue, output_keyanswer ) qa ConversationalRetrievalChain.from_llm( llmllm, retrieverretriever, memorymemory ) result qa({question: 藜怎么防治虫害}) print(result[answer])这里有个非常隐蔽的坑如果不设output_keyanswer运行时会报Missing some input keys: {chat_history}之类的错误。原因是ConversationBufferMemory默认读取输出中的output键而ConversationalRetrievalChain的输出键叫answer两边对不上。我在第一次跑的时候就栽在这里报错信息还不直观得去翻源码才定位到。获取的result是一个字典里面包含question、chat_history和answer三个键。chat_history里的对话会累积这就是为什么它能回答“那病害呢”这种省略了主语的追问。5.3 Prompt 模板约束大模型“只读背景知识”RAG 的翻车现场里最常见的是大模型不遵守“只根据检索内容回答”的指令自己凭训练时的知识脑补。资料的 prompt 设计给出了一个可用的约束模板template 【任务描述】 请根据用户输入的上下文回答问题并遵守回答要求。 【背景知识】 {context} 【回答要求】 - 你需要严格根据背景知识的内容回答禁止根据常识和已知信息回答问题。 - 对于不知道的信息直接回答“未找到相关答案” ----------- {question} 这段模板有几个设计要点值得在面试里讲出来明确划分角色用“任务描述”“背景知识”“回答要求”三段式让模型清楚哪里是检索来的事实、哪里是行为约束。负面指令约束“禁止根据常识回答”这个否定式指令非常关键它比“请根据背景知识回答”这种正面指令效果强得多。兜底答案“未找到相关答案”给模型一条退路防止它编造答案。这在企业客服场景里尤其重要——宁可说不知道也不能胡说。6. 常见问题排查从报错到回答质量的完整调优6.1 三个必踩的坑坑一安装依赖后 import 报错现象from langchain.embeddings import HuggingFaceBgeEmbeddings执行时提示ModuleNotFoundError。原因大多是新版 LangChain 把 embeddings 模块拆到了langchain_community。解决了两个思路一是降级pip install langchain0.0.350二是改 import 路径from langchain_community.embeddings import HuggingFaceBgeEmbeddings。我建议用后者不用动其他代码。坑二query_instruction 与文档向量化指令不一致现象检索出来的结果语义牛头不对马嘴。原因很玄学文档入库时如果没传query_instruction而检索时传了两者prompt格式不一致导致向量空间偏移。解决方法是入库和检索用同一个 embedding 实例——也就是同一个HuggingFaceBgeEmbeddings对象不要各建各的。坑三ConversationalRetrievalChain 报 chat_history 键缺失现象qa({question: ...})时抛Input should be a dictionary or an instance of XXX。原因是 memory 的output_key没设对。解决在ConversationBufferMemory里加上output_keyanswer并设置return_messagesTrue。6.2 回答质量不行按这个顺序排查如果系统跑通了但答得不好我一般按这个顺序排查# 1. 先看检索结果对不对 python -c import chromadb; print(ok)先用db.similarity_search(你的问题, k4)打印检索到的 chunk。如果检索出的内容和问题毫不相关说明是 embedding 或分割的问题不是大模型的问题。这一步能把问题定位在前半程还是后半程。如果检索结果相关但大模型回答还是不对就要看 prompt。我调试时的习惯是先把{context}和{question}的最终拼接结果打印出来人工看一眼模型到底收到了什么。很多时候答案是 chunk 里的但 prompt 结构乱了模型没有识别出哪部分是“背景知识”。6.3 进阶配置多轮对话压缩与文档融合资料最后提供了一个“高级用法”核心是拆掉ConversationalRetrievalChain的默认内置组件换成自己定义的combine_docs_chain和question_generator。这相当于把黑匣子拆开两个环节都换成可控的实现。from langchain.chains import LLMChain, StuffDocumentsChain from langchain.prompts import PromptTemplate, ChatPromptTemplate from langchain.prompts.chat import SystemMessagePromptTemplate, HumanMessagePromptTemplate # 生成独立问题的 prompt qa_condense_template Given the following conversation and a follow up question, rephrase the follow up question to be a standalone question. Chat History: {chat_history} Follow Up Input: {question} Standalone question: q_gen_chain LLMChain( llmllm, promptPromptTemplate.from_template(qa_condense_template) ) # 用于融合多文档的 prompt qa_template 请根据背景知识回答问题禁止根据常识回答。 背景知识 {context} 问题{question} messages [ SystemMessagePromptTemplate.from_template(qa_template), HumanMessagePromptTemplate.from_template({question}) ] prompt ChatPromptTemplate.from_messages(messages) llm_chain LLMChain(llmllm, promptprompt) combine_docs_chain StuffDocumentsChain( llm_chainllm_chain, document_separator\n\n, document_variable_namecontext ) # 组装高级版 QA qa ConversationalRetrievalChain( combine_docs_chaincombine_docs_chain, question_generatorq_gen_chain, return_source_documentsTrue, return_generated_questionTrue, retrieverretriever ) result qa({question: 藜麦怎么防治虫害, chat_history: []}) print(回答:, result[answer]) print(引用来源:, result[source_documents])这段代码里最值得学习的是StuffDocumentsChain的两个参数document_separator\n\n多个文档拼接时用空行分隔避免不同 chunk 的文字粘连在一起语义边界清晰。document_variable_namecontext这个变量名必须和 prompt 模板里的{context}占位符一致。不一致的话模板里的{context}不会被填充模型收到的就是一个残缺的 prompt——这个问题特别隐蔽因为不报错只是回答质量差。从那段经历里我总结出一个习惯凡是涉及 LangChain 的链式结构我再也不把“默认行为”当黑匣子用。每次跑通一个 demo都会强制自己把chain.llm_chain.prompt.template和实际传进去的变量打印出来亲眼确认模型接收到的是什么。这么做虽然啰嗦但能省掉大量“为什么回答不对”的排查时间。如果这份资料也能帮你把 RAG 这条链路真正跑通理解每个环节的变量和边界那它就是一份合格的实战笔记——希望帮到你。本文还有配套的精品资源点击获取
返回列表