如果你是第一次接触 LangChain,不妨先想一个画面:一个孩子面前摆着一大箱乐高。每个零件各有形状——有门板、有车轮、有小人——但只要它们是正品,任意两块都能咔哒一声拼在一起。为什么?因为乐高定死了拼接口的直径和卡槽深度。LangChain 要做的事,就是把 AI 工具箱里那些“零件”——大模型、提示词、工具、解析器——全部统一成一套拼接口,让你像拼乐高一样,把一个个组件拼成一条完整的工作流水线。
这套“统一拼接口”在 LangChain 里叫 Runnable 协议。只要是符合这个协议的组件,无论内部多复杂,外部都能用同一套方式调用:同步调用、批量调用、流式输出、异步执行。更进一步,LangChain 还发明了一套“拼插语法” LCEL(LangChain Expression Language),用竖线 | 就能把组件串成链。而工具与链(Chain)的开发,本质上就是制造“标准乐高零件”的过程。
本文就用“乐高流水线工厂”这个比喻贯穿始终,带你从零理解 Runnable 接口规范、流式输出、异步执行、LCEL 组合范式,再到生产环境的落地与可观测,最后手写一个组件并跑通整条链。
对 AI 从业者来说,掌握工具与链的开发规范,意味着你不只是“调用”别人的模型,而是能像乐高工程师一样,自由设计、组装、调试自己的 AI 流水线——这正是 LangChain 贡献给整个生态的核心工程能力。
可视化演示
演示LangChain中遵循Runnable接口的组件链,按输入注入、Prompt模板、LLM调用与输出解析顺序执行,并支持流式输出与异步编排。
本节配有交互动效(进入视口后自动加载,约 3KB)。
Runnable链式编排 输入注入 Prompt LLM调用 输出解析 可组合可观测
一、Runnable 统一接口
乐高积木为什么能跨越几十年的产品线互相拼插?因为每一块积木的拼接口从诞生那天起就没变过。LangChain 面对的局面比乐高更乱:有几百个模型厂商、几十种工具调用方式、各式各样的输出解析需求。如果每个组件都用自己的“私用接口”,那组装 AI 应用就成了焊水管,而不是插乐高。于是 LangChain 制定了 Runnable 协议——一套所有组件都必须遵守的“拼接口规格”。
任何工具或链,只要实现了 Runnable,就承诺提供一组标准动作:invoke(拿起一个输入,产出一个输出)、batch(一次处理一批输入)、stream(边算边吐结果)、ainvoke(异步执行)。调用方不需要关心内部是大模型、是 Python 函数还是数据库查询,只需要对着拼接口喊一声,零件自己会把活干完。这就是“面向接口编程”在 AI 工程里的又一次胜利。
协议还要求声明输入和输出的类型,比如 Runnable[str, dict] 表示“吃进字符串,吐出字典”。这好比乐高包装盒上印的适用年龄和能力标签,让组装者一眼判断这块零件能不能接进当前管线。
为什么这套协议如此重要?核心在于“确定性替换”。只要两个组件都符合 Runnable[str, dict] 的规格,它们就能在链中互换位置而不破坏整条流水线。这种标准化让你在换模型、换工具、加解析器时,只需要动拼接口的那一小块,而不是重写整条链。
回看乐高工厂:零件的生命力在于它能被装进任何一套套装。Runnable 协议就是 LangChain 给所有 AI 零件刻上的统一卡槽——有了它,组件生态才能像乐高那样无限组合。
- 输入集合$\mathcal{X}$ 表示所有合法输入的集合。它可以是字符串、消息列表、数据库记录,甚至是一段视频的元数据。
- 输出集合$\mathcal{Y}$ 表示所有可能输出的集合。组件内部不管多复杂,最终都要落到一个明确的输出类型上。
- 映射关系$\rightarrow$ 是箭头符号,表示“转换”。它强调:一次 invoke 就是一次确定的转换过程,正如乐高凸起一定能卡进凹槽那样,合法输入一定能得到对应输出。
二、流式输出支持
想象一下你去餐厅点了一份现做的披萨。你可以选择干等 20 分钟,然后收到完整的一张;也可以选一家“边烤边切”的店,先吃第一块热乎的,后续的慢慢跟上。大模型生成文本就是这样——它不是一个词一个词瞬间吐完,而是像流水线上逐个滚下的零件,一个 token 一个 token 地生产。流式输出(stream)就是让你不必等整条链跑完,而是边生产边消费。
invoke 返回的是一个完整结果;而 stream 返回的是一个迭代器,用 Python 的 yield 逐块产出中间结果。对用户而言,最直观的变化是“首字延迟”骤降:原本要等 3 秒才看到一句话,现在 0.3 秒就看到了第一个字,剩下的字像打字机一样蹦出来。这种体验上的提升,让 AI 应用从“生成中……请等待”变成了“正在输入”,本质上是把用户焦虑期变成了期待期。
在组件实现层面,流式输出要求开发者把输出拆成多个“块”并依次送出。如果链条上的每个组件都支持流式,整条链就会像接力传递一样,前一个组件的输出块刚落地,后一个组件立刻接过继续处理。这个机制让大模型驱动的聊天机器人、代码补全、翻译工具都获得了“边想边说”的自然感。
流式输出不是简单的“切片返回”。它的灵魂在于惰性生产:只有消费者迭代到哪,生产者才计算到哪。这意味着如果你只需要前三个 token,就可以提前终止调用,省下大量算力。这也要求在管道中传递流时,不能预先攒齐所有结果再交棒,而是真的让数据块滚起来。
在乐高流水线上,这就是“传送带”思维:不是把一堆成品推到质检区再统一打包,而是每完成一个零件就立刻往下游送。对用户而言,等待时间的大头从“全部生产时间”变成了“首个零件到达时间”,这正是流式输出的核心竞争力。
- 流的起点$x$ 是进入管道的输入。它告诉组件“你要处理的是这个请求”。
- 流的片段$y_i$ 表示第 $i$ 个输出块。每个块可能是一个 token、一句话或一段结构化数据,取决于组件设计者如何切分。
- 流的终点$y_n$ 表示最后一个块。流是有限序列,末尾必须有一个明确收尾的标记(或直接停止产出)。
三、异步执行适配
一个优秀的厨师大厨不会只开一口锅:他会一边炖汤、一边炒菜、一边蒸饭,利用等待的时间穿插做事。在程序世界里,这就是异步执行。LangChain 的 Runnable 协议为每个同步方法都配了异步版本:ainvoke、abatch、astream。它们背后是 Python 的 async/await 与事件循环机制。
为什么要异步?因为大模型 API 调用是典型的 IO 密集型操作——大多数时间花在网络等待上,而不是 CPU 计算。如果同步阻塞,一个慢请求会卡住整个应用;而异步执行时,程序可以在“等待模型返回”的间隙去处理其他请求。就像大厨等水开的 3 分钟里,顺手切完了配菜。这样一来,单个服务进程就能同时支撑几十甚至上百个并发请求,吞吐量大幅提升。
但异步不是“加个关键字”就完事。它要求整条调用链都保持异步化:如果你在 ainvoke 内部调用了某个同步阻塞的第三方库,事件循环照样会被卡住。所以 LangChain 要求自定义组件同时实现 invoke 和 ainvoke,并确保两种模式的行为一致。这就好比:你可以选择“一道菜一道菜做”,也可以“多线并进地做”,但不管哪种模式,最终上桌的菜必须一样好吃。
异步设计带来的瓶颈往往不在语言层面,而在设计习惯。很多初学者在异步函数里偷偷调用了 time.sleep 或同步的 requests.get,这就好比大厨在做菜间隙刷手机刷到入迷,把整个厨房都拖着等——事件循环被阻塞,异步等于白做。
回到乐高工厂视角:同步是一条传送带,异步是十条传送带并行运转。LangChain 把异步能力标准化为协议的一部分,让“多线开工”成为每个组件与生俱来的能力,而组件开发者只需遵守“不要在异步路径上阻塞”这一条铁律。
- 异步调用$\text{ainvoke}(x)$ 是异步版本的调用入口。它处理与 invoke 相同的输入 $x$。
- 协程对象$\text{await}$ 是等待符号。调用 ainvoke 得到的不是结果本身,而是一个协程对象,需要交给事件循环调度。
- 结果回收当事件循环轮到这个协程时,它最终会执行内部的 invoke 逻辑,并产出一个与同步调用完全一致的结果类型。
四、LCEL 组合范式
乐高的终极乐趣不在于单个零件,而在于按图纸把零件拼成机器。LCEL(LangChain Expression Language)就是 LangChain 的“官方图纸语法”。它用一个竖线运算符 | 来表达“把 A 的输出接到 B 的输入”——这比乐高更像水管工:上一段管子的出水口,恰好是下一段管子的入水口。
chain = prompt | model | parser 这样一行代码,就完成了一条完整的数据链。RunnableLambda 更进一步,它能把任何普通 Python 函数包装成 Runnable:你不需要把函数写成一个类,只需要 RunnableLambda(extract_keywords) 就能把它插进链条。这种做法叫声明式组合——你描述的是“数据怎么流动”,而不是“代码怎么执行”。
LCEL 的威力在于,由 | 拼接出的链本身仍然是一个 Runnable。这意味着:链还可以继续被拼进更大的链,链可以整体调用 stream、ainvoke,可以被批处理,可以被追踪。正如积木拼出的汽车还能被装进更大的飞船,组合成果又变成了更高一层的零件。这种自相似的递归结构,让系统的复杂度从“混乱的胶水代码”变成了“清晰的层级嵌套”。
LCEL 让组件开发从“面向继承”转向“面向数据流”。过去你想扩展一条链,得修改原始代码;现在你只需要在链的末尾再拼一个 | 新组件。组合不是侵入性的,而是累加性的——这就像乐高不提供“焊死”的结构,一切修改都发生在拼接口上。
回看乐高图纸:一张好图纸的价值不在于描述零件的形状,而在于演示它们如何连接。LCEL 就是这样的图纸语言——它让 AI 工程师把“实现组件”和“组装系统”两件事彻底分开,极大降低了大型 AI 应用的搭建成本。
- 前段组件$f$ 是第一个组件。它接收原始输入 $x$,产出中间结果。例如 prompt 把用户问题包装成带上下文的提示词。
- 后段组件$g$ 是第二个组件。它消费中间结果 $m$,产出最终结果。例如 model 基于提示词生成文本。
- 嵌套组合$g(f(x))$ 表示两个组件从内到外依次执行。LCEL 的
|只是把这个嵌套写成更易读的顺序形式。
五、组件复用规范
为什么乐高零件能在不同套装之间通用?因为它们的拼接口统一,而且不携带“上一套装”的烙印。同理,一个优秀的 LangChain 组件应该做到“状态无关”:它不依赖全局变量、不保留调用之间的隐藏状态、不假设自己处在哪条链中。一次输入对应一次输出,可预测、可测试、可移植。
状态无关设计之外,LangChain 还提倡配置注入。组件在运行时可以通过 config 参数接收可选项,比如温度参数、超时时间、重试次数。这样一来,同一个组件的“默认行为”写在实现里,而“个性化行为”由组装者按需注入。就比如一个乐高小人的手,既可以在汽车里当驾驶员,也可以换到飞机里当机长——小人本身没变,是装配环境提供了不同角色。
测试友好也是复用规范的重要一环。一个 Runnable 组件的输入输出类型明确,就天然适合单元测试:给定固定输入,断言输出是否匹配。LangChain 开发者常为组件准备“黄金测试集”,确保重构后行为不变。这也像是乐高工厂的出厂质检——每个零件在出厂前都要在标准拼接口上试插一遍。
一个可复用组件就像一把瑞士军刀:刀体的形状固定,但可以换上不同的工具头。关键原则是不要把场景写死进组件——不要在组件里硬编码提示词模板、不要把 API key 存在全局里、不要假设输入的格式永远不会变。一旦组件与具体场景解耦,它就能从一条链中“拆下来”,装进另一条完全不同的链。
乐高的魅力同样在此:一套城堡套装里的士兵,可以放到太空飞船套装里当船员。LangChain 组件复用规范的本质,就是让组件像乐高零件一样“可用但不受限”——不携带记忆,只提供能力。
- 固定输入$x$ 是组件必须处理的输入。无论什么场景,组件的“主输入”类型不能变,这是协议的契约。
- 配置参数$\theta$ 是配置集合。它们不改变组件的用途,但可以调整组件的表现方式,例如切换模型温度。
- 映射输出$\mapsto y$ 表示输出仍然是由输入决定的。配置只会影响“怎么产出”,不会改变“产出什么类型”。
六、生产环境落地
在乐高工厂的展间里,一个孩子可以慢慢拼一辆小车;但到了量产车间,要考虑的是:传送带速度、每小时产量、次品处理流程。LangChain 组件从“能跑”到“生产可用”之间,也隔着同样的鸿沟。最典型的三个问题分别是:批量、容错、规模。
批量处理。如果用户请求源源不断,逐个调用 invoke 会浪费大量网络往返。batch 方法允许一次接收一批输入,内部可作并发调度,聚合提升吞吐。这在处理离线数据集、批量审核、定时报告等场景下尤其有效。容错则是生产环境的底线:模型 API 可能超时、服务可能抖动。LangChain 提供 with_retry 等机制实现指数退避重试——第一次失败等 1 秒再试,第二次等 2 秒,第三次等 4 秒,避免风暴式重试压垮下游服务。规模则意味着从几行原型代码到服务化部署:限流保护依赖方、超时管理止损、并发控制防止资源耗尽。
生产环境的收益可以量化成一道简单的算术题:系统的吞吐量不仅取决于单次调用的速度,还取决于同时有多少请求在途。就像传送带上的零件越多,单位时间产出的成品也越多——前提是别把传送带塞爆。
生产环境的每一条经验都可以映射回乐高工厂:批量处理好比把多个小订单合成一个大批次开模,重试策略好比加装质检回退皮带,限流则是给传送带装上变频器。从原型到规模,不是把原型代码原样丢进生产,而是给每个环节加上“工程护栏”。
组件开发者的终极目标不是写出能跑的 demo,而是写出扛得住流量、经得起故障、说得清成本的生产级组件。这正是 LangChain 贡献给开源社区的核心方法论:让每个工具/链都能从个人玩具变成工业零件。
- 并发数量$N$ 是同时在流水线上的请求数。它不是越高越好——受下游 API 配额和系统资源限制。
- 平均延迟$\bar{L}$ 是单个请求从进入管道到完成的平均耗时。延迟越高,传送带越慢。
- 吞吐比例除法关系说明吞吐与并发成正比、与延迟成反比。生产优化的两条路就是“更多并行”与“更快响应”。
七、可观测与调试
想象你管理一条庞大的乐高流水线:几十台机器,几百个环节。突然某个环节出了问题,成品质检不通过。如果没有监控,你只能停机、逐个机器拆开检查,一天就过去了。可观测性就是给流水线装上摄像头、传感器和记录仪——每个环节的状态一目了然,出了问题能立刻定位到是哪台机器、哪个时间点、哪个输入导致的。
LangChain 在 Runnable 协议中内置了回调机制(callbacks)。链条执行时,每个组件的开始、结束、失败、流块产出都会触发回调事件。你可以挂载一个自定义回调处理器,把事件记录到日志、数据库或链路追踪系统。这正是生产排查的关键:一次完整的链调用会展开成一棵“执行树”,哪个环节耗时最长、哪个环节抛错、哪个环节消耗了多少 token,全部有迹可循。
成本核算是 LangChain 应用的另一项必修课。LLM 调用按 token 计费,而一条链可能调用多次模型。没有可观测性,月底账单就是一笔糊涂账。通过在回调中统计每次调用的输入/输出 token 数,再乘以对应单价,就能精确算出每条链、每个用户的成本。这就像乐高工厂统计每个零件的原料成本——只有知道每个零件花了多少钱,才能决定要不要换更便宜的供应商。
可观测性不是“上线之后再加的奢侈品”,而是组件从第一天就该具备的素质。LangChain 的设计巧妙在于:只要你的组件实现了 Runnable,回调追踪就自动可用,无需额外侵入。这就像乐高积木本身自带 RFID 芯片——不管被拼进哪个套装,工厂都能追踪到它的位置和状态。
回到最初的那箱乐高。Runnable 协议是统一的拼接口,LCEL 是图纸语言,流式和异步是传送带的两种提速方式,复用规范是零件的质量标准,而可观测性是质检摄像头。当这些全部就位,你搭的就不再是一次性的积木模型,而是一条真正能交付价值的生产流水线。
---
- 输入计费$c_{\text{in}}$ 是每千个输入 token 的价格,$T_{\text{in}}$ 是实际消耗的输入 token 数(包括提示词、历史消息、工具返回文本)。
- 输出计费$c_{\text{out}}$ 是每千个输出 token 的价格,$T_{\text{out}}$ 是模型生成的输出 token 数。通常输出单价高于输入单价。
- 成本求和加号把两部分费用汇总。注意,一条链中多次调用模型,就要把每次的公式结果累加。
AI 应用场景
智能客服知识库问答链
阶段:模型调用与后处理(Runnable 组件的组装与编排)
解决的问题:传统客服系统基于关键词匹配,答非所问率高。AI 问答需要“检索 → 增强 → 生成”多步协作,过去需要手工胶水代码串联,难以维护和扩展。借助 LCEL,可以把检索器、提示词模板、大模型、输出解析器用 | 拼成一条标准 Runnable 链,且天然支持流式与追踪。
具体案例:某电商平台搭建售后问答机器人:用户提问 → 检索商品知识库 → 拼装带上下文的提示词 → 调用大模型 → 解析出结构化回答。实际落地中,这条链用 batch 批量处理夜间积压的客户消息,用 ainvoke 支撑白天高并发聊天,并在回调中统计每次回答消耗的 token 成本。
流式代码助手
阶段:模型推理输出(stream 流式接口的工程应用)
解决的问题:传统“等待完整回复再显示”的交互方式,在代码补全场景下会让用户感到明显的卡顿。流式输出把整体延迟感分拆为“首 token 延迟”和“后续 token 速率”,用户看到第一行代码出现即可开始阅读。
具体案例:某 IDE 插件接入 LangChain 的 stream 接口,用户输入注释后,模型逐 token 生成代码建议。中间经过 RunnableLambda 做的格式校验组件,每产生一个代码块就立即检查语法并高亮,实现“边写边验”的沉浸体验。
批量文档摘要管道
阶段:数据预处理与批处理(batch 与异步)
解决的问题:企业每天产生数百份合同、周报、论文,逐份串行调用大模型既慢又贵。Runnable 的 batch 接口允许批量喂入文档,内部并发调度,配合限流和重试,让处理时间从“小时级”降到“分钟级”。
具体案例:某律所知识管理系统每天自动摘要 500 份合同。开发团队实现了一个 DocSummarizerRunnable,用 batch 并发处理并配合 with_retry 应对模型服务波动;每个摘要任务都在回调中记录输入文档数、输出 token 数,月底按项目回填成本。
Python 演示
import asyncioimport timefrom typing import Any, Callable, Iterator class Runnable: """简化版 Runnable 协议:invoke / stream / ainvoke + 管道运算符。""" def invoke(self, x: Any) -> Any: raise NotImplementedError def stream(self, x: Any) -> Iterator[Any]: yield self.invoke(x) async def ainvoke(self, x: Any) -> Any: return self.invoke(x) def __or__(self, other: "Runnable") -> "RunnableChain": return RunnableChain(self, other) class RunnableChain(Runnable): def __init__(self, first: Runnable, second: Runnable) -> None: self._first = first self._second = second def invoke(self, x: Any) -> Any: return self._second.invoke(self._first.invoke(x)) def stream(self, x: Any) -> Iterator[Any]: mid = "" for piece in self._first.stream(x): mid += piece yield from self._second.stream(mid) class UpperCase(Runnable): def invoke(self, x: str) -> str: return x.upper() def stream(self, x: str) -> Iterator[str]: for ch in x: yield ch.upper() class Exclaim(Runnable): def invoke(self, x: str) -> str: return x + "!!!" def stream(self, x: str) -> Iterator[str]: n = len(x) for i, ch in enumerate(x): if i == n - 1: yield ch + "!!!" else: yield ch def main() -> None: chain = UpperCase() | Exclaim() print("invoke:", chain.invoke("hello")) print("stream:", end=" ") for piece in chain.stream("hello"): print(piece, end="") time.sleep(0.05) print() async def demo() -> None: result = await chain.ainvoke("async") print("ainvoke:", result) asyncio.run(demo()) if __name__ == "__main__": main()
| 组合、流式输出和异步调用三大核心机制。def __or__(self, other: "Runnable") -> "RunnableChain":重载竖线运算符,让组件能用A | B拼成链,这是 LCEL 组合的基础。class RunnableChain(Runnable)组合后的链本身仍然是一个 Runnable,可以继续被组合、被流式调用。def stream(self, x: str) -> Iterator[str]:用yield逐块产出结果,消费者可以边取边处理,不必等全部完成。for piece in self._first.stream(x): mid += piece链条的流式实现会先收集前段组件的全部产出,再交给后段组件,保整正确性。async def ainvoke(self, x: Any) -> Any:异步版本的调用入口,返回协程,由asyncio.run驱动事件循环执行。chain = UpperCase() | Exclaim()两行组件定义,一行组合,这行代码就是 LCEL 声明式风格的缩影。
常见误区
误区“我想写个自定义链,是不是得先 `class MyChain(Runnable)`?不继承是不是就不算 Runnable?”
正解LangChain 的协议更接近“鸭子类型”——只要一个对象实现了 invoke、stream、ainvoke 等核心方法,并且行为符合语义,它就是一个 Runnable,不需要继承任何父类。好处是同一套协议可以兼容你已有的普通类和第三方库函数,只需用 RunnableLambda 包装即可。继承只是获得默认实现和类型提示的便捷途径,不是硬性门槛。
为何常见:面向对象编程的教育让开发者习惯“继承即归属”,看到接口规范的第一反应就是找基类,而忽略了协议在 Python 中常常是“约定胜于继承”。
误区“流式输出不就是等模型生成完,然后把字符串按 10 个字符切一刀,分别发给前端吗?”
正解真正的流式核心是生产者边计算边交付。invoke 要等所有计算完成才返回,而 stream 在第一个 token 产出时就把结果交给消费者。如果内部还是先完整生成再切片,那只是“伪流式”——首 token 延迟并没有降低,内存占用也没有改善。只有使用 yield 让生成器真正惰性产出,才能获得流式的体验与性能收益。
为何常见:从外部看,流式结果确实是一段一段返回的,初学者容易把“表现层切片”误认为“生成层流式”,忽略了底层链路是否真正逐块生产。
误区“我写了 `async def ainvoke`,里面调用了 `requests.post`,再用 `await` 接收结果,这就算支持异步了吧?”
正解async def 只是声明这个函数是协程,但如果函数体内使用了同步阻塞的调用(如 requests、time.sleep),事件循环依然会被卡住,其他并发的协程全部排队等待。正确的做法是使用异步客户端(如 httpx.AsyncClient、aiohttp),并确保整条链路上的 IO 操作都是非阻塞的。这也解释了为什么 LangChain 的组件要单独实现 ainvoke——同步和异步是两套不同的执行路径。
为何常见:语法层面 await 让代码看起来像异步,但阻塞发生在更底层;很多人把“可以 await”误认为“不会阻塞”,忽视了事件循环的协作式调度原理。
误区“链里添加更多组件、更多 RunnableLambda 回调、更多分支逻辑,工程上显得更高级。”
正解每增加一个组件,就增加一层延迟、一类失败可能性和一块需要观测的面。生产环境推崇的是“最简可用链”:能一个组件解决的绝不拆成三个,能合并的解析步骤尽量合并。复杂逻辑应优先放在组件内部,而不是堆成超长链条。可观测数据也表明,链条越短,定位问题的平均时间越短,稳定性越高。
为何常见:技术圈常把“高复杂度”和“高水平”画等号,加之 LCEL 组合无比丝滑,容易让人把组合本身当成了目的,而忘了链是服务于业务目标的手段。
面试题
Q1. Runnable 协议的核心作用是什么?
- A.让所有组件拥有统一的调用与组合方式← 正确答案
- B.让所有模型使用完全相同的算法
- C.让 LangChain 不再依赖 GPU 算力
- D.让所有工具只能串行执行任务
答案:A。Runnable 协议定义了 invoke / batch / stream / ainvoke 等统一动作,让工具、链、模型变成可互换的乐高零件。B 混淆了“统一接口”与“统一实现”;C 与算力无关;D 相反,Runnable 天然支持流式、异步和并行。
Q2. 在 LCEL 中,表达式 prompt | model | parser 的含义是什么?
- A.三个组件串联成一条数据管道← 正确答案
- B.三个组件并行处理同一输入
- C.从 prompt 中删除 model 与 parser
- D.将 model 复制两份再拼接
答案:A。| 是 LCEL 的管道运算符,表示前一个组件的输出流入下一个组件的输入。A 准确描述了这一语义。B 是并发语义而非管道语义;C 与 D 是对运算符的误解。
Q3. 自定义 Runnable 组件时,以下哪些行为符合 LangChain 的协议规范?
- A.实现 stream 方法并支持流式产出← 正确答案
- B.同步方法内不阻塞事件循环← 正确答案
- C.在 invoke 中修改全局可变状态
- D.输入输出声明的类型清晰可预期← 正确答案
答案:ABD。规范组件应支持流式、异步友好、类型明确。C 违反状态无关设计原则,会让组件在不同链和并发环境下产生不可预测的行为,正确做法是保持组件无隐藏状态。
Q4. stream 方法与 invoke 方法的关键区别是什么?
- A.stream 返回生成器,边计算边产出← 正确答案
- B.stream 比 invoke 节省模型成本
- C.stream 不需要提供任何输入参数
- D.stream 只能在异步环境下运行
答案:A。stream 用 yield 惰性产出结果块,消费者拿到第一块时计算尚未完成。B 成本取决于总 token 数而非调用方式;C 仍需输入;D 同步异步都有 stream 版本。
Q5. 调用 ainvoke 方法后,立刻得到的是什么?
- A.一个需要 await 的协程对象← 正确答案
- B.一个包含全部结果的列表
- C.一个已完成的最终字符串
- D.一个自动执行的生成器
答案:A。异步函数调用后返回协程,要交给事件循环(如 await)才会执行并取得结果。B/C 是同步世界的结果形式;D 错在协程不会因调用而自动执行。
Q6. 实现组件的流式输出时,需要注意哪些要点?
- A.使用 yield 逐块产出结果← 正确答案
- B.保证产出顺序稳定有序← 正确答案
- C.内部调用 time.sleep 模拟耗时
- D.输出块类型与 invoke 语义一致← 正确答案
答案:ABD。流式需要惰性逐块产出,且块的顺序应稳定可预期,块的粒度不能改变输出语义。C 会阻塞事件循环,破坏异步能力,应用 asyncio.sleep 代替。
Q7. RunnableLambda 的主要用途是什么?
- A.把普通函数包装成 Runnable 参与链← 正确答案
- B.用来训练新的语言模型
- C.自动为组件生成文档注释
- D.把链转换为 JSON 配置文件
答案:A。RunnableLambda 是适配器,让无类声明的普通函数也能获得 | 组合、流式与追踪能力。B/C/D 都不是它的职责。
Q8. batch 方法相对于循环调用 invoke 的优势是什么?
- A.一次接收多个输入并返回对应输出列表← 正确答案
- B.自动把多个输出合并成一个结果
- C.失败时自动忽略所有错误
- D.只支持异步运行模式
答案:A。batch 封装了“多输入→多输出”的批量语义,为内部并发调度留出空间。B 是聚合语义而非批处理;C 错误处理仍需显式配置;D 同步版 batch 同样存在。
Q9. 将 Runnable 链部署到生产环境时,合理的做法包括?
- A.对关键调用配置超时与重试← 正确答案
- B.全链路日志记录追踪与成本← 正确答案
- C.关闭流式输出以保证稳定性
- D.利用批处理聚合低峰期请求← 正确答案
答案:ABD。生产需要超时兜底、可观测追踪与批量吞吐优化。C 没有必要,流式是 Runnable 的标准能力,关闭流式既不能提升稳定性也牺牲了用户体验。
Q10. LCEL 链中前一组件的流式输出如何传递给下一组件?
- A.前组件逐块产出时后组件逐块消费← 正确答案
- B.前组件全部结束才交给后组件
- C.前后组件并行运行互不等待
- D.后组件主动拉取整段输入数据
答案:A。LangChain 的 RunnableSequence 在流式模式下会尽量保持逐块传递:前一个组件每 yield 一块,就尽力喂给下一个组件继续处理,实现端到端流式。B 是“缓冲再传”的降级路径;C/D 不符合 LCEL 顺序管道的语义。