🔥2026从零学Langchain——Python版本:
【Python版 2026 从零学Langchain 1.x】(一)快速开始和LCEL
【Python版 2026 从零学Langchain 1.x】(二)结构化输出和工具调用
【Python版 2026 从零学Langchain 1.x】(三)Agent和Memory
🔥2026从零学Langchain——TS版本:
【TS版 2026 从零学Langchain 1.x】(一)快速开始和LCEL
【TS版 2026 从零学Langchain 1.x】(二)结构化输出和工具调用
【TS版 2026 从零学Langchain 1.x】(三)Agent和Memory
一、背景知识
1. LLM
Transformer
下图是 2017年的论文 《Attention is All You Need》 中的 Transformer 架构图。其中的核心是 self-attention 机制。
这篇论文主要目的是用 Transformer 架构来实现一个端到端的自动翻译系统。
分为Encoder(左边部分)和Decoder(右边部分)。现在的GPT(Generative Pre-trained Transformer)被称为Decoder-only架构,是因为它只用到了右边的Decoder(编码器)。
我们可以简单把一个单词视为一个词元(token),比如单词"I","love","you"分别都是一个token。宏观来看,GPT的本质就是预测下一词,在Decoder中,总是根据前面的词元,预测下一个词元,预测的新词元会被加入到输入,继续预测下一个词元,如此循环直到达到停止条件。
下图解释了Decoder内部是如何工作的。
-
单词转为向量(专业的说法叫
embedding) -
然后每个词向量,会生成q,k,v向量,一次self attention就会对应有一个Q,K,V矩阵,这个中间会经过矩阵乘法,得到更新的语义向量(此时的向量包含了单词本身和语境含义等更丰富的特征)
-
最后这些语义向量经过线性变换,转换为一个概率列表,找到概率最高的索引,用索引(在词表中)查询出单词,这个就是要生成的单词。
LLM 结构化输出和工具调用
LLM 结构化输出
LLM 结构化输出的发展经历了三个主要阶段:
- 软约束阶段:提示词工程 (Prompt Engineering) 提示词里写明“请以 JSON 格式输出”,然后用正则去匹配,并验证是否符合JSON schema,如果报错就反馈给LLM重新生成。
- 增强引导阶段:模式化训练与 JSON Mode 指令微调 (SFT):在微调阶段加入大量结构化数据,让模型“内化”结构化格式的概率分布。 JSON Mode:OpenAI 等厂商推出的功能。模型在生成时被告知:你必须输出 JSON,否则报错。
- 硬约束阶段:受限解码 (Constrained Decoding) 通过在推理层 直接干预概率分布,从根本上杜绝错误。100% 符合预定义的 JSON Schema,开发者不再需要重试逻辑。
受限解码
这是保证“结构化”万无一失的技术手段。其工作流程如下:
- JSON Schema 转有限状态机 (FSM):
系统会将用户要求的 JSON Schema(例如:必须包含
name字符串和age数字)转换为一个有限状态机 (Finite State Machine)。这个状态机定义了在生成的每一个位置,哪些字符是“合法”的。 - Logit 掩码 (Logit Masking):
在模型每生成一个 Token 的瞬间,系统会检查状态机:
- 模型计算出词表中所有 Token 的原始概率(Logits)。
- 状态机判定:此时只能出现双引号
"(开始一个键名),或者大括号}(结束对象)。 - 掩码干预:系统将所有不符合规则的 Token 概率设为负无穷(),强制将其过滤掉。
- 模型最终只能从剩下的“合规 Token”中按概率选一个。
- 自动填充 (Token Forcing):
对于那些“确定性”的符号(如 JSON 里的
":),系统甚至不需要模型预测,直接由引擎强制填充,提高生成速度和准确性。
工具调用
工具调用 其实 就是依赖 结构化输出。用户通过提示词把工具信息告诉LLM,LLM以某个格式返回数据给「应用程序」,然后程序侧执行工具(函数/协程)
思维链 - CoT
1.首先,CoT (Chain of Thought)这种能力最初不是在训练阶段产生的,模型只会根据目前词元预测下一个词元。CoT更像是人们发现有个好的方式引导模型,输出更准确的答案,而采用的一种“引导”方式,然后被主流LLM provider采用作为其工程化的一部分。有些模型也会专门用推理的数据集来微调LLM,让其这种能力表现的更好。
2.我们经常听到zero-shot、 few-shot这些词,它们都是什么意思呢?
zero-shot意思直接让LLM告诉我答案,few-shot则是给他一些例子,让他按例子中那样输出(包括固定格式,一步步推理出答案等)。
3.然后引出著名的一句话 let's think step by step。研究员发现在问题后面加上这句话能让LLM涨点(即输出答案更好更准)。这个时候模型的输出就不仅仅直接给你答案,还会给你他的一个推理过程(这会增加计算,消耗更多token)
4.然后介绍下面两个概念
1)zero-shot CoT : 给问题加上 let's think step by step 的提示,就是zero-shot CoT。
2)few-shot CoT : 通过示例( 给出推理过程),引导LLM推理,然后给出正确答案,就是few-shot CoT
5.few-shot CoT 的效果自然是比zero-shot CoT 效果好,但是每次都是自己来写几个例子,太麻烦了!能不能自动化呢?
6.于是一些论文提出来auto CoT。能自动构建few shot和给出推理过程,实现分两阶段:
- 阶段1:聚类的方法从题库找例子(包含问题和推理过程)。
- 阶段2:将few shot + 问题 + "let's think step by step", 送给LLM生成推理。
7.现在的“思考”模型大多是专门训练过的,比如DeepSeek-R1。它们通过:
- SFT(监督微调): 在大量包含“思考过程”的数据集上训练,让模型学会这种表达范式。
- RL(强化学习): 这是目前最前沿的方向。通过奖励正确的最终答案,模型在尝试各种推理路径时,会自我总结哪些“思考链条”是有效的,从而进化出极强的逻辑能力。
2. Agent
概念
Agent 这个词相信大家已经不陌生了,大家的一个共识就是:Agent = LLM(大脑)+ 规划(Planning)+ 记忆(Memory)+ 工具使用(Tool Use)。
Agent和LLM的区别?
LLM 只能回答你的问题,而Agent可以依据LLM的回答来操作真实物理世界和规划流程。
Agent和传统工作流的区别?
传统工作流(传统自动化)是一种固定的流程,是预先设计好的流程,无法应对复杂多变的需求。而Agent 能应付“意料之外”的情况,能处理更复杂的场景。
范式
下面三张图来源于 datawhalechina老师的 Hello-Agents教程。
1.ReAct范式:先思考再行动,然后观察(循环)
2.Plan-and-Solve范式:先列出计划清单(task集合),然后依次实现每个task。
3.Reflect范式:
-
执行,用前面的方法(如 ReAct 或 Plan-and-Solve)执行
-
反思,然后对结果评估(比如性能,可扩展性等)
-
优化,将执行结果和反思给LLM,重新生成结果。
3. langchain知识点和生态
首先介绍下langchain生态的相关包
| 包名 | 定位 | 职责 |
|---|---|---|
@langchain/core | 底层抽象层(基石) | 定义所有组件的标准接口与基础结构,不包含任何具体模型或第三方集成。例如: • BaseChatModel, BaseTool, Runnable • 消息类型: HumanMessage, AIMessage • LCEL 管道语法/可运行对象编排(如 prompt.pipe(model)、RunnableSequence) • 输出解析器、提示模板等基础结构 |
langchain | 主应用入口包(开箱即用) | 提供更高阶、易用的封装,聚焦现代 Agent 应用开发,整合常用能力,适合大多数开发者直接使用。例如: • Agents / Tool calling 相关封装 • Chains / Retrievers 等常用组合组件 • 回调(callbacks)、流式输出(streaming)等工程化能力 • 重新导出 @langchain/core 中的常用类依赖 @langchain/core |
@langchain/openai | OpenAI 模型集成适配器 | 实现 OpenAI 聊天/嵌入模型接入: • ChatOpenAI • OpenAIEmbeddings依赖 @langchain/core |
@langchain/langgraph | 有状态智能体工作流编排引擎 | 构建图结构智能体: • 节点(Node) • 边(Edge) • 全局状态(State) • 支持循环、分支、持久化 依赖 @langchain/core(可选集成 langchain) |
langchain的核心内容大纲
二、环境和项目准备
1.安装bun。
bun是一种js运行时,同时也是一个包管理器。作为Nodejs的替代方案,比Node有如下优势:
- 高性能:启动速度是Nodejs的3-4倍,很多场景运行速度优于Node
- 包管理:本身作为包管理器,替代pnpm,并且安装速度更快。
- All-in-One:原生支持TS,支持热重载和转译打包等。
安装方式和更多介绍,见bun中文网
2.使用bun初始化项目 (截止目前 bun的版本是1.3.3)
mkdir my_project && cd my_project bun init
3.安装相关依赖
bun add openai langchain @langchain/openai @langchain/langgraph
bun add zod dotenv
(dotenv是可选的,bun原生支持自动从.env加载环境变量)
下面的内容用到了 Deepseek 和 硅基流动平台提供的模型,请注册账号和生成api_key,然后把api_key 放入项目langchain-tutorials的.env文件中。
.env文件配置:
bun自动会加载.env的变量到环境变量, 然后用到zod校验这些环境变量。
import { z } from "zod";
const envSchema = z.object({
// SiliconFlow Configuration
SILICONFLOW_BASE_URL: z.string().url(),
SILICONFLOW_API_KEY: z.string(),
DEEPSEEK_API_KEY: z.string(),
// Model Names
DS_MODEL: z.string(),
DSR1_MODEL: z.string(),
GLM_MODEL: z.string(),
Qwen3_32B_MODEL: z.string(),
EMBEDDING_MODEL: z.string(),
});
// 解析环境变量
const parsedEnv = envSchema.safeParse(process.env);
if (!parsedEnv.success) {
console.error("❌ Invalid environment variables:", parsedEnv.error.format());
process.exit(1);
}
export const settings = {
siliconflow_base_url: parsedEnv.data.SILICONFLOW_BASE_URL,
siliconflow_api_key: parsedEnv.data.SILICONFLOW_API_KEY,
deepseek_api_key: parsedEnv.data.DEEPSEEK_API_KEY,
ds_model: parsedEnv.data.DS_MODEL,
dsr1_model: parsedEnv.data.DSR1_MODEL,
glm_model: parsedEnv.data.GLM_MODEL,
qwen3_32b_model: parsedEnv.data.Qwen3_32B_MODEL,
embedding_model: parsedEnv.data.EMBEDDING_MODEL,
};
三、快速开始案例
下面是官方的一个最简单的示例,介绍了一个标准的agent创建和使用流程。
import { settings } from "@/config";
import { MemorySaver } from "@langchain/langgraph";
import { ChatOpenAI } from "@langchain/openai";
import type { RunnableConfig } from "@langchain/core/runnables";
import { createAgent, tool, toolStrategy } from "langchain";
import { z } from "zod";
/*
标准的agent创建和使用流程:
1.定义提示词
2.定义工具
3.构建chat model
4.结构化输出
5.memory
6.创建agent
*/
// 1.定义提示词
const SYSTEM_PROMPT =
"你是天气助手,如果被问到天气问题,请先确定地点,然后调用相关工具获取实际天气";
// 2.定义工具
const get_weather_for_location = tool(({ city }: { city: string }) => {
return `${city}总是晴日`;
}, {
name: "get_weather_for_location",
description: "获取指定城市的天气",
schema: z.object({
city: z.string(),
}),
});
const get_user_location = tool(
async (_: Record<string, never>, config?: RunnableConfig) => {
const user_id = String(
(config?.configurable as Record<string, unknown> | undefined)?.user_id ??
"",
);
return user_id === "1" ? "北京" : "上海";
},
{
name: "get_user_location",
description: "根据用户ID获取用户位置",
schema: z.object({}),
},
);
// 3.构建chat model
// 符合openai规范的api,可以使用 @langchain/openai。我们使用 SiliconFlow 提供的 GLM-4.7 模型
// 注意:尽量使用一些新模型,一些旧模型可能会存在一些特性不支持
const model = new ChatOpenAI({
model: settings.glm_model,
apiKey: settings.siliconflow_api_key,
configuration: {
baseURL: settings.siliconflow_base_url,
},
temperature: 0.9,
maxTokens: 5000,
timeout: 60000,
});
// 4.结构化输出
// dataclass 和 Pydantic 都是支持的,用来定义结构化输出的格式。
const ResponseFormat = z.object({
// 一语双关的回答 (必要)
punny_response: z.string().describe("一语双关的回答(必要)"),
// 和天气相关的信息点(可选)
weather_conditions: z
.string()
.nullable()
.optional()
.describe("和天气相关的信息点(可选)"),
// 字符串,用于描述响应的长度,取值为"short"、"medium"、"long"之一
length: z
.enum(["short", "medium", "long"])
.default("short")
.describe('响应长度,取值为"short"、"medium"、"long"之一'),
}).describe("agent的响应格式");
// 5.memory
const checkpointer = new MemorySaver();
// 6.创建agent
const agent = createAgent({
model,
systemPrompt: SYSTEM_PROMPT,
tools: [get_user_location, get_weather_for_location],
responseFormat: toolStrategy(ResponseFormat),
checkpointer,
// debug: true, # 开启debug模式,会打印出agent的运行过程
});
async function main() {
// `thread_id`一次会话的唯一标识符
const config = { configurable: { thread_id: "1", user_id: "1" } };
const response1 = await agent.invoke(
{ messages: [{ role: "user", content: "今天天气如何?" }] },
config,
);
console.log(response1.structuredResponse);
// {
// punny_response: "北京总是晴日,今天也要保持阳光好心情!",
// weather_conditions: "晴朗",
// length: "short",
// }
// 注意:我们可以用同一个`thread_id`继续这个对话.
const response2 = await agent.invoke(
{ messages: [{ role: "user", content: "thank you!" }] },
config,
);
console.log(response2.structuredResponse);
// {
// punny_response: "不客气!祝你有个阳光灿烂的一天!",
// length: "short",
// }
}
if (import.meta.main) {
await main();
}
里面的structured output(结构化输出)和Tool calling (工具调用) 放到第二篇详细介绍, 里面的Memory 和 Agent 放到第三篇详细介绍。
四、模型 - Models(核心组件)
1. 模型的集成
有下面两种方式:
方式一:
initChatmodel,只需要传入LLM Provider的官方文档指定的名字。
方式二:
使用对应包的ChatXxx对象,比如ChatDeepSeek/ChatOpenAI 。 (使用ChatDeepSeek就需要安装@langchain/deepseek;使用ChatOpenAI则需要安装@langchain/openai) 参考官方集成文档
可以查看模型文档,文档说明了支持哪些模型(通过映射表自动推导LLM provider)
下面演示了两种方式创建模型。
async function testDeepSeekModel() {
// 使用 ChatDeepSeek
// const model = new ChatDeepSeek({
// model: "deepseek-chat",
// apiKey: settings.deepseek_api_key,
// temperature: 0.9,
// maxTokens: 1000,
// timeout: 60000, // TS 中 timeout 通常是毫秒
// });
const model = await initChatModel("deepseek-chat", {
modelProvider: "deepseek",
apiKey: settings.deepseek_api_key,
temperature: 0.9,
maxTokens: 1000,
timeout: 60000, // TS 中 timeout 通常是毫秒
});
const response = await model.invoke("你好");
console.log(response.content);
}
还有一些平台不自研LLM,而是部署其他家的开源LLM,一般来说这些都会遵循OpenAI的API标准,那么就可以用ChatOpenAI接入,比如:接入硅基流动部署的DeepSeek
/**
* 测试第三方模型,只要符合 openai 规范的 api,都可以用 ChatOpenAI 来创建该模型的实例
*/
async function testThirdPartModel() {
const model = new ChatOpenAI({
model: settings.ds_model,
apiKey: settings.siliconflow_api_key,
configuration: {
baseURL: settings.siliconflow_base_url,
},
temperature: 0.9,
maxTokens: 1000,
timeout: 60000,
});
const response = await model.invoke("你好");
console.log(response.content);
}
其他参数解释:
temperature :温度参数,0-1之间的浮点数,用于控制模型的随机性。值越高,输出越随机(适合比较创新的场景);值越低,输出越确定(适合严谨的场景)。
maxTokens :最大输出 token 数,用于限制模型生成的文本长度。
timeout :请求超时时间,单位为秒。
2. 调用
模型提供了下面这些方法调用 (实际上一个Runnable对象都提供了这几个方法,下面会介绍):
invoke(): 运行单个输入。stream(): 流式输出。batch(): 并行运行多个输入。
下面分别举例说明。
async function testInvoke() {
/*
invoke 调用
*/
// const res = await model.invoke("Translate 'I love programming' into Chinese.");
// 等价于
// const res = await model.invoke([{ role: "user", content: "Translate 'I love programming' into Chinese." }]);
// 等价于
const res = await model.invoke([new HumanMessage({ content: "Translate 'I love programming' into Chinese." })]);
console.log(res);
/*
返回:AIMessage
{
"content": "我喜欢编程。",
"response_metadata": {...},
"id": "...",
...
}
*/
// 通过管道pipe 链接模型和解析器,返回一个增强的链。 新的链调用模型的结果 会从 AIMessage 转换为字符串
const chain = model.pipe(new StringOutputParser()); // 这就是 LCEL (LangChain Expression Language)
const ans = await chain.invoke("Translate 'I love programming' into Chinese.");
console.log(ans);
/*
输出:我喜欢编程。
*/
}
async function testStream() {
/*
Stream 调用 (流式输出)
*/
const chain = model.pipe(new StringOutputParser());
const messages: [string, string][] = [
[
"system",
"You are a helpful translator. Translate the user sentence to Chinese.",
],
["human", "I love programming."],
];
const stream = await chain.stream(messages); // 返回一个异步迭代器,可以用for await 遍历
for await (const chunk of stream) {
console.log(chunk);
}
/*
输出:
我很
喜欢
编程。
*/
}
async function testBatch() {
/*
# Batch 调用 (并行执行多个请求)
数组的每一项是一个请求, 请求是并行的
*/
const chain = model.pipe(new StringOutputParser());
const batches = await chain.batch([
[
{
role: "system",
content: "You are a helpful translator. Translate the sentence to Chinese.",
},
{ role: "human", content: "I love programming." },
],
[{ role: "human", content: "100字内,介绍下langchain。" }],
]);
for (let i = 0; i < batches.length; i += 1) {
console.log(`Result ${i + 1}:\n${batches[i]}\n`);
}
/*
输出:
Result 1:
我非常热爱编程。
Result 2:
LangChain是一个开源框架,旨在简化基于大型语言模型(LLM)的应用程序开发。它通过提供模块化组件和工具链,帮助开发者轻松连接LLM与外部数据源、API或计算资源,实现数据感知和代理式交互应用。核心功能包括Prompt模板化、记忆管理、链式调用及多工具集成,大幅提升开发效率。
*/
// 控制并发
// const batches = await chain.batch(
// [
// '翻译"I love programming."成中文',
// "100字内,介绍下框架langchain。",
// "100字内,介绍下js运行时 bun。",
// ],
// {
// maxConcurrency: 2, // 限制并发数为2
// }
// )
// console.log(batches);
}
五、链表达式 - LCEL
前面提到了面向应用的高层用法,底层是 @langchain/core这个包在做支撑.
接下来,将介绍Runnable的概念。提示词模版,模型,解析器这些都是 Runnable,意味着它们返回的对象,都可以使用invoke方法。
基于 Runnable, @langchain/core实现了LCEL,且看下面的介绍。
1. LCEL 思想
LCEL(LangChain Expression Language,LangChain 表达式语言)是 LangChain 框架推出的一种声明式语法,旨在让开发者能够以最简洁、统一的方式构建和组合复杂的“链”(Chains),见官方博客.
简单来说,就是下面的逻辑,最终的「链」就是一个Runnable
// 链:提示词 -> 模型 -> 字符串解析器
chain = prompt.pipe(model).pipe(output_parser)
先介绍下@langchain/core 提供提示词模板和 结构化输出 两个子模块。
@langchain/core/output_parsers提供了一些解析器,比如StringOutputParser就是将返回结果转为字符串(原本返回是一个json对象)。@langchain/core/prompts提供了构建和管理 提示词模板的方法。
import { StringOutputParser } from "@langchain/core/output_parsers";
import {
AIMessagePromptTemplate,
ChatPromptTemplate,
HumanMessagePromptTemplate,
SystemMessagePromptTemplate,
} from "@langchain/core/prompts";
2. LCEL 的核心组件:Runnable
Runnable
在 LCEL 中,所有的组件都被抽象为 "Runnable"(可运行对象)。它们遵循统一的接口:
invoke(): 运行单个输入。batch(): 并行运行多个输入。stream(): 流式输出。
PromptTemplate(提示词)、ChatOpenAI(模型)和StringOutputParser(解析器)都是Runnable,即它们都有上述的接口(这些接口/方法 是异步的)。
PromptTemplate 示例:
async function testPromptTemplate() {
/*
测试提示模板:
SystemMessagePromptTemplate
HumanMessagePromptTemplate
AIMessagePromptTemplate
ChatPromptTemplate
*/
const humanPrompt = HumanMessagePromptTemplate.fromTemplate(
"你是一个专业的翻译。请将以下文本从英文翻译为中文:\n {input}",
);
const humanMessage = await humanPrompt.format({ input: "I love programming." });
console.log(
"h_message 是 HumanMessage 类型吗?",
humanMessage instanceof HumanMessage,
); // true
console.log(humanMessage);
// 输出
// HumanMessage(content='你是一个专业的翻译。请将以下文本从英文翻译为中文:\n I love programming.', additional_kwargs={}, response_metadata={})
const chatPrompt = ChatPromptTemplate.fromMessages([
["system", "你是一个{role}。"],
["human", "请将以下文本从英文翻译为中文:\n {userInput}"],
]);
// ChatPromptTemplate 是runnable, 调用invoke 。 返回 Message列表
const chatMessages = await chatPrompt.invoke({
role: "专业的翻译",
userInput: "I love programming.",
});
console.log(chatMessages);
// 输出:
/*
ChatPromptValue {
lc_serializable: true,
...
messages: [
SystemMessage {
"content": "你是一个专业的翻译。",
"additional_kwargs": {},
"response_metadata": {}
}, HumanMessage {
"content": "请将以下文本从英文翻译为中文:\n I love programming.",
"additional_kwargs": {},
"response_metadata": {}
}
],
}
*/
// !注意:format 方法返回的是一个字符串
const chatPromptStr = await chatPrompt.format({
role: "专业的翻译",
userInput: "I love programming.",
});
console.log(typeof chatPromptStr); // string
console.log(chatPromptStr);
/*
System: 你是一个专业的翻译。
Human: 请将以下文本从英文翻译为中文:
I love programming.
*/
}
StringOutputParser 示例:
async function testOutputParser() {
/*
测试输出解析器:StringOutputParser
*/
const parser = new StringOutputParser();
const ans = await parser.invoke("翻译 'I love programming' 成中文。");
console.log(ans);
// 输出:
// 翻译 'I love programming' 成中文。
}
一个简单的LCEL 示例:
async function testLcel() {
/*
测试 LCEL (LangChain Expression Language)
*/
const chatPrompt = ChatPromptTemplate.fromMessages([
["system", "你是一个{role}。"],
["human", "请将以下文本从英文翻译为中文:\n {userInput}"],
]);
const chain = chatPrompt.pipe(model).pipe(new StringOutputParser());
const ans = await chain.invoke({
role: "专业的翻译",
userInput: "I love programming.",
});
console.log(ans);
// 输出:
// 我热爱编程。
}
async function testRunnableSequence() {
const prompt = ChatPromptTemplate.fromMessages([
["system", "你是一个{type},请根据用户的提问进行回答"],
["system", "{instruction}"],
["human", "{question}"],
]);
const classifier = (input: { question: string; instruction: string }) => {
const { question, instruction } = input;
const typeValue = question.includes("科普") ? "科普专家" : "智能助手";
return {
type: typeValue,
question,
instruction,
};
};
// RunnableLambda.from 将classifier包装成一个Runnable,使它可以在LCEL中使用
// const chain = RunnableLambda.from(classifier).pipe(prompt).pipe(model).pipe(new StringOutputParser());
// 等价于
// const chain = RunnableSequence.from([
// RunnableLambda.from(classifier),
// prompt,
// model,
// new StringOutputParser(),
// ]);
// 等价于
const chain = RunnableSequence.from([
classifier,//直接传入函数, RunnableSequence内会被自动包装成Runnable
prompt,
model,
new StringOutputParser(),
]);
const ans = await chain.invoke({
question: "科普,鲸鱼是哺乳动物么?只需要回答是或不是",
instruction: "用中文回答",
});
console.log(ans);
// 是
}
RunableXxx 工具
langchain_core.runnables 提供了下面一些工具方法:
RunnableSequence: 构建一条链。当你使用pipe连接两个组件时(或者使用RunnableSequence.from组合)都会返回一个RunnableSequence对象。RunnableBranch: 选择子链(相当于if else)。将逐步被淘汰,用LangGraph替代其分支功能。RunnableLambda: 一个封装器,它将一个普通的 函数转换为一个 Runnable 对象。RunnablePassthrough: 透传链的参数,不做任何处理。其中assign方法是 RunnablePassthrough 最强大的功能。它允许你在不改变原始输入字典的情况下,向其中添加新的键值对。RunnableParallel: 顾名思义,将参数同时传递给多个子链,子链并行处理完返回结果汇总。
a.我们通过下面的例子演示了RunnableSequence和RunnableLambda .
判断用户的问题是否为科普问题,从而动态修改系统提示词。
async function testRunnableSequence() {
const prompt = ChatPromptTemplate.fromMessages([
["system", "你是一个{type},请根据用户的提问进行回答"],
["system", "{instruction}"],
["human", "{question}"],
]);
const classifier = (input: { question: string; instruction: string }) => {
const { question, instruction } = input;
const typeValue = question.includes("科普") ? "科普专家" : "智能助手";
return {
type: typeValue,
question,
instruction,
};
};
// RunnableLambda.from 将classifier包装成一个Runnable,使它可以在LCEL中使用
// const chain = RunnableLambda.from(classifier).pipe(prompt).pipe(model).pipe(new StringOutputParser());
// 等价于
// const chain = RunnableSequence.from([
// RunnableLambda.from(classifier),
// prompt,
// model,
// new StringOutputParser(),
// ]);
// 等价于
const chain = RunnableSequence.from([
classifier,//直接传入函数, RunnableSequence内会被自动包装成Runnable
prompt,
model,
new StringOutputParser(),
]);
const ans = await chain.invoke({
question: "科普,鲸鱼是哺乳动物么?只需要回答是或不是",
instruction: "用中文回答",
});
console.log(ans);
// 是
}
b.进一步,用下面的例子演示了RunnableBranch和 RunnablePassthrough .
设有不同的“专家链”,分析用户的问题转发给不同专家模型。
async function testRunnableBranch() {
const classifyResultSchema = z.object({
type: z.enum(["科普", "编程", "其他"]),
});
const structuredModel = model.withStructuredOutput(classifyResultSchema);
const sciencePrompt = ChatPromptTemplate.fromMessages([
["system", "你是科普专家,通俗准确、简洁回答。"],
["human", "{question}"],
]);
const scienceExpert = RunnableSequence.from([
sciencePrompt,
model,
new StringOutputParser(),
]);
const codePrompt = ChatPromptTemplate.fromMessages([
["system", "你是编程专家,提供代码或技术解答。"],
["human", "{question}"],
]);
const codeExpert = RunnableSequence.from([codePrompt, model, new StringOutputParser()]);
const generalPrompt = ChatPromptTemplate.fromMessages([
["system", "你是智能助手,简洁中文回答。"],
["human", "{question}"],
]);
const generalExpert = RunnableSequence.from([
generalPrompt,
model,
new StringOutputParser(),
]);
const classifier = RunnablePassthrough.assign({
// classifyResult 会 赋值给 input["classifyResult"]
classifyResult: async (input: { question: string }) => {
return structuredModel.invoke(
`请判断以下问题的类型,并只返回JSON:{"type": "科普"|"编程"|"其他"}。问题:${input.question}`,
);
},
});
const debugRunnable = RunnableLambda.from((input: unknown) => {
console.log("中间结果:", input);
return input;
});
// RunnableBranch 将从三个分支中选取一个,根据lambda函数的返回值判断选择哪一个。
const branch = RunnableSequence.from([
classifier,
debugRunnable,
RunnableBranch.from([
[
(input: { classifyResult?: { type?: string } }) =>
input.classifyResult?.type === "科普",
scienceExpert,
],
[
(input: { classifyResult?: { type?: string } }) =>
input.classifyResult?.type === "编程",
codeExpert,
],
generalExpert,
]),
]);
const result = await branch.invoke({ question: "简单回答下,鲸鱼是哺乳动物吗?" });
console.log(result);
/*
中间结果: {
question: "简单回答下,鲸鱼是哺乳动物吗?",
classifyResult: {
type: "科普",
},
}
是的,鲸鱼是哺乳动物。
*/
}
3. 为什么需要 LCEL?
对比:
传统写法:
const chain = LLMChain(prompt=prompt, llm=model)
const result = chain.run("主题")
声明式写法:
// 链:提示词 -> 模型 -> 字符串解析器
const chain = prompt.pipe(model).pipe(new StringOutputParser())
const result = chain.invoke({"topic": "主题"})
可以看出「声明式写法」更直观清晰,可以看出顺序关系。
LCEL 不仅仅是“语法糖”,它在底层解决了大模型应用从原型到生产环境的许多痛点:
- 流式处理支持 (Streaming): 只要链中有一个组件支持流式输出,整个链就能自动流式传输结果,大大提升了 UI 交互的响应速度。
- 异步支持 (Async): 同样的链代码,既可以用 .invoke() 同步调用,也可以用 .ainvoke() 异步调用,非常适合高并发的生产环境。
- 并行执行 (Parallelism): 如果你有多个任务(比如同时搜索两个搜索引擎),使用 RunnableParallel 可以自动实现并行处理,降低总延迟。
- 自动重试与回退 (Retries/Fallbacks): 你可以为链的任何部分配置重试机制或备用模型。如果 GPT-4 挂了,系统能自动切换到 Claude 或本地模型。
- 访问中间结果: 在复杂的链中,你可以轻松提取中间步骤的输出,方便调试和展示过程。
- 无缝集成 LangSmith: 所有步骤都会自动记录到 LangSmith 追踪平台,开发者能一眼看到每一步消耗了多少 Token、响应时间是多少。
附录
代码 - 传送门
本章节对应的代码: