一. LCEL是什么?
LCEL = LangChain Expression Language(LangChain 表达式语言)
它不是一门新语言,而是一套用管道操作符 | 把各种组件串起来 的声明式写法。你可以把它理解成 LangChain 里的 "管道语法" ,类似 Unix 的 cat file | grep xxx | sort。
他只是一种简便写法,LCEL 就是实现了 Runnbale 接口的一些 api 组合成 chain,然后统一执行。
1.1 对比
普通代码
js
// 1. 手动格式化 prompt
const messages = await prompt.formatMessages({ input: '25°C' });
// 2. 手动调模型
const response = await model.invoke(messages);
// 3. 手动解析输出
const text = response.content;
console.log(text);
LCEL的代码
js
import { StringOutputParser } from '@langchain/core/output_parsers';
const chain = prompt | model | new StringOutputParser();
const result = await chain.invoke({ input: '25°C' });
console.log(result); // 直接拿到字符串 "77°F"
就这三行,完成了 格式化 → 调模型 → 解析输出 的全部流程。
1.2 总结
LCEL 的本质 :把 LangChain 里所有组件(Prompt、Model、Parser、Retriever、Tool...)统一成同一个接口
Runnable,然后用|把它们串起来。任何实现了Runnable接口的东西,都能互相 pipe。
| 组件 | 执行方式 | 类比 | 典型用途 |
|---|---|---|---|
| RunnableSequence | 串行 --- 一步一步,上一步输出作为下一步输入 | 工厂流水线 | Prompt → LLM → Parser 标准三步链 |
| RunnableLambda | 转换 --- 对输入执行自定义函数,返回处理结果 | 流水线上的加工工位 | 数据清洗、字段提取、格式化、打日志 |
| RunnableMap | 并行 --- 所有分支同时执行,共享同一输入,结果收集为对象 | 分兵并进 / Promise.all() |
同时准备多路数据(如检索 docs + 历史摘要 + 原始问题) |
| RunnableBranch | 条件路由 --- 按顺序评估条件,执行第一个匹配的分支 | if-elif-else |
根据输入内容/数值走不同处理逻辑(如数学题 vs 代码题) |
| RouterRunnable | Key 路由 --- 按输入中的 key 字段精确匹配,执行对应分支 |
switch-case |
Intent 已分类好,直接分发到对应 handler(如客服多意图路由) |
| RunnablePassthrough | 透传 --- 原样返回输入,不做任何修改 | 透明传送带 | 在 RunnableMap 中保留原始输入、调试插入点、占位对齐接口 |
| RunnableEach | 批量逐一 --- 对数组中的每个元素独立应用同一条链 | Array.map() |
批量处理消息列表、逐条翻译、对多个文档逐一生成摘要 |
| RunnablePick | 字段提取 --- 从字典中按指定 key 提取字段,向下游传递 | 从文件夹里挑出指定文件 | 从并行分支输出中只挑选 Prompt 需要的字段(如只取 context + question) |
| RunnableWithMessageHistory ⚠️ 已弃用 | 包装增强 --- 加载历史 → 注入 Prompt → 执行链 → 保存新消息 | 给无状态链装上"记忆芯片" | 多轮对话场景(已被 LangGraph Checkpointer 取代,新项目不建议使用) |
二.案例
1.1 基础案例
js
import 'dotenv/config';
import { StructuredOutputParser } from "@langchain/core/output_parsers";
import { PromptTemplate } from "@langchain/core/prompts";
import { ChatOpenAI } from "@langchain/openai";
import { z } from "zod";
const model = new ChatOpenAI({
modelName: process.env.MODEL_NAME,
apiKey: process.env.OPENAI_API_KEY,
temperature: 0,
configuration: {
baseURL: process.env.OPENAI_BASE_URL,
},
});
// 定义输出结构 schema
const schema = z.object({
translation: z.string().describe("翻译后的英文文本"),
keywords: z.array(z.string()).length(3).describe("3个关键词")
});
const outputParser = StructuredOutputParser.fromZodSchema(schema);
const promptTemplate = PromptTemplate.fromTemplate(
'将以下文本翻译成英文,然后总结为3个关键词。\n\n文本:{text}\n\n{format_instructions}'
);
const input = {
text: 'LangChain 是一个强大的 AI 应用开发框架',
format_instructions: outputParser.getFormatInstructions()
};
const response = await model.invoke(await promptTemplate.format(input));
console.log("调用模型后的response",response.content)
// 步骤 1: 格式化 prompt
// console.log("input:",input)
// const formattedPrompt = await promptTemplate.format(input);//就是把iput里面的对象格式化成prompt里面的模板形式,并把format_instructions换成实际的指令。
// console.log("格式haunt以后的formattedPrompt",formattedPrompt)
// // 步骤 2: 调用模型
// const response = await model.invoke(formattedPrompt);
// console.log("调用模型后的response",response.content)
// // 步骤 3: 解析输出
// const result = await outputParser.invoke(response);//把response里面的内容MD形式,解析成我们想要的格式,并把结果赋值给result
// console.log("格式化以后的response",result);

