T-CREATOR

LangChain × LCEL 徹底解説:Runnable で組む宣言的パイプライン

LangChain × LCEL 徹底解説:Runnable で組む宣言的パイプライン

AI アプリケーション開発において、複雑な処理フローを効率的に構築することは非常に重要です。しかし、従来の命令的なプログラミングアプローチでは、コードの可読性や保守性に課題がありました。

LangChain の LCEL(LangChain Expression Language)は、この課題を解決する革新的なソリューションです。Runnable インターフェースを基盤とした宣言的なパイプライン構築により、複雑な AI ワークフローを直感的かつ効率的に実装できるようになります。

本記事では、LCEL の基礎から実践的な活用方法まで、初心者の方にもわかりやすく解説いたします。

LangChain と LCEL の基礎知識

LangChain の概要

LangChain は、大規模言語モデル(LLM)を活用したアプリケーション開発を支援するフレームワークです。プロンプト管理、チェーン構築、エージェント機能など、AI アプリケーション開発に必要な機能を包括的に提供しています。

以下の図は、LangChain のコアコンポーネントとその関係性を示しています。

mermaidflowchart TD
    apps[アプリケーション] --> chains[チェーン/LCEL]
    chains --> components[コンポーネント]
    components --> prompts[プロンプト]
    components --> llms[LLM]
    components --> parsers[出力パーサー]
    components --> memory[メモリ]
    components --> tools[ツール]
    components --> vectorstores[ベクトルストア]

LangChain は、これらのコンポーネントを組み合わせて、複雑な AI ワークフローを構築できる設計になっています。特に LCEL は、これらのコンポーネントを効率的に連携させるための言語として位置づけられています。

LCEL(LangChain Expression Language)とは

LCEL は、LangChain のコンポーネントを宣言的に組み合わせるための専用言語です。SQL のように、「何をしたいか」を記述するだけで、「どのように実行するか」はフレームワークが自動的に処理してくれます。

typescript// LCEL の基本例
const chain = prompt | llm | outputParser;

このシンプルな記述だけで、プロンプト処理、LLM 実行、出力パースまでの完全なパイプラインが構築されます。従来の命令的なアプローチと比較すると、コードの簡潔性と可読性が大幅に向上していることがお分かりいただけるでしょう。

Runnable インターフェースの理解

Runnable は、LCEL の中核となるインターフェースです。すべての LangChain コンポーネントが Runnable を実装しており、統一された方法で実行・組み合わせが可能になっています。

以下の図は、Runnable インターフェースの基本構造を表しています。

mermaidclassDiagram
    class Runnable {
        +invoke(input) any
        +stream(input) AsyncIterator
        +batch(inputs) any[]
        +pipe(runnable) RunnableSequence
    }
    
    class RunnableSequence {
        +first: Runnable
        +last: Runnable
    }
    
    class RunnableParallel {
        +steps: Record~string, Runnable~
    }
    
    Runnable <|-- RunnableSequence
    Runnable <|-- RunnableParallel
    Runnable <|-- PromptTemplate
    Runnable <|-- LLM
    Runnable <|-- OutputParser

Runnable インターフェースが提供する主要なメソッドは以下の通りです:

#メソッド説明用途
1invoke()単一入力で同期実行一般的な処理実行
2stream()ストリーミング実行リアルタイム出力
3batch()複数入力でバッチ実行大量データ処理
4pipe()パイプライン接続チェーン構築

従来のパイプライン vs LCEL パイプライン

命令的プログラミングの問題点

従来の LangChain では、チェーンの構築に命令的なアプローチを使用していました。これには以下のような問題がありました。

typescript// 従来の命令的アプローチ
import { LLMChain } from "langchain/chains";
import { PromptTemplate } from "langchain/prompts";
import { OpenAI } from "langchain/llms/openai";

const prompt = new PromptTemplate({
  template: "質問: {question}\n回答:",
  inputVariables: ["question"]
});

const llm = new OpenAI({ temperature: 0.7 });

const chain = new LLMChain({
  llm: llm,
  prompt: prompt
});

// 実行時の処理
const result = await chain.call({
  question: "LangChain とは何ですか?"
});

