ai agent -- LCEL 汇总

36 阅读9分钟

一. LCEL是什么?

LCEL = LangChain Expression Language(LangChain 表达式语言)

它不是一门新语言,而是一套用管道操作符 | 把各种组件串起来的声明式写法。你可以把它理解成 LangChain 里的 "管道语法" ,类似 Unix 的 cat file | grep xxx | sort

他只是一种简便写法,LCEL 就是实现了 Runnbale 接口的一些 api 组合成 chain,然后统一执行。

1.1 对比

普通代码

// 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的代码

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"

就这三行,完成了 格式化 → 调模型 → 解析输出​ 的全部流程。

LCEL 的本质:把 LangChain 里所有组件(Prompt、Model、Parser、Retriever、Tool…)统一成同一个接口 Runnable,然后用 | 把它们串起来。任何实现了 Runnable 接口的东西,都能互相 pipe。

二.案例

1.1 基础案例

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);


image.png

model.invoke(); 里面的参数需要用 promptTemplate 格式化,invoke 的结果 responseoutputParser 去格式化。

1.2 使用RunnableSequence

RunnableSequence​ 是 LangChain 中用于按顺序串联多个 Runnable 组件的组合原语——前一个组件的输出自动作为下一个组件的输入,形成一条线性处理管道。

统一接口:生成的 RunnableSequence 本身也是 Runnable,自动支持 .invoke().batch().stream()(流式)、.ainvoke()(异步)。

管道符 和 显式构造

image.png

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);

把基础案例写成管道符

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);


image.png

1.3 使用RunnableLambda

RunnableLambda:把单个简单函数包装成 Runnable

const toUpper = RunnableLambda.from((s: string) => s.toUpperCase());

基本用法

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);

image.png

RunnableSequence和RunnableLambda联合使用

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 } 对象返回。

基本用法

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"
// }

高级用法

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);

image.png

RunnableSequence和RunnableMap的区别

image.png

RunnableSequence和RunnableMap联合使用

a.经典使用场景

image.png

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 一样,根据条件函数从多个分支中选择第一个匹配的分支执行,实现动态工作流路由。

基本写法

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
)

高级写法

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}`);
}

image.png

1.6 使用RouterRunnable

RouterRunnable​ 是 LangChain 中用于按 key 显式路由到对应 Runnable​ 的组件——类似 switch-case,根据输入中的 key 字段选择执行哪条已命名的子链,并返回该链的输出。

基本用法

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"

高级用法

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);

image.png

1.7 使用RunnablePassthrough

RunnablePassthrough = "什么都不做,在并行分支中需要把原始输入也作为一个 key 传下去

基本用法

import { RunnablePassthrough } from "@langchain/core/runnables";

const passthrough = new RunnablePassthrough();
//const passthrough = RunnableSequence.from()

const result = await passthrough.invoke("hello");
console.log(result); // "hello" — 原样返回

高级案例

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联合使用

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"        ← 原始输入被原样保留
// }

调试打印日志

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 链的组合原语。

基本用法

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);

image.png

对比RunnableEach和RunnableMap

image.png

RunnableEach = LCEL 世界里的 Array.map(),专门用来对数组中的每一项跑同一条处理链。

1.9 使用RunnablePick

RunnablePick​ 是 LangChain 中用于从字典(dict / object)输入中提取指定字段的 Runnable 组件——它从上游传来的字典中按指定 key 提取字段,并把提取出的字段(或字段值)传给下游。

基本用法

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"

高级案例

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)机制

替换办法

// 旧方式 (你截图中的代码)
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 对应的历史存储中

1.11 Runnable-WithRetry

LangChain(langchain-core) ​ 中,RunnableWithRetry​ 通常指通过 Runnable.with_retry() 方法生成的带自动重试能力的 Runnable 包装对象(底层为 RunnableRetry / RunnableBinding),用于在调用失败时进行自动重试,常用于处理网络抖动、API 限流

案例

在管道里面使用,所有的Runnable类里面的对象都可以使用这个方法with_retry(),进行重试。

import { ChatOpenAI } from "@langchain/openai";
import { PromptTemplate } from "@langchain/core/prompts";

const model = new ChatOpenAI({ temperature: 0.7 });

// 模板
const prompt = PromptTemplate.fromTemplate(
  "用 {tone} 的语气,用一句话解释:{concept}"
);

// ✅ 只对模型加重试,不对整个链包
const chain = prompt.pipe(
  model.withRetry({
    stopAfterAttempt: 3, // 最多尝试 3 次(含首次)
    onFailedAttempt: (e) => {
      console.log(`🔄 模型第 ${e.attemptNumber} 次失败,重试中...`);
    },
  })
);

async function main() {
  const res = await chain.invoke({
    tone: "幽默",
    concept: "指数退避重试策略",
  });
  console.log(res.content);
}

main();

你也可以在简单函数上使用,前提条件是用RunnableLambda包含普通函数,生成Runnable对象,然后调用withRetry方法即可。

import { RunnableLambda } from "@langchain/core/runnables";

let callCount = 0;

const flakyFn = new RunnableLambda({
  func: async (input) => {
    callCount++;
    if (callCount < 3) {
      throw new Error(`模拟瞬时失败 #${callCount}`);
    }
    return `成功!输入=${input},共尝试 ${callCount} 次`;
  },
});

const retryFn = flakyFn.withRetry({
  stopAfterAttempt: 5,
  onFailedAttempt: (e) => {
    console.log(`❌ 第 ${e.attemptNumber} 次失败:${e.message}`);
  },
});

// 测试
const result = await retryFn.invoke("hello");
console.log(result);
// ❌ 第 1 次失败:模拟瞬时失败 #1
// ❌ 第 2 次失败:模拟瞬时失败 #2
// 成功!输入=hello,共尝试 3 次

1.12使用Runnable-WithFallbacks

Runnable-WithFallbacks​ 是 LangChain 中通过 .withFallbacks() 方法创建的一个 Runnable 包装器,它的作用是:

当主 Runnable 调用失败时,自动按顺序尝试备用 Runnable,直到有一个成功为止。

image.png

用法

const primary = new ChatOpenAI({ model: "gpt-4" });
const fallback1 = new ChatOpenAI({ model: "gpt-3.5-turbo" });  // 次选
const fallback2 = new ChatAnthropic({ model: "claude-3-haiku" }); // 最后兜底

const chain = prompt.pipe(
  primary.withFallbacks([fallback1, fallback2])
);

// 尝试顺序:gpt-4 → gpt-3.5-turbo → claude-3-haiku

primary.withFallbacks([fallback1, fallback2]) 在主大模型里面添加上2个备用模型

案例

import { ChatOpenAI } from "@langchain/openai";
import { ChatAnthropic } from "@langchain/anthropic";
import { PromptTemplate } from "@langchain/core/prompts";

// 主模型(故意用个不存在的模型名,让它必挂)
const primary = new ChatOpenAI({
  model: "gpt-99-turbo",  // 不存在 → 必失败
  temperature: 0,
  maxRetries: 0,
});

// 备用模型
const fallback = new ChatAnthropic({
  model: "claude-3-haiku-20240307",
  temperature: 0,
});

const prompt = PromptTemplate.fromTemplate(
  "用一句话解释:{concept}"
);

// ✅ 主 → 备
const chain = prompt.pipe(
  primary.withFallbacks([fallback])
);

async function main() {
  try {
    const res = await chain.invoke({
      concept: "RunnableWithFallbacks",
    });
    console.log("✅ 成功(来自备用模型):");
    console.log(res.content);
  } catch (err) {
    console.error("❌ 主备全部失败:", err.message);
  }
}

main();

你也可以在Runnable对象后面使用写备用方案

import "dotenv/config";
import { RunnableLambda } from "@langchain/core/runnables";

// 模拟三个"翻译服务",优先级从高到低

const premiumTranslator = RunnableLambda.from(async (text) => {
  console.log("[Premium] 尝试翻译...");
  // 模拟高级服务不可用
  throw new Error("Premium 服务超时");
});

const standardTranslator = RunnableLambda.from(async (text) => {
  console.log("[Standard] 尝试翻译...");
  // 模拟标准服务也挂了
  throw new Error("Standard 服务限流");
});

const localTranslator = RunnableLambda.from(async (text) => {
  console.log("[Local] 使用本地词典翻译...");
  const dict = { hello: "你好", world: "世界", goodbye: "再见" };
  const words = text.toLowerCase().split(" ");
  return words.map((w) => dict[w] ?? w).join("");
});

// withFallbacks: 依次尝试 premium -> standard -> local
const translator = premiumTranslator.withFallbacks({
  fallbacks: [standardTranslator, localTranslator],
});

const result = await translator.invoke("hello world");
console.log("翻译结果:", result);

1.13使用Runnable-WithConfig

你看到的 Runnable-WithConfig 指的是 Runnable.with_config()(JS 里是 .withConfig() ——它返回的是一个 RunnableBinding(把配置绑到 Runnable 上的装饰器),作用是给 Runnable 预绑定一份 RunnableConfig,以后调用不用再重复传

案例

在大模型上配置信息后,直接调用invoke()

import { ChatOpenAI } from "@langchain/openai";
import { PromptTemplate } from "@langchain/core/prompts";

const model = new ChatOpenAI({ model: "gpt-3.5-turbo", temperature: 0 });

// ✅ 用 withConfig 预绑配置:以后 invoke 不用再传
const tracedModel = model.withConfig({
  run_name: "TracedGPTCall",
  tags: ["prod", "chat"],
  metadata: { owner: "backend-team" },
  maxConcurrency: 2,
});

const prompt = PromptTemplate.fromTemplate("用一句话解释:{topic}");

const chain = prompt.pipe(tracedModel);

// 调用时干干净净,配置已经在链上了
const res = await chain.invoke({ topic: "RunnableBinding" });
console.log(res.content);

你还可以给Runnable函数配置withConfig

import "dotenv/config";
import { RunnableLambda, RunnableSequence } from "@langchain/core/runnables";

// 模拟一个简单的"用户数据库"
const mockUsers = new Map([
  [
    "user-123",
    {
      id: "009",
      name: "snow",
      email: "snow@qq.com",
    },
  ],
]);

// 节点1: 根据 config.configurable.userId 查用户
const fun1 = RunnableLambda.from(async (input, config) => {
  const userId = config?.configurable?.userId;
  const user = userId ? mockUsers.get(userId) : null;

  if (!user) {
    throw new Error("未找到用户,无法发送通知");
  }

  return {
    user,
    notification: input,
  };
});

// 节点2: 根据 config.configurable.role 做权限判断
const fun2 = RunnableLambda.from(async (state, config) => {
  const role = config?.configurable?.role ?? "普通用户";

  const canSend =
    role === "管理员" ||
    role === "运营" ||
    role === "系统";

  if (!canSend) {
    throw new Error(`角色「${role}」无权限发送系统通知`);
  }

  return {
    ...state,
    role,
  };
});

// 节点3: 根据 locale 生成最终通知文案
const fun3 = RunnableLambda.from(async (state, config) => {
  const locale = config?.configurable?.locale ?? "zh-CN";

  let content;
  if (locale === "en-US") {
    content = `Dear ${state.user.name},\n\n${state.notification}\n\n(from role: ${state.role})`;
  } else {
    content = `亲爱的 ${state.user.name}, \n\n${state.notification}\n\n(发送人角色: ${state.role})`;
  }

  return {
    ...state,
    locale,
    finalContent: content,
  };
});

// 把三个节点串起来
const chain = RunnableSequence.from([fun1, fun2, fun3]);

const chainWithConfig = chain.withConfig({
  tags: ["demo", "withConfig", "notification"],
  metadata: {
    demoName: "RunnableWithConfig",
  },
  configurable: {
    userId: "user-123",
    role: "管理员",
    locale: "zh-CN",
  },
});
const result = await chainWithConfig.invoke("你有一条新的系统通知,请及时查看。");
console.log("✅ 最终通知内容:\n", result.finalContent);
console.log("\n--- chainWithConfig2 ---\n");

// 再创建一个不同配置的 chainWithConfig2,使用英文 locale
const chainWithConfig2 = chain.withConfig({
  tags: ["demo", "withConfig", "notification-en"],
  metadata: {
    demoName: "RunnableWithConfig2",
  },
  configurable: {
    userId: "user-123",
    role: "运营",
    locale: "en-US",
  },
});

// 输入为要发送的通知文案

const result2 = await chainWithConfig2.invoke("System maintenance scheduled tonight.");
console.log("✅ 最终通知内容:\n", result2.finalContent);

1.14使用Runnable-WithCallbacks

它指的是 Runnable.withCallbacks()(JS 里是 .withCallbacks() ——给 Runnable 绑定回调函数,在链执行的不同阶段自动触发(如开始、结束、出错时),常用于日志、监控、追踪、Token 统计等。

一个类继承BaseCallbackHandler后,你可以监听以下事件:

事件触发时机
handleChainStart链/Runnable 开始执行
handleChainEnd链/Runnable 执行完成
handleChainError链/Runnable 执行出错
handleLLMStartLLM 开始生成
handleLLMNewTokenLLM 流式输出新 token
handleLLMEndLLM 生成结束
handleToolStart / handleToolEnd工具调用开始/结束
handleRetrieverStart / handleRetrieverEnd检索开始/结束

class案例

import { ChatOpenAI } from "@langchain/openai";
import { PromptTemplate } from "@langchain/core/prompts";
import { BaseCallbackHandler } from "@langchain/core/callbacks/base";
import "dotenv/config";

// 自定义回调处理器
class LogCallbackHandler extends BaseCallbackHandler {
  constructor() {
    super();
    this.name = "LogCallbackHandler";
  }

  async handleChainStart(serialized, inputs, runId) {
    console.log(`🔵 [ChainStart] runId=${runId} inputs=`, inputs);
  }

  async handleChainEnd(outputs, runId) {
    console.log(`✅ [ChainEnd] runId=${runId} outputs=`, outputs);
  }

  async handleChainError(err, runId) {
    console.log(`❌ [ChainError] runId=${runId} error=`, err.message);
  }

  async handleLLMStart(serialized, prompts, runId) {
    console.log(`🤖 [LLMStart] runId=${runId} prompt=`, prompts[0]?.slice(0, 50));
  }

  async handleLLMNewToken(token, runId) {
    process.stdout.write(`📝 [Token] ${token}`);
  }

  async handleLLMEnd(output, runId) {
    const text = output.generations?.[0]?.[0]?.text ?? "";
    console.log(`\n🏁 [LLMEnd] runId=${runId} totalLength=${text.length}`);
  }
}

const model = new ChatOpenAI({
  modelName: process.env.MODEL_NAME,
  apiKey: process.env.OPENAI_API_KEY,
  temperature: 0,
  configuration: {
    baseURL: process.env.OPENAI_BASE_URL,
  },
});
const prompt = PromptTemplate.fromTemplate("用一句话解释:{topic}");

// ✅ 修正:使用 withConfig 绑定回调
const chain = prompt.pipe(model).withConfig({
  callbacks: [new LogCallbackHandler()],
});

async function main() {
  const result = await chain.invoke({ topic: "RunnableWithCallbacks" });
  console.log("\n\n📦 最终结果:", result.content);
}

main();

执行结果

image.png

object案例

可以把他的执行函数写到一个对象里面,加到callback里面。

import "dotenv/config";
import { RunnableLambda, RunnableSequence } from "@langchain/core/runnables";

// 文本处理链:清洗 -> 分词 -> 统计
const clean = RunnableLambda.from((text) => {
  return text+'你是谁?';
});

const tokenize = RunnableLambda.from((text) => {
  return text+'他是猪八戒';
});

const count = RunnableLambda.from((tokens) => {
  return { tokens, wordCount: tokens.length };
});

const chain = RunnableSequence.from([clean, tokenize, count]);

// 用 callbacks 观测每一步的输出
const callback = {
  handleChainStart(chain) {
    const step = chain?.id?[chain.id.length - 1] : "unknown";
    console.log(`[START] ${step}`);
  },
  handleChainEnd(output) {
    console.log(`[END] 输出=${JSON.stringify(output)}\n`);
  },
  handleChainError(err) {
    console.log(`[ERROR] ${err.message}\n`);
  },
};

const result = await chain.invoke("hello", {
  callbacks: [callback],
});

console.log("最后结果:", result.content);

image.png

三.总结

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

组件执行方式类比典型用途
RunnableSequence串行​ — 一步一步,上一步输出作为下一步输入工厂流水线Prompt → LLM → Parser 标准三步链
RunnableLambda转换​ — 对输入执行自定义函数,返回处理结果流水线上的加工工位数据清洗、字段提取、格式化、打日志
RunnableMap并行​ — 所有分支同时执行,共享同一输入,结果收集为对象分兵并进 / Promise.all()同时准备多路数据(如检索 docs + 历史摘要 + 原始问题)
RunnableBranch条件路由​ — 按顺序评估条件,执行第一个匹配的分支if-elif-else根据输入内容/数值走不同处理逻辑(如数学题 vs 代码题)
RouterRunnableKey 路由​ — 按输入中的 key 字段精确匹配,执行对应分支switch-caseIntent 已分类好,直接分发到对应 handler(如客服多意图路由)
RunnablePassthrough透传​ — 原样返回输入,不做任何修改透明传送带在 RunnableMap 中保留原始输入、调试插入点、占位对齐接口
RunnableEach批量逐一​ — 对数组中的每个元素独立应用同一条链Array.map()批量处理消息列表、逐条翻译、对多个文档逐一生成摘要
RunnablePick字段提取​ — 从字典中按指定 key 提取字段,向下游传递从文件夹里挑出指定文件从并行分支输出中只挑选 Prompt 需要的字段(如只取 context + question
RunnableWithMessageHistory​ ⚠️ 已弃用包装增强​ — 加载历史 → 注入 Prompt → 执行链 → 保存新消息给无状态链装上"记忆芯片"多轮对话场景(已被 LangGraph Checkpointer 取代,新项目不建议使用)

image.png