model.invoke(); 里面的参数需要用 promptTemplate 格式化,invoke 的结果 response 用 outputParser 去格式化。
1.2 使用RunnableSequence
RunnableSequence 是 LangChain 中用于按顺序串联多个 Runnable 组件的组合原语------前一个组件的输出自动作为下一个组件的输入,形成一条线性处理管道。
统一接口 :生成的 RunnableSequence 本身也是 Runnable,自动支持 .invoke()、.batch()、.stream()(流式)、.ainvoke()(异步)。
管道符 和 显式构造

js
import { RunnableSequence } from "@langchain/core/runnables";
import { PromptTemplate } from "@langchain/core/prompts";
import { ChatOpenAI } from "@langchain/openai";
import { StringOutputParser } from "@langchain/core/output_parsers";
import 'dotenv/config';
const prompt = PromptTemplate.fromTemplate("讲个关于{topic}的笑话");
const model = new ChatOpenAI({
modelName: 'qwen-max',
apiKey: process.env.OPENAI_API_KEY,
temperature: 0,
configuration: {
baseURL: process.env.OPENAI_BASE_URL,
},
timeout: 60000,
maxRetries: 3,
});
const parser = new StringOutputParser();
// 写法一:管道符(推荐,简洁)
//const chain1 = prompt.pipe(model).pipe(parser);
//const result1 = await chain1.invoke({ topic: "程序员" });
console.log(result1);
// 写法二:显式构造 RunnableSequence
const chain2 = RunnableSequence.from([prompt, model, parser]);
const result2 = await chain2.invoke({ topic: "程序员" });
console.log(result2);
把基础案例写成管道符
js
import 'dotenv/config';
import { StructuredOutputParser } from "@langchain/core/output_parsers";
import { PromptTemplate } from "@langchain/core/prompts";
import { RunnableSequence } from "@langchain/core/runnables";
import { ChatOpenAI } from "@langchain/openai";
import { z } from "zod";
const model = new ChatOpenAI({
modelName: process.env.MODEL_NAME,
apiKey: process.env.OPENAI_API_KEY,
temperature: 0,
configuration: {
baseURL: process.env.OPENAI_BASE_URL,
},
});
// 定义输出结构 schema
const schema = z.object({
translation: z.string().describe("翻译后的英文文本"),
keywords: z.array(z.string()).length(3).describe("3个关键词")
});
const outputParser = StructuredOutputParser.fromZodSchema(schema);
const promptTemplate = PromptTemplate.fromTemplate(
'将以下文本翻译成英文,然后总结为3个关键词。\n\n文本:{text}\n\n{format_instructions}'
);
//const chain = RunnableSequence.from([promptTemplate, model, outputParser]);
const chain = promptTemplate.pipe(model).pipe(outputParser);
const input = {
text: 'LangChain 是一个强大的 AI 应用开发框架',
format_instructions: outputParser.getFormatInstructions()
};
const result = await chain.invoke(input);
console.log(result)
// //步骤 1: 格式化 prompt
// console.log("input:",input)
// const formattedPrompt = await promptTemplate.format(input);//就是把iput里面的对象格式化成prompt里面的模板形式,并把format_instructions换成实际的指令。
// console.log("格式haunt以后的formattedPrompt",formattedPrompt)
// // 步骤 2: 调用模型
// const response = await model.invoke(formattedPrompt);
// console.log("调用模型后的response",response.content)
// // 步骤 3: 解析输出
// const result = await outputParser.invoke(response);//把response里面的内容MD形式,解析成我们想要的格式,并把结果赋值给result
// console.log("格式化以后的response",result);

