LangChain 教學:LCEL(LangChain Expression Language)完整介紹
簡介
在使用 LangChain 建構 Chain(流程串接)時,我們常會遇到「如何把多個 LLM 呼叫、工具、條件判斷與資料前處理」以簡潔、可讀的方式寫在同一段程式碼中。
傳統的做法是手寫大量的 await、if/else、map、reduce 等程式邏輯,既冗長又容易出錯。
LCEL(LangChain Expression Language) 正是為了解決這個痛點而誕生的。它是一套 宣告式 的 DSL(Domain Specific Language),讓開發者可以在一行或幾行文字裡,描述完整的模型流程、資料流向與條件分支。
透過 LCEL,我們不僅能 提升程式可讀性、減少樣板程式碼,還能讓 Chain 的組合更具彈性,快速在開發與測試階段切換不同的 LLM 或工具。
本篇文章將從概念說明、實作範例、常見陷阱與最佳實踐,帶你一步步掌握 LCEL,並了解它在真實專案中的應用價值。
核心概念
1. LCEL 的基本語法
LCEL 使用 JSON‑like 的結構描述每個「節點」(node) 與「資料流」(edge)。最常見的屬性包括:
| 屬性 | 說明 | 範例 |
|---|---|---|
type |
節點類型:llm, tool, prompt, condition 等 |
"type": "llm" |
name |
節點名稱,供後續引用 | "name": "summarize" |
input |
輸入來源,可以是前一節點的輸出或外部變數 | "input": "$.question" |
output |
輸出別名,供後續節點使用 | "output": "summary" |
condition |
條件判斷,支援 ==, !=, >, < 等 |
"condition": "$.score > 0.8" |
範例(最小化的 LCEL)
{ "type": "llm", "name": "answer", "input": "$.question", "output": "reply" }
在 JavaScript 中,我們會把這段 JSON 直接傳給 LCELChain.fromExpression(),LangChain 會自動把它編譯成可執行的 Chain。
2. 連接多個節點:pipeline 與 branch
LCEL 支援兩種主要的流程組合方式:
- pipeline:線性串接,前一節點的
output成為下一節點的input。 - branch:根據條件分支執行不同的子流程。
{
"pipeline": [
{ "type": "prompt", "name": "genPrompt", "input": "$.question", "output": "promptText" },
{ "type": "llm", "name": "gpt4", "input": "$.promptText", "output": "answer" },
{
"type": "condition",
"condition": "$.answer.includes('抱歉')",
"true": [{ "type": "tool", "name": "search", "input": "$.question", "output": "searchResult" }],
"false": [{ "type": "prompt", "name": "refine", "input": "$.answer", "output": "finalAnswer" }]
}
]
}
上例先產生 Prompt → 呼叫 LLM → 若回覆包含「抱歉」則改走搜尋工具,否則直接精煉答案。
3. 變數與參照 ($)
LCEL 使用 $. 前綴來存取當前執行環境的變數或先前節點的輸出。例如:
$.question→ 入口參數question$.searchResult→ 先前search節點的output
變數可以在 context 中預先定義,也可以在流程中動態加入。
4. 內建工具與自訂工具
LangChain 內建了常見的工具(如 SerpAPI, Wikipedia, PythonREPL),在 LCEL 中只要指定 type: "tool" 並提供 name 即可使用。
若要 自訂工具,先在程式碼中註冊一個 Tool 實例,然後在 LCEL 表達式裡使用相同的 name。
程式碼範例
以下範例皆使用 Node.js(langchain v0.2+)與 JavaScript,請先安裝相依套件:
npm install langchain openai @langchain/community
範例 1:最簡單的 LLM 呼叫
import { ChatOpenAI } from "@langchain/openai";
import { LCELChain } from "@langchain/core/chains/lcel";
// 建立 LLM 實例
const llm = new ChatOpenAI({ modelName: "gpt-4o-mini", temperature: 0 });
// LCEL 表達式:直接把問題傳給 LLM,輸出叫 answer
const expr = {
type: "llm",
name: "answer",
input: "$.question",
output: "answer"
};
// 建立 Chain
const chain = await LCELChain.fromExpression(expr, { llm });
// 執行
const result = await chain.run({ question: "什麼是 LCEL?" });
console.log(result.answer);
說明:
$.question代表外部傳入的question變數。- 執行結果會在
result.answer中取得。
範例 2:結合 Prompt 與 LLM
import { PromptTemplate } from "@langchain/core/prompts";
const prompt = new PromptTemplate({
template: "請用繁體中文說明以下概念:{concept}",
inputVariables: ["concept"]
});
const expr = {
pipeline: [
{
type: "prompt",
name: "genPrompt",
input: "$.concept",
output: "promptText",
prompt: prompt // 直接傳入 PromptTemplate 物件
},
{
type: "llm",
name: "gpt",
input: "$.promptText",
output: "explanation"
}
]
};
const chain = await LCELChain.fromExpression(expr, { llm });
const out = await chain.run({ concept: "LangChain Expression Language" });
console.log(out.explanation);
重點:在
prompt節點可以直接放入PromptTemplate,LCEL 會自動把input填入模板中。
範例 3:條件分支 + 搜尋工具
import { SerpAPI } from "@langchain/community/tools/serpapi";
const searchTool = new SerpAPI(process.env.SERPAPI_API_KEY);
const expr = {
pipeline: [
{
type: "llm",
name: "answer",
input: "$.question",
output: "rawAnswer"
},
{
type: "condition",
condition: "$.rawAnswer.includes('不知道')",
true: [
{
type: "tool",
name: "search",
input: "$.question",
output: "searchResult",
tool: searchTool
},
{
type: "prompt",
name: "refine",
input: "$.searchResult",
output: "finalAnswer",
prompt: new PromptTemplate({
template: "請根據以下搜尋結果,簡潔回答使用者的問題:\n{searchResult}",
inputVariables: ["searchResult"]
})
}
],
false: [
{
type: "prompt",
name: "final",
input: "$.rawAnswer",
output: "finalAnswer",
prompt: new PromptTemplate({
template: "以下是 AI 的直接回覆,請簡化語句:\n{rawAnswer}",
inputVariables: ["rawAnswer"]
})
}
]
}
]
};
const chain = await LCELChain.fromExpression(expr, { llm });
const res = await chain.run({ question: "台北 2024 年的天氣趨勢是什麼?" });
console.log(res.finalAnswer);
說明:
condition節點會檢查 LLM 是否回傳「不知道」的字樣。- 若為
true,會觸發search工具,再把結果交給refinePrompt。- 若為
false,直接走finalPrompt 進行文字精煉。
範例 4:使用 branch 產生多條平行路徑
const expr = {
pipeline: [
{
type: "llm",
name: "classify",
input: "$.question",
output: "topic"
},
{
type: "branch",
selector: "$.topic",
cases: {
"程式設計": [
{ type: "tool", name: "codeSearch", input: "$.question", output: "codeResult" }
],
"旅遊": [
{ type: "tool", name: "travelInfo", input: "$.question", output: "travelResult" }
],
"default": [
{ type: "prompt", name: "fallback", input: "$.question", output: "fallbackAnswer",
prompt: new PromptTemplate({template: "抱歉,我無法辨識此類別。", inputVariables: []}) }
]
}
}
]
};
const chain = await LCELChain.fromExpression(expr, { llm, tools: { codeSearch, travelInfo } });
const ans = await chain.run({ question: "請告訴我 Python 的 list comprehension 用法" });
console.log(ans);
關鍵:
branch允許根據前一步的分類結果,選擇不同的子流程,實現 動態路由。
範例 5:將前置資料清理與後置格式化結合
import { JsonOutputParser } from "@langchain/core/output_parsers";
const expr = {
pipeline: [
{
type: "prompt",
name: "clean",
input: "$.rawText",
output: "cleaned",
prompt: new PromptTemplate({
template: "請把以下文字整理成純文字,去除所有 Markdown 標記:\n{rawText}",
inputVariables: ["rawText"]
})
},
{
type: "llm",
name: "extract",
input: "$.cleaned",
output: "jsonString",
llmOptions: { stop: ["\n"] } // 防止過長回覆
},
{
type: "parser",
name: "jsonParse",
input: "$.jsonString",
output: "structuredData",
parser: new JsonOutputParser()
}
]
};
const chain = await LCELChain.fromExpression(expr, { llm });
const result = await chain.run({ rawText: "# 標題\n\n內容 **粗體**" });
console.log(result.structuredData);
技巧:LCEL 支援
parser節點,讓你直接把 LLM 的文字結果轉成結構化資料,省去手寫JSON.parse的步驟。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方式 |
|---|---|---|
| 變數命名衝突 | 若兩個節點使用相同 output 名稱,後者會覆蓋前者。 |
為每個 output 使用具體且唯一的名稱,如 answer_v1、answer_v2。 |
| 條件判斷的字串比較 | condition 只支援簡易的比較,無法直接使用正則。 |
先在 llm 或 tool 中產生布林值,再於 condition 直接判斷 $..isRelevant。 |
| 工具未註冊 | 在 LCEL 中引用的 tool 必須先於 LCELChain.fromExpression 注入。 |
使用 LCELChain.fromExpression(expr, { llm, tools: { myTool } }),或在 expr 中的 tool 節點直接提供實例。 |
| 大量分支導致表達式過長 | 多層 branch 會讓 JSON 變得難以閱讀。 |
把子流程抽成 子表達式(sub‑expression),使用 $ref 引用。 |
| LLM 回傳非預期格式 | 若 LLM 回傳的文字不符合 parser 需求,會拋出例外。 |
在 llm 節點加入 stop、temperature 等參數,或在 parser 前加上 prompt 讓 LLM 明確說「請回傳 JSON」。 |
最佳實踐
- 保持單一職責:每個節點只做一件事(產生 Prompt、呼叫 LLM、執行工具、或做條件判斷)。
- 使用
output別名:即使是同一個工具,也盡量使用不同的別名,以免在後續分支中混淆。 - 加入
metadata:在run()時傳入metadata,可在條件或工具中參考,例如$.metadata.userId。 - 測試子表達式:將複雜的子流程抽成獨立的 LCEL JSON,先單獨測試再組合。
- 記錄 LLM 設定:在
llmOptions中明確寫出temperature、maxTokens、stop等,確保每次執行行為一致。
實際應用場景
| 場景 | 需求 | LCEL 怎麼幫助 |
|---|---|---|
| 客服聊天機器人 | 需要根據使用者問題決定是直接回覆、搜尋資料或升級給人工客服。 | 使用 condition + branch 判斷信心分數,動態切換 LLM、搜尋工具或 humanHandOff 節點。 |
| 文件摘要與結構化 | 從 PDF、Word 等文件抽取關鍵資訊,並輸出 JSON 給後端。 | prompt 先清理文字 → llm 產生摘要 → parser 轉成 JSON → tool 寫入資料庫。 |
| 程式碼生成輔助 | 依使用者需求生成程式碼,並即時測試。 | prompt 產生程式碼 → tool (PythonREPL) 執行 → condition 檢查執行結果 → 若失敗走 refine Prompt 再產生。 |
| 多語言翻譯工作流 | 先偵測語言,再選擇適合的翻譯模型或工具。 | llm 判斷語言 → branch 根據語言呼叫不同的翻譯 API → prompt 統一格式化輸出。 |
| 資料探索與可視化 | 使用者以自然語言詢問資料庫統計,系統自動產生 SQL 並回傳圖表。 | prompt 產生 SQL → tool 執行資料庫查詢 → condition 檢查結果是否為空 → tool 產生圖表 → 最後 prompt 整理回覆。 |
總結
LCEL 為 LangChain 的 Chains 提供了一層 宣告式、可組合、易除錯 的語法,使得模型流程的設計不再是繁瑣的程式碼堆砌,而是像寫配置檔一樣直觀。
透過本文的 概念說明、實作範例、以及 最佳實踐,你應該已經能夠:
- 使用 JSON‑style 表達式快速串接 Prompt、LLM、工具與條件分支。
- 在同一個 Chain 中同時處理文字前處理、模型呼叫、結果解析與後置格式化。
- 針對不同業務需求(客服、文件處理、程式碼輔助等)設計彈性且可維護的工作流。
未來隨著 LangChain 社群持續擴充工具與模型支援,LCEL 也會加入更多功能(如迴圈、錯誤重試策略),讓開發者能以更少的程式碼,完成更複雜的 AI 應用。現在就把 LCEL 帶入你的專案,體驗 「寫一次、跑多次」 的開發快感吧!