AI时代,使用大模型学习LangChain (3)——LCEL

前言

上一章了解了RAG,使得AI拥有了检索知识库的能力,这一章终于要触摸到LangChain框架的核心------------LCEL。

如果你也和我一样好奇LangChain的名字有什么含义,这一章就能得到答案了,Chain(锁链)就是LangChain的核心,我们之前在开发简易Agent的时候,还没有用到LangChain的核心设计,因此在大模型处理用户输入时,需要有很多的处理逻辑,我们需要自己编写流程控制判断是否要查询文档,是否需要使用工具,并且帮手动将每一个阶段的输出给到下一个阶段的输入。逻辑繁琐且麻烦,这时候就需要用到LCEL。

示例 :翻译链 ------ 不用 LCEL

typescript 复制代码
async function translateChainManual(input: { target_language: string; input: string }) {
  // 步骤 1:手动构建 messages
  const messages = [
    new SystemMessage(
      `你是一个专业的翻译助手。将用户输入翻译成${input.target_language}。只输出译文,不要解释。`
    ),
    new HumanMessage(input.input),
  ];

  // 步骤 2:手动调用 model,拿到 AIMessage
  const aiMessage = await model.invoke(messages);

  // 步骤 3:手动从 AIMessage.content 里提取纯文本
  return aiMessage.content as string;
}

const result = await translateChainManual({
  target_language: "英文",
  input: "今天天气真好,适合出去散步",
});

示例 :翻译链 ------ 用 LCEL

typescript 复制代码
const translateChain = RunnableSequence.from([translatePrompt, model, outputParser]);

const result = await translateChain.invoke({
  target_language: "英文",
  input: "今天天气真好,适合出去散步",
});  // 一步完成!

这里可以直观的看到,使用LCEL减少了代码量,并且步骤更加抽象化。

为什么用 LCEL?(优势对比)

