Chapter 03 · 自测题库
自测:在真实场景里调用这条分界
前两章建立了 Runnable / LCEL 的概念与设计原理——这章逼你在真实场景里调用它们。三层梯度共 16 题:概念层对应 01 章、原理层对应 02 章、应用判别层把 01 + 02 拧在一起,给一段代码或一个流程,让你判断它该停在哪一层。所有答案集中在文末单个折叠块里,做完再展开;提前瞄一眼等于把这一章当再读一遍。代码均为判断素材,未在本机执行(标 2026-06)。
怎么用这一章
- 概念层——检索式回忆:不看正文,把 01 章的词汇默写出来;卡住再点每题后的链接回正文那一节。
- 原理层——解释式回忆:要能讲出"为什么",不是说出结论。答不出"为什么",就回 02 章对应机制。
- 应用判别层——迁移训练:这层是本章重心,也是这份概念教程的 capstone 替代。每题给一个陌生场景,要求说清判据与代价,而不是给一个结论词。
A概念层(对应 01 章)
合上正文,把答案写在纸上或编辑器里。卡住再点题后的链接回 01 章那一节——直接点开答案等于把这一节当再读一遍。
- 用一句话说出 Runnable 的"统一签名"指的是哪三个方法名,以及它们彼此是什么关系(谁是谁的变体)。 提示链回 01 章 §1.1 Runnable
chain = prompt | model | parser这一行执行完,chain里已经发生过一次模型调用了吗?换句话说,|构造出来的是一段描述还是一次执行? 提示链回 01 章 §1.2 「|」与 LCEL- 说出
RunnableSequence、RunnableParallel、RunnablePassthrough、RunnableLambda、RunnableBranch这五个里,每一个解决的是"把数据怎么搬"中的哪一种搬法。 提示链回 01 章 §1.3 五个 Runnable - 把一个普通 Python 函数
lambda x: x["text"].upper()包成RunnableLambda接进链里,对这条链整体的.stream()有什么影响?这个普通函数自己会"流式"吗? 提示链回 01 章 §1.4 默认能力 - 为什么
.stream()、.batch()、.with_retry()、.with_fallbacks()这些能力,是"串好链就默认拥有",而不是 prompt、model、parser 各自实现一遍?这个"默认拥有"的来源是哪一个东西? 提示链回 01 章 §1.4 默认能力 - 同一个
ChatPromptTemplate,喂给它一个 dict 和喂给它一串消息列表,出来的对象类型一样吗?解析器(如StrOutputParser)接在 model 后面,把什么类型转成了什么类型? 提示链回 01 章 §1.5 消息·prompt·解析器
B原理层(对应 02 章)
这层要求讲出"为什么",不是复述结论。每题先在心里讲完整一遍,再回 02 章对应机制核对因果链。
- "统一一个接口"和"自动获得 stream / batch / retry / fallback"之间,因果链是怎么连起来的?为什么统一签名这一个设计决定,能让一整排能力变成组合的副产品? 提示链回 02 章 §2.1 设计机制
- LCEL 能干净表达的是哪一类控制流?给出两个 LCEL 仍然够用、不必下沉的具体形状(不是泛泛说"简单的就行")。 提示链回 02 章 §2.2 何时下沉
- 分支、循环、人工中断(HITL)这三类需求,为什么会逼着流程从 LCEL 下沉到 LangGraph?用 LCEL 的
|硬表达一个"模型决定要不要再调一次工具"的循环,会卡在哪一步? 提示链回 02 章 §2.2 何时下沉 - Agent API 经历了
AgentExecutor→create_react_agent→create_agent三次迁移。逐个说出每一代各自为什么被取代(是表达力不够、是被并入新引擎、还是别的),以及各停在哪个时间点。 提示链回 02 章 §2.3 Agent API 迁移 - "2025-10 起 LangChain 的 agent 跑在 LangGraph 引擎上"——这句话里的 on-LangGraph 到底改变了什么?这是"LangChain 被 LangGraph 取代",还是别的关系?把它们的分工讲清楚。 提示链回 02 章 §2.4 on-LangGraph
- 把流程下沉到 LangGraph,换来表达力,代价付在哪里?至少说出两项被换走的东西(对照 LCEL 一条
|链的轻量)。 提示链回 02 章 §2.5 备选与痛点
合上教程,在纸上或 Excalidraw 里画一条最小管道 prompt | model | parser——只画三个方框加两条 | 箭头,标出数据从左到右每一步变成的类型(dict → 消息列表 → 文本)。
画完做一件事:在这条链上标红——如果在哪一步插进一个"模型自己决定要不要回到上一步再来一遍"的循环,或一个"按模型输出走两条不同后续"的分支,那这一步就是该从 LCEL 下沉 LangGraph 的临界点。把这个临界点在你的图上圈出来。
画完回到 02 章 §2.2 对照——你圈的临界点,和那一节说的"分支 / 循环 / HITL"分界落在了同一个位置吗?如果你的链是纯线性的、圈不出任何临界点,那正说明这条链就该留在 LCEL,不必下沉。
C应用判别层(综合 01 + 02 · ≥3 道场景题)
这一层是本章重心。每题给一个陌生场景,要求你说清判据与代价,而不是给一个结论词。判别的价值,正在于你被迫在两条路之间举证,而不是被讲解抚平。
| 构造的是什么,又要判断它够不够),这正是"判别"区别于"回忆"的地方:答案不在任何单独一节里。C1 · 这段流程该停在 LCEL,还是下沉 LangGraph?
有一个客服意图分类 + 答复流程,需求如下:
- 先把用户问题分类成"退款 / 物流 / 其他"三类(一次模型调用)。
- 按分类走三条不同的后续处理:退款类要查订单再答复,物流类直接答复,其他类转人工。
- 退款类查订单这一步,如果订单服务超时,要最多重试 2 次。
问:第 1 步、第 2 步、第 3 步,分别该用 LCEL 还是下沉 LangGraph?给出每一步的判据;如果是混合方案,说清边界画在哪里、各自的代价是什么。 牵动 01 §1.2 + 02 §2.2
C2 · 一段 2023 年的 LLMChain 老代码,怎么迁移到 v1.0?
评审到这样一段(判断素材,未在本机执行):
# 2023 年的写法(判断素材,已过时)
from langchain import LLMChain, PromptTemplate
from langchain.chat_models import ChatOpenAI
from langchain.agents import initialize_agent, Tool
llm = ChatOpenAI(temperature=0)
prompt = PromptTemplate.from_template("把下面这段话改写得更正式:{text}")
# 用法 a:一条单纯的 prompt → model → 取文本
chain = LLMChain(llm=llm, prompt=prompt)
print(chain.run(text="这个事儿到底咋整啊"))
# 用法 b:一个会自己决定调用工具的 agent
tools = [Tool(name="search", func=do_search, description="联网搜索")]
agent = initialize_agent(tools, llm, agent="zero-shot-react-description")
print(agent.run("今天上海天气怎么样?"))
问:用法 a 和用法 b 在 v1.0 下分别迁移成什么?哪一个换成纯 LCEL 链就够、哪一个必须落到 create_agent(而 create_agent 又跑在什么之上)?为什么不能两个都简单地换成 create_agent?说清判据。 牵动 01 §1.2 + 02 §2.3 + 02 §2.4
C3 · 直接用 create_agent,还是手搭 LangGraph StateGraph?
要做一个"研究助手":接到问题后,模型自主决定调哪些工具(搜索、计算器、读文档),往往要循环调好几轮,直到它认为信息够了再总结。两种实现路线摆在面前:
- 路线甲:直接用官方
create_agent,传入工具列表和模型,开箱即用。 - 路线乙:手搭 LangGraph
StateGraph,自己定义状态、节点、边和循环条件。
问:默认该先选哪条?在什么具体情况下,路线甲会"够不着"、必须改用路线乙?反过来,过早选路线乙付出的代价是什么?给出可操作的判据(不是"看情况")。 牵动 02 §2.3 + 02 §2.4 + 02 §2.5
C4 · 同一条链,开发期顺手、上线就慢——病根在哪一层?
一条 prompt | model | parser 链,单条调用一切正常。上线后要一次处理一批用户请求,工程师写了个 Python for 循环挨个 chain.invoke(x),发现总耗时随批量线性增长、慢得离谱;同时偶发的模型 429 限流会让整批中途崩掉。
问:这两个问题分别该用 Runnable 接口上本来就有的哪个能力来解,而不是自己手写循环和 try/except?为什么说工程师这里是"绕开了组合本该白给的东西"?这道题的病根在概念层还是原理层,说清楚。 牵动 01 §1.4 + 02 §2.1
把"分界"画成一条可执行的判定流程
前面所有判别题,背后是同一条判定逻辑:拿到任意一段 LangChain 需求,怎么机械地走几个 yes/no 问题,就落到"纯 LCEL 链 / LCEL + 局部 retry-fallback / create_agent / 手搭 StateGraph"四个出口之一?
试着把它写成一棵不超过 4 个判定节点的决策树(纸上画,或写成嵌套 if 的伪代码)。难点不在画树,在于每个判定节点该问什么——问得太粗会把该下沉的留在 LCEL,问得太细会退化成逐场景死记。
提示(卡住再展开)
第一刀几乎总是同一个问题:这段流程里,后续走哪条路、要不要回头再来一遍,是不是由模型的输出在运行时决定的?答"否"(路径在写代码时就定死)→ 留在 LCEL 那一侧,retry / fallback 按需局部加。答"是"→ 进入 agent / 图那一侧,再用第二刀区分"标准 agent 循环够不够"。把"运行时由模型决定控制流"当成下沉的根判据,比记"有循环就下沉"更准——因为固定次数的循环用 LCEL 也能摊平。
·答案
三层答案集中在下面这一个折叠块里。做完三层再展开——提前瞄一眼,testing effect 就归零了。
展开全部答案(三层都做完再点)
概念层 · 答案
- 三个方法:
invoke/stream/batch(及各自的a前缀异步版)。关系是:invoke是单输入单输出的基准;batch是"多个输入并行跑invoke";stream是"invoke的结果分块吐出"。三者不是三个独立功能,而是同一个调用语义的三种形状——这就是"统一签名"的含义:任何实现 Runnable 的对象都长这同一副样子,因此能被|无差别地接起来。 - 是一段描述,不是一次执行。
prompt | model | parser只是把三个 Runnable 组合成一条RunnableSequence,此刻零次模型调用发生。真正的调用要等到对chain调.invoke()/.stream()/.batch()那一刻。|构图、调用才执行——这条"构图 ≠ 执行"的界线是后面一切默认能力的前提。 - 五种"搬法":
RunnableSequence= 串行(上一步输出喂给下一步);RunnableParallel= 同一份输入分发给多个分支并行,结果汇成一个 dict;RunnablePassthrough= 原样透传(常用来在RunnableParallel里保留原始输入);RunnableLambda= 把任意普通函数包成 Runnable 接进链;RunnableBranch= 按条件在几条预设支路里选一条(注意:这是写代码时定死的条件分支,不是 agent 那种运行时由模型决定的控制流)。 - 不影响整条链"能调
.stream()"这件事,但这个普通函数本身不会真正流式产出。包成RunnableLambda后它获得了 Runnable 的统一签名,所以链整体仍可.stream();但一个return x.upper()的同步函数只能等它整个算完才把结果作为"一整块"交出去——流式在它这一站退化成"一次性吐一块"。陷阱在于:链能.stream()≠ 每一站都能增量产出;某一站是阻塞函数,流式体验就卡在那一站。 - 来源是 Runnable 接口本身。这些能力(
stream/batch/with_retry/with_fallbacks)定义在 Runnable 这一个接口上,prompt | model | parser组合出的RunnableSequence也是一个 Runnable,于是天然带着这整排方法。所以不是 prompt、model、parser 各实现一遍,而是"组合结果是 Runnable"这一个事实把能力一次性带进来——能力来自接口,不来自任何单个组件。 - 类型不一样:喂 dict 时
ChatPromptTemplate先用 dict 填模板、再渲染成消息列表;喂已经成形的消息列表则基本按消息透传。StrOutputParser接在 model 后面,把模型返回的消息对象(如AIMessage)转成纯字符串——它的职责就是"消息 → 文本"这一步类型收口,好让链的最终产出是干净的str而不是带元数据的消息对象。
原理层 · 答案
- 因果链:统一签名 → 组合结果仍是同一接口 → 能力定义在接口上就被组合结果继承。具体说,因为所有组件都实现同一个 Runnable 接口,
|组合出来的东西也是 Runnable;而stream/batch/retry/fallback是写在接口层面的通用逻辑(batch 就是并行跑多次 invoke,retry 就是包一层重试装饰),它们不关心里面具体是 prompt 还是 model。统一签名这一个决定,把"逐组件实现 N 种能力"变成了"在接口实现一次、所有组合白拿"——这是接口抽象换来的杠杆。 - LCEL 干净表达的是静态的、有向无环的数据流——管道在写代码时就定死,运行时只是把数据从左推到右。两个仍够用的形状:①纯线性
prompt | model | parser;②扇出再汇聚的RunnableParallel(比如同一个问题并行问两个模型、结果拼成一个 dict 再交给下游),它有"分叉"但分叉在编译期就定死、且不回头,仍是 DAG。判据是"控制流在写代码时是否已经完全确定",不是"看起来简不简单"。 - 会卡在"循环要不要再来一遍由运行时的模型输出决定"这一点上。LCEL 的
|表达的是固定走向的 DAG,没有"回边"——它无法表达"模型看了工具结果后,动态决定是再调一次工具还是收尾"。分支(RunnableBranch能做静态分支,但 agent 的分支依据是模型当场的决定)、循环(DAG 无环,画不出回边)、HITL(要在中途暂停、把状态挂起等人工介入再恢复)这三类,本质都需要一个可持有状态、可回边、可中断恢复的执行模型——这正是 LangGraph 的状态图提供、而 LCEL 的纯管道提供不了的。 - 三代迁移:
AgentExecutor——最早的 agent 运行器,把"模型决策 + 工具调用 + 循环"裹成一个黑盒,难定制、难观测、难做 HITL;被取代是因为表达力和可控性都不够,维护到 2026-12。create_react_agent——LangGraph 早期提供的预制 ReAct 图构造器,比AgentExecutor透明,但随着 agent 能力统一收口到新入口,它已弃用。create_agent——v1.0 起的当前推荐入口,底层直接构建在 LangGraph 运行时上,既开箱即用又能在需要时把图拿出来定制。一句话:从"黑盒运行器"→"预制图"→"建在统一图引擎上的标准入口",每一步都在换更透明、更可控的执行模型。 - on-LangGraph 改变的是 agent 的执行底座:2025-10 起,LangChain 的 agent 不再用自己那套循环器,而是把"模型决策—工具调用—循环—中断"跑在 LangGraph 的状态图运行时上。这不是"LangChain 被取代",而是分工:LangChain 仍提供 Runnable / LCEL 这层组件与组合抽象、以及
create_agent这样的高层入口;LangGraph 提供有状态、可回边、可中断的执行引擎。两者都已 v1.0,是互补的两层,不是二选一——简单线性流程停在 LCEL,需要 agent / 复杂控制流时自然落到跑在 LangGraph 上的create_agent或手搭图。 - 代价至少两项:①心智与样板成本——LCEL 一条
|链几行就读完,下沉到StateGraph要显式定义状态结构、注册节点、连边、设循环/中断条件,理解成本和代码量都上一个台阶;②失去"组合即白拿"的轻量——纯 LCEL 链调一次.invoke()就跑,而图带来状态管理、检查点、调度这些运行时机制的开销与复杂度。换句话说,下沉买到的是表达力(分支/循环/HITL/可观测),付出的是简洁性与直接性——所以判据是"确实需要那份表达力再下沉",不是默认就上图。
应用判别层 · 答案
- C1(混合方案,边界在第 2 步)。
- 第 1 步(一次分类调用):纯 LCEL。它是
prompt | model | parser这种静态线性流,控制流写死、无回头,|链正好。 - 第 2 步(按分类走三条不同后续,其中"其他类转人工"):这是分界落点。如果三条支路只是静态分流、依据是上一步已产出的分类标签,
RunnableBranch还能勉强表达;但"转人工"本质是 HITL(要中途暂停等人介入),一旦含 HITL 就该下沉 LangGraph。判据:分支依据是否运行时动态、是否需要暂停/恢复——含 HITL → 下沉。 - 第 3 步(查订单最多重试 2 次):不需要为这一点下沉。固定上限的重试正是 Runnable 自带的
.with_retry(),在 LCEL 这一侧局部加即可——固定次数重试不等于"运行时由模型决定的循环",别误判成必须上图。 - 边界与代价:把分类(LCEL)+ 订单查询带 retry(LCEL 局部能力)作为图里的节点,整体编排(含 HITL 转人工)交给 LangGraph。代价是为了第 2 步的 HITL,引入了一层状态图样板;收益是转人工的暂停/恢复能被干净表达。
- 第 1 步(一次分类调用):纯 LCEL。它是
- C2(两个用法迁移到不同出口)。
- 用法 a(
LLMChain做 prompt → model → 取文本):换成纯 LCEL ——chain = prompt | model | StrOutputParser(),调用从.run(text=...)改成.invoke({"text": ...})。它是静态线性流,create_agent是杀鸡用牛刀。 - 用法 b(
initialize_agent的 ReAct agent,模型自主决定调工具、常常多轮):必须落到create_agent—— 因为控制流是运行时由模型决定的(要不要调工具、调几轮),这是 agent 循环,LCEL 的 DAG 表达不了。而create_agent底层跑在 LangGraph 运行时上,正好提供那套可回边、可循环的执行模型。 - 为什么不能都换成
create_agent:用法 a 没有工具、没有运行时决策,硬塞进 agent 会平白背上状态图的复杂度与开销,违反"确实需要表达力才下沉"的判据。判据始终是"控制流是否运行时由模型决定"——是→agent,否→LCEL。
- 用法 a(
- C3(默认选路线甲,越过边界才换乙)。
- 默认先选路线甲
create_agent:这个"研究助手"是标准的工具调用 + 多轮循环 agent,正是create_agent开箱覆盖的形态;而且它本就建在 LangGraph 上,后续要定制还能把图取出来,起步成本最低。 - 路线甲"够不着"、必须改乙的具体情形:当你需要标准 agent 循环之外的自定义控制结构——例如多个 agent 协作并按自定义规则在它们之间转交、需要在特定节点插入人工审批(HITL)、需要非标准的状态流转或回边逻辑、需要对每一步状态做精细的检查点/回滚。这些超出"单 agent 标准循环"的需求,
create_agent的预制结构覆盖不了,就手搭StateGraph。 - 过早选乙的代价:在标准 agent 循环就够用时手搭图,等于自己重写一遍
create_agent已经封好的东西——多写状态/节点/边的样板、自担维护、更易出错,却没换来任何额外表达力。判据:先用create_agent,只有当出现它表达不了的控制结构时再下沉手搭图。
- 默认先选路线甲
- C4(两个问题都用 Runnable 自带能力,病根在原理层而非概念层)。
- 批量慢:用
.batch([...])取代手写for+.invoke()。batch定义在 Runnable 接口上、对一批输入并行调度,而手写 for 循环是串行的,所以耗时随批量线性涨。 - 429 中途崩:用
.with_retry()(必要时叠.with_fallbacks()切备用模型)取代手写 try/except。重试/降级也是接口自带能力。 - "绕开了白给的东西"的含义:这条链是 Runnable,组合后本就继承了
batch/retry/fallback;工程师手写循环和异常处理,等于在接口已经白给的能力之外又造了一遍轮子,还造得更差(串行、易崩)。 - 病根在原理层:表层看是 API 没用对(概念层),但真正没理解的是"组合结果是 Runnable,所以这些能力本该免费继承"这条原理——理解了来源,就不会再手写循环。所以归到原理层。
- 批量慢:用