news 2026/10/17 3:46:27

用LangChain搭建RAG基线:解决大模型幻觉的知识库问答实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
用LangChain搭建RAG基线:解决大模型幻觉的知识库问答实战

做AI应用开发的人都有过这种体验:模型一脸正经地胡说八道,你问它内部流程,它张口就来一套看似合理但完全不存在的规定。这其实就是大模型最典型的“幻觉”问题。RAG(检索增强生成)就是目前工业界最常用、最有效的缓解手段之一,而LangChain则是把这套流程打包成顺手工具链的框架。这篇文章是我在实际项目里搭RAG基线时的完整经验记录,从流程设计、组件选型,到最小可用代码和后续调优思路都有,适合刚接触LangChain、准备做知识库问答或者想要系统搭一套RAG基础链路的朋友参考。

我先把话说在前面:RAG本身不难,难点全在细节。流程谁都能背下来,但“为什么这么切文本”“为什么召回不准”“为什么答案还是瞎编”这些才是真正拉开差距的地方。下面我就按实际落地的顺序,把整套东西从头讲一遍。

1. RAG在解决什么问题:先理解检索增强的底层逻辑

1.1 大模型的三类硬伤

你没法靠一个通用大模型解决所有业务问题,这不是模型不够强,而是它天生有三个短板。

第一是幻觉。模型在生成时本质上是在做概率预测,它没有能力知道自己“不知道什么”。只要问题的答案不在它的参数记忆里,它就会用最流畅的方式编一个出来,而且语气越笃定,越容易骗到人。

第二是知识陈旧。大模型的训练数据有截止时间,之后发生的事它完全不知道。你问它最近一个月的行业政策,它只能给出“抱歉,我无法访问实时信息”或者更糟糕的——编造一条。

第三是缺少领域知识。通用模型见过公开互联网的数据,但你的公司制度、内部操作规程、产品手册、专业领域的私有文档,它一概没见过。这类知识恰恰是业务场景里提问最密集的部分。

RAG的思路很简单:不逼模型去“记住”你的知识,而是在每次提问时,先到你的文档库里检索相关片段,把命中内容连同问题一起喂给模型生成答案。换个生活化的说法,闭卷考试改成开卷考试,模型拿到问题先查资料再作答,答案的“事实出处”不在模型脑子里,而在你提供的资料里。

1.2 RAG的标准流程拆解

RAG全链路可以拆成两个阶段、六个环节。

索引阶段负责把原始文档变成可检索的向量形式:

  • 文档加载:读取PDF、Word、Markdown、网页等各类来源。
  • 文本切分:把长文档切成合适大小的文本块,这是后续所有质量的基础。
  • 向量化:用Embedding模型把每个文本块编码成一串浮点数向量。
  • 向量存储:把向量连同原文、元数据一起写入向量数据库。

检索生成阶段负责应答:

  • 查询向量化:用户提问时,用同一个Embedding模型把问题也转成向量。
  • 相似度检索:在向量库里找出与问题向量最接近的若干文本块。
  • 拼接生成:把检索结果作为上下文,连同原始问题一起交给大模型生成回答。

这个流程里最核心的一条原则是:知识库内容决定事实边界,大模型只负责用流畅的语言把事实组织成答案。理解了这条原则,你后面调试所有问题都有方向了。

2. 方案选型:别急着写代码,先决定用哪套组件

2.1 大模型接口怎么选

搭RAG链路时,生成环节的模型选择直接影响效果和成本。我自己的基线方案是“商用API为主、本地开源模型兜底”。

商用API的优势是省心,不用管推理服务部署,模型能力和上下文长度都有保障。代价是按量计费,如果知识库文档量大、检索频繁,日积月累的成本不能忽略。本地部署开源模型则相反,前期要搞定推理服务,显存和并发需要考虑,但数据不出内网,隐私性更强,长期跑起来成本更可控。

我的建议很直接:第一版基线用商用API把链路跑通,确认业务价值和效果之后,再评估是否有必要迁移到本地模型。不要在项目第一天就花大量时间折腾本地推理环境的性能调优,那样会让你迟迟进入不了真正的业务验证环节。

