
简介这份PDF面向大模型与人工智能方向的学习者及面试备考者围绕LangChain框架下的RAG问答应用展开实战讲解帮助读者理解检索增强生成从数据准备到问答落地的完整链路。资源包内仅含1个PDF文件约390KB内容以图文与代码示例为主便于在电脑端阅读和对照练习。目前已有281人学习下载适合作为大模型八股文面试的补充材料。文中以百度百科藜麦数据模拟私域语料依次演示环境搭建、本地文档加载、字符分割、向量化入库与问答实现并给出CUDA 11.7、Python 3.10、PyTorch 1.13.1及m3e-base、Chroma等版本与工具选型同时穿插OCR识别误差处理、藜麦背景知识等细节可帮助读者掌握数据构建、模型调用与系统部署的实战技巧。1. 从一份藜麦百科数据说起这套 LangChain RAG 实战到底能跑通什么很多人第一次接触 RAG是被“大模型能回答私有数据”这句话吸引的结果一上手就卡在环境、依赖、向量库、Prompt 这四道坎上。这份《基于 langchain RAG 问答应用实战》给了一条最小可复现路径用百度百科“藜麦”词条当私域数据走完加载、分割、向量化、入库、检索、生成全链路。它不追求生产级性能而是让你在单机 CPU 上把 RAG 的每个环节亲手摸一遍。适合刚入门大模型应用、想搞懂 LangChain 和 Chroma 怎么串起来的人也适合面试前需要把 RAG 流程讲清楚的人。代码量不大但坑点密度不低下面按实际落地顺序拆开说。2. 环境搭建与依赖选型CUDA 11.7 和 Python 3.10 不是随便写的2.1 为什么锁定 Python 3.10 和 pytorch 1.13.1cu117这套代码的依赖链里langchain、sentence_transformers、chromadb对 Python 版本和 PyTorch 版本有隐性约束。Python 3.10 是当前大模型工具链兼容性最好的版本之一3.11 以上有些包还没跟上3.8 又太老。PyTorch 1.13.1cu117 对应 CUDA 11.7如果你机器上没有 GPUCPU 版本也能跑只是 embedding 阶段会慢一些。原文明确写了这三个版本号不是随便选的是踩过兼容性坑之后定下来的组合。常见做法是用 conda 隔离环境避免和系统 Python 打架conda create -n py310_chat python3.10 source activate py310_chat第一行创建名为py310_chat的虚拟环境指定 Python 3.10第二行激活它。Windows 下激活命令是conda activate py310_chat原文用的是source activate在 Linux/macOS 上等效。环境建好后所有 pip 安装都落在这个隔离环境里后面出问题直接删环境重来不会污染主环境。2.2 依赖安装与版本冲突排查原文给的安装命令是pip install datasets langchain sentence_transformers tqdm chromadb langchain_wenxin这里有几个点值得展开。datasets是 HuggingFace 的数据集库虽然这个项目没直接用它加载数据但sentence_transformers会间接依赖它。langchain是主框架sentence_transformers提供 embedding 模型加载能力chromadb是向量数据库langchain_wenxin是 LangChain 对接文心一言的适配层。tqdm是进度条调试时看 embedding 进度用。实际安装时最容易翻车的是chromadb和langchain的版本匹配。LangChain 迭代快不同小版本之间 API 可能不兼容。如果你装完跑from langchain.vectorstores import Chroma报ImportError大概率是 LangChain 版本太新把旧路径改了。稳妥做法是装一个已知能跑的版本组合比如langchain0.0.xxx配合chromadb0.3.xx。原文没写具体版本号但这是实际落地时绕不开的一步。提示如果 pip 安装chromadb时卡在编译hnswlib先确认有没有装 C 编译工具链。Linux 下apt install build-essentialWindows 下装 Visual Studio Build Tools。2.3 文心一言 API 密钥的获取与配置代码里用到了Wenxin这个 LLM 封装需要baidu_api_key和baidu_secret_key。这两个值从百度智能云千帆平台申请创建应用后能拿到。原文代码里写的是占位符llm Wenxin(modelernie-bot, baidu_api_keybaidu_api_key, baidu_secret_keybaidu_secret_key)实际跑的时候要替换成真实值。modelernie-bot指定用文心一言的 ERNIE-Bot 模型。如果你没有百度智能云的账号这一步会卡住整个问答链路就跑不通。替代方案是换成本地部署的模型比如用ChatGLM或Qwen的 LangChain 封装但那就偏离原文的最小示例了。3. 数据加载与向量化入库从 txt 文件到 Chroma 的完整链路3.1 TextLoader 加载本地文件的编码坑原文把藜麦百科内容保存为藜.txt然后用TextLoader加载from langchain.document_loaders import TextLoader loader TextLoader(./藜.txt) documents loader.load()TextLoader默认用 UTF-8 编码读取。如果 txt 文件是 GBK 编码Windows 下记事本默认可能是 GBK加载后会乱码或者直接抛UnicodeDecodeError。解决办法是在TextLoader里显式指定编码loader TextLoader(./藜.txt, encodingutf-8)如果文件确实是 GBK就改成encodinggbk。加载后的documents是一个Document对象列表每个对象有page_content和metadata两个属性。page_content是文本内容metadata里存了source路径。这个 metadata 在后面检索溯源时有用能告诉你答案是从哪个文件来的。3.2 CharacterTextSplitter 的 chunk_size 与 chunk_overlap 怎么定原文用固定字符长度分割chunk_size128chunk_overlap0from langchain.text_splitter import CharacterTextSplitter text_splitter CharacterTextSplitter(chunk_size128, chunk_overlap0) documents text_splitter.split_documents(documents)chunk_size128意味着每段最多 128 个字符。这个值偏小适合演示因为藜麦百科的段落本身就不长。实际业务里128 太碎检索时可能召回不完整语义。常见做法是设 500 到 1000具体看文档平均段落长度。chunk_overlap0表示相邻 chunk 不重叠这会导致跨 chunk 的语义被切断。比如一句话正好被切在两段之间检索时可能两边都召回不全。一般建议设chunk_overlap为chunk_size的 10% 到 20%比如chunk_size500, chunk_overlap50。CharacterTextSplitter的分割逻辑是按分隔符切默认分隔符是\n\n。如果文本里没有双换行它会退化成按单字符切这时候chunk_size就不准了。更可控的是RecursiveCharacterTextSplitter它按[\n\n, \n, , ]的顺序递归尝试尽量保持段落完整。原文用CharacterTextSplitter是为了简化但实际项目里我一般会换成递归分割器。3.3 m3e-base embedding 模型加载与 normalize_embeddings 的作用向量化部分用的是HuggingFaceBgeEmbeddings加载moka-ai/m3e-basefrom 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为文本生成向量表示用于文本检索 )m3e-base是一个中文 embedding 模型对中文语义相似度表现不错模型体积也不大CPU 上能跑。model_kwargs{device: cpu}指定用 CPU 推理如果你有 GPU 可以改成cuda。encode_kwargs{normalize_embeddings: True}表示对输出的向量做 L2 归一化这样余弦相似度计算就等价于点积Chroma 内部检索时效率更高。query_instruction这个参数容易被忽略。m3e 系列模型在训练时用了指令前缀检索时 query 和 document 的编码方式略有不同。HuggingFaceBgeEmbeddings会把query_instruction拼在 query 前面再编码document 则不加。如果你手动调model.encode()而不加指令检索效果会下降。这是 m3e 的隐藏用法原文写出来了但没解释为什么。3.4 Chroma.from_documents 入库与 similarity_search 验证入库就一行from langchain.vectorstores import Chroma db Chroma.from_documents(documents, embedding)from_documents会先对每个 document 调 embedding 模型生成向量然后写入 Chroma 的默认集合。Chroma 默认是内存模式进程结束数据就没了。如果要持久化得传persist_directory参数db Chroma.from_documents(documents, embedding, persist_directory./chroma_db) db.persist()检索验证db.similarity_search(藜一般在几月播种)这会返回与 query 最相似的若干 document。默认返回 4 个可以通过k参数调整。如果返回结果不相关先检查 embedding 模型是否加载正确再检查 chunk 分割是否把关键信息切碎了。检索质量差后面 LLM 生成再强也救不回来。4. Prompt 设计与 ConversationalRetrievalChain 组装多轮对话怎么不丢上下文4.1 Prompt 模板里的“禁止根据常识回答”为什么重要原文的 prompt 模板template 【任务描述】 请根据用户输入的上下文回答问题并遵守回答要求。 【背景知识】 {{context}} 【回答要求】 - 你需要严格根据背景知识的内容回答禁止根据常识和已知信息回答问题。 - 对于不知道的信息直接回答“未找到相关答案” ----------- {question} 这个模板的核心约束是“禁止根据常识回答”。RAG 场景下LLM 最大的问题是它会“编”——检索没召回到的内容它用自己的知识补上导致答案看似合理但和你的私域数据不符。加上这条约束后模型会倾向于只从context里找答案。{{context}}是双重花括号因为后面要用PromptTemplate格式化单花括号会被当成变量占位符。“未找到相关答案”这个兜底话术也关键。没有它模型在 context 为空时可能胡编。有了它至少你能知道检索没命中而不是被假答案骗过去。4.2 ConversationBufferMemory 与 chat_history 的传递机制from langchain.memory import ConversationBufferMemory memory ConversationBufferMemory(memory_keychat_history, return_messagesTrue)ConversationBufferMemory把每轮对话的 human 和 ai 消息都存下来。memory_keychat_history指定在 chain 里用哪个变量名引用历史记录。return_messagesTrue表示返回HumanMessage和AIMessage对象列表而不是纯字符串。这个设置和后面的ConversationalRetrievalChain配合使用chain 内部会把历史消息和当前问题一起处理。ConversationBufferMemory的问题是它会无限增长对话轮次多了之后 token 消耗爆炸。生产环境一般用ConversationBufferWindowMemory限制保留最近 k 轮或者ConversationSummaryMemory把历史压缩成摘要。原文用 buffer 是为了演示简单实际用的时候要换。4.3 ConversationalRetrievalChain 的 question_generator 与 combine_docs_chain基础版 chain 构建from langchain.chains import ConversationalRetrievalChain retriever db.as_retriever() qa ConversationalRetrievalChain.from_llm(llm, retriever, memorymemory) qa({question: 藜怎么防治虫害})db.as_retriever()把 Chroma 向量库转成 retriever 接口chain 内部会用它做相似度检索。from_llm是快捷构建方法它内部自动创建了question_generator和combine_docs_chain。question_generator的作用是把历史对话和当前问题融合成一个独立问题。比如你先问“藜麦是什么”再问“它怎么防治虫害”question_generator会把第二个问题改写成“藜麦怎么防治虫害”这样检索时不会因为指代不明而召回错误内容。高级用法里手动构建了这两个组件from langchain.chains import StuffDocumentsChain from langchain.chains.qa_with_sources import load_qa_with_sources_chain combine_docs_chain StuffDocumentsChain( llm_chainllm_chain, document_separator\n\n, document_variable_namecontext, ) q_gen_chain LLMChain(llmllm, promptPromptTemplate.from_template(qa_condense_template)) qa ConversationalRetrievalChain( combine_docs_chaincombine_docs_chain, question_generatorq_gen_chain, return_source_documentsTrue, return_generated_questionTrue, retrieverretriever )StuffDocumentsChain是最简单的文档合并策略把所有检索到的 document 拼成一个长字符串塞进 prompt。document_separator\n\n指定拼接分隔符。return_source_documentsTrue让 chain 返回检索到的原始文档方便溯源。return_generated_questionTrue返回question_generator改写后的问题调试时能看出指代消解是否正确。StuffDocumentsChain的缺点是文档多了会超 token 限制。替代方案有MapReduceDocumentsChain、RefineDocumentsChain但实现复杂度更高。原文用 stuff 是因为藜麦数据量小实际项目里如果检索返回 10 个以上 chunk就得考虑换策略。5. 避坑与排查这套 RAG 示例跑不通时先看这五条5.1 现象ImportError: cannot import name Chroma from langchain.vectorstores原因LangChain 版本更新后Chroma的导入路径变了或者chromadb没装成功。解决先pip show langchain看版本如果是最新版尝试降级到langchain0.0.200左右的版本。同时确认pip show chromadb有输出没有的话重装chromadb。5.2 现象embedding 阶段卡住不动CPU 占用 100%原因m3e-base模型第一次加载要从 HuggingFace 下载权重国内网络可能超时。解决设置镜像源export HF_ENDPOINThttps://hf-mirror.com或者提前把模型下载到本地用model_name指向本地路径。另外devicecpu时 embedding 确实慢数据量大就换 GPU。5.3 现象检索返回结果和问题完全不相关原因chunk_size128太小关键信息被切碎或者normalize_embeddings没开相似度计算方式不对。解决把chunk_size调到 500 左右chunk_overlap设 50确认encode_kwargs{normalize_embeddings: True}已设置。如果还不行换RecursiveCharacterTextSplitter试试。5.4 现象文心一言 API 报AuthenticationError或Invalid API key原因baidu_api_key和baidu_secret_key填错或者千帆应用没开通 ERNIE-Bot 权限。解决登录百度智能云控制台确认应用已创建且模型已授权。密钥复制时注意不要带空格。如果用的是环境变量确认os.environ里能读到。5.5 现象多轮对话时第二轮回答丢失上下文原因ConversationBufferMemory的memory_key和 chain 期望的变量名不一致或者return_messages没设True。解决确认memory_keychat_historyreturn_messagesTrue。如果手动构建 chain检查question_generator的 prompt 里有没有{chat_history}占位符。6. 进阶技巧用 return_source_documents 做答案溯源与检索质量评估这套示例跑通之后最有价值的进阶动作是打开return_source_documentsTrue把每次回答引用的原始 chunk 打出来看。我一般会加一段这样的代码result qa({question: 藜麦的播种时间是什么时候, chat_history: []}) print(生成的问题, result.get(generated_question)) print(答案, result[answer]) for i, doc in enumerate(result[source_documents]): print(f--- 来源 {i1} ---) print(doc.page_content[:200]) print(metadata:, doc.metadata)generated_question能看出question_generator有没有正确改写问题。如果改写后的问题偏离原意检索肯定不准。source_documents是检索到的原始文档检查它们是否真的包含答案。如果答案在source_documents里但 LLM 没生成对那是 prompt 或 LLM 的问题如果source_documents里根本没有答案那是检索的问题得回去调 chunk_size 或换 embedding 模型。我还会做一个简单的命中率统计准备 10 到 20 个问题每个问题人工标注正确答案所在的 chunk然后跑一遍看检索 top-4 里有没有包含正确 chunk。这个指标叫 hit rate是 RAG 系统最直观的评估方式。如果 hit rate 低于 70%优先优化检索侧而不是换更大的 LLM。还有一个容易忽略的点metadata里的source字段。如果你后续要支持多文件检索可以在TextLoader加载时给每个文件打上不同的 metadata 标签检索时按标签过滤。Chroma 的similarity_search支持filter参数比如db.similarity_search(query, filter{source: ./藜.txt})。这样就能实现按文件范围检索避免不同来源的数据互相干扰。从那以后我每次搭 RAG 原型都强制先跑一遍 hit rate 统计再动 LLM 和 prompt。检索没调好后面全是白费功夫。希望帮到你。本文还有配套的精品资源点击获取