iT邦幫忙

2025 iThome 鐵人賽

DAY 11
1
生成式 AI

用 Node.js 打造生成式 AI 應用:從 Prompt 到 Agent 開發實戰系列 第 11

Day 11 - 流程鏈組合應用:使用 LCEL 打造靈活應用邏輯

  • 分享至 

  • xImage
  •  

在前幾篇文章中,我們學會了如何使用 LangChain 串接模型、設計提示模板與控制回應格式,這些功能在單一輸入輸出邏輯下已經很強大,但若想要組合多個處理步驟,或根據輸入情境做不同邏輯處理,就需要更進階的流程控制能力。

今天我們要介紹 LangChain 的一項核心功能:LCEL(LangChain Expression Language)。LCEL 是一種宣告式語法,讓我們能以函數串接的方式,將多個 Chain 組合成一個完整的工作流程。透過這種設計,你可以像撰寫函數組合一樣,清晰且有結構地定義並串接各種 AI 應用流程。

為什麼需要 LCEL?

在開發 LLM 應用時,我們通常不會只停留在「輸入文字 → 取得回覆」這麼單純的模式。實際場景中,應用往往涉及多個處理階段與不同的決策邏輯,例如:

  • 多步驟處理:輸入資料可能要先經過清理或轉換,再套用提示模板送入模型,最後還需要解析模型回覆並轉成指定格式。
  • 多模型串接:不同任務需要不同的提示與模型組合,例如「摘要器」與「問答系統」的邏輯就完全不同。
  • 條件式流程分支:根據使用者輸入的內容動態決定處理邏輯,例如在客服系統中,先判斷訊息是「抱怨」還是「詢問」,再分別導向不同的回覆流程。
  • 模組化重用:希望將每個處理階段封裝成可重複利用的模組,方便在不同應用中快速拼裝。

若單純依靠傳統函式呼叫或巢狀控制流程來實作這些需求,程式碼往往會變得冗長且難以維護,資料流向也不直觀,不利於除錯與後續擴充。

LCEL(LangChain Expression Language) 正是為了解決這些痛點而設計。它提供一種宣告式、組合式的語法,把每個處理步驟抽象為 可執行單元(Runnable),並能像拼積木一樣靈活拼接成完整的應用流程。這種設計不僅讓程式碼更具可讀性與模組化,也讓整體流程更容易被觀察、測試與維護。

Runnable:LCEL 的基本組成

在 LCEL 中,所有流程的核心單元都是 Runnable。簡單來說,Runnable 是一種具備「輸入 → 處理 → 輸出」能力的可執行元件。只要符合這個資料處理模式,就可以被納入 LCEL 的流程鏈中,並與其他元件自由組合。

舉例來說:

  • LLM 本身是一個 Runnable:它接收文字或結構化提示,回傳模型生成的內容。
  • 提示模板是一個 Runnable:它接收使用者輸入的參數,套入提示模板,產生最終送給模型的 prompt。
  • 輸出解析器也是 Runnable:它負責將模型輸出的內容解析成結構化資料,方便後續應用程式處理。

這些元件都實作了統一的 Runnable 介面,因此可以透過一致的方式進行組合,形成一條清晰、可觀察、可除錯的流程鏈。

接下來,我們將示範這些元件的各種組合方式,並透過實際案例,逐步建構出更完整且靈活的 AI 工作流程。

RunnableSequence:順序執行 Runnable

當我們需要按照固定順序執行多個處理步驟時,可以透過 RunnableSequence 來建立一條流程鏈。它會自動將前一個步驟的輸出傳遞給下一個步驟,讓整個處理流程被明確定義並依序執行。

這是一種直覺的方式來建構線性流程,特別適合用在「提示 → 模型 → 解析」這樣的典型工作流。

以下是一個簡單範例:

import { ChatOpenAI } from '@langchain/openai';
import { PromptTemplate } from '@langchain/core/prompts';
import { RunnableSequence } from '@langchain/core/runnables';
import { StringOutputParser } from '@langchain/core/output_parsers';

const promptTemplate = PromptTemplate.fromTemplate(
  '請用一句話介紹 {topic}',
);
const llm = new ChatOpenAI({ model: 'gpt-4o-mini' });
const parser = new StringOutputParser();

const chain = RunnableSequence.from([
  promptTemplate,
  llm,
  parser,
]);

const result = await chain.invoke({ topic: 'LangChain' });
console.log(result);

在這個例子中,輸入 { topic: 'LangChain' } 會先由 PromptTemplate 套入成完整提示詞,再交給 LLM 生成回覆,最後透過 StringOutputParser 轉成純文字。最終輸出會是一段簡短的 AI 介紹,例如:

LangChain 是一個用於構建基於大型語言模型的應用框架,旨在簡化自然語言處理任務的開發過程。

其實,這段流程等同於以下「逐步手動呼叫」的寫法:

const prompt = await promptTemplate.invoke({ topic: 'LangChain' });
const llmResponse = await llm.invoke(prompt);
const finalOutput = await parser.invoke(llmResponse);
console.log(finalOutput);