2.2 向量数据库选型对比

向量数据库负责存储和检索文本向量,选型标准主要看三个维度:数据规模、运维复杂度、是否需要和现有存储打通。

我在项目里实际比较过几类方案,列个表给你参考:

方案特点适用场景踩坑提醒
Chroma本地轻量,零配置,API简单原型验证、小规模知识库数据量大了性能下降明显
FAISS纯向量索引库,性能好,资源占用低单机中等规模检索需要自己管理持久化
Milvus分布式向量数据库,功能完整百万级以上向量、多租户部署组件多,运维门槛高
PGVectorPostgreSQL扩展向量数据与业务数据同库管理检索性能和扩展性受限于PG

对于第一次搭建RAG基础链路的场景,我推荐Chroma,没有之一。它装完就能用,LangChain集成最成熟,代码量最少。等你的文档量级明显上来了,再平滑迁移到Milvus这类生产级方案。前期不要过度设计,RAG链路里的业务逻辑还没验证清楚就去搭分布式集群,大概率是在给自己挖坑。

2.3 Embedding模型的选择逻辑

不少初学者容易忽略Embedding模型,觉得随便选一个就行。实际上,向量质量是RAG效果的上限,而生成模型只是在检索结果的基础上作答。检索不到正确内容,后面所有努力都白费。

选择Embedding模型时,我主要看三点。

第一是与文档语言的匹配度。如果你的知识库是中文为主,那就优先选中文语料训练效果好的模型,而不是直接用面向英文的通用模型。第二是向量维度,维度越高,存储成本越高,检索计算越慢,但通常精度更好,需要在效果和成本之间权衡。第三是和服务端的一致性,这一点最容易踩坑——索引和查询必须用同一个Embedding模型,否则两个环节的向量不在同一语义空间里,检索结果必然离谱。

顺带提一句,如果走本地方案,像开源的中文向量模型这一类的路径在LangChain里集成很成熟,加载本地模型权重的代码也不复杂,把模型文件放到指定目录就行。

3. 实战:用LangChain搭一个最小可用的RAG基线

3.1 安装与准备

进入代码环节。先装依赖。需要说明的是,LangChain版本迭代很快,API变动频繁,我的示例基于常见稳定版本,你实操时务必锁定版本。

pip install langchain langchain-community chromadb

如果你用开源Embedding模型,还要装对应的依赖库和模型权重。我建议在项目根目录建一个Python虚拟环境,别全局装,否则以后处理版本冲突会非常痛苦。

3.2 文档加载与文本切分

加载文档最常用的方式是LangChain提供的文档加载器。比如本地PDF:

from langchain_community.document_loaders import PyPDFLoader loader = PyPDFLoader("知识库文档.pdf") documents = loader.load()

这里的documents是一个包含页面级文本和元数据的列表。加载器能帮你省去写PDF解析代码的时间,但要注意它只是把PDF的文本抽取出来,如果你的PDF是扫描图片,那得先走OCR环节,这一步容易漏掉,我后面还会细说。

文本切分是重头戏。我几乎都用RecursiveCharacterTextSplitter:

from langchain.text_splitter import RecursiveCharacterTextSplitter text_splitter = RecursiveCharacterTextSplitter( chunk_size=500, chunk_overlap=50, separators=["\n\n", "\n", "。", "!", "?", ",", " ", ""] ) split_docs = text_splitter.split_documents(documents)

这里的chunk_size=500指的是每个文本块最多约500个字符,chunk_overlap=50表示相邻两个文本块之间保留50个字符的重叠。为什么要重叠?想象一下,某句话的上半句在上一块的末尾,下半句在下一块的开头,如果不设重叠,检索时可能因为只命中半句话而丢失语境。重叠的本质是给语义的连续性留一个缓冲带。

但chunk_size也不是越大越好。块太大,向量中包含太多无关信息,检索精度会下降;块太小,单块语义不完整,召回内容碎片化。500到800是我的经验区间,你可以根据你的文档类型做实验调整,后面我专门讲这个。

3.3 向量化与存储