1.3 使用RunnableLambda
RunnableLambda :把单个简单函数包装成 Runnable
const toUpper = RunnableLambda.from((s: string) => s.toUpperCase());
基本用法
js
import "dotenv/config";
import {RunnableLambda,RunnableSequence} from "@langchain/core/runnables";
const addOne = RunnableLambda.from(x => x + 1);
const multiplyRwo = RunnableLambda.from(x => x * 2);
const chain = RunnableSequence.from([addOne, multiplyRwo, addOne]);
const result = await chain.invoke(5);
console.log(result);

RunnableSequence和RunnableLambda联合使用
js
const chain = RunnableSequence.from([
RunnableLambda.from((input) => ({ topic: input })), // ← 转换器(RunnableLambda)
prompt, // ← 组件
model, // ← 组件
RunnableLambda.from((output) => output.trim()), // ← 转换器(RunnableLambda)
]);
// ↑ 整个是一条链(RunnableSequence),里面嵌入了两个 RunnableLambda
1.4 使用RunnableMap
RunnableMap 是 LangChain 中用于并行执行多个 Runnable 并将结果收集到一个对象中的组合原语。
简单说:它让链中的多个分支同时跑 ,最后把每个分支的输出拼成一个
{ key: value }对象返回。
基本用法
js
import { RunnableMap, RunnableLambda } from "@langchain/core/runnables";
const chain = RunnableMap.from({
uppercase: RunnableLambda.from((text: string) => text.toUpperCase()),
length: RunnableLambda.from((text: string) => text.length),
reversed: RunnableLambda.from((text: string) =>
text.split("").reverse().join("")
),
});
const result = await chain.invoke("hello");
console.log(result);
// {
// uppercase: "HELLO",
// length: 5,
// reversed: "olleh"
// }
高级用法
js
import 'dotenv/config';
import { RunnableMap, RunnableLambda } from "@langchain/core/runnables";
import { PromptTemplate } from "@langchain/core/prompts";
const addOne = RunnableLambda.from((input) => input.num + 1);
const multiplyTwo = RunnableLambda.from((input) => input.num * 2);
const square = RunnableLambda.from((input) => input.num * input.num);
const greetTemplate = PromptTemplate.fromTemplate("你好,{name}!");
const weatherTemplate = PromptTemplate.fromTemplate("今天天气{weather}。");
// 创建 RunnableMap, 并行执行多个 runnable
const runnableMap = RunnableMap.from({
// 数学运算
add: addOne,
multiply: multiplyTwo,
square: square,
// prompt 格式化
greeting: greetTemplate,
weather: weatherTemplate,
});
// 测试输入
const input = {
name: "神光",
weather: "多云",
num: 5,
};
// 执行 RunnableMap
const result = await runnableMap.invoke(input);
console.log(result);

RunnableSequence和RunnableMap的区别

RunnableSequence和RunnableMap联合使用
a.经典使用场景

js
const fullChain =
// 第一步:并行准备
RunnableMap.from({
context: retriever,
question: new RunnablePassthrough(),
})
// 第二步:串行生成
.pipe(prompt)
.pipe(model)
.pipe(new StringOutputParser());
const answer = await fullChain.invoke("LangChain 支持哪些模型?");
1.5 使用RunnableBranch
RunnableBranch 是 LangChain 中实现条件路由(Conditional Routing) 的组合原语------它像
if-elif-else一样,根据条件函数从多个分支中选择第一个匹配的分支执行,实现动态工作流路由。
基本写法
js
from langchain_core.runnables import RunnableBranch
branch = RunnableBranch(
(lambda x: x > 100, chain_a), # 条件1成立 → 执行chain_a
(lambda x: x > 10, chain_b), # 条件2成立 → 执行chain_b
default_chain # 都不匹配 → 执行default
)
高级写法
js
import 'dotenv/config';
import { RunnableBranch, RunnableLambda } from "@langchain/core/runnables";
// 创建条件判断函数
const isPositive = RunnableLambda.from((input) => input > 0);
const isNegative = RunnableLambda.from((input) => input < 0);
const isEven = RunnableLambda.from((input) => input % 2 === 0);
// 创建分支处理函数
const handlePositive = RunnableLambda.from((input) => `正数: ${input} + 10 = ${input + 10}`);
const handleNegative = RunnableLambda.from((input) => `负数: ${input} - 10 = ${input - 10}`);
const handleEven = RunnableLambda.from((input) => `偶数: ${input} * 2 = ${input * 2}`);
const handleDefault = RunnableLambda.from((input) => `默认: ${input}`);
// 创建 RunnableBranch
const branch = RunnableBranch.from([
[isPositive, handlePositive],
[isNegative, handleNegative],
[isEven, handleEven],
handleDefault
]);
// 测试不同的输入
const testCases = [5, -3, 4, 0];
for (const testCase of testCases) {
const result = await branch.invoke(testCase);
console.log(`输入: ${testCase} => ${result}`);
}

