想象你是一家连锁奶茶店的老板,正把一家店开成一百家。要让几十家分店做出味道完全一致的杨枝甘露,靠的不是某个老师傅的玄学手感,而是一套标准化的经营体系:后厨按功能分成茶底区、配料区、封口区;每个工位都遵守同一份作业标准;店长会看菜单、调配方、指挥人手;墙上贴着清清楚楚的配方卡。LangChain 就是给大模型应用开设的这样一家“奶茶店总部”——它把模型调用、提示词封装、记忆管理、工具使用这些散落的能力,整理成一套分层清晰、可自由组合、能交给 Agent 灵活调度的框架。这篇文章会带你从奶茶店的视角,逐一拆解 LangChain 的抽象分层、Runnable 协议、Agent 循环、LCEL 表达式、模块化扩展,以及版本演进中那些让人拍大腿的坑。读完你会发现:所谓框架设计,不过是把“开店”的常识变成了代码。
如果你是 AI 应用工程师,理解 LangChain 的架构骨架,意味着你不再被 API 的表象牵着走——能读懂框架的取舍,能写自己的扩展,能在团队里设计出可靠、省钱的 LLM 应用。
可视化演示
展示LangChain四层抽象架构、Runnable统一协议接口(invoke/batch/stream),以及LCEL管道表达式声明式链路编排。
本节配有交互动效(进入视口后自动加载,约 3KB)。
四层抽象架构:Chain > Agent > Retrieval > Model CHN=链路层,编排组合 AGT=Agent层,工具调用 RET=检索层,RAG向量检索 每层独立演进,可替换实现
抽象分层设计
奶茶店的后厨从不一锅乱炖。茶底区只负责煮茶,配料区只负责备料,封口机只负责封装。店长绝不会让煮茶工去收银,也不会让收银员进后厨炒糖。每个功能区各管一摊、互不越界,整家店才能同时出几百杯单而不乱。这种“职责分离”是任何复杂系统能长大的第一块基石。
LangChain 的第一层设计智慧,正是这种后厨分区。它把一次 LLM 应用拆成四个职责清晰的层:模型层(Model)负责接入各家大模型,提示层(Prompt)负责管理提示词模板,记忆层(Memory)负责保存对话上下文,工具层(Tool)负责把外部能力注册进来。四个层各有标准接口,上层只依赖接口、不依赖具体实现。
模型层中,ChatOpenAI、ChatAnthropic 这些类都实现了统一的 BaseChatModel 接口;提示层用 PromptTemplate 把用户消息模板化、注入变量;记忆层用 ConversationBufferMemory 等组件在每次请求前把历史会话回填进上下文;工具层则通过 @tool 装饰器把任意 Python 函数暴露给模型。分层带来的直接好处是“换件不换机”:今天用 OpenAI,明天换成国产模型,只改模型层一行代码;想加长期记忆,也无需改动提示词和工具。
回到后厨:订单从收银台出发,先经配料柜补料、再被会员本写上“少糖”备注,最后由茶桶做出成品。每一站只认上一站递来的东西,但整张订单却因此井然有序——分层,让“复杂”变成了“各司其职”的简单。
- 上下文汇聚把用户输入 x、记忆 K、工具列表 T 汇集为当前上下文
- 提示词格式化提示层按模板 P 把上下文渲染为完整提示 p
- 模型推理模型层以 p 为输入,生成最终应答 y
- 四层复合一次请求等价于四个层的函数复合
Chain与Runnable
奶茶店要推一款“杨枝甘露”,店长写了张标准作业卡:先打茶底,再加芒果,接着倒椰浆,最后封口。四个工位无论谁来上班,都按同一份约定传递半成品:接料、加工、递给下一站。这份“接料-加工-传递”的约定,就是整条流水线的灵魂。
在 LangChain 里,这份约定叫 Runnable 协议。任何对象只要实现了 invoke(执行一次调用)就能入链;基类还会基于 invoke 自动派生出 batch(批量)和 stream(流式)能力。一条链(Chain)本质上就是一个 Runnable 的组合体——老版本里 Chain 是一个抽象类,新版本里它被彻底“协议化”了:一切皆 Runnable。
RunnableSequence 把多个 Runnable 顺序串起来,它的 invoke 内部会自动把上一个节点的输出作为下一个节点的输入。正因所有组件都遵守同一套输入输出约定,你才可以像拼管道一样自由拼接。为了衔接普通函数,框架还提供了 RunnableLambda、RunnablePassthrough 等适配器,把任何 Python 函数包装成 Runnable。这样,业务里的文本清洗、格式转换,也都能成为流水线上的一站。
有了这份“接料-加工-传递”的作业标准,换掉任何一个工位都不影响其他工位:把椰浆工换成芝士工,前端的茶底工和后端的封口工完全无感。这就是协议的力量——它让流水线既不挑人,也不怕换人。
- 首节点消费输入组件 A 接收原始输入 x,产出中间结果 a
- 中间节点接力组件 B 以 a 为输入继续加工,产出 b
- 末节点产出结果组件 C 消费 b,输出最终结果 y
- 整链等价复合整条链等价于复合函数,写作嵌套调用
Agent抽象机制
流水线再标准,也怕遇到“来杯不酸的”这种模糊需求。这时店长就得亲自出马:先打量店里有什么料,试做一杯让顾客尝,不行再换配方,直到满意为止。店长和普通工位最大的区别是——他会观察、会决策、会动手验证,还会根据结果调整下一步。
LangChain 里的 Agent 就是这位店长。它不是一个固定的流程,而是一套“推理-行动”循环:把当前状态(任务描述 + 已知信息 + 可用工具清单)交给 LLM,LLM 决定调用哪个工具或直接回答;工具返回结果后,状态被更新,再次交给 LLM……如此反复,直到任务完成。最经典的范式叫 ReAct(Reasoning + Acting,推理 + 行动)。
AgentExecutor 负责调度这个循环;用 create_tool_calling_agent 可以把工具列表绑定进提示词,让模型输出结构化的工具调用指令。每个“思考”都是一次模型调用,每个“动手”都是一次工具执行,所以 Agent 的成本通常远高于同等复杂度的 Chain。更进阶的思路是规划器-执行器分工:规划器把大任务拆成子任务清单,执行器逐个完成,适合多步骤复杂场景;多个 Agent 还能组成团队协作。
店长每想一步,脑力成本就涨一分;所以奶茶店只有遇到标准流程搞不定的单子,才舍得让店长出马。这也提醒工程师:能用 Chain 解决的事,别轻易上 Agent——会思考的店员,时薪贵着呢。
- 局面画像状态 s_t 里装着任务、历史动作与观察结果
- 策略决策LLM 充当策略网络,从局面中选出下一个动作
- 工具反馈动作交给工具执行,得到观察结果 o_t
- 状态更新循环新动作与观察并入状态,直到模型输出最终答案
LCEL表达式
传统后厨靠班长口头传达流程,人一多就乱。这家店的墙上却贴着配方卡,一行行竖线把工序串起来:“茶底 | 芒果 | 椰浆 | 封口”。配方卡只描述“成品是怎么一步步做出来的”,至于每个工位是真人还是机器,它一概不管。这就是声明式的魅力。
LCEL(LangChain Expression Language,LangChain 表达式语言)就是这张配方卡。它用管道运算符 | 把组件声明式地串起来:prompt | llm | parser 一行代码就构造出一条完整的可执行链。LCEL 还支持 RunnableParallel 做并行分支、用 .bind 绑定参数、用 .batch 自动批处理。声明式的最大价值在于:同一段配方,既能流式吐字,也能批量生产,还会在运行时自动做优化。
在底层,LCEL 的 | 并不是什么魔法——它是 Runnable 重载的 or 方法:a | b 返回一个 RunnableSequence。表达式被编译为可执行图,自动处理输入键的分配:上一个组件输出的哪些字段传给下一个。框架还内置了追踪能力,每次调用都记录中间结果,方便调试定位。理解了这一层,你就明白为什么官方强烈推荐“能写 LCEL 就别手写 for 循环”。
店长看配方卡,一眼就知道“芒果 → 椰浆 → 封口”的顺序;至于工位是自动机器还是新手店员(流式还是批处理),配方卡完全不关心。把流程写清楚、把执行交给系统,这正是声明式表达带来的从容。
- 左组件先执行竖线左侧的 A 先处理输入 x
- 右组件再消费竖线右侧的 B 以 u 为输入继续加工
- 整体等价复合a | b 整体等价于复合函数
- 批处理自动展开对一组输入,管道自动逐条执行
模块化扩展
生意红火后,店里要加一款“芝士葡萄”。好消息是,不用重装后厨:买一台新奶酪泵,把接口接到现有管路上;调一杯新品,配方卡上新增一行;后期还想监控每台设备的耗时,再在每个工位装个计时器。开店三件套解决,后厨照常运转。
LangChain 的扩展性也靠三件套。第一件是自定义组件:用 @tool 装饰器把任意函数注册成工具,或用 RunnableLambda 把函数包成 Runnable,它们就能无缝接入链路。第二件是回调系统:CallbackHandler 可以在链路的开始、结束、出错处挂上钩子,统一做日志、监控、限流。第三件是配置注入:通过 .bind 或 config 参数把运行时配置传进组件,而不需要改动组件定义。
组合复用是模块化的终极形态:小 Runnable 拼成大 Runnable,大 Runnable 又作为零件拼进更大的链路。官方生态也一样分层演进——需要复杂图状编排时用 LangGraph,大多数业务用 LCEL + 自定义组件就足够。模块化的工程收益很实在:组件可跨链路复用,减少重复开发,也减少重复的 token 消耗。下面用计费公式感受一下成本结构。
新奶酪泵接上就能用,是因为它的接口跟行业标准一致;你的自定义函数能被 Agent 调用,是因为 @tool 注册时遵守了 LangChain 的工具协议。模块化的本质,就是“接口统一加上即插即用”——后厨如此,框架亦然。
- 单节点计费第 i 个节点的成本等于单价乘以输入输出 token 总量
- 全链路求和一条链 n 个节点的总成本是所有 c_i 的和
- 复用零增量成本复用现成组件时,开发成本不增加
- 规模线性增长组件越多,成本随节点数线性上升
版本演进与反模式
奶茶店开了三年,店长没少踩坑:刚开店时,每款新品都让老师傅写一份“独家秘籍”,结果老师傅一走,配方就失传;后来统一成配方卡,才把分店开到全国。LangChain 的版本史,几乎就是这段“从独家秘籍到标准作业卡”的进化史。
0.x 时代的 LangChain,Chain 家族庞大臃肿:LLMChain、SequentialChain、各种 Agent 类各写各的样板代码,抽象混乱、学习成本极高。0.1 到 0.2 的过渡期,官方把核心抽象收敛到 Runnable + LCEL;1.0 时代,老式 Chain 逐步退场,RunnableSequence 全面上位,AgentExecutor 也把舞台让给了更通用的图编排方式(LangGraph)。版本演进的核心关键词就四个字:接口统一。
演进路上也留下了不少反模式供后人避坑。最常见的有:把整个业务逻辑塞进一个巨型 prompt 和一次调用,结果出错了根本不知道是哪句话带偏了模型;在链外手工拼字符串来“伪装”LCEL,丢了流式和追踪能力;忽略回调系统,等线上出问题才靠 print 大海捞针;滥用 .bind 传参,导致组件状态在并发请求间泄漏。最佳实践则是:小步组合、显式声明数据流、入口统一做错误处理、用 LangSmith 之类工具做全链路追踪。为什么串行链路要尽量短?用成功率公式说话。
老店长最后总结:配方卡越短越好,一张卡上写满二十行的大长篇最容易翻车;能并行的步骤就并排做——两台机器同时打茶和切芒果,整杯茶出得又快又好。设计框架如此,设计业务链路亦然。
- 单步成功率第 i 步的失败概率为 f_i,成功概率为
- 串行相乘整条链路成功要求每步都成功
- 长链趋于失效当每步成功率小于 1 时,链越长整体越接近 0
- 缩短串行路径把可并行步骤拆开,让关键路径更短
AI 应用场景
企业内部知识库问答机器人
阶段:应用编排(RAG 链路组装)
解决的问题:传统做法是把“检索 → 生成”写成一堆散落函数,参数传递靠人肉维护,一旦要换向量库或加流式输出就大改。用 Chain/Runnable 把“查询改写 → 向量检索 → 结果重排 → LLM 生成 → 引用格式化”串成一条 LCEL 链路,每步都是独立可替换的组件,还能自动获得流式与追踪能力。
具体案例:某互联网公司的 HR 问答机器人,原来每次回答都要重读整份制度文档,token 成本高。工程师把链路拆成“意图识别 → 制度库检索 → 精炼生成”三个 Runnable,并在回调里统计每步耗时与 token;上线后单次问答成本下降 60%。
电商客服工单自动处理 Agent
阶段:智能体开发(工具调用 + ReAct 循环)
解决的问题:固定链路无法应对“我的订单少了一件还延迟了”这类复合问题。用 Agent 抽象把查订单、查物流、发起退款等接口注册为工具,Agent 在 ReAct 循环中自己决定先查什么、再做什么,客服只需审核最终处置。
具体案例:某电商平台的大促期间,退款量暴涨。团队用 create_tool_calling_agent 接入订单与库存工具,Agent 循环中自动完成“核对订单 → 检查库存 → 生成补偿方案”,把人工工单处理时长从 12 分钟压到 3 分钟。
多模型批量评测流水线
阶段:模型评估与优化(模块化扩展)
解决的问题:评测不同模型时,评测逻辑和模型调用耦合在一起,每次换模型都改评测代码。把“取样本 → 模型回答 → 评分器打分”拆成模块化 Runnable 链路,模型层只换不修,评分器通过回调系统挂载。
具体案例:某大模型团队每两周要对 8 个候选模型跑一遍 5000 条评测集。他们用 LCEL 声明链路,模型层按配置注入自动切换,回调记录每条样本的耗时与分数,整条流水线复用同一份评测逻辑。
Python 演示
class Runnable: """迷你 Runnable 协议: 实现 invoke 的对象都可入链""" def invoke(self, x): raise NotImplementedError def __or__(self, other): return RunnableSequence(self, other) class RunnableSequence(Runnable): def __init__(self, *steps): self.steps = list(steps) def invoke(self, x): y = x for step in self.steps: y = step.invoke(y) return y def __or__(self, other): self.steps.append(other) return self class Prompt(Runnable): def __init__(self, template): self.template = template def invoke(self, x): return self.template.format(topic=x) class UpperParser(Runnable): def invoke(self, x): return x.upper() class TokenCounter(Runnable): def __init__(self): self.gc_count = 0 def invoke(self, x): self.gc_count += len(x) return x def main(): prompt = Prompt("请用一句话介绍{topic}:") chain = prompt | UpperParser() | TokenCounter() topics = ["LangChain", "Agent", "LCEL"] for topic in topics: print(chain.invoke(topic)) print("累计处理字符数:", chain.steps[-1].gc_count) if __name__ == "__main__": main()
__or__ 重载,实现 prompt | parser | counter 的声明式链路组装与数据流。def __or__(self, other):重载竖线运算符,让a | b变成组合链return RunnableSequence(self, other)组合结果仍是一个 Runnable,可继续被拼装y = step.invoke(y)LCEL 的数据流核心:上一节点输出喂给下一节点class TokenCounter(Runnable):自定义组件只需实现协议即可入链self.gc_count += len(x)每经过一次就累加字符数,演示有状态组件chain = prompt | UpperParser() | TokenCounter()一行声明式代码组装整条链路
常见误区
误区“我要先学 Chain,再学 LCEL,它们是两个不同的 API,写代码时得二选一。”
正解在新版 LangChain 里,Chain 就是 Runnable 的组合体(RunnableSequence),LCEL 只是声明式书写这种组合的语法。用 prompt | llm | parser 写的链,底层和手写 RunnableSequence 完全等价。就像“作业标准流程”和“配方卡”描述的是同一条流水线,只是表现形式不同。
为何常见:0.x 时代 Chain 单独成体系,网上老教程满天飞;版本升级后文档变了,但很多人的知识还停留在旧版。
误区“Agent 会思考会调用工具,肯定比死板的 Chain 强,所以任何需求都应该用 Agent 实现。”
正解Agent 每“思考”一轮就要调用一次 LLM,成本远高于固定链路。像“格式化输出、固定步骤检索”这种确定性需求,Chain 又快又稳又省钱;只有需要动态决策的任务(如多步骤工具调用)才值得上 Agent。店长虽然厉害,但每杯茶都让店长亲手做,店就亏死了。
为何常见:Agent 是热点概念,宣传多;而成本与稳定性这类工程约束,教程往往讲得少。
误区“`prompt | llm` 只是让代码更好看,跟位运算或装饰器差不多吧?”
正解竖线是 Runnable 重载的 __or__ 方法,执行时会创建一个 RunnableSequence,并且按声明顺序在运行时逐个 invoke,还配套流式、批处理和追踪。它不是装饰器,更不是位运算——是一等公民的“组合操作符”。
为何常见:| 在 Python 里最常见的是位或,看到在链路上也用 | 很容易先入为主。
误区“我的业务有 if-else、有并行,LCEL 只会一路竖线串到底,根本不够用。”
正解LCEL 有 RunnableParallel 做并行分支,有 RunnableBranch 做条件路由,还可以把任意 Runnable 当作参数传给另一个 Runnable,组合出 DAG 乃至图。顺序只是它最简单的一种形态。
为何常见:教程示例大多以 prompt | llm 开场,容易让人以为这就是 LCEL 的全部。
误区“代码能跑就行,回调、追踪是锦上添花,以后有时间再加。”
正解多节点链路中,问题可能出在提示词、模型、解析器、工具任意一环;没有全链路追踪,几乎不可能靠 print 定位。回调正是这种“定位能力”的基础设施,应该在写第一条链路时就接好。后厨不装计时器,永远不知道慢的是哪个工位。
为何常见:单机跑 demo 时链路短、出错直观,追踪的价值被严重低估;一上生产就原形毕露。
面试题
Q1. 关于 LangChain 的 Runnable 协议,下列说法正确的是?
- A.只要实现 invoke 方法即可入链← 正确答案
- B.必须同时实现 invoke 和 stream
- C.四个核心方法一个都不能少
- D.只要求实现 call 方法
答案:A。Runnable 基类以 invoke 为唯一必须实现的方法,并自动派生出 batch、stream 等默认实现;bind 属于配置能力而非入链门槛。像奶茶店工位只需遵守“接料-加工-传递”这一条约定就能上岗,其他能力都可以由总店(基类)补齐。
Q2. 在 LCEL 中,prompt | llm 这个表达式的本质是什么?
- A.等价于 Python 的按位或
- B.组合返回一条新的 Runnable 链← 正确答案
- C.立即触发一次模型推理
- D.只是语法糖没有实际作用
答案:B。| 是 Runnable 重载的 __or__ 方法,返回的是一个 RunnableSequence——它是一条可以后续继续拼装、invoke 的链,并不会立刻调用模型。这就像配方卡上写“茶底 | 芒果”只是把工序画出来,并不会真的做出一杯茶。
Q3. 下列关于 Agent 与 Chain 的区别,正确的是?
- A.Agent 一定比 Chain 回答得更准确
- B.Agent 会在循环中决策并调用工具← 正确答案
- C.Chain 可以自主选择调用任何工具
- D.Agent 不需要绑定任何大模型
答案:B。Agent 的核心是“推理-行动”循环:每轮由 LLM 决策、调用工具、观察结果再决定下一步;Chain 是固定流程,不会自主选择工具。就像店长会根据顾客反馈调整配方,而流水线工人只会照作业卡执行,谈不上谁一定更准,但 Agent 的灵活来自循环,不来自“更聪明的模型”。
Q4. 在 ReAct 循环中,第 t 轮的状态更新方式正确的是?
- A.每轮都从零开始重新构建状态
- B.只保留最近一轮的行为与结果
- C.新动作与观察结果追加进旧状态← 正确答案
- D.直接覆盖并丢弃全部历史信息
答案:C。状态更新公式是 s_{t+1} = s_t ∪ {a_t, o_t},即把本轮动作和观察追加进既有状态。像店长试做新配方时会记住“顾客嫌太酸”这个反馈,而不是丢掉前面的尝试重来——历史是 Agent 决策的依据。
Q5. 关于 LangChain 的模块化扩展,下列说法正确的是?
- A.自定义组件必须继承官方 Chain 类
- B.普通函数经装饰器即可注册为工具← 正确答案
- C.回调系统只适用于官方内置组件
- D.扩展组件必须修改框架源码
答案:B。用 @tool 装饰一个 Python 函数,它就能作为工具进入 Agent 的提示词和调用循环;用 RunnableLambda 包装函数即可入链。模块化扩展的核心是“接口统一、即插即用”,不需要继承特定 Chain 类,也不需要动框架源码——就像新奶酪泵只要接口标准就能接进后厨管路。
Q6. LCEL 声明式写法相比手写 for 循环的最大收益是?
- A.运行速度一定快十倍以上
- B.自动获得流式批处理与追踪← 正确答案
- C.可以完全替代所有编程语言
- D.不需要大模型也能独立运行
答案:B。LCEL 表达式在底层被编译为可执行图,框架自动提供 stream、batch 以及中间结果追踪,这些是手写 for 循环难以白拿的能力。声明式配方卡的价值不在“跑得快”,而在“执行交给系统、人只管描述流程”。
Q7. 一条五节点串行链路,每步成功率为 0.9,整条链成功率约为?
- A.约等于 0.90
- B.约等于 0.95
- C.约等于 0.59← 正确答案
- D.约等于 0.81
答案:C。串行链路成功率等于每步成功率的乘积:0.9^5 = 0.59049,约 0.59。这正是反模式公式 P = ∏(1 - f_i) 的含义——每步都可靠的流水线,串太多站后整体也会变得脆弱。这就是为什么配方卡要短、能并行要并行。
Q8. 生产环境中最推荐用哪种方式观测一条多节点链路?
- A.在每个节点里手工打印耗时
- B.用回调与追踪平台采集指标← 正确答案
- C.用线程定时器监控整个进程
- D.直接让模型输出运行时长
答案:B。CallbackHandler 可以在节点开始、结束、出错时统一采集耗时、token 与中间结果,再交给 LangSmith 等追踪平台展示。手工 print 在链路变长后无法关联上下文,线程监控只能看到进程级信息,靠模型自报时长更是不可靠——后厨要装的是每个工位的计时器,不是门口的秒表。
Q9. Runnable 协议提供的核心能力有哪些?(多选)
- A.同步执行 invoke← 正确答案
- B.流式输出 stream← 正确答案
- C.批量处理 batch← 正确答案
- D.指令编译 compile
答案:ABC。Runnable 协议的核心能力是 invoke(同步)、stream(流式)和 batch(批量),三者由基类统一提供。compile 不是 Runnable 协议的能力——那是机器学习编译优化里的概念。协议三件套对应工位的三种工作模式:做一杯、边做边出、一次做一批。
Q10. 下列关于 LangChain 版本演进的表述,正确的有哪些?(多选)
- A.老式 Chain 类已被逐步废弃← 正确答案
- B.Runnable 协议统一了组件接口← 正确答案
- C.LCEL 与 Chain 是两套互斥体系
- D.AgentExecutor 仍是官方唯一推荐
答案:AB。1.x 时代老式 Chain 类淡出,Runnable + LCEL 成为统一接口与声明式写法,A、B 正确。LCEL 是构建 Chain 的语法,二者并不互斥,C 错误;AgentExecutor 已把舞台让给更通用的图编排方式(LangGraph),并非唯一推荐,D 错误。这段演进如同奶茶店从“老师傅独家秘籍”走向“标准配方卡”——不是推翻做奶茶,而是把做法统一成可复制、可追责的体系。