切分完成后,用Embedding模型把每个文本块向量化,然后写入向量库。以Chroma为例:

from langchain_community.vectorstores import Chroma from langchain_community.embeddings import HuggingFaceEmbeddings embedding_model = HuggingFaceEmbeddings( model_name="你的本地中文Embedding模型路径" ) vectorstore = Chroma.from_documents( documents=split_docs, embedding=embedding_model, persist_directory="./chroma_store" )

persist_directory的效果是让向量数据落盘到本地目录,下次启动时不需要重新加载文档和切分,直接从磁盘读索引,省掉大量重复计算环节。这一点在文档多的时候非常关键。

3.4 检索器与问答链

向量库建好之后,先不做QA,单独看检索效果:

retriever = vectorstore.as_retriever( search_kwargs={"k": 4} ) docs = retriever.invoke("公司年假制度有哪些规定") print(len(docs))

先跑这一步,把返回的内容打印出来看,确认检索到的内容确实和问题相关。这个步骤没有太高的技术含量,但能帮你区分“链路问题”和“生成问题”。很多人的做法是从头到尾一条龙跑完,一报错就晕头转向,就是因为没拆开验证。

检索没问题之后,再组装完整的问答链:

from langchain.chains import RetrievalQA from langchain_community.chat_models import ChatOpenAI llm = ChatOpenAI( model="你选择的模型名称", api_key="你的API密钥", temperature=0 ) qa_chain = RetrievalQA.from_chain_type( llm=llm, retriever=retriever, chain_type="stuff" ) result = qa_chain.invoke("公司年假制度有哪些规定") print(result["result"])

temperature=0是我的习惯,知识问答场景下一定要把生成多样性降到最低。回答要忠实于检索内容,不需要模型发挥创造力。chain_type="stuff"代表把所有命中的文本块一次性全部塞进模型的上下文,这是最简单也是效果最稳的方式,但它对上下文长度有要求,后面我会说什么时候得换掉它。

3.5 完整流程串联

最后给一个完整基线代码,把上面的环节串起来:

from langchain_community.document_loaders import PyPDFLoader from langchain.text_splitter import RecursiveCharacterTextSplitter from langchain_community.vectorstores import Chroma from langchain_community.embeddings import HuggingFaceEmbeddings from langchain.chains import RetrievalQA from langchain_community.chat_models import ChatOpenAI # 1. 加载 loader = PyPDFLoader("知识库文档.pdf") documents = loader.load() # 2. 切分 text_splitter = RecursiveCharacterTextSplitter( chunk_size=500, chunk_overlap=50 ) split_docs = text_splitter.split_documents(documents) # 3. 向量化 + 存储 embedding_model = HuggingFaceEmbeddings( model_name="你的本地中文Embedding模型路径" ) vectorstore = Chroma.from_documents( documents=split_docs, embedding=embedding_model, persist_directory="./chroma_store" ) # 4. 检索器 retriever = vectorstore.as_retriever( search_kwargs={"k": 4} ) # 5. QA链 llm = ChatOpenAI( model="你选择的模型名称", api_key="你的API密钥", temperature=0 ) qa_chain = RetrievalQA.from_chain_type( llm=llm, retriever=retriever, chain_type="stuff" ) result = qa_chain.invoke("公司年假制度有哪些规定") print(result["result"])

这套代码就是RAG的最省心形态,跑通它,基础链路就在你手里了。

4. 把基线调好:检索质量优化的关键细节

4.1 切分策略直接决定召回质量

我在实际项目中调试RAG时,花时间最多的不是在模型选择上,而是调文本切分的参数。切分粒度影响的是语义完整性,而语义完整性直接影响检索命中率。

有一回我把一份产品规格文档按固定字符长度硬切,结果很多技术参数被切到了不同文本块里。用户问“某型号的功耗是多少”,检索返回的块里只有产品名,没有功耗数据,模型自然答不上来。后来改成按章节标题和段落结构切分,问题立刻消失。

如果你用的RecursiveCharacterTextSplitter发现效果不佳,可以考虑两种策略。