この実装には以下の課題がありました:

  • 冗長なコード: 多くのボイラープレートコードが必要
  • 型安全性の欠如: 入力と出力の型チェックが困難
  • テストの困難さ: チェーン全体のテストが複雑
  • 再利用性の低さ: コンポーネントの独立したテストが困難

宣言的パイプラインのメリット

LCEL を使用した宣言的アプローチでは、これらの問題が解決されます。

typescript// LCEL を使用した宣言的アプローチ
import { PromptTemplate } from "langchain/prompts";
import { OpenAI } from "langchain/llms/openai";
import { StringOutputParser } from "langchain/schema/output_parser";

const prompt = PromptTemplate.fromTemplate("質問: {question}\n回答:");
const llm = new OpenAI({ temperature: 0.7 });
const outputParser = new StringOutputParser();

// パイプラインの宣言的定義
const chain = prompt | llm | outputParser;

// 実行
const result = await chain.invoke({
  question: "LangChain とは何ですか?"
});

LCEL の宣言的アプローチがもたらすメリットを以下の図で示します。

mermaidflowchart LR
    subgraph legacy[従来のアプローチ]
        imperative[命令的実装] --> complex[複雑なコード]
        complex --> maintenance[保守困難]
    end
    
    subgraph lcel[LCEL アプローチ]
        declarative[宣言的実装] --> simple[シンプルなコード]
        simple --> readable[高い可読性]
        readable --> testable[テスト容易]
    end

主なメリットは以下の通りです:

  • 簡潔性: パイプライン演算子(|)による直感的な記述
  • 型安全性: TypeScript との親和性が高く、コンパイル時エラー検出
  • 再利用性: 各コンポーネントの独立性が高い
  • テスタビリティ: 個別コンポーネントのテストが簡単

LCEL の基本操作とシンタックス

パイプライン演算子(|)の使い方

LCEL の中核となるのが、パイプライン演算子(|)です。Unix のパイプライン概念を AI ワークフローに応用しており、データの流れが直感的に理解できます。

typescript// 基本的なパイプライン構文
const result = input | component1 | component2 | component3;

パイプライン演算子の動作原理を以下のコードで確認しましょう。

typescriptimport { PromptTemplate } from "langchain/prompts";
import { OpenAI } from "langchain/llms/openai";

// 各コンポーネントの定義
const promptTemplate = PromptTemplate.fromTemplate(
  "以下の質問に日本語で回答してください: {question}"
);

const llm = new OpenAI({
  modelName: "gpt-3.5-turbo",
  temperature: 0.3
});

パイプライン演算子により、データが順次処理される流れを示します。

typescript// パイプラインの組み立て
const qaChain = promptTemplate | llm;

// 実行例
const response = await qaChain.invoke({
  question: "TypeScript の型安全性について教えてください"
});

console.log(response);

Runnable の種類と特徴

LCEL で使用できる主要な Runnable の種類と特徴を整理します。

#Runnable 種類役割主な用途
1PromptTemplateプロンプト生成動的なプロンプト作成
2LLM/ChatModel言語モデル実行テキスト生成・推論
3OutputParser出力解析構造化データ抽出
4RunnableSequence順次実行線形ワークフロー
5RunnableParallel並列実行同時処理

各 Runnable の基本的な使用方法を見てみましょう。

typescriptimport { 
  PromptTemplate,
  StringOutputParser,
  JsonOutputParser 
} from "langchain/prompts";
import { OpenAI } from "langchain/llms/openai";

// プロンプトテンプレートの作成
const classificationPrompt = PromptTemplate.fromTemplate(`
以下のテキストを分類してください:
テキスト: {text}
分類: 
`);

// LLM の初期化
const llm = new OpenAI({ temperature: 0.1 });

// 出力パーサーの設定
const stringParser = new StringOutputParser();

基本的なチェーン構築

シンプルなテキスト分類チェーンを構築してみましょう。

typescript// 基本的なチェーンの構築
const classificationChain = classificationPrompt | llm | stringParser;

// チェーンの実行
async function classifyText(text: string) {
  try {
    const result = await classificationChain.invoke({ text });
    return result;
  } catch (error) {
    console.error("分類処理でエラーが発生しました:", error);
    throw error;
  }
}

実際にチェーンを使用する例を示します。