相較之下,使用 RunnableSequence 可以讓程式碼更簡潔,並且更容易理解資料在每個步驟間的流動。

使用 pipe() 串接流程

除了 RunnableSequence,LCEL 也提供了更直觀的 .pipe() 語法,讓我們以鏈式的方式組合多個 Runnable。這是 LCEL 中最常見、最易讀的寫法:

import { ChatOpenAI } from '@langchain/openai';
import { PromptTemplate } from '@langchain/core/prompts';
import { StringOutputParser } from '@langchain/core/output_parsers';

const promptTemplate = PromptTemplate.fromTemplate(
  '請用一句話介紹 {topic}',
);
const model = new ChatOpenAI({ model: 'gpt-4o-mini' });
const parser = new StringOutputParser();

const chain = promptTemplate.pipe(model).pipe(parser);

const result = await chain.invoke({ topic: 'LangChain' });
console.log(result);

這段程式的邏輯與 RunnableSequence.from([...]) 完全等價,但使用 .pipe() 寫法不僅簡潔易讀,也更直觀地呈現多階段處理的概念,非常適合用來建構包含多個步驟的應用流程。

RunnableParallel:多個 Runnable 平行執行

在某些情境下,我們希望同時執行多個處理步驟,而不是逐一依序進行。這時就可以使用 RunnableParallel。它允許多個 Runnable 共用相同的輸入,並各自產生對應的輸出,最後再整合成一個物件回傳。

以下是一個多語言翻譯的範例:

import { ChatOpenAI } from '@langchain/openai';
import { PromptTemplate } from '@langchain/core/prompts';
import { StringOutputParser } from '@langchain/core/output_parsers';
import { RunnableParallel } from '@langchain/core/runnables';

const model = new ChatOpenAI({ model: 'gpt-4o-mini' });
const parser = new StringOutputParser();

const enPrompt = PromptTemplate.fromTemplate('將以下文字翻譯成英文:{text}');
const jpPrompt = PromptTemplate.fromTemplate('將以下文字翻譯成日文:{text}');
const frPrompt = PromptTemplate.fromTemplate('將以下文字翻譯成法文:{text}');

const enChain = enPrompt.pipe(model).pipe(parser);
const jpChain = jpPrompt.pipe(model).pipe(parser);
const frChain = frPrompt.pipe(model).pipe(parser);

const chain = RunnableParallel.from({
  english: enChain,
  japanese: jpChain,
  french: frChain,
});

const result = await chain.invoke({ text: '人工智慧正在改變軟體開發的方式。' });
console.log(result);

在這個例子中,輸入的 人工智慧正在改變軟體開發的方式。 會同時送往三個不同的翻譯流程,最終回傳一個物件,內含三種語言的翻譯結果:

{
  english: 'Artificial intelligence is changing the way software development is done.',
  japanese: '人工知能はソフトウェア開発の方法を変えています。',
  french: "L'intelligence artificielle change la manière de développer des logiciels."
}

與順序執行的 RunnableSequence 不同,RunnableParallel 會同時啟動多個 Runnable,因此整體耗時通常取決於最慢的一個,而不是所有步驟的執行時間總和。這種平行化設計能有效縮短等待時間,非常適合需要一次產生多個結果的應用場景。

RunnableLambda:自定義處理邏輯

在 LCEL 中,除了使用內建的大型語言模型、提示模板或輸出解析器元件之外,有時我們也需要在流程中插入一些自訂邏輯,例如資料格式轉換、欄位萃取或條件過濾。這時就可以透過 RunnableLambda,將任意的 JavaScript 函式封裝成一個 Runnable,並自然地嵌入整體流程鏈中。

來看一個簡單的實作範例,示範如何在流程的最後一步額外加上時間戳記:

import { ChatOpenAI } from '@langchain/openai';
import { PromptTemplate } from '@langchain/core/prompts';
import { StringOutputParser } from '@langchain/core/output_parsers';
import { RunnableLambda } from '@langchain/core/runnables';

const prompt = PromptTemplate.fromTemplate('請用一句話介紹 {topic}');
const llm = new ChatOpenAI({ model: 'gpt-4o-mini' });
const parser = new StringOutputParser();

const format = RunnableLambda.from((input: string) => {
  return { output: input, time: new Date().toISOString() };
});

const chain = prompt.pipe(llm).pipe(parser).pipe(format);

const result = await chain.invoke({ topic: '人工智慧' });
console.log(result);

在這個例子中,輸入 { topic: '人工智慧' } 會依序經過 Prompt、LLM 與 Parser,最後再由自訂的 RunnableLambda 加上時間戳。這樣一來,最終結果不僅包含模型生成的回覆,也會同時記錄輸出的時間,方便後續儲存或追蹤。

輸出結果可能如下:

{
  output: '人工智慧是模擬人類智能的技術,使機器能夠學習、推理和解決問題。',
  time: '2025-08-31T15:00:56.379Z'
}

