做应用开发总要面对一个尴尬大模型看起来什么都懂但一旦问到你们公司的报销制度、项目记录、售后口径它就露馅了——要么不知道要么编一个像模像样的答案。这也是这几年大模型落地时最常被追问的问题。RAG检索增强生成是目前公认最稳的一条解决路径而LangChain又是把这条路铺平的那套编排框架。这篇文章我就从环境配置开始一路走到最终推理调用完整走一遍LangChain RAG的工程链路文档怎么加载、文本怎么切分、向量怎么建、命中片段怎么召回、Prompt怎么拼、模型怎么回答以及过程中一定要避开的那些坑。适合谁看有Python基础想在企业内部知识库、私有文档问答、客服辅助这类场景里快速搭出一个能用的大模型应用。我不会只丢给你几个API拼盘而是把每个环节背后的“为什么”也讲清楚。看完之后你不仅能跑通这份代码还能根据自己手里的文档调整参数遇到问题知道往哪儿查。1. 先理解RAG为何成为必备技能1.1 大模型的短板刚好是RAG的长板大模型本质是一个“根据训练数据预测下一个token”的概率系统。它的知识来自训练语料所以有两个天生缺陷第一知识有时间截止点训练完之后发生的事情它不知道第二对私有领域数据一无所知你公司的SOP、产品说明书、历史工单它从来没看过。更麻烦的是大模型在不确定时倾向于“编”一个看起来合理的回答这就是大家常说的幻觉。RAG的思路很直接既然模型不知道那就先把相关资料找出来喂给它让它“开卷考试”。模型不需要记住你的制度文档只需要根据临时给的资料片段去做语言组织和推理。这样知识来源变成了外部向量索引可以随时更新、权限可控也顺带缓解了幻觉问题。你可以把它理解成一个博学但记忆有限的顾问RAG就是在他手边放了几本指定的参考书回答问题必须翻书不能凭印象瞎说。1.2 一条标准RAG流水线加载、切分、向量化、检索、生成网上关于RAG的流程图很多核心其实就五个环节。加载把PDF、Word、Markdown、数据库导出文件等原始资料读入为纯文本。切分把长文档切成若干短片段因为模型上下文窗口有限检索也是以片段为单位的。向量化用嵌入模型把每个片段转成一维向量把文本语义变成可计算的坐标。检索拿到用户问题后把问题也转成向量在向量库中找最相近的片段。生成把检索到的片段作为上下文连同用户问题一起交给大模型让它生成最终答案。前三个环节通常是一次性离线完成的可以理解为“建索引”后两个环节是每次问答都要跑的在线流程。初学者最容易忽视的是切分和检索质量总觉得模型强就行。实际上RAG的上限由检索决定模型只是把检索结果加工成答案。如果你的库里面根本没有正确答案换再大的模型也没用。1.3 LangChain在其中的角色与取舍LangChain不是一个模型也不是一个向量库而是一套连接器与编排工具。它把“加载文档”“调嵌入模型”“读写向量库”“调大模型”这些动作统一成模块提供统一的接口然后用一套管道语法把这些模块串起来。好处很明显你不用自己写嵌入调用、不用自己封装向量库连接代码量少很多原型迭代速度极快。但我也要泼盆冷水。LangChain版本迭代很快接口变动频繁网上很多旧教程代码已经跑不通。如果你只做一个非常简单的问答服务徒手写加载、检索、拼Prompt也不过几百行代码不一定非得引入框架。LangChain的最大价值在于复杂链路需要多步工具调用、记忆、路由、批量处理时它的模块化优势才真正体现出来。这篇教程采用LangChain是因为它覆盖面广、资料多、方便你后续扩展但请记住框架是工具不是目的。2. 环境配置先搭一个不打架的Python环境2.1 环境规划为什么不能省掉虚拟环境这一步我见过太多人一上来就在系统Python里pip install langchain结果半个月后项目多个依赖互相冲突langchain要这个版本的pydantic另一个库要那个版本最后只能重装系统环境。解决这个问题其实很简单每个项目建一个独立的虚拟环境。Python 3.10以上版本自带venv不需要额外安装。这是我常用的初始化命令mkdir rag-demo cd rag-demo python -m venv .venv source .venv/bin/activate # Windows 下执行 .venv\Scripts\activate python -m pip install --upgrade pip你会在提示符前面看到(.venv)说明已经进入了独立环境。后续所有包都装在这里删除项目时直接删掉整个目录不会污染系统。2.2 安装核心依赖LangChain及周边组件先解释一下要装哪些东西免得你看安装日志时一头雾水。核心依赖分四块LangChain主框架、文本加载与拆分、向量数据库集成、嵌入模型库。目前主流版本是0.2系列包已经拆得比较细需要按需安装。pip install langchain0.2,0.3 pip install langchain-community langchain-text-splitters pip install langchain-chroma chromadb pip install langchain-huggingface pip install pypdf python-dotenvlangchain核心编排框架提供Prompt模板、Chain、LCEL等功能。langchain-community社区维护的文档加载器、模型封装等扩展组件。langchain-text-splitters文本切分器。langchain-chroma和chromadb本地轻量向量数据库及其LangChain适配。langchain-huggingface加载开源嵌入模型的适配器。pypdf用来读PDF文档。python-dotenv读取.env环境变量文件。如果你之前装过旧版LangChain最好先pip uninstall langchain langchain-community langchain-chroma再重新安装避免残留版本干扰。2.3 模型接入API密钥与本地推理两种方式RAG链路里需要两个模型一个是负责生成答案的大语言模型一个是负责文本向量化的嵌入模型。这里先说大模型。目前主流的选择有两种。第一种是调用商业大模型API省心、效果强但需要密钥并且数据会离开本地敏感项目要慎重。第二种是在本机部署开源模型通过本地推理服务暴露一个接口LangChain去调用它。考虑到成本和隐私我建议原型阶段用API正式项目看合规要求决定是否本地化。先建一个.env文件存放配置# 这里填写你的模型参数 MODEL_NAMEyour-model-name MODEL_API_KEYyour-api-key MODEL_API_BASEhttp://localhost:11434再写一个llm.py来统一获取模型实例。为了不让代码绑定某一家服务商我采用一个工厂函数import os from dotenv import load_dotenv load_dotenv() def get_llm(): # 请根据你使用的模型接入方式替换成对应的 LangChain 模型封装类 # 本地推理服务示例使用 langchain_community.llms 中的 Ollama 封装 from langchain_community.llms import Ollama return Ollama( modelos.getenv(MODEL_NAME, your-local-model), temperature0.0, )如果你用的是商业大模型API通常对应的LangChain集成包会提供一个ChatXxx类把api_key和model参数填进去即可。所有的模型封装都遵循LangChain统一的调用接口后续链路代码不用改。2.4 跑通前的极小健康检查套这么多依赖建议先写一个20行的脚本验证环境不要一上来就加载几万字PDF到时候不知道是依赖坏了还是代码错了。# check_env.py from langchain_huggingface import HuggingFaceEmbeddings from langchain_chroma import Chroma from llm import get_llm # 1. 嵌入模型能否加载 embeddings HuggingFaceEmbeddings(model_nameyour-embedding-model) print(embedding ok) # 2. 大模型能否响应 llm get_llm() print(llm.invoke(你好请回复环境正常。)) # 3. 向量库能否创建 db Chroma.from_texts([测试句子], embeddingembeddings) print(chroma ok)跑之前把your-embedding-model替换成你打算使用的开源中文嵌入模型标识。如果这几步都通过环境就算立住了。别小看这个检查它能帮你把“模型的问题”和“代码的问题”分开。3. 数据加工把文档变成能检索的向量3.1 文档加载从 PDF、Word、Markdown 中抽出文本我们以一份模拟的公司内部制度文档为例包含《费用报销管理制度》和《员工手册》。先把PDF放到docs/目录然后用LangChain的加载器读进来。from langchain_community.document_loaders import PyPDFLoader, DirectoryLoader loader DirectoryLoader( docs, glob**/*.pdf, loader_clsPyPDFLoader, ) documents loader.load() print(f加载了 {len(documents)} 个文档片段)每个PDF可能被拆成多个页面对象每个页面是一个Document包含page_content文本内容和metadata来源页码等。注意如果PDF是扫描图片而没有文本层pypdf是读不出内容的那要先接OCR这是另一个话题。Word文档可以用Docx2txtLoaderMarkdown直接用TextLoader套路一样只需要换加载器类。3.2 文本切分chunk_size不是越大越好这一步是整个RAG链路里最考验经验的地方。先看代码from langchain_text_splitters import RecursiveCharacterTextSplitter text_splitter RecursiveCharacterTextSplitter( chunk_size600, chunk_overlap100, separators[\n\n, \n, 。, , , , , ], ) chunks text_splitter.split_documents(documents) print(f切分后得到 {len(chunks)} 个片段)为什么需要切分因为一个PDF可能几十页塞进Prompt会超长而且检索器无法对一个冗长的文档做精细的定位。为什么不能无脑切很小因为片段太小语义不完整检索到的片段缺乏上下文模型也很难组织答案。chunk_size600表示每个片段目标长度约600个字符。这个数不是拍脑袋定的需要看三个因素你的嵌入模型适合多长文本、大模型上下文窗口大小、问题的粒度大小。一般建议中文场景控制在300-800字符。chunk_overlap100表示前后两个片段之间保留100字符的重叠目的是避免一个完整句子或段落刚好被切在边界上导致检索时漏掉关键信息。很多初学者只调chunk_size不看切出来的内容。建议第一次切完后打印几个片段看一眼for i, chunk in enumerate(chunks[:3]): print(f--- chunk {i} ---) print(chunk.page_content)如果发现句子被拦腰截断就调整separators和chunk_overlap。中文文本要把句号、问号、分号都放进去递归切分器会优先按separators的顺序寻找切割点。3.3 嵌入模型与向量库怎么选才不踩坑嵌入模型的任务是把“报销流程”和“费用报销怎么走”这两句话映射到向量空间里让它们的距离比较近。这里最关键的是语言匹配度你的文档是中文就不要用那个默认的英文通用嵌入模型否则检索出来会非常离谱。尽量选择针对中文优化过的开源文本向量模型选择时主要看三个维度语义准确度、向量维度决定存储空间和检索速度、服务化部署成本。下面这段代码加载一个嵌入模型from langchain_huggingface import HuggingFaceEmbeddings embedding_model HuggingFaceEmbeddings( model_nameyour-chinese-embedding-model, encode_kwargs{normalize_embeddings: True}, )normalize_embeddings把向量归一化为单位向量这样计算余弦相似度时数值更稳定。模型首次加载会从网络下载权重建议先手动拉取一次避免在线上环境首次运行超时。向量数据库这边原型阶段我非常推荐Chroma因为它是文件型存储不需要单独部署服务pip安装就能用非常适合做技术验证。生产方式可以根据数据规模换用其他专业向量库但LangChain的接口是统一的后面切换成本很低。选型时主要看是否需要高并发、是否支持混合检索、是否能与已有鉴权体系集成。3.4 建立索引并持久化不能每次问答都重新建库建索引是一次性工作也是整个RAG系统最重要的资产。代码如下from langchain_chroma import Chroma persist_dir data/vectorstore # 第一次创建 vectorstore Chroma.from_documents( documentschunks, embeddingembedding_model, persist_directorypersist_dir, ) print(f向量库创建完成共 {vectorstore._collection.count()} 条向量)建索引时会逐条调用嵌入模型如果文档很多可以先分批嵌入再批量写入避免内存爆炸。Chroma会把元数据和向量持久化到本地目录后续再查询时不需要重新嵌入vectorstore Chroma( persist_directorypersist_dir, embedding_functionembedding_model, )这里有一个我踩过的坑如果切分后的Document数量很大一次性调用from_documents会非常慢而且容易在内存不足时崩掉。稳妥的做法是分批写入300-500个片段为一批写一批刷新一次。另外记得在metadata里保留文档来源。例如for doc in chunks: doc.metadata[source_file] doc.metadata.get(source, unknown)这会对后面的答案溯源很有帮助。4. 推理链路把“检索”接到“生成”上4.1 先验证检索器召回的结果到底靠不靠谱很多人上来就把检索和生成接在一起回答不对也不知道是检索的问题还是模型的问题。我建议先把检索器拿出来单独测。retriever vectorstore.as_retriever( search_typesimilarity, search_kwargs{k: 4}, ) question 部门聚餐的报销标准是多少 docs retriever.invoke(question) for i, doc in enumerate(docs): print(f--- 第{i 1}条 ---) print(doc.page_content) print(来源:, doc.metadata.get(source_file))k4表示召回复4个片段。K值太小容易漏K值太大会把不相关的内容也塞进Prompt干扰模型判断。经验上先设4再根据问题复杂度和文档粒度调。这一步打印出来后你要扮演一个“人工检索器”判断一下这4个片段是不是真的围绕用户问题如果明显不相关别急着调模型回去检查嵌入模型和切分策略。如果想进一步提高召回精度可以试试search_typemmr它会做最大边际相关排序去重效果更好。再专业一点还可以结合关键词检索和向量检索做混合检索但这属于进阶优化原型阶段先不用加。4.2 设计Prompt让模型只说“根据资料”的话检索出来的片段不会自动变成答案它们需要被组装进Prompt。Prompt设计的好与坏直接决定模型是“照着资料说”还是“凭感觉编”。from langchain_core.prompts import ChatPromptTemplate prompt ChatPromptTemplate.from_messages([ (system, 你是一名企业内部文档助手。请只根据以下资料回答问题如果资料中没有相关信息请直接说明‘资料中未找到相关内容’不要编造。), (human, 资料如下\n{context}\n\n问题{question}), ])这个模板把上下文和问题分开让模型明确知道哪些是外部资料哪些是需要回答的问题。我习惯在system里写两层约束第一层是身份与目标第二层是“不知道就直说”的保底指令。注意只靠Prompt不能百分百消除幻觉但对抑制自由发挥非常有效。在生成回答时temperature一定要设置低一些。RAG问答场景要的是稳定和可复现不是创意写作temperature一般设置在0到0.3之间。我用的是0.0。4.3 组装成RAG ChainLCEL的写法与老写法的区别接下来把检索器、Prompt、模型、输出解析串成一条链。LangChain 0.2推荐用LCEL写法就是用|把可运行对象连起来。它是懒执行的内部会处理并行与错误传播。from langchain_core.output_parsers import StrOutputParser from langchain_core.runnables import RunnableParallel, RunnablePassthrough def format_docs(docs): return \n\n.join(doc.page_content for doc in docs) rag_chain ( RunnableParallel( contextretriever | format_docs, questionRunnablePassthrough(), ) | prompt | llm | StrOutputParser() ) answer rag_chain.invoke(部门聚餐的报销标准是多少) print(answer)流程是这样的用户问题先进RunnableParallel一路交给question原样保存另一路传给retriever检索再用format_docs拼成上下文然后prompt把上下文和问题填进模板接着llm生成回答最后StrOutputParser把模型输出转成字符串。老版本有一个封装好的RetrievalQA类内部帮你完成了这些步骤。我为什么不推荐直接用因为封装程度太高你想看中间结果、想改检索逻辑都不方便。LCEL里每一步都是显式的Runnable能很容易插入日志、调试、并行处理长期维护起来清晰很多。如果你喜欢旧写法也可以保留但新项目建议直接学LCEL。4.4 流式输出与答案溯源正式应用必须做的两件事本地工具问答还能接受“转圈后一次性出结果”但面向真实用户的系统体验上一定需要流式输出。LangChain的LCEL天然支持.stream()for chunk in rag_chain.stream(部门聚餐的报销标准是多少): print(chunk, end, flushTrue)流式返回的是增量文本你可以把它推到前端的事件流里。如果还想同时返回引用来源就不能只调rag_chain我们需要把检索结果单独拿出来def ask_with_sources(question: str): contexts retriever.invoke(question) context_text format_docs(contexts) prompt_input {context: context_text, question: question} answer rag_chain.invoke(prompt_input) # 注意此时还需要将 question 直接传入吗 sources [] for doc in contexts: source doc.metadata.get(source_file, unknown) if source not in sources: sources.append(source) return answer, sources这里要注意如果你在rag_chain里已经定义好了questionRunnablePassthrough()那么invoke(prompt_input)会因为传入的是dict而不太对。稳妥一点的做法是把rag_chain改成接收{context: ..., question: ...}的普通调用或者直接调用prompt | llm | parser。更简单的方式是保留一条只做生成用的链generate_chain prompt | llm | StrOutputParser() answer generate_chain.invoke({context: context_text, question: question})最终返回的答案附带sources列表前端可以展示成“参考了哪些文档”这会让用户对结果更有信心也让错误定位更容易。5. 实战中必然会踩的坑与排查思路5.1 英文嵌入模型用在中文文档上检索结果惨不忍睹最常见的问题是检索Top K返回的结果看起来“毫不相关”。十有八九是嵌入模型选错了。英文通用模型对中文的编码能力很弱导致语义相近的中文句子向量距离远。解决办法很直接换成中文语料训练向量模型并用一个小样本集做测试。所谓“小样本测试”就是准备5到10个你业务里典型的问句人工判断召回Top1是否命中。另外文本切分时中文标点也要注意。如果separators里没有句号分号切分器会在奇怪的位置硬切语义断裂检索质量一样直线下降。打印几个切分片段检查一下比调半天参数都管用。5.2 上下文太长API直接报超限大模型输入长度是硬限制。你的chunk是600字k是8再加上Prompt和问题可能一次进去5000多字小上下文模型就爆了。解决方案四个方向方向做法影响减小k值从8降到4甚至3召回的候选变少上下文变短缩小chunk_size从600降到400每个片段内容更聚焦选择大上下文模型换更长窗口的模型成本可能上升对检索结果做压缩只保留片段中最相关的句子实现复杂度增加我建议先调k和chunk_size因为最简单。如果公司文档经常是长段落再考虑用文本压缩模块把每个片段里的关键句抽出来再喂给模型这样既能保留语义又控制长度。5.3 模型明明拿到了资料回答还是“自由发挥”Prompt里写了“只根据资料回答”模型还是引用外部知识这事太常见了。原因不只是Prompt还有两个隐藏因素第一temperature设得太高模型在抽样时偏向流畅表达而不是忠实原文第二检索到的片段里确实没有答案模型又不肯承认不知道只能强行编。对策是三层先把temperature调到0再把“没有资料信息就直说”写进system最后在代码层做兜底比如判断检索结果的相关度分数如果太低就不走生成直接返回“知识库中未找到相关内容”。相关度阈值需要你统计一批badcase后定我的经验是初始设为0.3到0.4再根据实测调整。5.4 LangChain版本更新导致接口不兼容网上教程说langchain.vectorstores新版已经迁到langchain_community或者独立集成包昨天还能用的Chroma.from_documents升级后可能参数变化。这个问题只能靠“锁版本”来缓解。项目一跑通马上导出固定依赖清单pip freeze requirements.txt另外安装时尽量用范围锁定比如langchain0.2,0.3而不是langchain直接装最新版。升级LangChain前先看它的升级迁移文档别在生产环境用latest。5.5 响应慢、并发能力差RAG链路里最耗时的通常是两个地方大模型推理和嵌入计算。大模型的推理速度取决于你选的模型和硬件这个短期不好优化。嵌入计算则有很多工程技巧文档量大的时候离线把全量文档嵌入好并存入向量库不要每次问答都重新嵌入在线问答时用户问题只需要嵌入一次可以把这一步缓存起来相同或相似问题直接复用。如果同一套服务要支撑多用户建议用异步方式运行模型调用和向量检索并在服务外面套一层请求队列避免突发流量把本机推理服务打挂。真要上线高并发还得考虑把向量库部署成独立服务和批量推理服务但这些是后话了。最后再分享一个我做这类项目的体会不要去迷信“换一个更强的大模型效果就会好”这句话。RAG系统的效果瓶颈通常出现在数据加工和检索召回上你花一小时优化切分策略和检索TopK可能比换模型带来更明显的提升。建一套自己的badcase表格把每个答错的用户问题记下来反推是嵌入模型、切分还是Prompt的问题迭代几轮之后系统才能真正从“能跑”变成“好用”。这套LangChain RAG的流程本质上是在帮你把答案的“证据链”补全而证据链的质量永远是你自己要守住的底线。