typescript// 使用例
const sampleTexts = [
  "今日の天気は晴れで気持ちが良いです",
  "株価が急落しており、投資家は懸念を示している",
  "新しいプログラミング言語の学習を始めました"
];

for (const text of sampleTexts) {
  const classification = await classifyText(text);
  console.log(`テキスト: ${text}`);
  console.log(`分類結果: ${classification}\n`);
}

実践:LCEL でパイプラインを構築

シンプルな Q&A パイプライン

実際に動作する Q&A パイプラインを段階的に構築していきます。まずは、必要な依存関係をインストールしましょう。

bashyarn add langchain @langchain/openai @langchain/core

環境変数の設定も必要です。

typescript// 環境設定
import * as dotenv from 'dotenv';
dotenv.config();

// OpenAI API キーの確認
if (!process.env.OPENAI_API_KEY) {
  throw new Error("OPENAI_API_KEY が設定されていません");
}

基本的な Q&A パイプラインの実装を進めます。

typescriptimport { PromptTemplate } from "langchain/prompts";
import { OpenAI } from "@langchain/openai";
import { StringOutputParser } from "@langchain/core/output_parsers";

// プロンプトテンプレートの定義
const qaPromptTemplate = PromptTemplate.fromTemplate(`
あなたは親切で知識豊富なアシスタントです。
以下の質問に正確で分かりやすい日本語で回答してください。

質問: {question}

回答:
`);

LLM と出力パーサーの設定を行います。

typescript// LLM の初期化
const llm = new OpenAI({
  modelName: "gpt-3.5-turbo",
  temperature: 0.7,
  maxTokens: 500
});

// 出力パーサーの設定
const outputParser = new StringOutputParser();

// パイプラインの構築
const qaChain = qaPromptTemplate | llm | outputParser;

プロンプトテンプレート + LLM + 出力パーサー

より高度な出力フォーマットを扱うパイプラインを構築してみましょう。JSON 形式で構造化された回答を得るパイプラインです。

typescriptimport { JsonOutputParser } from "@langchain/core/output_parsers";

// JSON 出力用プロンプトテンプレート
const structuredQaPrompt = PromptTemplate.fromTemplate(`
以下の質問に対して、JSON 形式で回答してください。

質問: {question}

以下の形式で回答してください:
{{
  "answer": "質問への回答",
  "confidence": "回答の信頼度(1-10)",
  "sources": ["参考情報1", "参考情報2"],
  "category": "質問のカテゴリ"
}}

JSON:
`);

// JSON パーサーの設定
const jsonParser = new JsonOutputParser();

// 構造化回答パイプライン
const structuredQaChain = structuredQaPrompt | llm | jsonParser;

実際にパイプラインを実行してみます。

typescript// 実行例
async function askQuestion(question: string) {
  try {
    const response = await structuredQaChain.invoke({ question });
    return response;
  } catch (error) {
    console.error("Q&A 処理中にエラーが発生しました:", error);
    return null;
  }
}

// 使用例
const question = "TypeScript の主な利点は何ですか?";
const answer = await askQuestion(question);
console.log("構造化された回答:", JSON.stringify(answer, null, 2));

エラーハンドリングとバリデーション

LCEL パイプラインでは、適切なエラーハンドリングとバリデーションが重要です。以下のパターンで実装できます。

typescriptimport { z } from "zod";
import { StructuredOutputParser } from "langchain/output_parsers";

// バリデーションスキーマの定義
const responseSchema = z.object({
  answer: z.string().min(10, "回答は10文字以上である必要があります"),
  confidence: z.number().min(1).max(10),
  category: z.enum(["技術", "ビジネス", "一般", "その他"])
});

// 構造化出力パーサーの作成
const validatingParser = StructuredOutputParser.fromZodSchema(responseSchema);

バリデーション機能付きのパイプラインを構築します。

typescript// バリデーション付きプロンプトテンプレート
const validatedPrompt = PromptTemplate.fromTemplate(`
質問: {question}

{format_instructions}

回答:
`);

// パイプライン構築(フォーマット指示を含む)
const validatedChain = validatedPrompt | llm | validatingParser;