维度 手动实现 LCEL 说明
统一接口 每步函数签名各异,得自己对接 所有步骤都是 Runnable.invoke() 通用 像 React 组件都实现同一 interface,可自由替换
链式组合 嵌套 await,中间变量一堆 RunnableSequence.from([...]) 或 `a b
流式输出 得自己手写 generator / async iterator .stream() 自动逐 token 返回 这是 LCEL 最大的价值,手动实现流式很麻烦
批量执行 自己写 Promise.all(inputs.map(...)) .batch([i1, i2, i3]) 内置并发调度 自动处理并发数
并行 手写 Promise.all({a, b}) 并对齐字段 RunnableParallel.from({a, b}) 声明式 见上方"三种核心组合方式"
回调/可观测 自己埋点 内置 callbacksRunnableTrace,LangSmith 自动 trace 生产环境调试利器
异步同步统一 写两个版本 invoke/stream/batch 三方法一致 切换调用模式零成本
复用/重构 改链结构要重写函数 加减一个 Runnable 即可 像搭积木

LCEL

LCEL是(LangChain Expression Language),是LangChain设计的一种表达式,是一种声明式的组合方法,开发者不再需要详细描述"如何"执行每一步,而是通过一种简洁的语法来描述"做什么"。这种声明式的特性将执行优化的责任交给了 LangChain 框架本身。开发者只需要关注agent有哪些步骤,每个步骤要做什么,每个步骤的输入是什么,每个步骤的输出是什么,而不需要关心每个步骤之间要怎么链接起来。

一句话总结

**LCEL = LangChain 的组件组合语法。用 RunnableSequence 把 Prompt、Model、Parser 串联成一条可运行的数据管道。

Runnable

在理解LCEL之前,需要理解一下什么是Runnable,LangChain的Runnable接口是一个标准接口。从字面上理解是可以运行的,可以理解为是一个带入口和出口的黑盒,内部做什么不重要,对外接口都一样(invoke / stream / batch)。LangChain提供了很多接口实现了Runnable,可以将这些Runnable像串一条链子一样串起来,形成完整的工作流。

基础LCEL

以一个简易的翻译器为例子

ts 复制代码
  // ─── 示例 1:Prompt + Model + OutputParser ────
  console.log("📌 示例 1:翻译链 ------ Prompt → Model → Parser\n");
  
  const model = new ChatDeepSeek({
    model: config.model,
    apiKey: config.apiKey,
    temperature: 0.7,
  });

  // 创建提示词模板
  const translatePrompt = ChatPromptTemplate.fromMessages([
    SystemMessagePromptTemplate.fromTemplate(
      "你是一个专业的翻译助手。将用户输入翻译成{target_language}。只输出译文,不要解释。"
    ),
    HumanMessagePromptTemplate.fromTemplate("{input}"),
  ]);

  // 输出解析器:从 AIMessage 中提取纯文本
  const outputParser = new StringOutputParser();

  // 用 RunnableSequence 构建链
  // 类比: const pipeline = compose(promptTemplate, model, parser)
  const translateChain = RunnableSequence.from([
    translatePrompt,
    model,
    outputParser,
  ]);
  
  console.log("链的结构:");
  console.log("  PromptTemplate → ChatDeepSeek → StringOutputParser\n");

  // invoke() 时传入模板中的变量
  const result1 = await translateChain.invoke({
    target_language: "英文",
    input: "今天天气真好,适合出去散步",
  });

这个例子中,modeltranslatePromptoutputParser都是RunnableRunnableSequence.from 将这三个Runnable组成了一个链,他们是顺序执行的。

  • 第一个translatePrompt负责将target_language加到系统Prompt中规定翻译的语言,以及将需要翻译的内容加到用户Prompt中。
  • 第二步model将上一步拼好的prompt给到大模型,并得到大模型的回答。
  • 第三步outputParser从大模型的回答中提取纯文本。

RunnableParallel

上一个例子中展示了一个顺序执行的链,LangChain还提供了并行执行的链

ts 复制代码
// ─── 示例 2:RunnableParallel 并行执行 ──────
  console.log("📌 示例 2:RunnableParallel 并行执行多个任务\n");

  // 两条翻译链(共用同一个 prompt 模板结构)
  const englishChain = RunnableSequence.from([
    ChatPromptTemplate.fromMessages([
      SystemMessagePromptTemplate.fromTemplate(
        "你是一个专业的翻译助手。将用户输入翻译成英文。只输出译文,不要解释。"
      ),
      HumanMessagePromptTemplate.fromTemplate("{input}"),
    ]),
    model,
    outputParser,
  ]);

  const japaneseChain = RunnableSequence.from([
    ChatPromptTemplate.fromMessages([
      SystemMessagePromptTemplate.fromTemplate(
        "你是一个专业的翻译助手。将用户输入翻译成日文。只输出译文,不要解释。"
      ),
      HumanMessagePromptTemplate.fromTemplate("{input}"),
    ]),
    model,
    outputParser,
  ]);

  // RunnableParallel 同时执行多个链
  
  const multiTranslateChain = RunnableSequence.from([
    // 步骤 1:准备输入
    RunnableLambda.from((input: string) => ({ input })),
    // 步骤 2:并行执行两条翻译链 + 统计信息
    RunnableParallel.from({
      english: englishChain,
      japanese: japaneseChain,
      stats: RunnableLambda.from((data: { input: string }) => ({
        charCount: data.input.length,
        wordCount: data.input.split(/[,。!?\s]+/).filter(Boolean).length,
      })),
    }),
  ]);

  console.log("并行执行 3 个任务:翻译(英文) + 翻译(日文) + 字数统计\n");

  const analyzeResult = await multiTranslateChain.invoke(
    "人工智能正在改变世界,LangChain 让 AI 开发更简单。"
  );

  console.log(`  英文翻译: ${(analyzeResult as any).english}`);
  console.log(`  日文翻译: ${(analyzeResult as any).japanese}`);
  console.log(`  统计信息: ${JSON.stringify((analyzeResult as any).stats)}\n`)

以上例子中,RunnableParallel.from构造了一个并行的链,他会并行执行englishjapanesestats三个任务,并将三个任务的结果都给到下一个步骤。由于上面的例子是最后一个动作,因此直接打印结果。

RunnableBranch

LangChian还提供了条件分支,能实现更复杂的分支处理

typescript 复制代码
// ─── 1. RunnableBranch 条件分支 ────────────
  console.log("📌 1. RunnableBranch ------ 根据输入决定走哪条路\n");

  // 场景:根据用户意图路由到不同的处理链
  // 类比前端的:switch/case 或者三元表达式

  // 不同的处理链
  const translateChain = RunnableSequence.from([
    ChatPromptTemplate.fromMessages([
      SystemMessagePromptTemplate.fromTemplate("你是一个翻译助手,将用户输入翻译成英文。只输出译文。"),
      HumanMessagePromptTemplate.fromTemplate("{input}"),
    ]),
    model,
    outputParser,
  ]);

  const summarizeChain = RunnableSequence.from([
    ChatPromptTemplate.fromMessages([
      SystemMessagePromptTemplate.fromTemplate("你是一个摘要助手,用一句话总结用户输入的核心内容。"),
      HumanMessagePromptTemplate.fromTemplate("{input}"),
    ]),
    model,
    outputParser,
  ]);

  const chatChain = RunnableSequence.from([
    ChatPromptTemplate.fromMessages([
      SystemMessagePromptTemplate.fromTemplate("你是一个友好的聊天助手,用中文回答用户。"),
      HumanMessagePromptTemplate.fromTemplate("{input}"),
    ]),
    model,
    outputParser,
  ]);

  /**
   * RunnableBranch 用法:
   *
   * RunnableBranch.from([
   *   [条件函数1, 处理链1],
   *   [条件函数2, 处理链2],
   *   默认处理链
   * ])
   *
   * 条件函数接收输入,返回 boolean
   * 类比前端的:声明式 if/else if/else
   *
   * ⚠️ 注意:使用 RunnableSequence 包装让类型统一
   */
  const router = RunnableBranch.from([
    [
      (input: { input: string }) =>
        input.input.includes("翻译") || input.input.includes("translate"),
      translateChain,
    ],
    [
      (input: { input: string }) =>
        input.input.includes("总结") || input.input.includes("摘要") || input.input.includes("概括"),
      summarizeChain,
    ],
    chatChain, // 默认分支
  ]);

  console.log("智能路由:根据输入内容自动选择翻译/摘要/聊天链\n");

  const testInputs = [
    "把「人工智能正在改变世界」翻译成英文",
    "帮我总结一下 TypeScript 的核心特点:静态类型、类型推断、接口、泛型、枚举",
    "今天心情不错,想去爬山",
  ];

  for (const input of testInputs) {
    const result = await router.invoke({ input });
    console.log(`  输入: "${input.substring(0, 50)}..."`);
    console.log(`  输出: ${result}\n`);
  }

在上面的例子中,RunnableBranch.from构造了一个分支处理动作,它接受一个元组,第一个值表示判断条件的函数,第二个值表示条件满足时运行的Runnable行为。在运行时,会根据用户是否包含了"翻译"或者"总结"关键词,进入特定的步骤。

相关推荐
冬哥聊AI1 小时前
三面追问:百万行代码库怎么跑好Claude Code?harness五层才是关键
人工智能
用户667675093791 小时前
LangChain 向量检索为什么要用 as_retriever?从 LCEL 到 RunnableParallel 一次讲清
langchain
云杨1 小时前
SnakeEats 技术解剖:扫描器是蛇,文档是它会生长的身体——人机协作的新数据层
人工智能·开源
小鬼头编程1 小时前
信息学竞赛体系(CSP-J/S、NOIP、NOI、IOI)
c++·人工智能·青少年编程
MRfyyt1 小时前
一个 for 循环搞定 AI Agent:Function Calling + ReAct 实战
人工智能
正在走向自律1 小时前
AI 辅助研发内部复盘(4/5):约束、上下文与技能——把“人的判断”工程化
人工智能·ai编程·ai代码生成·上下文工程·ai辅助研发·skills封装经验
Bigger2 小时前
因为一句「华流才是最屌的」,我做了一个中国风设计的 Skill
前端·人工智能·设计
陈天伟教授2 小时前
图解人工智能(99)人工智能前沿-走向未来
人工智能
科研小刘带你玩学术2 小时前
【科研快讯】从自动执行到自主决策:具身智能正在推动AI进入机器人智能时代
人工智能·机器学习·具身智能·未来趋势·ai机器人·智能系统