简介:这份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.1+cu117
这套代码的依赖链里,langchain、sentence_transformers、chromadb对 Python 版本和 PyTorch 版本有隐性约束。Python 3.10 是当前大模型工具链兼容性最好的版本之一,3.11 以上有些包还没跟上,3.8 又太老。PyTorch 1.13.1+cu117 对应 CUDA 11.7,如果你机器上没有 GPU,CPU 版本也能跑,只是 embedding 阶段会慢一些。原文明确写了这三个版本号,不是随便选的,是踩过兼容性坑之后定下来的组合。
常见做法是用 conda 隔离环境,避免和系统 Python 打架:
conda create -n py310_chat python=3.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 版本太新,把旧路径改了。稳妥做法是装一个已知能跑的版本组合,比如langchain==0.0.xxx配合chromadb==0.3.xx。原文没写具体版本号,但这是实际落地时绕不开的一步。
提示:如果 pip 安装
chromadb时卡在编译hnswlib,先确认有没有装 C++ 编译工具链。Linux 下apt install build-essential,Windows 下装 Visual Studio Build Tools。
2.3 文心一言 API 密钥的获取与配置
代码里用到了Wenxin这个 LLM 封装,需要baidu_api_key和baidu_secret_key。这两个值从百度智能云千帆平台申请,创建应用后能拿到。原文代码里写的是占位符:
llm = Wenxin(model="ernie-bot", baidu_api_key="baidu_api_key", baidu_secret_key="baidu_secret_key")实际跑的时候要替换成真实值。model="ernie-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", encoding="utf-8")如果文件确实是 GBK,就改成encoding="gbk"。加载后的documents是一个Document对象列表,每个对象有page_content和metadata两个属性。page_content是文本内容,metadata里存了source路径。这个 metadata 在后面检索溯源时有用,能告诉你答案是从哪个文件来的。
3.2 CharacterTextSplitter 的 chunk_size 与 chunk_overlap 怎么定
原文用固定字符长度分割,chunk_size=128,chunk_overlap=0:
from langchain.text_splitter import CharacterTextSplitter text_splitter = CharacterTextSplitter(chunk_size=128, chunk_overlap=0) documents = text_splitter.split_documents(documents)chunk_size=128意味着每段最多 128 个字符。这个值偏小,适合演示,因为藜麦百科的段落本身就不长。实际业务里,128 太碎,检索时可能召回不完整语义。常见做法是设 500 到 1000,具体看文档平均段落长度。chunk_overlap=0表示相邻 chunk 不重叠,这会导致跨 chunk 的语义被切断。比如一句话正好被切在两段之间,检索时可能两边都召回不全。一般建议设chunk_overlap为chunk_size的 10% 到 20%,比如chunk_size=500, chunk_overlap=50。
CharacterTextSplitter的分割逻辑是按分隔符切,默认分隔符是\n\n。如果文本里没有双换行,它会退化成按单字符切,这时候chunk_size就不准了。更可控的是RecursiveCharacterTextSplitter,它按["\n\n", "\n", " ", ""]的顺序递归尝试,尽量保持段落完整。原文用CharacterTextSplitter是为了简化,但实际项目里我一般会换成递归分割器。
3.3 m3e-base embedding 模型加载与 normalize_embeddings 的作用
向量化部分用的是HuggingFaceBgeEmbeddings加载moka-ai/m3e-base:
from langchain.embeddings import HuggingFaceBgeEmbeddings model_name = "moka-ai/m3e-base" model_kwargs = {'device': 'cpu'} encode_kwargs = {'normalize_embeddings': True} embedding = HuggingFaceBgeEmbeddings( model_name=model_name, model_kwargs=model_kwargs, encode_kwargs=encode_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_key="chat_history", return_messages=True)ConversationBufferMemory把每轮对话的 human 和 ai 消息都存下来。memory_key="chat_history"指定在 chain 里用哪个变量名引用历史记录。return_messages=True表示返回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, memory=memory) 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_chain=llm_chain, document_separator="\n\n", document_variable_name="context", ) q_gen_chain = LLMChain(llm=llm, prompt=PromptTemplate.from_template(qa_condense_template)) qa = ConversationalRetrievalChain( combine_docs_chain=combine_docs_chain, question_generator=q_gen_chain, return_source_documents=True, return_generated_question=True, retriever=retriever )StuffDocumentsChain是最简单的文档合并策略,把所有检索到的 document 拼成一个长字符串塞进 prompt。document_separator="\n\n"指定拼接分隔符。return_source_documents=True让 chain 返回检索到的原始文档,方便溯源。return_generated_question=True返回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看版本,如果是最新版,尝试降级到langchain==0.0.200左右的版本。同时确认pip show chromadb有输出,没有的话重装chromadb。
5.2 现象:embedding 阶段卡住不动,CPU 占用 100%
原因:m3e-base模型第一次加载要从 HuggingFace 下载权重,国内网络可能超时。解决:设置镜像源export HF_ENDPOINT=https://hf-mirror.com,或者提前把模型下载到本地,用model_name指向本地路径。另外device='cpu'时 embedding 确实慢,数据量大就换 GPU。
5.3 现象:检索返回结果和问题完全不相关
原因:chunk_size=128太小,关键信息被切碎;或者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_key="chat_history",return_messages=True。如果手动构建 chain,检查question_generator的 prompt 里有没有{chat_history}占位符。
6. 进阶技巧:用 return_source_documents 做答案溯源与检索质量评估
这套示例跑通之后,最有价值的进阶动作是打开return_source_documents=True,把每次回答引用的原始 chunk 打出来看。我一般会加一段这样的代码:
result = qa({"question": "藜麦的播种时间是什么时候?", "chat_history": []}) print("生成的问题:", result.get("generated_question")) print("答案:", result["answer"]) for i, doc in enumerate(result["source_documents"]): print(f"--- 来源 {i+1} ---") 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。检索没调好,后面全是白费功夫。希望帮到你。
本文还有配套的精品资源,点击获取