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 是"转换器"。

相关推荐
萧行之1 小时前
可视化技术复盘:从图形语法看 D3、Vega-Lite、ECharts、Observable Plot
前端·学习·数据可视化
尾善爱看海9 小时前
Vue 面试收官篇:SSR、性能优化落地、30 道高频面试题精讲(附标准答案)
前端·javascript·vue.js·面试·vue
驳是10 小时前
一个组件,提升 react-router 开发的幸福感
前端·preact
GreenTea10 小时前
OpenAI Agents API 上手实测:一次调用把整个 agent loop 甩给 OpenAI
前端·后端·算法
星云API技术支持10 小时前
企业微信二次开发:群权限设置、成员管理与群资料维护的接口组合实践
java·前端·企业微信
京东云开发者10 小时前
81.8 秒的视频,我们改了 15 个版本:一次纯 Codex 驱动的 AI-native 视频实践
前端·aigc
Csvn11 小时前
TypeScript 大型项目架构:从单体 tsconfig 到分层可扩展的工程
前端
计算机魔术师12 小时前
Dario Amodei 发文呼吁为前沿 AI 降速并提出三点计划
前端
计算机魔术师13 小时前
OpenAI的AI代理偷偷给RubyGems下毒,我们却毫无察觉
前端
甲维斯13 小时前
0代码,0建模,3句话开发一个3D游戏!
前端·游戏·游戏开发