Chapter 02 · LangChain v1.0
工作原理与设计权衡
上一章建立了 Runnable / LCEL 概念——这章讲它们为什么这么设计,以及 v1.0 之后 agent 到底该怎么写。基于 LangChain v1.0 / LangGraph v1.0(2025-10-22 发布),写于 2026-06。下文代码用来演示分界判断,均未在本机执行(标 2026-06)。
本章你要建立的心智模型
- Runnable 的统一接口不是为了省打字,而是把"组合性"和"四项默认能力"从每个组件里抽到协议层一次性实现——能力来自接口,不来自组件。
- LCEL 的
|只擅长一件事:线性、无状态的 DAG。一旦出现分支、循环、检查点、人工中断,表达力就到顶,必须下沉到 LangGraph。 - Agent 入口经历三次迁移(
AgentExecutor→create_react_agent→create_agent),每次都是为了把 agent 循环从黑盒变成可控的状态图。 - v1.0 之后 LangChain 与 LangGraph 不是二选一:
create_agent是跑在 LangGraph 执行引擎上的高层入口,要更多控制就下沉到StateGraph——同一条连续谱的两端。
·整体架构:一层协议 + 一道下沉边界
把这一章所有机制放进一张图,LangChain v1.0 的形状就清楚了:底层是 Runnable 协议,| 在协议上把组件组合成 LCEL 链;当线性链表达不了的需求出现,整条管道就"下沉"到 LangGraph 执行引擎——而 create_agent 正是已经站在那条边界另一侧的高层入口。
prompt → | → 链 是最常走的线性热路径,绝大多数 RAG / 抽取任务停在这里。
数据如何流——所有框(含 LangGraph 引擎的产物)都垂直连回最底下的 Runnable 协议层,说明无论上层是链还是 agent,对外都还是同一种 Runnable 调用形状。2.1Runnable 设计机制:能力为什么长在接口上
统一接口先逼出组合性,组合性再让 stream / batch / retry / fallback 只需在协议层实现一次。
01 章把 Runnable 当词汇引入(见 §1.1):一个统一接口,凡实现它的对象都有 invoke / stream / batch。这一节回答更深的问题——为什么把接口统一,就能让一连串高级能力"自动"出现。
运行方式拆成两步因果链。第一步,统一接口逼出组合性:因为 prompt、model、parser 都暴露同一种"吃一个输入、吐一个输出"的形状,RunnableSequence(也就是 a | b | c 编译出的对象)才能把它们首尾相接,而不必关心每一段内部是什么。第二步,组合性让能力下沉到协议层:RunnableSequence 实现 batch 的方式,是对它持有的每一个子 Runnable 依次调用各自的 batch;它实现 stream 的方式,是把上一段的流式输出接到下一段的流式输入。于是只要协议层把"组合体如何 batch、如何 stream、如何 retry、如何 fallback"定义清楚一次,任意一条新链——哪怕组件是你今天刚写的——立刻就拥有这四项能力。能力是接口的性质,不是组件逐个攒出来的功能。
# 演示:能力来自组合结果,不来自任一组件(2026-06,未本机执行)
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain.chat_models import init_chat_model
prompt = ChatPromptTemplate.from_template("一句话解释 {topic}")
model = init_chat_model("openai:gpt-5.4")
parser = StrOutputParser()
chain = prompt | model | parser # 编译出一个 RunnableSequence
# .batch 不是 prompt 的、也不是 model 的方法;
# 它是 chain(组合结果)的方法 —— 协议层统一实现的。
results = chain.batch([
{"topic": "向量检索"},
{"topic": "重排序"},
{"topic": "提示注入"},
])
# 三条输入并发跑完,返回三个字符串。没有任何组件单独写过 batch 逻辑。
chain 是 RunnableSequence;| 编译的产物。
.batch 定义在协议层,对每个子 Runnable 各调一次它自己的 batch —— 这就是"组合性让能力下沉"的字面含义。
| 方案 | 四项能力如何获得 | 为什么没选 / 代价 |
|---|---|---|
| 无统一接口:每个组件各写一套 | prompt 写一遍 stream/retry/batch,model 再写一遍,parser 又写一遍,组合时手动把它们的胶水拼起来。 | N 个组件 × 4 项能力 = 4N 份重复实现;新加一个组件就得补四份;组合时还要手写串接逻辑,错误率随链长增长。 |
| 继承一个臃肿基类 | 所有组件继承同一个父类,能力写在父类里。 | 能力是有了,但继承把"是什么"和"能做什么"绑死;多重能力要靠多继承或混入,组合两个不同子树时类型很快失控。 |
| Runnable 协议 + 组合在协议层实现 | 组件只需满足接口;RunnableSequence 在协议层统一定义"组合体如何 stream/batch/retry/fallback"。 |
选中。能力实现一次、复用到所有链。代价见下方"带来的代价"。 |
抽象税:统一接口要求每个组件的输入/输出被塞进"一进一出"的形状。当某个步骤天然需要多路输入或副作用(比如同时读两个上游、又要写日志),就得用 RunnableParallel、RunnablePassthrough 这类胶水把它掰回单值形状,读代码的人要先在脑子里反编译这些胶水。
调试纵深:能力下沉到协议层的另一面,是出错时栈帧也下沉。一条 a | b | c 抛异常,栈顶往往停在 RunnableSequence.invoke 里,而不是你写的某一行——真正的定位要靠 trace(LangSmith)回放每一步的输入输出,而不是读栈。这就是"能力来自接口"的账单:收益统一了,故障现场也统一到了接口层。
把 parser 换成一个普通 Python 函数 lambda s: s.strip(),写成 prompt | model | (lambda s: s.strip())。这条链还有 .batch() 吗?那个 lambda 自己并没有 batch 方法。
展开答案(先停 10 秒再点)
有。| 在遇到普通可调用对象时,会自动把它包成 RunnableLambda——于是它也满足 Runnable 接口,组合体照样是 RunnableSequence,照样在协议层拥有 batch。
这道题指向的设计洞察:"实现 Runnable"的门槛被压到几乎为零,任何函数都能被自动抬升进协议。正因为门槛这么低,统一接口才能真的覆盖整个生态,而不是只覆盖官方组件——组合性的普适,靠的就是这种"零成本入会"。
2.2LCEL 何时够、何时下沉 LangGraph
LCEL 只表达线性无状态 DAG;出现分支 / 循环 / 检查点 / 人工中断 / 中途可观测,就到了边界,要换 LangGraph。
运行方式:| 编译出的 RunnableSequence 本质是一张有向无环图——数据从入口单向流到出口,每个节点执行一次,没有回头边。RunnableParallel 能让它分叉再汇合,但仍然无环、仍然每节点一次。这意味着 LCEL 能优雅表达的,恰好是"输入 → 一串确定的变换 → 输出"这类线性、无状态的流程:prompt 拼装、模型调用、解析、RAG 的检索-拼接-生成,都落在这里。
边界从哪里开始?当流程需要"根据中间结果决定下一步走哪条路"(条件分支)、"反复执行直到满足某条件"(循环,agent 的工具调用循环就是典型)、"把执行暂停、存盘、之后恢复"(检查点 / 持久化)、"中途交给人确认再继续"(human-in-the-loop,下称 HITL)、或"逐节点观测与回放状态"——这五类需求里任意一条出现,DAG 模型就表达不了了。因为它们都要求"带状态地、允许成环地"推进,而这正是 StateGraph 的定义域:显式的状态对象、节点、可以成环的边、内建检查点。
| 需求信号 | LCEL 能做到吗 | 该停在哪 |
|---|---|---|
| 输入 → 固定变换 → 输出(RAG、抽取、改写) | 能,这是它的主场 | LCEL | |
| 多路并行后汇合(同时查两个检索器) | 能,用 RunnableParallel |
LCEL | |
| 按中间结果选不同分支 | 勉强(RunnableBranch),但分支一多就难读、无法回看状态 |
下沉 LangGraph |
| 循环执行到满足条件(agent 工具循环) | 不能,DAG 无环 | 下沉 LangGraph |
| 暂停 / 存盘 / 恢复(长任务、断点续跑) | 不能,LCEL 无状态、无检查点 | 下沉 LangGraph |
| 中途交人确认(HITL)、逐节点观测 | 不能,链是一次性穿过的黑管 | 下沉 LangGraph |
下沉不是免费升级:换到 StateGraph 后,原本一行 a | b | c 要拆成"定义状态 → 注册节点 → 连边 → 编译"四步,代码量和概念量都上一个台阶。为一个本可线性表达的流程过早下沉,等于给简单问题付了状态机的税。
误判边界的两个方向都疼:该下沉却硬留在 LCEL,会写出层层嵌套的 RunnableBranch + 外层 Python while 手动驱动循环,既失去检查点又难调试;不该下沉却提前用 StateGraph,则把一条直管包装成一张只有两个节点的图,徒增读者负担。判准始终是图 2.1 那一个问句,不是"显得更高级"。
有人用 LCEL 写了一个"问答 → 若答案含'不确定'就再问一次模型、最多重试三轮"的流程,外面套了个 Python for 循环驱动这条链。这能跑,但它踩到边界了吗?踩在哪条信号上?
展开答案(先停 10 秒再点)
踩到了,踩在"循环执行到满足条件"这条信号上。链本身仍是无状态 DAG,真正的循环逻辑被挤到了链外面的 Python for 里——这正是"该下沉却留在 LCEL"的症状:循环控制和重试计数游离在框架之外,没有检查点、无法在第二轮中途暂停或观测,换人接手要同时读链和外层循环两套逻辑。
设计洞察:当你发现自己在框架外面手写 while / for 来驱动一条链,几乎总是边界信号——循环属于状态机,应该进 StateGraph 的边,而不是留在宿主语言里。
2.3Agent API 三次迁移:从黑盒到可控状态图
三代入口同一条主线——把 agent 的"思考-调用工具-再思考"循环,从藏在黑盒里逐步搬到可观测、可干预的状态图上。
运行方式:agent 的内核都是同一个循环——模型决定调哪个工具、执行工具、把结果喂回模型、再决定下一步,直到模型认为任务完成。三代 API 的差异不在这个循环本身,而在这个循环对你是否透明、能否在中途干预。第一代 AgentExecutor 把循环封在内部,你只能从外面传工具、收结果,循环中途的状态拿不到、改不了。第二代 create_react_agent(来自 langgraph.prebuilt)把循环重写成一张 LangGraph 图,循环第一次变成可观测、可加检查点的对象。第三代 create_agent(来自 langchain.agents)在第二代的图之上加了 middleware(中间件)层,让你能在循环的具体节点前后插入自定义逻辑,而不必亲手改图。
# 三代 agent 入口对照(2026-06,未本机执行)
# 第一代 —— AgentExecutor(黑盒循环,维护到 2026-12)
# from langchain.agents import AgentExecutor, create_tool_calling_agent
# agent = create_tool_calling_agent(model, tools, prompt)
# executor = AgentExecutor(agent=agent, tools=tools) # 循环封在内部,中途状态不可见
# 第二代 —— create_react_agent(已弃用,原在 langgraph.prebuilt)
# from langgraph.prebuilt import create_react_agent
# agent = create_react_agent(model, tools) # 循环已是 LangGraph 图,但入口被 v1.0 迁走
# 第三代 —— create_agent(当前推荐入口)
from langchain.agents import create_agent
agent = create_agent("openai:gpt-5.4", tools=tools) # 跑在 LangGraph runtime 上;
# 可挂 middleware 在循环节点前后插逻辑
result = agent.invoke({"messages": [{"role": "user", "content": "查一下今天的汇率"}]})
create_agent 来自 langchain.agents,模型可直接用 "provider:model" 字符串。
middleware 是第三代相对第二代的关键增量——把"改循环行为"从"改图"降级成"挂钩子"。
| 入口 | 定位 / 来源 | 为何被取代 / 现状 |
|---|---|---|
AgentExecutor |
第一代;langchain.agents(经典) |
循环是黑盒,缺循环控制、无法观测或在中途干预;遗留功能迁入 langchain-classic,维护到 2026-12,新代码不该从这里起步。 |
create_react_agent |
第二代;langgraph.prebuilt |
首次把循环做成 LangGraph 图(解决了黑盒问题),但 v1.0 把增强后的入口统一搬到 langchain.agents,此函数已弃用,仅留向后兼容。 |
create_agent |
第三代;langchain.agents |
当前推荐入口。跑在 LangGraph runtime 上,支持 middleware 做逐步控制,"Agent = Model + Harness"。要更深控制时下沉 StateGraph(见 §2.4)。 |
三次迁移本身就是代价:三年里 agent 入口换了三次包名和签名,意味着网上大量教程、Stack Overflow 回答、甚至自动补全里仍充斥前两代写法。判断一段 agent 代码的"新旧"成了评审的常规动作——看到 AgentExecutor 或从 langgraph.prebuilt 导入 create_react_agent,基本可判定是该迁移的旧码。
抽象上移换来控制下移:create_agent 把常见循环行为收进 middleware,日常很省事;但当需求超出 middleware 能表达的范围(比如要彻底改变循环的拓扑、加多个并行子图),高层入口反而成了天花板,必须放弃它、回到手写 StateGraph。便利和控制在这一代被显式地分到了两层。
评审时看到一段 2024 年的代码:from langgraph.prebuilt import create_react_agent。它能跑(向后兼容还在),那要不要拦下来要求改?理由是什么?
展开答案(先停 10 秒再点)
要拦。它不是"错",但它是第二代弃用入口——v1.0 已把增强后的 agent 统一到 langchain.agents.create_agent。继续用它意味着拿不到第三代的 middleware 能力,且踩在弃用路径上,未来升级会断。正确动作是迁到 from langchain.agents import create_agent。
设计洞察:"能跑"和"该用"在快速演进的框架里是两回事。弃用不等于立刻报错,但它标记了"维护者已经把投入移走"的方向——评审要按方向判,不按报不报错判。
2.4LangChain 跑在 LangGraph 上(v1.0):互补,不是二选一
2025-10 起 create_agent 跑在 LangGraph 执行引擎上;它是高层入口,要更多控制就下沉到 StateGraph——同一连续谱的两端。
运行方式:v1.0 之前,很多人把 LangChain 和 LangGraph 当成两个要"选一个"的框架。v1.0(2025-10-22)之后,这个对立被取消了——LangChain 的 agent 直接构建在 LangGraph runtime 之上。官方的定位是:LangChain 是"构建 agent 最快的方式",LangGraph 是"更底层的框架与运行时,用于高度定制、高度可控的 agent"。create_agent 用一句 "Agent = Model + Harness" 概括:harness(模型外围的那套循环、工具、middleware)由 LangGraph 引擎提供,你拿到的是预组装好的高层入口。
关键的心智模型是连续谱而非两个孤岛:同一个 agent 需求,可以停在 create_agent(高层、少代码、middleware 调参),也可以一路下沉到手写 StateGraph(底层、全控制、自定义拓扑),中间没有需要换框架的断点——因为它们本就是同一套引擎的不同高度。create_agent 的产物仍是一个可 invoke / stream 的 Runnable(呼应图 2.0 最底层),所以下沉前后,对外的调用形状不变。
| 你的需求 | 停在 create_agent(高层) | 下沉 StateGraph(底层) |
|---|---|---|
| 标准"模型 + 工具 + 系统提示"循环 | 一句 create_agent(model, tools=...) 即可 |
没必要,徒增代码 |
| 在工具调用前后插入校验 / 日志 / 限流 | 挂 middleware,仍用高层入口 | 可以,但 middleware 已够 |
| 多个 agent 子图并行 + 自定义路由拓扑 | 到顶,高层入口表达不了 | 手写 StateGraph,自定义节点与边 |
| 对状态结构、检查点策略要完全掌控 | 受限于预设 | 下沉,自己定义 state 与 checkpointer |
"同一引擎"模糊了两个包的心智边界:好处是迁移连续、无断点;代价是初学者更难分清"此刻到底在写 LangChain 还是 LangGraph"。create_agent 来自 langchain.agents,但它的运行时行为(检查点、中断、状态)其实是 LangGraph 的概念——要排查一个 agent 卡在某节点的问题,得去读 LangGraph 的文档,而不是 LangChain 的。
高层便利的尽头是必须懂底层:连续谱意味着只要需求够复杂,迟早要下沉到 StateGraph。也就是说 create_agent 省下的学习成本是延迟、不是免除——把 LangGraph 的状态机模型当成"以后用不到的高级话题"会在第一个非标准 agent 需求上撞墙。
团队里有人说"项目只用 LangChain,所以不用学 LangGraph"。在 v1.0 语境下,这句话的隐含假设错在哪?
展开答案(先停 10 秒再点)
错在把两者当互斥产品。v1.0 之后用 create_agent 写 agent,就已经在跑 LangGraph runtime 了——只是被高层入口包住没察觉。一旦需求越过 middleware 的表达边界(多子图、自定义拓扑、精细检查点),就必须下沉到 StateGraph,那时"没学 LangGraph"就是直接的阻塞。
设计洞察:"用 LangChain"在 v1.0 里不再是"不碰 LangGraph"的同义词。正确表述是"先用高层入口、按需下沉"——LangGraph 的知识是这条连续谱右半段的入场券,迟早要买。
2.5备选方案与痛点:综合一张表
把前四节的设计选择并排:每个"没有它时手写什么"的痛点,对应 LangChain 的一个机制,也各自留下一笔代价。
前四节分别拆了一个机制。这一节把它们拼回一张总表,用"痛点 → 设计回应 → 代价"的三栏,让整套设计的取舍一眼可比——这也是评审或选型时最该带在手边的一张表。
| 没有这个机制时,要手写什么 | LangChain 的设计回应 | 副作用 / 代价 |
|---|---|---|
| 每个组件各写一套 stream / batch / retry / fallback,组合时手拼胶水 | Runnable 协议层统一实现组合体的四项能力(§2.1) | 抽象税:多输入/副作用步骤要靠 RunnableParallel 等胶水掰回单值;故障栈停在协议层,靠 trace 而非栈定位 |
| 用裸 Python 手写 prompt 拼接 → 调模型 → 解析的串接与并行 | LCEL 的 | 把线性 DAG 写成一行(§2.2) |
只覆盖线性无状态;分支/循环/检查点/HITL 一出现就到顶,必须换层 |
| 手写 agent 的"思考-调工具-再思考"循环,自己管中断与状态 | 循环做成 LangGraph 图,create_agent 提供高层入口 + middleware(§2.3) |
入口三次迁移,旧写法(AgentExecutor / create_react_agent)遗留极多,评审要逐一判新旧 |
| 在 LangChain 与一个独立的图框架之间手动搭桥、切换数据格式 | v1.0 让 agent 直接跑在 LangGraph 引擎上,高层与底层是同一连续谱(§2.4) | 两包心智边界变模糊;高层便利的尽头仍要懂 StateGraph,学习成本是延迟非免除 |
四个机制看似各管一摊,主线只有一条:把重复的、易错的、本该框架承担的工程,从工程师手里收上去——收上去的代价,是多了一层要理解的抽象,和一条必须认清的边界。Runnable 收走了"能力胶水",LCEL 收走了"线性串接",create_agent 收走了"agent 循环",v1.0 收走了"两框架搭桥"。每一次收编都换来便利、也都立了一道"到这里就得下沉"的边界。理解 LangChain,本质就是认清这几道边界落在哪。
·跨概念综合:一个场景里串起三件事
把 Runnable、LCEL 边界、下沉判断揉进一个具体场景,看它们如何协同。
要做一个"客服工单助手":① 先把用户原话用一条 prompt 改写成规范问题,② 再交给模型,③ 但模型可以调用"查订单""查物流"两个工具,往往要调好几轮才能答全,④ 调"退款"工具前必须停下来等人工审批,⑤ 全程要能在 LangSmith 里逐节点回放。问:这套东西,哪部分停在 LCEL、哪部分必须下沉?三件事(Runnable / LCEL 边界 / 下沉)各自落在哪里?
把这五条需求逐条对到图 2.1 的判据上,分出"线性段"和"状态机段",并说明为什么整套对外仍是一个 Runnable。
展开梳理(先在纸上分好再点)
第①步(改写问题)单独看是线性无状态——一条 prompt | model | parser 的 LCEL 链就够,这里用到的是 §2.1 的组合性与默认能力(这一小段免费拥有 stream / retry)。
③④⑤ 触碰边界:③ 是"循环执行到答全"(工具循环),④ 是 HITL(退款前人工中断),⑤ 是逐节点可观测——按 §2.2 的决策表,这三条任意一条都已越过 LCEL,必须下沉。所以从第②步把控制权交给"模型 + 工具循环"开始,整体进入 StateGraph 的定义域。
落地方式(§2.3 + §2.4):用 create_agent("openai:gpt-5.4", tools=[查订单, 查物流, 退款]) 作高层入口拿到工具循环;退款前的人工审批用 LangGraph 的中断 / HITL 机制实现——若 middleware 表达不了这个中断点的细节,就下沉到手写 StateGraph 自定义那条边。第①步的改写链可以作为图里的一个前置节点接进去。
为什么对外仍是一个 Runnable(§2.1 + §2.4):无论内部是线性链还是状态图,create_agent / StateGraph 编译出的产物都实现 Runnable 接口(图 2.0 最底层、图 2.2 底座)。所以调用方始终只看到一个能 invoke / stream 的对象——"下沉"改变的是内部能力,不是对外形状。这正是三件事协同的关键:边界在内部移动,接口在外部不变。
§本章 self-check
先合上教程,把答案写在纸上或编辑器里。写完再点开对照——直接点开等于把这一节当再读一遍。
- 用一句因果链说清:为什么"统一接口"会导致"stream / batch / retry / fallback 默认就有"?这两件事之间的中间环节是什么?
RunnableSequence本质是什么图结构?这个结构的哪条性质,正好划出了 LCEL 表达力的上界?- 三代 agent 入口里,
create_react_agent解决了AgentExecutor的什么问题,又被create_agent在什么维度上超越? - (综合)一个流程:检索两个数据源(并行)→ 拼成上下文 → 模型作答 → 若答案不完整就带着已知信息重问、最多三轮。逐段判断它该停 LCEL 还是下沉 LangGraph,并指出整体对外是不是一个 Runnable、为什么。
答案(先做完再展开)
- 中间环节是"组合性"。统一接口让
prompt/model/parser有同一种输入输出形状 →(中间环节)于是它们能被RunnableSequence组合 → 而协议层只需为"组合体"定义一次如何 stream / batch / retry / fallback,任意新链就免费拥有这四项。能力来自接口(经由组合),不来自任一组件。 - 它本质是有向无环图(DAG):数据单向流、每节点执行一次、无环。"无环"这条性质划出上界——任何需要回头边的需求(循环、反复重试到满足条件、agent 工具循环)DAG 都表达不了,这就是要下沉 LangGraph 的根本原因;分支/检查点/HITL 则是"无状态、一次性穿过"这条性质的另一面。
create_react_agent把原本封在AgentExecutor内部的黑盒循环重写成 LangGraph 图,让循环第一次可观测、可加检查点(解决了"中途状态拿不到、改不了")。create_agent在它之上加了 middleware 层并统一到langchain.agents,把"改循环行为"从"改图"降级成"挂钩子",并跑在 LangGraph runtime 上、可按需下沉StateGraph——超越维度是"可控性的获取成本"。- 分段判断:检索两源并行 + 拼上下文 + 一次作答——这一段是线性无状态 DAG,
RunnableParallel+|即可,停 LCEL。"若不完整就最多重三轮"——这是循环执行到满足条件,越过 §2.2 的循环信号,必须下沉到StateGraph(循环进图的边,不要在框架外写for)。整体对外仍是一个 Runnable:状态图编译出的产物实现 Runnable 接口,调用方只看到一个能invoke/stream的对象——下沉改变内部能力,不改变对外调用形状。
把"下沉"画成一条可逆的路
本章把 LCEL → LangGraph 讲成单向的"表达力到顶就下沉"。但反向呢:一段已经写成 StateGraph 的逻辑,在什么条件下应该"上浮"回 LCEL 或 create_agent?请给出一条可操作的判据——不是"变简单了就上浮"这种空话,而是一条能在代码评审里直接套用的信号。
提示(卡住再展开)
从图 2.1 的那个问句反着用:如果一张 StateGraph 里没有任何成环的边、没有条件路由、没有用到检查点/中断,它其实只是被状态机语法包装的线性 DAG——这就是上浮信号。再想一层:判据应该落在"图的结构特征"上(有没有环、有没有条件边),而不落在"主观觉得简单"上,才能在评审里被客观核验。