// 実行時のエラーハンドリング
async function safeAskQuestion(question: string) {
  try {
    const response = await validatedChain.invoke({
      question,
      format_instructions: validatingParser.getFormatInstructions()
    });
    
    console.log("バリデーション成功:", response);
    return response;
  } catch (error) {
    if (error instanceof z.ZodError) {
      console.error("バリデーションエラー:", error.issues);
    } else {
      console.error("実行エラー:", error.message);
    }
    return null;
  }
}

高度な LCEL 機能

並列処理(RunnableParallel)

複数の処理を同時実行する場合、RunnableParallel を使用します。これにより、処理時間の大幅な短縮が可能です。

typescriptimport { RunnableParallel, RunnableSequence } from "@langchain/core/runnables";

// 異なる観点からの分析プロンプト
const sentimentPrompt = PromptTemplate.fromTemplate(
  "以下のテキストの感情を分析してください: {text}"
);

const summaryPrompt = PromptTemplate.fromTemplate(
  "以下のテキストを要約してください: {text}"
);

const keywordPrompt = PromptTemplate.fromTemplate(
  "以下のテキストからキーワードを抽出してください: {text}"
);

並列パイプラインの構築を行います。

typescript// 並列処理パイプラインの構築
const parallelAnalysis = RunnableParallel.from({
  sentiment: sentimentPrompt | llm | outputParser,
  summary: summaryPrompt | llm | outputParser,
  keywords: keywordPrompt | llm | outputParser
});

// 実行例
async function analyzeText(text: string) {
  const results = await parallelAnalysis.invoke({ text });
  return results;
}

並列処理の効果を以下の図で示します。

mermaidsequenceDiagram
    participant User
    participant Pipeline
    participant LLM1 as LLM (感情分析)
    participant LLM2 as LLM (要約)
    participant LLM3 as LLM (キーワード抽出)
    
    User->>Pipeline: テキスト入力
    
    par 並列実行
        Pipeline->>LLM1: 感情分析リクエスト
        Pipeline->>LLM2: 要約リクエスト
        Pipeline->>LLM3: キーワード抽出リクエスト
    end
    
    LLM1-->>Pipeline: 感情分析結果
    LLM2-->>Pipeline: 要約結果
    LLM3-->>Pipeline: キーワード結果
    
    Pipeline->>User: 統合結果

条件分岐(RunnableBranch)

入力内容に応じて異なる処理を実行する条件分岐パイプラインを実装します。

typescriptimport { RunnableBranch } from "@langchain/core/runnables";

// 質問の種類に応じた専用プロンプト
const technicalPrompt = PromptTemplate.fromTemplate(
  "技術的な質問です: {question}\n詳細な技術説明をしてください:"
);

const generalPrompt = PromptTemplate.fromTemplate(
  "一般的な質問です: {question}\n分かりやすく説明してください:"
);

const businessPrompt = PromptTemplate.fromTemplate(
  "ビジネス関連の質問です: {question}\nビジネス視点で回答してください:"
);

条件分岐ロジックを実装します。

typescript// 質問分類関数
function categorizeQuestion(input: { question: string }) {
  const question = input.question.toLowerCase();
  
  if (question.includes("コード") || question.includes("プログラミング") || 
      question.includes("アルゴリズム")) {
    return "technical";
  } else if (question.includes("ビジネス") || question.includes("マーケティング") || 
             question.includes("売上")) {
    return "business";
  } else {
    return "general";
  }
}

// 条件分岐パイプラインの構築
const branchingChain = RunnableBranch.from([
  [
    (input) => categorizeQuestion(input) === "technical",
    technicalPrompt | llm | outputParser
  ],
  [
    (input) => categorizeQuestion(input) === "business",
    businessPrompt | llm | outputParser
  ],
  generalPrompt | llm | outputParser // デフォルト分岐
]);

動的パイプライン構築

実行時に動的にパイプラインを構築する高度な機能も実装できます。

typescript// 動的パイプライン生成関数
function createDynamicChain(options: {
  useJsonOutput: boolean;
  includeConfidence: boolean;
  language: "ja" | "en";
}) {
  let prompt = PromptTemplate.fromTemplate("質問: {question}");
  
  if (options.language === "en") {
    prompt = PromptTemplate.fromTemplate("Question: {question}\nAnswer:");
  }
  
  let parser = new StringOutputParser();
  if (options.useJsonOutput) {
    parser = new JsonOutputParser();
  }
  
  return prompt | llm | parser;
}