1.6 使用RouterRunnable
RouterRunnable 是 LangChain 中用于按 key 显式路由到对应 Runnable 的组件------类似
switch-case,根据输入中的key字段选择执行哪条已命名的子链,并返回该链的输出。
基本用法
js
import { RouterRunnable, RunnableLambda } from "@langchain/core/runnables";
const router = new RouterRunnable({
runnables: {
toUpperCase: RunnableLambda.from((text: string) => text.toUpperCase()),
reverseText: RunnableLambda.from((text: string) =>
text.split("").reverse().join("")
),
},
});
// 路由到 reverseText
const r1 = await router.invoke({ key: "reverseText", input: "Hello" });
console.log(r1); // "olleH"
// 路由到 toUpperCase
const r2 = await router.invoke({ key: "toUpperCase", input: "Hello" });
console.log(r2); // "HELLO"
高级用法
js
import 'dotenv/config';
import { RouterRunnable, RunnableLambda } from "@langchain/core/runnables";
// 创建两个简单的 RunnableLambda
const toUpperCase = RunnableLambda.from((text) => text.toUpperCase());
const reverseText = RunnableLambda.from((text) => text.split("").reverse().join(""));
// 创建 RouterRunnable: 根据 key 选择要调用的 runnable
const router = new RouterRunnable({
runnables: {
toUpperCase,
reverseText,
},
});
// 测试: 调用 reverseText
const result1 = await router.invoke({ key: "reverseText", input: "Hello World" });
console.log('reverseText 结果:', result1);
// 测试: 调用 toUpperCase
const result2 = await router.invoke({ key: "toUpperCase", input: "Hello World" });
console.log('toUpperCase 结果:', result2);

1.7 使用RunnablePassthrough
RunnablePassthrough = "什么都不做,在并行分支中需要把原始输入也作为一个 key 传下去
基本用法
js
import { RunnablePassthrough } from "@langchain/core/runnables";
const passthrough = new RunnablePassthrough();
//const passthrough = RunnableSequence.from()
const result = await passthrough.invoke("hello");
console.log(result); // "hello" --- 原样返回
高级案例
js
import 'dotenv/config';
import { RunnablePassthrough, RunnableLambda, RunnableSequence, RunnableMap } from "@langchain/core/runnables";
const chain = RunnableSequence.from([
RunnableLambda.from((input) => ({ concept: input })),
RunnableMap.from({
original: new RunnablePassthrough(),
processed: RunnableLambda.from((obj) => ({
concept: input,
upper: obj.concept.toUpperCase(),
length: obj.concept.length,
}))
})
]);
const input = "你在说什么?听说你会原样返回的。";
const result = await chain.invoke(input);
console.log(result);
和RunnableMap联合使用
js
import { RunnableMap, RunnablePassthrough, RunnableLambda } from "@langchain/core/runnables";
const chain = RunnableMap.from({
// 分支1:对输入做大写转换
uppercase: RunnableLambda.from((text: string) => text.toUpperCase()),
// 分支2:对输入求长度
length: RunnableLambda.from((text: string) => text.length),
// 分支3:原样保留原始输入
original: new RunnablePassthrough(),
});
const result = await chain.invoke("hello");
console.log(result);
// {
// uppercase: "HELLO",
// length: 5,
// original: "hello" ← 原始输入被原样保留
// }
调试打印日志
js
import { RunnablePassthrough } from "@langchain/core/runnables";
const debugPassthrough = new RunnablePassthrough({
func: (input) => {
console.log("🔍 当前输入:", JSON.stringify(input));
return input; // 仍然原样返回
},
});
const chain = prompt.pipe(debugPassthrough).pipe(model).pipe(parser);
1.8 使用RunnableEach
RunnableEach 是 LangChain 中用于对数组中的每个元素逐一应用同一条 Runnable 链的组合原语。
基本用法
js
import 'dotenv/config';
import { RunnableEach, RunnableLambda, RunnableSequence } from "@langchain/core/runnables";
const toUpperCase = RunnableLambda.from((input) => input.toUpperCase());
const addGreeting = RunnableLambda.from((input) => `你好, ${input}!`);
const processItem = RunnableSequence.from([
toUpperCase,
addGreeting,
]);
// 使用 RunnableEach 对数组中的每个元素应用这个链
const chain = new RunnableEach({
bound: processItem,
});
const input = ["alice", "bob", "carol"];
const result = await chain.invoke(input);
console.log('✅ RunnableEach - 数组元素处理:');
console.log('输入:', input);
console.log('输出:', result);

