讲到 LangChain,很多刚接触的人会先被它庞大的模块列表劝退一圈。Models、Prompts、Chains、Agents、Memory、Callbacks、Output Parsers,光这些名词就够让人头大的。我第一次完整翻 LangChain 文档时,最大的困惑不是某个 API 不会用,而是搞不清这些东西为什么要凑在一起,各自解决什么问题。后来自己动手搭过几个应用,才慢慢理解:LangChain 本质上是一个构建大模型应用的集成框架,核心价值是让模型从“单个 API 能力”变成“一条能完成实际任务的流水线”。这篇文章不扣某个具体技巧,而是从宏观角度把 LangChain 的骨架拆开看一遍,适合刚入门想建立整体认知的同学,也适合已经写过几个 Demo、但还没理清模块关系的朋友。
1. 项目概述:LangChain 解决的到底是什么问题
1.1 一个模型 API 和一套系统之间的差距
如果只是调一个聊天模型的接口,直接写 HTTP 请求就够了,根本不需要 LangChain。但真实业务里,一个可用的大模型应用远远不止“把问题发给模型、拿到回答”这么简单。
举个例子,你想做一款“公司内部知识库问答助手”。光有模型 API 是不够的,你至少要处理这些事:文档怎么加载、怎么切分成适合模型阅读的片段、用户问题怎么跟片段拼成提示词、多轮对话的历史怎么保存、模型回答格式怎么控制、调用失败怎么重试、每次请求花了多少钱怎么统计。这些问题如果全部自己手搓,每个项目都要重复一遍,而且很容易踩坑。
LangChain 所做的,就是把这套“模型应用周边的工程问题”沉淀成统一接口和组合方式。你可以理解成:如果说大模型是发动机,LangChain 就是底盘、管线、仪表盘和方向盘,它不负责产生动力,但负责让动力真正跑成一辆车。用官方文档里的话来说,它是一个“基于语言模型构建应用的框架”。
1.2 为什么不能只用“提示词”搞定一切
有朋友跟我说过一句话:现在模型能力这么强,我写个好提示词不就行了,为什么要引入这么多抽象概念?
这个想法在任务简单时是对的。但当你面对这些场景时,提示词是无能为力的:
- 模型不知道你公司内部文档里的内容,因为它没有接受过这些私有数据的训练;
- 模型需要调用数据库查询库存、调用计算器算指标、调用搜索获取实时信息,这些不是提示词能“劝”出来的;
- 业务要求每次回答都走固定的审核流程、记录日志、甚至路由到不同的子模型,这更像企业级流程,而不是一次提示词问答。
LangChain 的思路是把“提示词”扩大为“编排”:模型仍然干它擅长的事,比如理解意图、生成文本、抽取信息,但应用的整体流程由你来定义。刚入门的时候不需要理解全部模块,但至少要知道“每个模块是为了解决哪一类问题而存在的”,这就够用了。
2. 整体架构:从三个层次看懂 LangChain 的设计思路
2.1 模型层、编排层、智能体层
看了很多文档之后,我习惯把 LangChain 分三个层次去理解。第一个是模型层,主要负责跟不同的大模型厂商接口打交道,统一成一套调用方式;第二个是编排层,负责把提示词、模型、输出解析、检索等步骤组合成一条链;第三个是智能体层,让模型自己判断下一步该调用哪个工具、执行哪一步操作。
这三个层次不是并列关系,而是逐渐复杂的递进关系。你写一个最简单的“翻译助手”,只需要模型层就够了;你要做一个“固定流程的文档问答”,需要用到编排层;你要做一个“能自己决定先查数据库、再调模型总结、最后发邮件的助手”,才会真正需要智能体层。
这样分层的好处是:每一次复杂的提升都是增量式的,不会推翻前面已有的代码。刚入门时先专注模型层和编排层,等真正理解了“链”的含义,再去碰智能体,会顺很多。
2.2 核心模块全景速览
LangChain 的模块确实多,但如果只看最核心的几个,关系并不复杂。我经常用一张表来帮朋友建立整体印象:
| 模块 | 解决的问题 | 生活化类比 |
|---|---|---|
| Models | 统一不同模型提供商的调用接口 | 各类电器共用的标准插座 |
| Prompts | 管理提示词模板与变量填充 | 按固定格式填写的表单 |
| Chains | 把多个步骤按顺序编排起来 | 装配线上的工位顺序 |
| Agents | 让模型自主决定下一步动作 | 现场项目经理 |
| Tools | 给模型提供外部能力与接口 | 工具箱里的各种工具 |
| Memory | 保存并管理多轮对话状态 | 记事本或对话档案 |
| Retrievers | 从外部知识库检索相关内容 | 图书管理员 |
| Callbacks | 监控、日志、追踪与事件回调 | 车间里的记录员和监控器 |
刚开始不用急着把每个模块都吃透,先记住一点:前面几个模块解决的是“模型能力怎么复用和组合”,后面几个模块解决的是“模型怎么跟外部世界交互”。
2.3 LCEL:理解“链”是理解一切的关键
LCEL,全称是 LangChain Expression Language,中文一般叫 LangChain 表达式语言。从 LangChain 0.1 开始,官方强烈推荐用 LCEL 来定义链,而不是老版本那种直接继承类的方式。
LCEL 的核心语法其实就是一个管道符|,跟 Unix 管道类似。在命令行里,cat file | grep keyword表示把前一个命令的输出送给后一个命令处理。LCEL 里也是一样的逻辑:prompt | llm | output_parser表示提示词模板输出的内容送给模型,模型输出的内容再送给解析器处理。
用 LCEL 有三个直观好处。一是代码可读性很强,整条链的执行顺序一眼就能看明白。二是框架会自动处理stream(流式输出)、batch(批量调用)、async(异步执行)等并发操作,你不用自己写一堆协程代码。三是每一步都可以被追踪和调试,方便定位问题。
这里值得提一句:很多新手刚开始不习惯 LCEL,反而去看老教程去继承Chain类,结果越写越乱。我的建议是直接学 LCEL,哪怕写的链很简单,也用管道符组合起来,因为这是 LangChain 目前和未来都主推的写法。
3. 核心概念逐个拆解:Models、Prompts、Chains、Agents、Memory、RAG
3.1 Models:一次接入,多处调用
Models 这块,LangChain 抽象出了两种主要类型:LLM 和 Chat Models。LLM 是传统的“文本进、文本出”模型,Chat Models 则是基于消息角色的对话模型,比如用户消息、助手消息、系统消息这样的结构。现在的应用绝大多数都在用 Chat Models。
用 LangChain 定义一个模型,通常只需要指定模型名称和关键参数:
from langchain_openai import ChatOpenAI llm = ChatOpenAI( model="gpt-4o-mini", temperature=0, )这里的重点是:LangChain 统一了不同厂商模型的接入方式。你今天用某家的模型,明天想换成另一家的,或者从云端模型换成本地部署的模型,业务代码不用大面积改动,只需要换掉这个模型对象。这一点在真实的项目里非常值钱,因为模型服务商的报价和性能经常变化,你总想留出替换空间。
3.2 Prompts:把提示词当成正规工程
提示词在 LangChain 里不是一段简单的字符串,而是被当成完整的模板工程在管理。最常用的类是PromptTemplate和ChatPromptTemplate,它们支持定义变量、设置占位符、组织系统消息与人类消息。
from langchain_core.prompts import ChatPromptTemplate prompt = ChatPromptTemplate.from_template( "你是{role}。请用{language}回答下面的问题:\n{question}" ) chain = prompt | llm这样做的好处是:提示词可以被复用、版本化、测试。你不需要在每个调用点重新拼接字符串,也不用担心某个地方漏了角色设定。实际做项目时,我习惯把提示词单独放到一个文件或者配置项里管理,这样产品同事调整措辞时不需要动代码逻辑。
3.3 Chains:把复杂任务拆成流水线
Chains 就是 LCEL 管道符连接起来的整条执行流。你可以只连接一个提示词和一个模型,也可以把多个模型调用串联起来,前一个模型的输出作为后一个步骤的输入。
举个常见的例子,你想做一个“写文案并翻译成英文”的功能,可以这样组织:
from langchain_core.output_parsers import StrOutputParser write_prompt = ChatPromptTemplate.from_template("请写一句关于{product}的宣传语") translate_prompt = ChatPromptTemplate.from_template("请把下面这句翻译成英文:{sentence}") write_chain = write_prompt | llm | StrOutputParser() translate_chain = translate_prompt | llm | StrOutputParser() full_chain = write_chain | translate_chain用管道符串起来之后,整条链的输入是product,输出是翻译后的英文宣传语。中间任何一步要调整,只需要替换对应节点,不会牵一发而动全身。这也是 LangChain 在复杂应用里最有价值的地方:组合性。
3.4 Agents 与 Tools:让模型从“回答问题”到“解决问题”
前面提到的链,执行路径是开发者写死的。但真实场景里,有些任务不到运行时刻你根本不知道需要哪几步。比如用户问“帮我对比三款产品的库存和价格”,模型必须自己去查数据库、调接口、再做计算。这时候就要用 Agent(智能体)。
Agent 的核心思路是:把“选择下一步动作”的权力交给模型。模型根据当前任务,在提供的 Tool 列表里决定调用哪一个,观察返回结果,再决定下一步,循环直到任务完成。
工具接入也很简单,比如你可以给模型注册一个查询库存的函数:
from langchain_core.tools import tool @tool def query_stock(product_id: str) -> str: """根据商品ID查询库存数量""" # 模拟查询库存 return f"商品{product_id}当前库存为20件"然后把这个工具传给 Agent,模型就能在需要时主动调用query_stock。这里最容易犯的错是把所有逻辑塞进提示词里“教”模型做,而不是给它真正的工具入口。记住 Agent 的核心不是话术,是可调用的真实工具。
3.5 Memory:给应用加上“记忆”
默认情况下,模型是无状态的——你每次调用它,它都不记得之前聊过什么。但对话类应用必须记住上下文,于是就有了 Memory。
在 LangChain 里,最简单的方式是把对话历史拼到提示词里。LCEL 里可以通过一个RunnableWithMessageHistory包装器来管理对话历史,也可以用链式结构手动维护历史消息。关键参数是get_session_history,它决定不同用户、不同会话之间的隔离方式。
使用 Memory 时有几个坑要提醒。第一,历史消息不能无限累积,否则很快会超出模型的上下文长度,通常只保留最近几轮;第二,最好不要把原始输入和模型输出都原样存下来,可以考虑只存关键信息或者做摘要;第三,涉及隐私数据时,要注意历史记录的脱敏和清理。Memory 看起来很简单,实际生产里往往是问题最多的地方。
3.6 RAG:用检索补充模型的知识盲区
RAG(Retrieval-Augmented Generation)是目前 LangChain 生态里最热的方向。它的思路用大白话说就是:让模型在回答问题之前,先从一个外部知识库中检索相关资料,再结合这些资料来回答。大模型的训练数据有截止时间,也没有你的私有数据,RAG 就是用来解决这个“知识盲区”的。
一个标准 RAG 流程通常包含四步:
- 加载文档,比如 PDF、网页、数据库里的文本;
- 把长文档切分成片段,并向量化存入向量数据库;
- 用户提问时,先做相似度检索,找到最相关的几个片段;
- 把片段和问题拼进提示词,交给模型生成答案。
LangChain 把这四步对应的组件都抽象出来了:文档加载器、文本分割器、向量存储、检索器、问答链。你不需要从头实现这个流程,只需要把这些组件组合在一起。下一章我会用一个完整示例演示这件事。
4. 实操演示:用一个最小例子把“宏观认识”落地
4.1 场景与前置条件
我准备用一个完全虚构的场景:某公司有一批产品手册的 PDF,希望做一个问答助手,让我根据手册内容回答员工提出的问题,比如“这款设备的日常维护频率是多少”“清洁时要注意哪些事项”。
这个场景很适合第一个 LangChain 项目,因为它覆盖了文档加载、文本切分、向量检索、提示词模板、链式组合这几个核心概念,代码量又不大。开始之前,假定你已经有:
- Python 3.9 以上的环境;
- 一个可用的大模型 API,兼容 openai 接口规范,并配置好环境变量;
- 本地已经安装好下面的依赖。
安装命令如下:
pip install langchain langchain-core langchain-openai langchain-community langchain-chroma pypdflangchain-openai目前不只对接一家特定的模型服务,只要接口风格兼容,也可以通过自定义base_url接入其他模型。为了演示方便,我用ChatOpenAI这个类,实际项目中你可以替换成其他厂商对应的类。
4.2 完整代码演示
把下面代码保存为rag_demo.py,并在脚本同目录放一个名为产品手册.pdf的文件:
import os from langchain_openai import ChatOpenAI, OpenAIEmbeddings from langchain_community.document_loaders import PyPDFLoader from langchain.text_splitter import RecursiveCharacterTextSplitter from langchain_chroma import Chroma from langchain.retrievers.multi_query import MultiQueryRetriever from langchain.chains import create_retrieval_chain from langchain.chains.combine_documents.stuff import create_stuff_documents_chain from langchain_core.prompts import ChatPromptTemplate os.environ["OPENAI_API_KEY"] = "sk-你的密钥" # 如果你接的是兼容openai接口的其他模型服务,可以这样指定端点 # os.environ["OPENAI_BASE_URL"] = "https://你的模型服务端点/v1" llm = ChatOpenAI(model="gpt-4o-mini", temperature=0) embeddings = OpenAIEmbeddings(model="text-embedding-3-small") # 1. 加载文档 loader = PyPDFLoader("产品手册.pdf") docs = loader.load() # 2. 切分文档 text_splitter = RecursiveCharacterTextSplitter( chunk_size=500, chunk_overlap=50, separators=["\n\n", "\n", "。", "!", "?", " ", ""], ) splits = text_splitter.split_documents(docs) # 3. 构建向量库 vectorstore = Chroma.from_documents( documents=splits, embedding=embeddings, persist_directory="./chroma_db", ) # 4. 使用多查询检索器提升召回质量 retriever = MultiQueryRetriever.from_llm( retriever=vectorstore.as_retriever(search_kwargs={"k": 4}), llm=llm, ) # 5. 组装问答链 prompt = ChatPromptTemplate.from_template( """你是产品文档问答助手。请只依据下面给出的上下文回答用户问题。 如果上下文中没有足够信息,请明确回答“文档中没有直接说明”。 上下文: {context} 用户问题:{input} 回答:""" ) combine_docs_chain = create_stuff_documents_chain(llm, prompt) rag_chain = create_retrieval_chain(retriever, combine_docs_chain) # 6. 提问 answer = rag_chain.invoke({"input": "这款设备的日常维护频率是多少?"}) print(answer["answer"])这段代码在常见模型服务都可以跑通,只是不同版本的 LangChain 导入路径可能会有细微差别,安装时留意依赖版本就好。
4.3 代码逐步讲解与原理对应
一步步拆开看。
PyPDFLoader负责把 PDF 解析成文档对象。它的输出结果是一个列表,里面每个元素都包含页面文本内容和元信息。如果你要处理的是网页、Word、数据库里的内容,LangChain 都有对应的加载器,不需要自己写爬虫或者解析逻辑。
RecursiveCharacterTextSplitter的作用是把长文本切成适合模型阅读的片段。为什么一定要切?因为大模型的输入长度有上限,而且检索的精度跟片段大小直接相关:片段太大,检索结果会混入太多无关信息;片段太小,语义又不完整。chunk_size=500表示每个片段大约 500 个字符,chunk_overlap=50表示相邻片段有 50 个字符的重叠,用来避免关键信息恰好被切在边界上。这个值没有绝对标准,通常建议先按 300 到 800 这个范围实验,对比答案质量再调整。分隔符里我特意加了中文句号、感叹号和问号,就是为了避免在中文句中被硬切。
Chroma.from_documents这一步会把切分好的文本用嵌入模型转成向量,并存入本地向量数据库。向量化之后,文本之间的语义相似度就可以用向量距离来度量。后续检索时,系统会把用户问题也转成向量,然后从库里找出最相似的那几个片段。persist_directory表示向量库的存储目录,这样第一次建库之后,后续运行不需要重复处理原始文档。
MultiQueryRetriever.from_llm是一个很实用的增强组件。它会先用大模型把用户原问题改写生成多个不同角度的子问题,然后用这些子问题分别去检索,最后合并去重。这样做的意义在于:用户提问的措辞往往和文档原文不完全一致,比如文档里写的是“保养周期”,用户问的是“多久维护一次”。通过多查询改写,能明显提升召回效果。代价是每次提问会多几次模型调用,成本会略有上升,但绝大多数场景这点成本换来的准确率提升是值得的。
create_stuff_documents_chain用的是“Stuff”策略:把所有检索到的文档一次性塞进提示词,让模型基于完整上下文生成回答。这适合文档片段较少、总体 token 可控的情况。如果检索出来的片段非常多,总量超过了模型上下文窗口,那就得考虑“Map-Reduce”或“Refine”等更复杂的策略。create_retrieval_chain则负责把检索器和问答链串成一条完整链路。最终调用invoke之后,返回的是一个字典结构,里面包含input、context和answer三个字段,其中context就是检索到的文档片段,调试时非常有用。
5. 常见问题、避坑清单与经验建议
5.1 新手最容易踩的坑
我根据自己的使用经验,把常见问题整理成了一张速查表:
| 典型现象 | 可能原因 | 解决思路 |
|---|---|---|
ModuleNotFoundError或导入路径报错 | LangChain 版本不断调整,不同版本的导入路径不同 | 优先看当前安装版本的官方文档,不要照抄旧教程随机贴一个路径 |
| 模型回答明显在编造知识 | 检索结果不相关或上下文缺失 | 调大k、优化文本切分、尝试MultiQueryRetriever |
| 中文回答质量差 | 切分时在句中硬切,导致语义碎片化 | 在分隔符中加入中文句号、感叹号、问号,并调整chunk_overlap |
| 对话几次后报“上下文超长” | 历史消息无限制累积 | 限制历史轮数,或对早期对话做摘要 |
| 向量库重复写入导致检索混乱 | 每次都重新from_documents创建库 | 复用持久化目录,或在运行时先检查库是否已存在 |
项目代码里到处是chain.retry | 没有统一配置重试与错误处理 | 用 LCEL 的with_retry()或自定义回调统一处理 |
表格里列的前三条,几乎是每个 RAG 新手都会碰到的。尤其是第二条“幻觉”问题,很多人一开始以为是模型不行,其实往往是你没把检索到的有效信息交给模型。我实测下来,MultiQueryRetriever配合合适的chunk_size和k,通常能把答案质量提升一大截,这是性价比很高的调整。
5.2 对“是否要用 LangChain”的一点个人建议
要不要在生产项目里用 LangChain,这个话题在社区里面争议不小。反对的人会说:它引入的抽象太多,版本更新又快,不如直接写几百行代码调模型 API。支持的人会说:它把常见组件都封装好了,开发效率高,不用重复造轮子。
我的态度比较实在。如果你只是调用一个模型做一个简单的单轮问答,确实没有必要引入 LangChain,直接用模型厂商的 SDK 就够了。但只要你开始面对多步编排、外部工具、知识库检索、多轮记忆、批量调用这些事情,LangChain 的组件化设计就很有价值。你不需要把它的每一个模块都用上,完全可以只挑其中的 Loader、Retriever、Prompt 来用,照样能省下不少工作量。
另外,建议你在写代码之前先想清楚一个问题:这个项目的核心复杂度到底在哪里?是提示词、是检索、还是工具编排?LangChain 只是工具箱,不是银弹。如果一个场景用原生代码更清晰,就不要硬套框架。
5.3 学习路线与资料选择的建议
如果让我给一个学习 LangChain 的路线,我会这样推荐:
第一步,只看官方文档的“Conceptual Guide”部分,把每个模块是干什么的看一遍,不要深究 API 细节。第二步,照着官方示例,快速跑通一个最简单的链,感受|管道的写法。第三步,选一个自己日常工作或学习里的真实问题,比如“根据我的笔记文件回答提问”,做一个最小可用版 RAG,这比做十个无关 Demo 都管用。第四步,遇到报错时,先看当前安装的 LangChain 版本对应的文档,而不是去搜三五年前的旧教程。
资料选择上,一个常见误区是收集一堆教程却从不看版本号。LangChain 的 API 迭代确实快,这也被很多人吐槽。我自己的习惯是把官方 API 文档当作最终依据,把各种教程当作参考思路,看完之后一定要在本机跑一遍验证。教程里那些代码,经常会因为版本更新变得不能直接运行,这很正常,不是你水平不行。
最后再分享一个我个人的学习习惯:不要试图一次性理解所有功能。LangChain 的官方文档里有几百种集成和工具,人不可能全部记住。每次只带着一个明确任务去查相关部分,遇到什么问题解决什么问题,用过几次之后,整体框架自然就熟了。做技术学习,最忌讳的就是空对空地把文档从头到尾读一遍,然后又忘了开头讲了什么。
实际用下来,LangChain 最大的价值不是某个具体模块,而是它逼着你用“工程思维”去思考大模型应用:数据从哪来,怎么处理,工具怎么接,上下文怎么管,成本怎么控。一旦你的思考方式转过来,再去看其他框架或自己从头实现,都会轻松很多。