Skip to content1. 调用
2.
概述
单独使用 ChatModel、PromptTemplate 和 OutputParser 时,每个组件负责一部分工作,但真正要走完“输入自然语言 → 得到结构化结果”的闭环,需要把它们串起来。前一篇已经拆开了这三个组件的行为,这一篇解决串联问题。
直接写调用代码不是不行:
ts
const messages = await prompt.formatMessages({ input: "我想订一张机票" });
const response = await model.invoke(messages);
const parsed = await parser.invoke(response);这段代码能跑,但每次调用都要手动编排步骤。如果多个地方都需要这条处理流程,重复代码会迅速膨胀。更关键的是,这种顺序执行无法利用 LangChain 提供的运行时能力——并行处理、自动日志记录、无缝切换流式输出等。
LangChain 为此提供了一套声明式组合语法,称为 LCEL(LangChain Expression Language),用于把多个组件串成一条 链(Chain)。链本身也是一个 Runnable,可以像单个组件一样被调用、复用和部署。
注意:如果只是单次 LLM 调用,没必要用 LCEL,直接调模型接口就行。当处理流程固定下来、需要反复执行时,用 LCEL 组合起来会更干净。
Runnable 接口
所有能参与 LCEL 组合的组件都实现了同一个接口——Runnable。这个接口约定了几个方法:
invoke(input):执行并获得完整结果batch(inputs):批量执行多条输入stream(input):逐步输出结果(流式)- 以及对应的异步版本
ainvoke、abatch、astream
这与 Java 中 Runnable 接口的设计思路类似——java.lang.Runnable 要求所有线程执行的对象都实现 run(),让调度者不关心具体任务是什么。LangChain 的 Runnable 提供了同样的“可运行契约”:链的调用者不需要知道内部有多少步骤,只要知道调用哪个方法就能拿到结果。
因此,PromptTemplate、ChatModel、OutputParser 乃至自定义函数,都可以通过实现或包装成 Runnable 参与组合。组合出来的链也实现了 Runnable 接口,意味着可以在链上直接调用 invoke、batch、stream,甚至把这条链再作为另一个更大链的一环。
管道组合
把 Runnable 串起来有两种写法:| 符号或者 .pipe() 方法,效果完全相同。
沿用前一篇的例子——已经有了 prompt、model 和 parser 三个 Runnable,构建链只需要一行:
ts
const chain = prompt.pipe(model).pipe(parser);
// 或者等价写法
const chain = prompt | model | parser;管道符左边的输出会成为右边 Runnable 的输入,数据流向:
PromptTemplate → ChatModel → OutputParserTypeScript 的类型检查在这里会发挥作用:如果某个环节的输入类型和上一个环节的输出不匹配,编译器就会报错,在组合阶段就能发现部分问题。
内部实现上,.pipe() 最终创建 RunnableSequence 实例,这个类按顺序管理中间的 Runnable 列表。两个以上的组件也可以直接传入数组:
ts
import { RunnableSequence } from "@langchain/core/runnables";
const chain = new RunnableSequence({
first: prompt,
middle: [model],
last: parser,
});日常使用中,管道符更简洁直观。
自定义函数参与组合
不是所有逻辑都来自现成组件,有时需要插入一段自己的处理逻辑,比如对前一步的输出做清洗。RunnableLambda 能把普通函数包装成 Runnable:
ts
import { RunnableLambda } from "@langchain/core/runnables";
const upperCaseFunc = RunnableLambda.from((input: string) => input.toUpperCase());
const chain = prompt | model | parser | upperCaseFunc;包装后的函数获得了 invoke、batch、stream 等方法,可以无缝埋入链中。
调用方式
回到那条典型链:prompt | model | parser。三个组件的职责在前一篇已经完整介绍过,这里只说明它们在链中的协作行为。
假定 prompt 是 ChatPromptTemplate,接收 {input: "..."} 并生成消息列表;model 是 ChatOpenAI 实例;parser 是 JsonOutputParser,负责把模型返回的 JSON 字符串解析为 JavaScript 对象。
链调用时的执行流程:
- 用户调用
chain.invoke({input: "今天天气怎么样"}),参数传给链的第一个 Runnable(prompt) - prompt 生成
[SystemMessage, HumanMessage(...)]列表 - 消息列表传给 model,model 发起 API 请求,返回
AIMessage - AIMessage 的内容传给 parser,parser 尝试
JSON.parse,最终返回一个对象
整个过程对调用方透明。如果中间某个环节不支持流式输出,链会自动降级为模拟流式,调用方仍然可以统一使用 stream 方法。
invoke
最基本的调用方式,接收一个输入,返回一个结果:
ts
const result = await chain.invoke({ input: "帮我查一下天气" });
console.log(result);
// { intent: "query_weather", location: null }invoke 接受的参数类型由链的第一个 Runnable 的输入类型决定,返回值类型由最后一个 Runnable 的输出决定。TypeScript 可以推导准确,不必手动标注。
batch
当需要处理一批数据时,batch 方法直接接收输入数组,返回对应的结果数组。内部会根据 Runnable 的能力决定并行还是串行执行——对于独立的模型调用,通常会并行发出请求以降低整体延迟。
ts
const inputs = [
{ input: "订机票" },
{ input: "查天气" },
{ input: "算一下数学题" },
];
const results = await chain.batch(inputs);
// results: [{intent:"book_flight"}, {intent:"query_weather"}, {intent:"calculate"}]默认情况下 batch 会尝试并发执行,但可以通过 { maxConcurrency: 2 } 等选项控制并发数,防止触发 API 速率限制。对于耗时相近的同类请求,batch 比循环调用 invoke 快得多。
batch 也支持异步版本 abatch。
stream
LLM 的生成过程是流式的——token 逐个产出。如果等整段文本全部生成完毕再返回,用户会感到明显延迟。stream 方法能实时推送每个增量结果块:
ts
const stream = await chain.stream({ input: "讲个笑话" });
for await (const chunk of stream) {
process.stdout.write(chunk);
}输出会随着模型生成逐步打印。这里 chunk 的类型是链最终输出类型的部分增量——parser 的流式输出可能是逐步构建的 JSON 片段,具体取决于解析器对流的支持。
并非所有组件都原生支持真正的流式传输。当上游不支持时,LangChain 会在内部收集完整输出后再模拟流式返回,调用方依然可以用 for await,但无法获得即时延迟的改善。ChatModel 普遍支持原生流式,自定义的 RunnableLambda 如果不显式处理 stream,就会触发模拟。
选用策略
三种方式的选择取决于输入规模和实时性要求:
- invoke:单条输入,等完整结果。适合 API 的单个请求处理、定时任务单次调用
- batch:多条输入,批量得到结果。适合离线处理数据集、批量标注、数据清洗管道
- stream:单条输入,实时推送增量。适合聊天界面、实时展示生成过程的场景
链的定义完全一样,切换调用方式只需改变方法名:
ts
// 单次
await chain.invoke(input);
// 批量
await chain.batch([input1, input2]);
// 流式
for await (const chunk of chain.stream(input)) { /* ... */ }异常处理
链的任何一步抛出异常,都会中止后续步骤,并将错误传递给调用方。
ts
try {
const result = await chain.invoke({ input: "..." });
} catch (error) {
console.error("链执行失败:", error);
}常见异常来源:API Key 错误、网络超时、模型返回格式不符合 parser 预期导致 JSON 解析失败等。invoke 和 batch 都会抛出异常。batch 如果其中某条输入失败,整个 batch 调用会失败,不会返回部分成功的结果。这是默认行为,可通过 { returnExceptions: true } 改变,让失败项以 Error 对象形式返回在同一位置的数组中。
stream 模式下的异常处理稍复杂一些。如果用 for await 循环消费流,当流中某一帧出错时,循环会抛出异常,之前拿到的 chunk 已经输出。如果需要在流式场景保证原子性,应该在外层 try-catch 并决定是否丢弃已部分输出的内容。
对于 parser 解析失败这类可预见的错误,可以在链中插入一个处理函数作为 fallback:
ts
const safeChain = prompt | model | RunnableLambda.from((raw) => {
try {
return parser.invoke(raw);
} catch {
return { error: true, rawText: raw.content };
}
});这样即使解析失败,链也不会整体崩溃,而是返回一个标记错误的对象。
示例:构建意图识别链
下面在基本链的基础上,展示 batch 的批量能力、并发控制以及错误处理。
假设有一个用户输入列表需要识别意图。直接调用 chain.batch 可能出现部分解析失败(比如模型返回了不合法的 JSON)。用 returnExceptions: true 和 maxConcurrency 来控制行为:
ts
const inputs = [
{ input: "我要订一张去北京的机票" },
{ input: "今天天气如何" },
{ input: "帮我算一下 2+3" },
{ input: "随便聊聊天" },
];
const results = await chain.batch(inputs, {
maxConcurrency: 3,
returnExceptions: true,
});
results.forEach((res, idx) => {
if (res instanceof Error) {
console.error(`第 ${idx} 条处理失败:`, res.message);
} else {
console.log(`第 ${idx} 条意图:`, res.intent);
}
});returnExceptions: true 让 batch 不会因为一条失败而中断所有处理,而是把错误对象放入结果数组。这对批量标注、数据清洗等流水线作业很有用——失败的条目可以记下日志,后续单独重试。
如果想在流式场景下展示每条意图识别过程的实时输出,可以先用 chain.stream 逐个处理,并将增量文本推送到 WebSocket 或控制台。不过 parser 的流式行为取决于具体实现,JsonOutputParser 并不原生支持流式,所以 stream 效果等同于拿到最终结果后一次性返回。若需要真正的 token 级别流式推送,可以将 parser 替换为简单的字符串输出,或在 parser 位置用一个自定义 Runnable 来逐步输出原文。
并发控制
对于免费 API Key 或受限的模型端点,并发过高可能触发 429 错误。通过 maxConcurrency 控制同时进行的请求数是一种温和的限流方式。也可以结合 p-queue 自行调度,但 batch 自带的并发控制通常足够简单场景使用。
常见问题
1. 调用 stream 时没有任何输出
确认用了 for await 循环,而不是直接用 await chain.stream(input) 后就不管了。stream 返回的是一个异步可迭代对象,必须被消费。
ts
// 错误:没有迭代,无输出
const stream = await chain.stream(input);
// 正确
const stream = await chain.stream(input);
for await (const chunk of stream) {
console.log(chunk);
}2. batch 结果顺序与输入不一致
batch 保证结果数组的顺序与输入数组一一对应。如果发现不对,检查是否在某个环节使用了无序的并发逻辑。默认实现是顺序收集的。
3. parser 报错 “Expected property name or '}' in JSON”
模型返回的文本不是有效的 JSON,通常是因为 prompt 指定了输出格式但模型没有遵循。可以在 prompt 中加强要求,或者使用 OutputFixingParser 等更高层的解析器包装(注意这会引入额外的模型调用)。更轻量的方式如前面示例中那样捕获异常做降级处理。
4. TypeScript 类型推断不到链的输出类型
如果链中某个环节使用了 RunnableLambda.from 且函数没有显式标注返回类型,或者链的组装方式不够直接,可能导致类型变成 Runnable<any, any>。给函数显式标注输入输出类型,或者使用 pipe 的组合链通常可以恢复准确的类型推导。
