【TS版 2026 从零学Langchain 1.x】(一)快速开始和LCEL

58 阅读11分钟

🔥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 机制。

image.png

这篇论文主要目的是用 Transformer 架构来实现一个端到端的自动翻译系统。 分为Encoder(左边部分)和Decoder(右边部分)。现在的GPT(Generative Pre-trained Transformer)被称为Decoder-only架构,是因为它只用到了右边的Decoder(编码器)。

我们可以简单把一个单词视为一个词元(token),比如单词"I","love","you"分别都是一个token。宏观来看,GPT的本质就是预测下一词,在Decoder中,总是根据前面的词元,预测下一个词元,预测的新词元会被加入到输入,继续预测下一个词元,如此循环直到达到停止条件。

image.png

下图解释了Decoder内部是如何工作的。

  • 单词转为向量(专业的说法叫embedding

  • 然后每个词向量,会生成q,k,v向量,一次self attention就会对应有一个Q,K,V矩阵,这个中间会经过矩阵乘法,得到更新的语义向量(此时的向量包含了单词本身和语境含义等更丰富的特征)

  • 最后这些语义向量经过线性变换,转换为一个概率列表,找到概率最高的索引,用索引(在词表中)查询出单词,这个就是要生成的单词。

image.png

LLM 结构化输出和工具调用

LLM 结构化输出

LLM 结构化输出的发展经历了三个主要阶段:

  1. 软约束阶段:提示词工程 (Prompt Engineering) 提示词里写明“请以 JSON 格式输出”,然后用正则去匹配,并验证是否符合JSON schema,如果报错就反馈给LLM重新生成。
  2. 增强引导阶段:模式化训练与 JSON Mode 指令微调 (SFT):在微调阶段加入大量结构化数据,让模型“内化”结构化格式的概率分布。 JSON Mode:OpenAI 等厂商推出的功能。模型在生成时被告知:你必须输出 JSON,否则报错。
  3. 硬约束阶段:受限解码 (Constrained Decoding) 通过在推理层 直接干预概率分布,从根本上杜绝错误。100% 符合预定义的 JSON Schema,开发者不再需要重试逻辑。
受限解码

这是保证“结构化”万无一失的技术手段。其工作流程如下:

  • JSON Schema 转有限状态机 (FSM): 系统会将用户要求的 JSON Schema(例如:必须包含 name 字符串和 age 数字)转换为一个有限状态机 (Finite State Machine)。这个状态机定义了在生成的每一个位置,哪些字符是“合法”的。
  • Logit 掩码 (Logit Masking): 在模型每生成一个 Token 的瞬间,系统会检查状态机:
    1. 模型计算出词表中所有 Token 的原始概率(Logits)。
    2. 状态机判定:此时只能出现双引号 "(开始一个键名),或者大括号 }(结束对象)。
    3. 掩码干预:系统将所有不符合规则的 Token 概率设为负无穷(-\infty),强制将其过滤掉。
    4. 模型最终只能从剩下的“合规 Token”中按概率选一个。
  • 自动填充 (Token Forcing): 对于那些“确定性”的符号(如 JSON 里的 ": ),系统甚至不需要模型预测,直接由引擎强制填充,提高生成速度和准确性。
工具调用

工具调用 其实 就是依赖 结构化输出。用户通过提示词把工具信息告诉LLM,LLM以某个格式返回数据给「应用程序」,然后程序侧执行工具(函数/协程)

思维链 - CoT

1.首先,CoT (Chain of Thought)这种能力最初不是在训练阶段产生的,模型只会根据目前词元预测下一个词元。CoT更像是人们发现有个好的方式引导模型,输出更准确的答案,而采用的一种“引导”方式,然后被主流LLM provider采用作为其工程化的一部分。有些模型也会专门用推理的数据集来微调LLM,让其这种能力表现的更好。

2.我们经常听到zero-shotfew-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生成推理。

image.png

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范式:先思考再行动,然后观察(循环)

image.png

2.Plan-and-Solve范式:先列出计划清单(task集合),然后依次实现每个task。

image.png

3.Reflect范式:

  • 执行,用前面的方法(如 ReAct 或 Plan-and-Solve)执行

  • 反思,然后对结果评估(比如性能,可扩展性等)

  • 优化,将执行结果和反思给LLM,重新生成结果。

image.png

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/openaiOpenAI 模型集成适配器实现 OpenAI 聊天/嵌入模型接入:
• ChatOpenAI
• OpenAIEmbeddings

依赖 @langchain/core
@langchain/langgraph有状态智能体工作流编排引擎构建图结构智能体:
• 节点(Node)
• 边(Edge)
• 全局状态(State)
• 支持循环、分支、持久化

依赖 @langchain/core(可选集成 langchain

langchain的核心内容大纲

image.png

二、环境和项目准备

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文件配置:

image.png

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 提供了下面一些工具方法:

  1. RunnableSequence: 构建一条链。当你使用pipe 连接两个组件时(或者使用 RunnableSequence.from组合)都会返回一个RunnableSequence对象。
  2. RunnableBranch: 选择子链(相当于if else)。将逐步被淘汰,用LangGraph替代其分支功能。
  3. RunnableLambda: 一个封装器,它将一个普通的 函数转换为一个 Runnable 对象
  4. RunnablePassthrough: 透传链的参数,不做任何处理。其中assign方法是 RunnablePassthrough 最强大的功能。它允许你在不改变原始输入字典的情况下,向其中添加新的键值对
  5. RunnableParallel: 顾名思义,将参数同时传递给多个子链,子链并行处理完返回结果汇总。

a.我们通过下面的例子演示了RunnableSequenceRunnableLambda . 判断用户的问题是否为科普问题,从而动态修改系统提示词。

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.进一步,用下面的例子演示了RunnableBranchRunnablePassthrough . 设有不同的“专家链”,分析用户的问题转发给不同专家模型。


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、响应时间是多少。

附录

代码 - 传送门

本章节对应的代码:

langchain_ts 篇1