对比RunnableEach和RunnableMap

RunnableEach = LCEL 世界里的 Array.map(),专门用来对数组中的每一项跑同一条处理链。
1.9 使用RunnablePick
RunnablePick 是 LangChain 中用于从字典(dict / object)输入中提取指定字段的 Runnable 组件------它从上游传来的字典中按指定 key 提取字段,并把提取出的字段(或字段值)传给下游。
基本用法
js
import { RunnablePick } from "@langchain/core/runnables";
const inputData = {
name: "John",
age: 30,
city: "New York",
};
// 提取多个 key
const pickBasic = new RunnablePick(["name", "city"]);
console.log(await pickBasic.invoke(inputData)); // { name: "John", city: "New York" }
// 提取单个 key → 返回值本身
const pickName = new RunnablePick("name");
console.log(await pickName.invoke(inputData)); // "John"
高级案例
js
import 'dotenv/config';
import { RunnablePick, RunnableSequence } from "@langchain/core/runnables";
const inputData = {
name: "snow",
age: 30,
city: "浙江",
country: "中国",
email: "545442765@example.com",
phone: "+86-13800189777",
};
const chain = RunnableSequence.from([
(input) => ({
...input,
fullInfo: `${input.name}, ${input.age}岁, 来自${input.city}`,
}),
new RunnablePick(["name", "fullInfo"]),
]);
const result = await chain.invoke(inputData);
console.log(result);
1.10 使用RunnableWithMessageHistory
RunnableWithMessageHistory 是 LangChain 中用来给任意 LCEL 链附加多轮对话记忆(Memory) 的包装器。它让原本无状态的链自动具备"读取历史 → 注入 Prompt → 执行 → 保存新消息"的能力,且包装后依然是标准 Runnable,可继续用
|拼接或嵌套
他已经被启用了。官方现在的战略是全面转向 LangGraph 的持久化(Persistence / Checkpointer)机制。
替换办法
js
// 旧方式 (你截图中的代码)
const chain = new RunnableWithMessageHistory({ runnable: simpleChain, ... });
// 新方式 (LangGraph 持久化)
import { StateGraph, MessagesAnnotation, MemorySaver } from "@langchain/langgraph";
const memory = new MemorySaver(); // 替代 InMemoryChatMessageHistory
const graph = new StateGraph(MessagesAnnotation)
.addNode("callModel", callModel)
.addEdge("__start__", "callModel")
.compile({ checkpointer: memory });
// 调用时直接传 thread_id 即可自动记忆
await graph.invoke({ messages: [...] }, { configurable: { thread_id: "user-123" } });
核心机制
它解决的核心痛点是:原生 LCEL 链(如 prompt | model | parser)是无状态的,每次调用互相独立,无法记住前文。
其工作流程可以理解为:
- 调用时传入
session_id(会话唯一标识) - 通过
get_session_history函数按session_id加载该会话的历史消息 - 把历史消息通过
MessagesPlaceholder注入 Prompt - 执行被包装的原始链
- 把本轮「用户提问 + AI 回复」追加保存 到该
session_id对应的历史存储中
js
三.总结
RunnableSequence 是"串联器",RunnableLambda 是"转换器"。