最近两三年,大模型应用开发圈子里最常被问到的一句话就是:智能体到底是什么?RAG和它有什么关系?LangChain又扮演什么角色?说实话,这三个词单独拆开各有各的文档,但放到一起,大多数教程要么讲得太浅,要么讲得太散。我自己接手过好几个从零搭建知识库问答和智能体项目的活,踩过召回不准、答案幻觉、链路割裂这些坑之后,才慢慢把这三者的关系理顺。这篇就按我实际落地的顺序来写:先把概念摆正,再走一遍可运行的RAG管道,然后把检索包装成智能体的一项技能,最后聊调试和评估。保证你照着做完,心里能有张完整的地图。
为避免纸上谈兵,下文里的代码我都尽量给了可以直接跑的形态,但故意没有绑定某一家的云服务接口,你把自己手头能用的模型和向量模型换进去就行。整个思路在本地小模型和云端大模型之间都能复用,差别主要在参数细节上。
1. 先理顺概念:RAG、Agent和LangChain各管哪一段
1.1 大模型的天然短板:知识截止和幻觉
很多人把大模型当成一个百科全书,上来就提问,结果发现它对自家产品一无所知,或者一本正经地编造答案。这背后其实是两个固定事实:第一,大模型的参数在训练完成后就冻结了,它只掌握训练数据截止日期之前的信息;第二,大模型的本质是"根据上文预测下一个词",它追求的是语言的连贯合理,而不是事实的绝对准确。当它不知道答案时,最自然的路径就是编一个听起来合理的答案,这就是幻觉的来源。
我用一个类比来解释:大模型像一个记忆力超强但被关在屋子里的实习生,他读过的书很多,但屋外最新发生的事情一概不知。你想让他回答"当前市场的最新情况",不能指望他凭空知道,而是应该把一份最新报告递到他手里,让他先看再答。RAG做的就是"递报告"这个动作。
理解了这一点,就明白为什么RAG不是可选项,而几乎是企业落地大模型应用的必经之路。它把"模型记忆"和"外部知识"彻底分开:模型只负责理解和生成,外部知识库负责提供实时、私有、可追溯的信息。
1.2 三项技术的分工
这三者的关系,我习惯用"做什么、怎么查、怎么串"来概括:
RAG解决的是"知识从哪来"。它的流程是把用户问题转成向量,去向量数据库里检索最相关的文档片段,再把片段拼进提示词交给模型生成答案。核心价值是让答案有出处、有时效、覆盖私有知识。
Agent解决的是"任务怎么做"。它让模型不再只是"回答",而是可以自主决定调用什么工具、调用几次、按什么顺序调用。比如用户问"帮我查一下某产品介绍,然后总结成一页纸",智能体会先触发检索工具、再触发写作输出,甚至拆成多步完成。
LangChain解决的是"这些东西怎么拼起来"。它是一个编排层,把模型、向量库、提示词、工具、记忆这些零散组件,用一套统一接口串成流水线。好处是你今天用开源向量库,明天换云上的向量服务,上层逻辑不用重写。
三者不是竞争关系,而是叠加关系。一个真正的智能体应用,底子往往就是RAG管道,LangChain负责把RAG和Agent拧成一条链路。
1.3 为什么是LangChain来串
有人会问:我自己写代码调模型API不行吗?当然行,而且小项目直接写其实更快。但一旦你的应用要处理多种文档格式、多个工具、多层记忆、多轮对话,手写代码的维护成本会成倍上升。LangChain的价值在于它把这些场景里最高频的模式抽象成了标准组件——加载器、切分器、向量库、检索器、工具、智能体执行器,每一层都有现成的接口。
但这里要提醒一句:LangChain本身也有学习成本,而且版本迭代快。我的建议是把它当成"结构化胶水"来用,不要追新版本功能,重点掌握稳定核心的几个抽象就够了。这也是我下面写示例时,刻意选择最朴素写法的原因。
2. 第一条RAG管道:从一堆文档到"能查资料的回答"
2.1 文档加载与切块的参数逻辑
RAG的第一步是把原始文档读进来,再切成合适的片段。很多人直接跳过这一步去调试后面的效果,这是最常见的错误。文档加载看似简单,实际坑很多:PDF的表格会被拆乱,HTML里的导航文字会混进正文,扫描件根本抽不出文字。所以加载后先打印前几页内容看一遍,再进入切分。
切分是整个RAG里最需要手工调的部分。切太小,单块信息不完整,检索到的内容可能只是半句话;切太大,向量里噪音太多,答案生成时上下文被无关信息稀释。我用过一个比较稳的起步组合:RecursiveCharacterTextSplitter,字符切分按段落层级递归,优先保留完整语义块。
from langchain_community.document_loaders import TextLoader, PyPDFLoader, DirectoryLoader from langchain.text_splitter import RecursiveCharacterTextSplitter # 1. 加载整个目录的txt文档 loader = DirectoryLoader("./docs", glob="*.txt", loader_cls=TextLoader) raw_docs = loader.load() # 2. 切分成可检索的片段 splitter = RecursiveCharacterTextSplitter( chunk_size=500, # 每块字符数,中文场景可以按字符算 chunk_overlap=80, # 相邻块重叠,避免语义被拦腰切断 separators=["\n\n", "\n", "。", "!", "?", ";", " ", ""], ) chunks = splitter.split_documents(raw_docs) print(len(chunks)) # 先看切出了多少块chunk_overlap的作用很多人忽视。文档在切分点附近经常有跨块才能理解的指代关系,比如前一块结尾出现"该方案",后一块开头才是具体描述。适当重叠能把这些上下文衔接起来。我的经验是:起步阶段chunk_size取300到800之间,overlap取size的10%到20%,先跑通再去针对数据调优。
2.2 向量化与检索存储选型
切好的片段要变成向量,这一步依赖嵌入模型。嵌入模型的质量直接决定了"检索相不相关",比后面换大模型的影响更明显。选择时记住一个原则:优先选跟你的业务内容语言匹配、且在你数据上验证过效果的模型。中文场景下,用开源中文向量模型在本地跑很稳,不依赖外网,也没有按量计费的压力。
from langchain_huggingface import HuggingFaceEmbeddings embedding_model = HuggingFaceEmbeddings( model_name="bge-small-zh-v1.5", # 换成你验证过效果的中文向量模型即可 encode_kwargs={"normalize_embeddings": True}, )向量存储方面,开发调试用Chroma或FAISS最省事,数据量到百万级再考虑分布式向量库。检索方式先用similarity按余弦相似度取TopK,后期可以再上混合检索和重排。存储和检索的代码很简单:
from langchain_chroma import Chroma vectorstore = Chroma.from_documents( documents=chunks, embedding=embedding_model, persist_directory="./chroma_db", ) retriever = vectorstore.as_retriever(search_kwargs={"k": 4})关于k的取值,4到6是一个比较平衡的范围。k太小,可能漏掉关键信息;k太大,上下文塞进太多无关片段,既浪费token又引入噪音。我通常在开发期用k=4,压测时再对比4和8的差距。
2.3 把检索和生成接成一条链
有了检索器和模型,下一步就是把"检索→拼上下文→生成"串成一条链。下面的写法没有引入复杂框架,只用了最基本的流水线组合,逻辑一目了然:
from langchain_openai import ChatOpenAI from langchain_core.prompts import ChatPromptTemplate llm = ChatOpenAI(model="your-chat-model", temperature=0) prompt = ChatPromptTemplate.from_template( """你是企业知识库问答助手。请严格根据以下资料回答问题。 如果资料中没有相关信息,请直接回答“资料中未找到相关内容”,不要编造。 资料: {context} 问题:{question} 回答:""" ) def format_docs(docs): return "\n\n".join(doc.page_content for doc in docs) rag_chain = ( {"context": retriever | format_docs, "question": lambda x: x["question"]} | prompt | llm ) result = rag_chain.invoke({"question": "公司的报销流程是什么?"}) print(result.content)这条链的每一步都值得解释一下。retriever | format_docs表示先从库中查回文档列表,再拼成字符串;lambda x: x["question"]取用户输入的问题;|是LangChain的管道操作符,代表数据依次经过后面组件。整个链的好处是中间任何一环都可以替换,比如把检索器换成熟练的混合检索,或者把输出换成流式,都不需要改其他部分。
第一条管道跑通之后,先别急着做Agent。我强烈建议你至少花一天时间,拿真实问题在现有RAG上测一轮,看看哪些问题答得好、哪些答得差。因为Agent只是在这条管道上加了"决策层",如果检索本身就不靠谱,Agent只会把错误放大。
3. 升级成智能体:把"检索"变成模型手上的工具
3.1 工具封装的正确姿势
RAG管道是人写死的一条流水线:不管什么问题,先检索、后生成。但真实场景里用户的问题五花八门。有人问知识库里的内容,有人问"帮我算一下这个月总支出",还有人问"现在几点"——这些问题根本不需要检索。智能体的思路正好相反:把检索封装成一个工具,让模型自己决定要不要用。
LangChain里封装检索工具最简单的方式是create_retriever_tool。这个封装看起来简单,真正的学问在description字段,因为模型就是靠这个描述来决定何时调用工具的。写得太笼统,模型啥问题都去查;写得太具体,该查的时候不查。
from langchain.tools.retriever import create_retriever_tool knowledge_tool = create_retriever_tool( retriever, name="knowledge_base_search", description="""当用户询问公司制度、产品说明、内部流程、技术文档等知识库中的内容时, 使用本工具从企业知识库中检索相关片段。当问题与上述内容无关时,不要调用本工具。""", )除了检索工具,你还可以顺手加一两个实用工具,比如计算器、时间查询,让Agent的能力面更完整。但注意:工具不是越多越好。每多一个工具,模型做决策的选择空间就大一分,误调用的概率也随之上升。起步阶段控制在三到五个工具以内。
3.2 记忆:让智能体记住对话上下文
大模型本身是无状态的,每一次调用都不记得上一轮说了什么。智能体要连续对话,就必须由我们手动把历史记录拼进每次调用的上下文里。LangChain的ChatPromptTemplate里专门留了个chat_history占位符用来放多轮消息:
from langchain_core.prompts import ChatPromptTemplate prompt = ChatPromptTemplate.from_messages([ ("system", "你是企业知识库智能助手,可以调用工具查询资料。"), ("placeholder", "{chat_history}"), ("human", "{input}"), ("placeholder", "{agent_scratchpad}"), ])这里chat_history通常由ConversationBufferMemory维护,或者你在外部自己维护一个列表、在每次调用前塞进去。记忆不是越多越好,超过模型上下文窗口会被截断,而且旧信息会干扰当前决策。我在实战里会加一个滑动窗口,只保留最近六到十轮,超过就丢弃,效果比"全部记住"稳定得多。
3.3 可运行的智能体最小示例
把工具、模型、提示词组合起来,就得到一个完整的智能体:
from langchain.agents import create_tool_calling_agent, AgentExecutor tools = [knowledge_tool] agent = create_tool_calling_agent(llm, tools, prompt) agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True) response = agent_executor.invoke({ "input": "公司的年假制度是什么?", }) print(response["output"])这段代码背后发生的事,值得拆开讲清楚。模型收到"公司的年假制度是什么"之后,不会直接回答,而是判断"我需要知识库检索",于是生成一次工具调用请求;执行器捕获到请求,执行检索并把结果回传给模型;模型看到检索内容后,生成最终答案。整个过程通常在几步之内完成,如果检索内容不够,模型还可以再查一次。这套"思考→行动→观察→再思考"的循环,就是Agent区别于普通RAG的核心。
如果是在本地环境跑,ChatOpenAI换成ChatOllama或者你本地部署模型的LangChain封装类,其余代码完全不动。工具调用能力依赖模型本身,所以选模型时记得确认它支持函数调用或工具调用,而不是只能纯文本对话。
4. 实测中踩过的坑:召回不准和答案不对,先别急着换模型
4.1 召回侧的三个调整方向
我调试过的知识库项目里,八成的效果问题都出在检索召回上,而不是模型太笨。召回不准时,第一反应别是换更强的模型,而是按下面三个方向排查。
第一是切分策略。如果检索出来的片段总是只有半句话、没有完整上下文,多半是chunk_size太小或者overlap不够。反过来,如果片段内容很大但和问题只沾一点边,说明切块太大,信息太杂。可以用"父文档检索"的思路:检索时用小片段保证精度,拼接时给模型返回小片段所在的大块原文,兼顾两头。
第二是查询改写。用户的问题往往很口语化:"那个流程咋走来着",直接拿这句话去向量库检索,效果可想而知。可以在检索前加一步"改写层",让模型把口语化问题改写成语义规范、信息完整的检索语句,再去做向量查询。这一步很便宜,但收益很明显。
第三是重排。TopK检索出的4个片段里,可能只有1个真正相关。靠相似度排序有时不准,可以加一个重排模型,对候选片段重新打分,把最相关的顶到前面。重排只针对少量候选,成本可控,是工业级RAG里的标配。
4.2 生成侧的护栏
检索对了,生成还是会翻车,主要因为提示词里没有设置边界。我见过很多demo的提示词把模型当搜索引擎用,让它"尽量回答",结果模型把检索片段里没有的细节自行脑补了。我的做法是在提示词里明确写三件事:只依据给定资料作答;资料不足时明说不知道;涉及数字和专有名词时保持原文,不做转述。
还可以让答案带上来源。方法很简单,在拼上下文时把每个片段对应的文档名和页码一起带进去,提示模型"引用[文档名-页码]":
def format_docs_with_source(docs): parts = [] for doc in docs: meta = doc.metadata source = f"{meta.get('source', 'unknown')}-{meta.get('page', '')}" parts.append(f"来源:{source}\n内容:{doc.page_content}") return "\n\n".join(parts)这个看似不起眼的功能,在真实项目里的价值极高。一方面方便用户点开原文验证,另一方面答案一旦能追溯到具体文档,模型就不敢乱编,对幻觉是很大的抑制。
4.3 成本延迟:容易被忽略
还有一类坑跟正确性无关但会卡死项目上线:成本和延迟。每轮问答都要做一次Embedding、一次向量检索、一次模型生成,看似单次成本很低,量大之后完全是两码事。
我常用的控制手段有三个。第一,设置检索后的上下文长度上限,只取最相关的若干片段,而不是把库里的东西全塞进去;第二,对高频重复问题做结果缓存,命中缓存直接返回,不重新走链路;第三,把"是否值得调大模型"的问题交给路由规则,简单问题用轻量模型回答,复杂问题才走完整RAG链。这些优化不影响效果,但对系统稳定性帮助很大。
5. 效果评测:用数据判断你的RAG到底行不行
5.1 检索侧的数字
很多项目上线前连一组评测集都没有,效果全靠开发者自己点几个问题感受一下,这是很危险的。我推荐至少从两个维度量化RAG:检索质量和生成质量。
检索侧最直观的指标是命中率。它的定义很简单:在一组已知答案的问题里,正确答案对应的文档片段是否出现在检索结果的前k个里。统计"命中问题数/总问题数"就是Hit Rate。另一个常用指标是MRR,它计算第一个正确答案在所有结果里的排序倒数。比如正确答案排第1位,得分是1;排第3位,得分是1/3。MRR越高,说明正确信息越靠前。
这两个指标不需要任何额外标注工具,你只要准备一批"问题+包含答案的文档片段"就能算。我一般会写一个几十行的小脚本,把评测集里的每个问题丢给检索器,输出命中率和平均MRR,用来判断切分、Embedding、重排到底有没有改进。
5.2 生成侧的数字
生成侧最核心的两个指标是忠实度和答案相关性。忠实度衡量的是:答案里的每个关键主张,是否都能在检索到的上下文里找到依据。如果答案写得很顺,但依据里根本没有,这就是幻觉,忠实度就要打低分。答案相关性衡量的是:回答是否切中问题本身。模型可能说得都对,但答非所问,相关度就得不了高分。
这两个指标可以人工判分,也可以用模型当裁判。我的做法是写一个打分提示词,把"问题、正确答案参考、检索上下文、模型答案"四样东西丢给裁判模型,让它按1到5分分别打忠实度和相关性。注意,裁判模型最好用和实际回答模型不同的模型,否则容易自带偏好。
5.3 评估集的构建
评估集的质量决定了评测的参考价值,而评估集最常见的问题是"只挑模型能答对的问题"。我的建议是分三类收集:一是知识库里明确能查到的标准问答,占六成;二是需要跨多个片段才能回答的复杂问题,占两成;三是知识库里根本没有答案的问题,占两成。第三类尤其重要,它专门用来检验模型会不会乱答。如果模型对这类问题也一本正经地输出,说明你的"资料不足就直说"护栏失效了。
评估集不用一开始就做很大,五十到一百条就能发现八成的严重问题。关键是把它当成一份会持续维护的资产:每次上线新功能前跑一遍,分数下降就回滚。
6. 什么项目真的适合这一套?我的判断标准
最后聊一个经常被忽略的问题:不是所有项目都该用RAG加Agent。我见过不少团队把技术栈搭得很豪华,业务上却完全不需要。
我自己的判断标准很简单。如果业务核心是"从一堆固定文档里快速找到答案",那一条朴素的RAG管道就够了,加Agent纯属增加不稳定因素。如果一个任务可以拆成多个步骤、需要调用多个外部工具、且步骤顺序不固定,比如"查库存→查物流→生成回复→提交工单",那才值得引入Agent。如果知识库很小且不常变,甚至直接微调一个模型走传统问答都行,RAG未必比你想象的更优越。
还要考虑维护成本。Agent引入了模型自主决策,意味着同样的用户问题,这次调工具、下次可能不调,排查问题会变难。所以我在每个项目里都会做一件事:给Agent执行器的每步调用都加上结构化日志,记录"模型决策依据、调用了哪个工具、工具返回了什么、最终答案是什么"。这个日志在调试和定责时能省下大量时间,比任何监控面板都管用。
这套技术栈走到现在,我的总体体会是:RAG管知识,Agent管行动,LangChain管串联,但真正决定项目成败的,永远是数据质量和评测闭环。把这三个环节做到位,哪怕模型不是最顶级的,产品体验也能立住。