ai agent -- LCEL 汇总

一. 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 的结果 responseoutputParser 去格式化。

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)是无状态的,每次调用互相独立,无法记住前文。

其工作流程可以理解为:

  1. 调用时传入 session_id(会话唯一标识)
  2. 通过 get_session_history 函数按 session_id 加载该会话的历史消息
  3. 把历史消息通过 MessagesPlaceholder 注入 Prompt
  4. 执行被包装的原始链
  5. 把本轮「用户提问 + AI 回复」追加保存 到该 session_id 对应的历史存储中
js 复制代码

三.总结

RunnableSequence 是"串联器",RunnableLambda 是"转换器"。

相关推荐
计算机魔术师2 小时前
NVIDIA 季度营收指引达 1080 亿美元,首次突破单季千亿大关
前端
数据狐(Datafox)2 小时前
京东商品列表API技术解析与落地应用(含标准 JSON 示例)
java·大数据·前端·人工智能·python·数据分析·json
风萧何2 小时前
架构艺术,是权衡的艺术。
前端·javascript·架构
YIAN2 小时前
ESLint 新版 Flat Config 实战:团队代码风格统一,从这一份配置开始
前端·团队管理
计算机魔术师2 小时前
英伟达单季营收逼近千亿美元,黄仁勋放话:真实需求远超想象
前端
明月_清风2 小时前
看完这段关于"全插件化架构"的技术分析后,我整理了一份笔记
前端·后端
YIAN2 小时前
Next.js App Router 全栈实战:从 0 到 1 写一个 Todo 应用,前端后端一个项目搞定
前端·全栈·next.js
计算机魔术师2 小时前
英伟达预计 2028 财年营收同比增 70%,黄仁勋称实际需求远高于此
前端
掘金酱2 小时前
🔥 AI 时代,Token 就是你的数字燃料!晒账单,赢好礼!
前端·人工智能·ai编程