// 動的チェーンの使用例
const englishJsonChain = createDynamicChain({
  useJsonOutput: true,
  includeConfidence: true,
  language: "en"
});

パフォーマンスとベストプラクティス

ストリーミング処理の活用

LCEL では、ストリーミング処理により、リアルタイムでの出力が可能です。これにより、ユーザーエクスペリエンスが大幅に向上します。

typescript// ストリーミング対応チェーン
const streamingChain = promptTemplate | llm;

async function streamResponse(question: string) {
  const stream = await streamingChain.stream({ question });
  
  for await (const chunk of stream) {
    process.stdout.write(chunk);
  }
}

バッチ処理による効率的なデータ処理も重要です。

typescript// バッチ処理の実装
async function processBatch(questions: string[]) {
  const inputs = questions.map(q => ({ question: q }));
  
  // 並列バッチ処理
  const results = await qaChain.batch(inputs);
  
  return results.map((result, index) => ({
    question: questions[index],
    answer: result
  }));
}

メモリ効率とキャッシュ戦略

大量のデータを処理する際のメモリ効率化手法を紹介します。

typescriptimport { InMemoryCache } from "@langchain/core/caches";

// キャッシュ機能付き LLM
const cachedLlm = new OpenAI({
  cache: new InMemoryCache(),
  temperature: 0.1
});

// キャッシュ効果のあるパイプライン
const cachedChain = promptTemplate | cachedLlm | outputParser;

パフォーマンス監視のための実装例を示します。

typescript// パフォーマンス測定付きパイプライン
async function measurePerformance<T>(
  operation: () => Promise<T>,
  operationName: string
): Promise<T> {
  const startTime = Date.now();
  
  try {
    const result = await operation();
    const endTime = Date.now();
    
    console.log(`${operationName}: ${endTime - startTime}ms`);
    return result;
  } catch (error) {
    const endTime = Date.now();
    console.error(`${operationName} failed after ${endTime - startTime}ms:`, error);
    throw error;
  }
}

// 使用例
const timedResult = await measurePerformance(
  () => qaChain.invoke({ question: "LangChain の利点は?" }),
  "Q&A パイプライン"
);

以下の図は、LCEL パイプラインのパフォーマンス最適化ポイントを示しています。

mermaidflowchart TD
    input[入力データ] --> cache{キャッシュ確認}
    cache -->|ヒット| cached_result[キャッシュ結果]
    cache -->|ミス| parallel[並列処理]
    
    parallel --> prompt_process[プロンプト処理]
    parallel --> llm_process[LLM 処理]
    parallel --> parse_process[パース処理]
    
    prompt_process --> combine[結果統合]
    llm_process --> combine
    parse_process --> combine
    
    combine --> cache_store[キャッシュ保存]
    cache_store --> result[最終結果]
    cached_result --> result

まとめ

LCEL(LangChain Expression Language)は、AI アプリケーション開発における複雑な処理フローを、宣言的で直感的な方法で構築できる強力なツールです。

本記事でご紹介した内容をまとめると、以下のような価値を提供します:

技術的価値

  • Runnable インターフェースによる統一された実行モデル
  • パイプライン演算子による直感的なデータフロー記述
  • 型安全性とテスタビリティの向上

開発効率の向上

  • ボイラープレートコードの大幅削減
  • 並列処理とストリーミングによるパフォーマンス最適化
  • 条件分岐と動的パイプライン構築による柔軟性

保守性とスケーラビリティ

  • コンポーネントの再利用性向上
  • デバッグとモニタリングの容易さ
  • チーム開発での一貫性確保

LCEL を活用することで、従来の命令的なアプローチでは困難だった、保守性が高く効率的な AI アプリケーションの開発が可能になります。特に複雑なワークフローを扱う際には、その真価を発揮するでしょう。

今後の AI アプリケーション開発において、LCEL は必須のスキルとなることが予想されます。ぜひ本記事の内容を参考に、実際のプロジェクトでの活用を検討してみてください。

関連リンク