一种是结构化切分,针对Markdown或HTML文档,按标题层级、表格、列表结构切分,让每个文本块都拥有相对独立的语义。另一种是父子切分,大块负责检索,小块负责给模型生成,兼顾上下文和精度的平衡。这个思路属于进阶调优,先不展开。

我的建议是:先用默认递归切分跑通,把每个分块打印出来人工检查一遍,确认没有明显切断语义的地方,再决定要不要上更复杂的方案。

4.2 检索参数不是越大越好

k值是最常见的检索参数,代表返回多少个文本块给模型。

初学者容易有个误区:k值越大,召回内容越多,答案越完整。实际上k值过大会引入大量无关噪声,模型分不清哪些该用来回答,反而被干扰。k值过小则可能漏掉关键内容。我通常从3到5起步,根据文档块大小和问题复杂度上下浮动。

除了k值,检索时的相似度阈值也值得关注。向量相似度分数低,说明检索结果和问题相关性弱,这种时候与其硬着头皮交给模型,不如直接返回“知识库中未找到相关内容”。在知识问答场景中,“明确告知没找到”远比“编造一个答案”更可靠。

Chroma的检索器也支持MMR模式,全称Maximal Marginal Relevance。和普通相似度检索的区别是,MMR会在保证相关性的同时降低返回结果之间的冗余度。比如一个问题对应的多个文本块内容高度重复,MMR会选择保留其中代表性的几块,让答案视角更全面。我在处理FAQ类文档时常用这个模式。

4.3 进阶视角:Rerank、HyDE和混合检索

如果你已经把基础链路调顺了,但还觉得召回质量不够,接下来的方向我来指个路。

重排序(Rerank)的思路是:第一轮用向量检索快速召回数量较大的候选集,再用一个专门的重排序模型对候选结果进行精排,让最相关的内容排在最前。这个方案在高精度场景下效果提升非常明显,代价是多一层模型调用。

查询改写(HyDE)的思路是:先用大模型根据原始问题生成一个虚构答案,再用这个答案去检索知识库,相当于给查询加了一层语义扩充。在问题表述模糊、用词和文档不匹配的场景下,效果提升很直接。

混合检索的思路是:把向量相似度检索和关键词匹配检索结合起来,用分数统一排序,解决向量检索对精确术语匹配不够敏感的问题。

这些都是RAG进阶的经典方向,等你在基础链路上积累了足够体感之后再尝试,会发现理解起来轻松得多。

5. 常见问题与排查技巧实录

5.1 召回一直不相关

召回结果不相关是最常见的问题。我的排查顺序是固定的。

先确认索引和查询用的是同一个Embedding模型。如果索引阶段用了模型A,查询阶段换成了模型B,两个向量不在同一语义空间,检索结果基本就是乱抓。其次检查切分块是否破坏了语义,把切出来的块打印出来逐条看,确认每块是否具备独立的可理解语境。再检查查询本身是否存在表述歧义,比如用户问“这个怎么弄”,这种问题谁来了也检索不好,需要考虑做查询理解或引导澄清。

5.2 回答还是幻觉

用了RAG之后模型依然胡编,要检查两个环节。

一个环节是检索结果里是否混入了和问题无关的内容。如果命中的数据本身就不相关,模型为了完成回答任务就只能强行编造。另一个环节是,即使检索到了相关内容,模型也可能不按检索内容作答。解决办法是把Prompt写强硬一些,明确告诉模型“只能根据提供的参考内容回答,参考内容不足时直接说明不知道”。如果你用的是LangChain的RetrievalQA,可以在构造时传入自定义的system prompt模板,或者直接用create_stuff_chain手动控制提示词。

另外一个容易被忽略的问题是引用缺失。业务场景里用户需要知道答案来自哪里,模型如果只是把检索结果“翻译”成了一段话,没有关联到原文出处,可信度就很难建立。这一步可以对文本块做映射处理,让答案在生成时携带来源片段。

5.3 Token超限报错

chain_type="stuff"把所有命中的文本块一次性全部塞给模型,当文本块数量多、单块大时,很容易超过模型的上下文长度限制,直接报错。

