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 正是已经站在那条边界另一侧的高层入口。

Runnable 协议层 invoke / stream / batch / 组合语义,所有对象共享 prompt · model · parser 都实现 Runnable a | b | c LCEL 线性链(DAG) stream / batch retry / fallback LangGraph 执行引擎(StateGraph) 状态 · 节点 · 边 · 检查点 支持分支 / 循环 / HITL / 可观测 create_agent 高层 agent 入口 实现 | 免费得到 表达力到顶 → 下沉 跑在其上 仍是 Runnable
图 2.0读这张图盯三件事: 边界在哪——朱红那条"下沉"箭头,是 LCEL 表达力到顶、必须换引擎的唯一分界,本章 §2.2 专讲它落在哪。 热路径在哪——上排 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"定义清楚一次,任意一条新链——哪怕组件是你今天刚写的——立刻就拥有这四项能力。能力是接口的性质,不是组件逐个攒出来的功能。

where_does_batch_live.py Python
# 演示:能力来自组合结果,不来自任一组件(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 —— 这就是"组合性让能力下沉"的字面含义。

表 2.1 · 备选方案对比 —— 没有统一接口时,stream / retry / 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:a | b | c 线性 DAG,停这里 下沉 LangGraph 分支 / 循环 / 检查点 HITL / 逐步可观测 是 否
图 2.1这道判定只有一个问句:流程能否表达成"单向、无状态、每节点一次"。 注意:判据是"流程的形状",不是"任务难不难"——一个复杂但线性的 RAG 仍停在 LCEL;一个简单但带工具循环的 agent 已经必须下沉。形状决定层级,规模不决定。
表 2.2 · 决策表 —— 出现哪类需求,画执行流时就该换层
需求信号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(中间件)层,让你能在循环的具体节点前后插入自定义逻辑,而不必亲手改图。

three_generations.py Python
# 三代 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 是第三代相对第二代的关键增量——把"改循环行为"从"改图"降级成"挂钩子"。

表 2.3 · 三代 agent 入口的定位、为何被取代、现在该用哪个
入口定位 / 来源为何被取代 / 现状
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 高层入口 · 少代码 middleware 调参 手写 StateGraph 底层 · 全控制 自定义拓扑 需要更多控制 → 下沉 同一个 LangGraph 执行引擎 两端产物都是 Runnable,对外调用形状一致
图 2.2把"LangChain vs LangGraph"从"二选一"改画成"一条连续谱"。 注意:两端之间是虚线滑动、不是实心切换——下沉过程没有"换框架"那一刻,因为底座(同一个执行引擎)从头到尾没变。选择的是"站在谱的哪个高度",不是"用哪个产品"。
表 2.4 · 同一 agent 需求,停在谱的哪一端
你的需求停在 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 的一个机制,也各自留下一笔代价。

前四节分别拆了一个机制。这一节把它们拼回一张总表,用"痛点 → 设计回应 → 代价"的三栏,让整套设计的取舍一眼可比——这也是评审或选型时最该带在手边的一张表。

表 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

先合上教程,把答案写在纸上或编辑器里。写完再点开对照——直接点开等于把这一节当再读一遍。

  1. 用一句因果链说清:为什么"统一接口"会导致"stream / batch / retry / fallback 默认就有"?这两件事之间的中间环节是什么?
  2. RunnableSequence 本质是什么图结构?这个结构的哪条性质,正好划出了 LCEL 表达力的上界?
  3. 三代 agent 入口里,create_react_agent 解决了 AgentExecutor 的什么问题,又被 create_agent 在什么维度上超越?
  4. (综合)一个流程:检索两个数据源(并行)→ 拼成上下文 → 模型作答 → 若答案不完整就带着已知信息重问、最多三轮。逐段判断它该停 LCEL 还是下沉 LangGraph,并指出整体对外是不是一个 Runnable、为什么。
答案(先做完再展开)
  1. 中间环节是"组合性"。统一接口让 prompt / model / parser 有同一种输入输出形状 →(中间环节)于是它们能被 RunnableSequence 组合 → 而协议层只需为"组合体"定义一次如何 stream / batch / retry / fallback,任意新链就免费拥有这四项。能力来自接口(经由组合),不来自任一组件。
  2. 它本质是有向无环图(DAG):数据单向流、每节点执行一次、无环。"无环"这条性质划出上界——任何需要回头边的需求(循环、反复重试到满足条件、agent 工具循环)DAG 都表达不了,这就是要下沉 LangGraph 的根本原因;分支/检查点/HITL 则是"无状态、一次性穿过"这条性质的另一面。
  3. create_react_agent 把原本封在 AgentExecutor 内部的黑盒循环重写成 LangGraph 图,让循环第一次可观测、可加检查点(解决了"中途状态拿不到、改不了")。create_agent 在它之上加了 middleware 层并统一到 langchain.agents,把"改循环行为"从"改图"降级成"挂钩子",并跑在 LangGraph runtime 上、可按需下沉 StateGraph——超越维度是"可控性的获取成本"。
  4. 分段判断:检索两源并行 + 拼上下文 + 一次作答——这一段是线性无状态 DAG,RunnableParallel + | 即可,停 LCEL。"若不完整就最多重三轮"——这是循环执行到满足条件,越过 §2.2 的循环信号,必须下沉到 StateGraph(循环进图的边,不要在框架外写 for)。整体对外仍是一个 Runnable:状态图编译出的产物实现 Runnable 接口,调用方只看到一个能 invoke / stream 的对象——下沉改变内部能力,不改变对外调用形状。
进阶挑战 · 刚好够不着

把"下沉"画成一条可逆的路

本章把 LCEL → LangGraph 讲成单向的"表达力到顶就下沉"。但反向呢:一段已经写成 StateGraph 的逻辑,在什么条件下应该"上浮"回 LCEL 或 create_agent?请给出一条可操作的判据——不是"变简单了就上浮"这种空话,而是一条能在代码评审里直接套用的信号。

提示(卡住再展开)

从图 2.1 的那个问句反着用:如果一张 StateGraph 里没有任何成环的边、没有条件路由、没有用到检查点/中断,它其实只是被状态机语法包装的线性 DAG——这就是上浮信号。再想一层:判据应该落在"图的结构特征"上(有没有环、有没有条件边),而不落在"主观觉得简单"上,才能在评审里被客观核验。