前言
上一章了解了RAG,使得AI拥有了检索知识库的能力,这一章终于要触摸到LangChain框架的核心------------LCEL。
如果你也和我一样好奇LangChain的名字有什么含义,这一章就能得到答案了,Chain(锁链)就是LangChain的核心,我们之前在开发简易Agent的时候,还没有用到LangChain的核心设计,因此在大模型处理用户输入时,需要有很多的处理逻辑,我们需要自己编写流程控制判断是否要查询文档,是否需要使用工具,并且帮手动将每一个阶段的输出给到下一个阶段的输入。逻辑繁琐且麻烦,这时候就需要用到LCEL。
示例 :翻译链 ------ 不用 LCEL
typescript
async function translateChainManual(input: { target_language: string; input: string }) {
// 步骤 1:手动构建 messages
const messages = [
new SystemMessage(
`你是一个专业的翻译助手。将用户输入翻译成${input.target_language}。只输出译文,不要解释。`
),
new HumanMessage(input.input),
];
// 步骤 2:手动调用 model,拿到 AIMessage
const aiMessage = await model.invoke(messages);
// 步骤 3:手动从 AIMessage.content 里提取纯文本
return aiMessage.content as string;
}
const result = await translateChainManual({
target_language: "英文",
input: "今天天气真好,适合出去散步",
});
示例 :翻译链 ------ 用 LCEL
typescript
const translateChain = RunnableSequence.from([translatePrompt, model, outputParser]);
const result = await translateChain.invoke({
target_language: "英文",
input: "今天天气真好,适合出去散步",
}); // 一步完成!
这里可以直观的看到,使用LCEL减少了代码量,并且步骤更加抽象化。
为什么用 LCEL?(优势对比)
| 维度 | 手动实现 | LCEL | 说明 |
|---|---|---|---|
| 统一接口 | 每步函数签名各异,得自己对接 | 所有步骤都是 Runnable,.invoke() 通用 |
像 React 组件都实现同一 interface,可自由替换 |
| 链式组合 | 嵌套 await,中间变量一堆 |
RunnableSequence.from([...]) 或 `a |
b |
| 流式输出 | 得自己手写 generator / async iterator | .stream() 自动逐 token 返回 |
这是 LCEL 最大的价值,手动实现流式很麻烦 |
| 批量执行 | 自己写 Promise.all(inputs.map(...)) |
.batch([i1, i2, i3]) 内置并发调度 |
自动处理并发数 |
| 并行 | 手写 Promise.all({a, b}) 并对齐字段 |
RunnableParallel.from({a, b}) 声明式 |
见上方"三种核心组合方式" |
| 回调/可观测 | 自己埋点 | 内置 callbacks、RunnableTrace,LangSmith 自动 trace |
生产环境调试利器 |
| 异步同步统一 | 写两个版本 | invoke/stream/batch 三方法一致 |
切换调用模式零成本 |
| 复用/重构 | 改链结构要重写函数 | 加减一个 Runnable 即可 | 像搭积木 |
LCEL
LCEL是(LangChain Expression Language),是LangChain设计的一种表达式,是一种声明式的组合方法,开发者不再需要详细描述"如何"执行每一步,而是通过一种简洁的语法来描述"做什么"。这种声明式的特性将执行优化的责任交给了 LangChain 框架本身。开发者只需要关注agent有哪些步骤,每个步骤要做什么,每个步骤的输入是什么,每个步骤的输出是什么,而不需要关心每个步骤之间要怎么链接起来。
一句话总结
**LCEL = LangChain 的组件组合语法。用
RunnableSequence把 Prompt、Model、Parser 串联成一条可运行的数据管道。
Runnable
在理解LCEL之前,需要理解一下什么是Runnable,LangChain的Runnable接口是一个标准接口。从字面上理解是可以运行的,可以理解为是一个带入口和出口的黑盒,内部做什么不重要,对外接口都一样(invoke / stream / batch)。LangChain提供了很多接口实现了Runnable,可以将这些Runnable像串一条链子一样串起来,形成完整的工作流。
基础LCEL
以一个简易的翻译器为例子
ts
// ─── 示例 1:Prompt + Model + OutputParser ────
console.log("📌 示例 1:翻译链 ------ Prompt → Model → Parser\n");
const model = new ChatDeepSeek({
model: config.model,
apiKey: config.apiKey,
temperature: 0.7,
});
// 创建提示词模板
const translatePrompt = ChatPromptTemplate.fromMessages([
SystemMessagePromptTemplate.fromTemplate(
"你是一个专业的翻译助手。将用户输入翻译成{target_language}。只输出译文,不要解释。"
),
HumanMessagePromptTemplate.fromTemplate("{input}"),
]);
// 输出解析器:从 AIMessage 中提取纯文本
const outputParser = new StringOutputParser();
// 用 RunnableSequence 构建链
// 类比: const pipeline = compose(promptTemplate, model, parser)
const translateChain = RunnableSequence.from([
translatePrompt,
model,
outputParser,
]);
console.log("链的结构:");
console.log(" PromptTemplate → ChatDeepSeek → StringOutputParser\n");
// invoke() 时传入模板中的变量
const result1 = await translateChain.invoke({
target_language: "英文",
input: "今天天气真好,适合出去散步",
});
这个例子中,model、translatePrompt、outputParser都是Runnable,RunnableSequence.from 将这三个Runnable组成了一个链,他们是顺序执行的。
- 第一个
translatePrompt负责将target_language加到系统Prompt中规定翻译的语言,以及将需要翻译的内容加到用户Prompt中。 - 第二步
model将上一步拼好的prompt给到大模型,并得到大模型的回答。 - 第三步
outputParser从大模型的回答中提取纯文本。
RunnableParallel
上一个例子中展示了一个顺序执行的链,LangChain还提供了并行执行的链
ts
// ─── 示例 2:RunnableParallel 并行执行 ──────
console.log("📌 示例 2:RunnableParallel 并行执行多个任务\n");
// 两条翻译链(共用同一个 prompt 模板结构)
const englishChain = RunnableSequence.from([
ChatPromptTemplate.fromMessages([
SystemMessagePromptTemplate.fromTemplate(
"你是一个专业的翻译助手。将用户输入翻译成英文。只输出译文,不要解释。"
),
HumanMessagePromptTemplate.fromTemplate("{input}"),
]),
model,
outputParser,
]);
const japaneseChain = RunnableSequence.from([
ChatPromptTemplate.fromMessages([
SystemMessagePromptTemplate.fromTemplate(
"你是一个专业的翻译助手。将用户输入翻译成日文。只输出译文,不要解释。"
),
HumanMessagePromptTemplate.fromTemplate("{input}"),
]),
model,
outputParser,
]);
// RunnableParallel 同时执行多个链
const multiTranslateChain = RunnableSequence.from([
// 步骤 1:准备输入
RunnableLambda.from((input: string) => ({ input })),
// 步骤 2:并行执行两条翻译链 + 统计信息
RunnableParallel.from({
english: englishChain,
japanese: japaneseChain,
stats: RunnableLambda.from((data: { input: string }) => ({
charCount: data.input.length,
wordCount: data.input.split(/[,。!?\s]+/).filter(Boolean).length,
})),
}),
]);
console.log("并行执行 3 个任务:翻译(英文) + 翻译(日文) + 字数统计\n");
const analyzeResult = await multiTranslateChain.invoke(
"人工智能正在改变世界,LangChain 让 AI 开发更简单。"
);
console.log(` 英文翻译: ${(analyzeResult as any).english}`);
console.log(` 日文翻译: ${(analyzeResult as any).japanese}`);
console.log(` 统计信息: ${JSON.stringify((analyzeResult as any).stats)}\n`)
以上例子中,RunnableParallel.from构造了一个并行的链,他会并行执行english、japanese、stats三个任务,并将三个任务的结果都给到下一个步骤。由于上面的例子是最后一个动作,因此直接打印结果。
RunnableBranch
LangChian还提供了条件分支,能实现更复杂的分支处理
typescript
// ─── 1. RunnableBranch 条件分支 ────────────
console.log("📌 1. RunnableBranch ------ 根据输入决定走哪条路\n");
// 场景:根据用户意图路由到不同的处理链
// 类比前端的:switch/case 或者三元表达式
// 不同的处理链
const translateChain = RunnableSequence.from([
ChatPromptTemplate.fromMessages([
SystemMessagePromptTemplate.fromTemplate("你是一个翻译助手,将用户输入翻译成英文。只输出译文。"),
HumanMessagePromptTemplate.fromTemplate("{input}"),
]),
model,
outputParser,
]);
const summarizeChain = RunnableSequence.from([
ChatPromptTemplate.fromMessages([
SystemMessagePromptTemplate.fromTemplate("你是一个摘要助手,用一句话总结用户输入的核心内容。"),
HumanMessagePromptTemplate.fromTemplate("{input}"),
]),
model,
outputParser,
]);
const chatChain = RunnableSequence.from([
ChatPromptTemplate.fromMessages([
SystemMessagePromptTemplate.fromTemplate("你是一个友好的聊天助手,用中文回答用户。"),
HumanMessagePromptTemplate.fromTemplate("{input}"),
]),
model,
outputParser,
]);
/**
* RunnableBranch 用法:
*
* RunnableBranch.from([
* [条件函数1, 处理链1],
* [条件函数2, 处理链2],
* 默认处理链
* ])
*
* 条件函数接收输入,返回 boolean
* 类比前端的:声明式 if/else if/else
*
* ⚠️ 注意:使用 RunnableSequence 包装让类型统一
*/
const router = RunnableBranch.from([
[
(input: { input: string }) =>
input.input.includes("翻译") || input.input.includes("translate"),
translateChain,
],
[
(input: { input: string }) =>
input.input.includes("总结") || input.input.includes("摘要") || input.input.includes("概括"),
summarizeChain,
],
chatChain, // 默认分支
]);
console.log("智能路由:根据输入内容自动选择翻译/摘要/聊天链\n");
const testInputs = [
"把「人工智能正在改变世界」翻译成英文",
"帮我总结一下 TypeScript 的核心特点:静态类型、类型推断、接口、泛型、枚举",
"今天心情不错,想去爬山",
];
for (const input of testInputs) {
const result = await router.invoke({ input });
console.log(` 输入: "${input.substring(0, 50)}..."`);
console.log(` 输出: ${result}\n`);
}
在上面的例子中,RunnableBranch.from构造了一个分支处理动作,它接受一个元组,第一个值表示判断条件的函数,第二个值表示条件满足时运行的Runnable行为。在运行时,会根据用户是否包含了"翻译"或者"总结"关键词,进入特定的步骤。