这时有两条路。一条是把chunk_size降下来,减少单块大小。另一条是换成其他处理模式,比如map_reduce,让模型先对每个文本块分别生成答案,再汇总生成最终答案。代价是调用次数增多、耗时变长,但可以处理更长内容。简单追问场景一般用不上,但知识库文档块大时值得备一个。

也可以在构造提示词时压缩上下文,把那些明确低相关的检索结果过滤掉,只留最核心的内容。

5.4 环境与版本兼容问题

LangChain的API变动相当频繁,不同版本之间的类名、方法签名经常变化。我在不同机器上复现同一个项目时,遇到过好几次因为版本不一致导致调用报错的情况。

应对办法是固化环境。在项目目录下生成并提交依赖锁定文件,把LangChain及相关库的版本钉死。新项目再建虚拟环境时,按照锁定文件一次性还原。还有一个坑是底层依赖库之间的兼容性,比如某个版本的LangChain要求Pydantic版本不能太高,安装时经常因为依赖冲突导致一堆报错。我的经验是在纯净的虚拟环境里逐个安装,谁报错解决谁,不要图省事一次装一堆。

典型问题直接原因排查思路
检索结果不相关Embedding模型不一致统一索引与查询阶段模型
切分截断语义chunk_size不当打印分块人工检查
答案仍然幻觉检索噪声或被模型忽略强化Prompt限制,过滤低相关结果
Token超限stuff方式塞入过多内容换map_reduce或降低chunk_size
导入报错LangChain版本变动锁定版本,依赖隔离

6. 最后再分享一点我的实际体会

RAG基础链路本质上是一套“组装方案”,每个组件都不难,难度在把它们的边界理清楚。我从零跑通第一个完整RAG系统之后,最大的感受是:不要急着追求复杂的检索策略、重排序、混合召回这些高阶玩法,先把加载-切分-向量化-检索-生成这条主线在每个环节上的表现摸清楚,哪一环有问题就在哪一环解决,比盲目堆技术组合要有效得多。

如果这篇内容能帮你把第一版RAG基线跑起来,后面再踩的坑应该会少一半。最后一个小建议:每改一次切分参数或检索策略,都记下当时的配置和效果数据,这些记录是后续优化时最宝贵的依据。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/17 3:46:23

AI渗透测试开源项目实战拆解:PentAGI、Strix与Decepticon

AI渗透测试的三个开源项目实战拆解:PentAGI、Strix与Decepticon做渗透测试做了这么多年,我越来越清楚一个事实:真正卡住我们的往往不是漏洞利用那一下,而是前期的信息收集、攻击路径规划,以及各种重复性的验证工作。你…

作者头像 李华
网站建设 2026/10/17 3:45:50

医学影像与病程融合的时序预测:从数据到模型全指南

结合历史影像与病程做时序预测,是我这两年被临床团队问得最多的一类医学AI问题。复查场景和单次检查不一样:手头不只有一张片子,还有前几次复查的历史影像、每次随访的检验指标、症状描述和用药记录,这些数据天然带着时间标签。临…

作者头像 李华
网站建设 2026/10/17 3:45:37

降AI率全攻略:从检测原理到六大改写工具实操指南

2025届同学的毕业季,基本都绕不开两件事:查重复,以及比查重复更让人头疼的“降AI率”。我最近帮几个学弟学妹看论文初稿,发现大部分人拿到检测报告时的第一反应是懵的——明明是自己一个字一个字敲出来的,却标出很高的…

作者头像 李华
网站建设 2026/10/17 3:45:21

大场景重建提速1.4倍:自适应分辨率与浮点精度调度实践

最近我在改一篇面向大场景重建的投稿,核心思路就是标题里那句话:让训练图像的分辨率跟着场景自己涨。这套方案跑下来,单块卡上训练速度能快1.4倍,重建精度还能涨0.9dB。今天不聊论文行文,就聊这个“分辨率怎么涨”“浮…

作者头像 李华
网站建设 2026/10/17 3:44:29

Cursor突然火了,TaoToken统一Key接入后码农真的要被干掉了吗?

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/17 3:44:14

两串锂电池均衡方案:MP2642GR-Z与STM32F091RC实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华