在實際設計流程時,若需要於某個環節額外加入資料轉換、欄位調整或條件判斷,就可以透過 RunnableLambda 來實現。它讓我們能靈活地把自訂邏輯嵌入流程,同時保有 LCEL 原本的組合式結構與清晰邏輯,使整體流程維持高度的可讀性與模組化。

RunnablePassthrough:保留輸入資料

在某些流程設計中,我們可能希望保留原始輸入,而不是讓它在處理過程中被完全覆蓋。這時就可以使用 RunnablePassthrough。顧名思義,它的功能非常單純:接收什麼,就輸出什麼,讓資料原封不動地通過。

以下範例展示如何在流程中同時取得原始輸入與模型回覆:

import { ChatOpenAI } from '@langchain/openai';
import { PromptTemplate } from '@langchain/core/prompts';
import { StringOutputParser } from '@langchain/core/output_parsers';
import { RunnableParallel, RunnablePassthrough } from '@langchain/core/runnables';

const model = new ChatOpenAI({ model: 'gpt-4o-mini' });
const parser = new StringOutputParser();

const prompt = PromptTemplate.fromTemplate('請用一句話回覆:{message}');
const chatChain = prompt.pipe(model).pipe(parser);

const chain = RunnableParallel.from({
  original: new RunnablePassthrough(),
  reply: chatChain,
});

const result = await chain.invoke({ message: '今天天氣很好' });
console.log(result);

在這個例子中,original 會保留使用者輸入 { message: '今天天氣很好' },而 reply 則是模型生成的回覆。最終輸出同時包含兩者:

{
  original: { message: '今天天氣很好' },
  reply: '是的,今天天氣真不錯!'
}

雖然 RunnablePassthrough 本身不會對資料做任何轉換,但它卻是流程設計中非常實用的工具。透過它,我們能在保持輸入原貌的同時,靈活地擴充其他處理邏輯。無論是要進行比對、追蹤、除錯,還是作為流程中的暫時佔位元件,RunnablePassthrough 都能讓整個應用更容易觀察、測試與維護。

什麼時候該使用 LCEL?

LCEL 適合用來處理具明確邏輯流程的 AI 應用,特別是在你希望以模組化方式組合多個步驟的情況下。它是一種流程編排(orchestration)解決方案,能幫助你建立可重用、可觀察的處理鏈。

不過,如果應用邏輯涉及複雜的條件判斷、狀態切換、循環控制、或多個 Agent 間的互動協作,那麼使用 LangGraph 會更加合適。在 LangGraph 中,你可以使用節點(Node)定義應用邏輯流程,每個節點內仍可使用 LCEL 撰寫具體處理邏輯。

在實務上,可以依循以下原則來選擇:

  • 單一模型呼叫:不需要 LCEL,直接呼叫模型即可。
  • 簡單線性流程:例如「Prompt → LLM → Parser」,使用 LCEL 即可清楚表達。
  • 複雜邏輯流程:包含分支、迴圈或多任務協作,建議用 LangGraph 管理全局,再在節點內以 LCEL 組裝局部流程。

簡單來說,LCEL 適合用來快速組裝與執行簡單的流程鏈,而更高層次的邏輯管理則交給 LangGraph 處理。兩者可以搭配使用,能同時兼顧靈活性與可維護性。

Note:其實 LangChain 本身也提供了一些可用於條件判斷的 Runnable 元件,能設計出更複雜的流程。不過這類需求現在已經能由 LangGraph 更完整地取代,因此本篇不再著墨。我們會在後續內容中詳細探討 LangGraph 的設計與應用。

小結

今天我們認識了 LangChain 的 LCEL(LangChain Expression Language),學會如何以宣告式語法把多個處理步驟組合成清晰且模組化的流程鏈:

  • LCEL 的核心概念是 Runnable,任何符合「輸入 → 處理 → 輸出」模式的元件(LLM、Prompt、Parser…)都能組合進流程。
  • RunnableSequence:用來建立線性流程,讓輸出自動傳給下一步。
  • .pipe():鏈式語法,讓流程更直觀、簡潔。
  • RunnableParallel:允許多個 Runnable 平行處理同一輸入,最後彙整成物件。
  • RunnableLambda:把任意函式包裝成 Runnable,插入自訂邏輯。
  • RunnablePassthrough:原樣保留輸入,常用於比對、追蹤或除錯。
  • 適用場景:單一模型呼叫可省略 LCEL;單純線性流程可用 LCEL;更複雜的分支、迴圈與多 Agent 協作則建議交給 LangGraph。

LCEL 幫助我們像拼積木一樣構建 AI 流程,讓程式碼更清晰、可觀察且可維護;而遇到更高層次的邏輯需求時,則可以交由 LangGraph 來接手,兩者搭配能打造靈活又穩健的應用架構。


上一篇
Day 10 - 輸出格式解析:運用 LangChain Output Parsers 控制回應內容
下一篇
Day 12 - 工具調用設計:使用 LangChain Tool Calling 整合外部功能
系列文
用 Node.js 打造生成式 AI 應用:從 Prompt 到 Agent 開發實戰14
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言