LCEL(LangChain Expression Language)によるAIパイプラインの宣言的構築

「動くが読めない」からの脱却。LCELで構築する保守性の高いAIパイプライン【コード比較付】

この記事は急速に進化する技術について解説しています。最新情報は公式ドキュメントをご確認ください。

約14分で読めます
文字サイズ:
「動くが読めない」からの脱却。LCELで構築する保守性の高いAIパイプライン【コード比較付】
目次

この記事の要点

  • 宣言的な記述によるAIパイプラインの構築
  • 高いモジュール性とコードの可読性・保守性の向上
  • ストリーミングや並列処理の容易な実装

実務の現場では、AI開発において共通の課題が聞かれます。

「プロトタイプは3日で動いた。でも、本番運用に向けた改修に3週間かかっている」
「以前の担当者が書いたLangChainのコードが複雑で理解しきれない」
「デバッグしようにも、Chainの中で何が起きているのかブラックボックスでわからない」

PythonでAIアプリケーションを開発するエンジニアなら、一度は直面する壁ではないでしょうか?

初期のLangChainはLLMアプリ開発の民主化に大きく貢献した反面、抽象化の代償として「隠れた複雑性」を生み、技術的負債の温床になりがちでした。ビジネスのスピード感を維持するためには、「まず動くものを作る」プロトタイプ思考が重要ですが、それが本番環境での保守性を犠牲にしては本末転倒です。

そこで登場したのが、LCEL(LangChain Expression Language)です。

LCELは単なるシンタックスシュガーではなく、「命令的なオブジェクト指向」から「宣言的なパイプライン処理」へのパラダイムシフトであり、Unix哲学への回帰と言えます。

本記事では、経営者視点での保守性・拡張性と、エンジニア視点での実装の美しさを交えながら、従来の書き方(Legacy Chains)からLCELへ移行すべき理由を、具体的なコードと設計思想に基づいて解説します。技術の本質を見抜き、ビジネスへの最短距離を描くためのヒントを探っていきましょう。

なぜ従来のLangChainコードは「スパゲッティ化」するのか

従来のChainAgentクラスを用いた実装(Legacy Chains)が保守困難になる理由は、抽象化の方向性が現在のAI開発トレンドと乖離したことに起因します。特にLangChain安定版リリース以降のパッケージ再編(CoreとCommunityの分離)において、この乖離は致命的です。

命令的記述が招く「隠れた依存関係」の罠

かつてのLangChainは、RetrievalQAConversationalRetrievalChainなど、高度に抽象化されたクラスを組み合わせるスタイルが主流でした。「一行でRAG(検索拡張生成)が書ける」反面、カスタマイズが必要になると問題が生じます。

# 従来の書き方のイメージ(非推奨)
# クラス継承によるカスタマイズは、内部状態への依存を生みやすい
class CustomChain(LLMChain):
    def _call(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
        # ここで親クラスのメソッドをオーバーライドしたり、
        # 独自のロジックを差し込もうとすると...
        if self.memory:
             # 内部状態の管理が複雑化し、データの流れが見えなくなる
             pass
        return super()._call(inputs)

振る舞いを変えるためにクラス継承や複雑なコールバック関数が必要となり、裏側ではクラス変数の状態依存やプロンプトテンプレートの結合が隠蔽されて行われます。

これは「命令的(Imperative)」アプローチの弊害であり、処理手順が内部に隠蔽されるためデータの流れ(Data Flow)が不透明になります。

現在のLangChainエコシステムは、中核機能(langchain-core)と外部連携機能(langchain-community)が明確に分離されています。多くの依存関係を内部に抱え込む従来のChainクラスは、この疎結合な設計思想と相性が悪く、依存関係の解決を複雑にします。

Chainクラスのブラックボックス問題とデバッグの限界

「入力Aを渡したのに、なぜ出力Bになったのか?」

従来のChainクラスでこれを調査するには、ライブラリの内部実装まで潜る必要がありました。verbose=Trueにしてもログは断片的で、データ変換の正確な追跡は困難です。

このブラックボックス化は、セキュリティや保守性のリスクとなります。最新コアライブラリの機能更新やPydanticのバージョン移行時にも、内部実装が隠蔽されていると影響範囲の特定が困難です。経営者視点で見れば、これは将来的な改修コストの増大を意味します。

LangChain安定版リリース以降はバージョン管理が厳格化され、古い構造への依存は「技術的負債」となります。

非同期処理や複雑なエージェントワークフローを従来のChainで実装すると、コードの複雑さが指数関数的に増大します。現在、高度な制御にはLangGraphの活用が推奨されています。LangGraphのcheckpoints APIでは、DynamoDBSaverなどを統合したエージェントの永続化や柔軟な条件分岐が可能です。

ただし、関連ライブラリは開発スピードが速いため、手元の環境で pip show langgraph コマンドを実行して現行バージョンを確認し、公式ドキュメント(docs.langchain.com/langgraph)で最新仕様や非推奨機能の代替手段(checkpoints APIの活用方法など)を確認するアプローチが確実です。

これが、LCELという宣言的アプローチへ移行し、コンポーネントの透明性を確保しながら最新エコシステムに追従すべき最大の理由です。

LCEL(LangChain Expression Language)の正体:Unixパイプラインの再来

Linux/Unixのコマンドライン操作を考えてみましょう。

cat access.log | grep "ERROR" | cut -d' ' -f4 | sort | uniq -c

この美しさは、「入力」→「処理」→「出力」という単純なコンポーネントを、パイプ(|)で繋いでいるだけという点にあります。各コマンドは独立し、前の出力を次の入力として受け取ります。

LCELの設計思想もこれと全く同じです。

「どうやるか」ではなく「何をするか」を記述する

LCELは、Pythonのビット演算子(OR演算子)である | をオーバーロードし、Unixパイプのようなデータフローを実現します。

# LCELのイメージ
chain = prompt | model | output_parser

「プロンプトに入力し、結果をモデルに渡し、出力をパースする」という流れが一目瞭然です。クラス継承や隠された状態管理はなく、純粋なデータ変換フローのみが存在します。これを宣言的(Declarative)記述と呼びます。

Runnableインターフェースという統一規格の威力

Unixコマンドが「標準入出力」で繋がるように、LCELの構成要素はすべてRunnableプロトコルという共通インターフェースを実装しています。

プロンプト、LLM、OutputParser、Retriever(検索機)のすべてがRunnableであり、任意のコンポーネントを | で繋ぐことができます。

Runnableプロトコルが保証する主要メソッドは以下の通りです。

  • invoke: 単一の入力に対する同期呼び出し
  • batch: 複数の入力に対するバッチ処理
  • stream: チャンクごとのストリーミング出力
  • ainvoke / abatch / astream: 上記の非同期バージョン

LCELでパイプラインを組むだけで、ストリーミングやバッチ処理、非同期処理が自動的にサポートされる点が特筆すべきメリットです。ストリーミング用の特別なコールバックハンドラは不要になります。

Unixのパイプ(|)演算子がAI開発にもたらす直感性

AIアプリケーションは本質的に「テキスト(またはマルチモーダルデータ)の変換パイプライン」です。

  1. ユーザーの質問を受け取る
  2. 関連情報を検索する(Contextの付与)
  3. プロンプトに埋め込む
  4. LLMに推論させる
  5. 結果を整形する

この一連の流れをコード構造そのもので表現できることが、LCELの最大のエンジニアリング的価値です。プロトタイプを即座に形にし、そのまま本番品質へと昇華させるための強力な武器となります。

【実証】命令的記述 vs LCEL:コード比較による生産性の証明

LCEL(LangChain Expression Language)の正体:Unixパイプラインの再来 - Section Image

実際のコードで比較検証します。

一般的なRAG(検索拡張生成)パイプラインを例に、従来の書き方とLCELのアプローチを比較します。要件は以下の通りです。

  1. ユーザーの質問を受け取る
  2. VectorStoreから関連ドキュメントを検索する
  3. ドキュメントと質問をプロンプトに埋め込む
  4. ChatModel(OpenAIのGPT-5.2など)で回答を生成する
  5. 文字列として出力する

Before:従来のChainクラスによる実装(Legacy)

かつて主流だったRetrievalQAなどの専用クラスを使用した実装です。現在では多くのケースでレガシーな手法となっています。

# 従来の書き方(v0.1以前のスタイル)
# ※現在はlangchain_openaiなどの別パッケージへの移行が推奨されています
from langchain.chains import RetrievalQA
from langchain.chat_models import ChatOpenAI
from langchain.vectorstores import FAISS
from langchain.embeddings import OpenAIEmbeddings

# セットアップ
vectorstore = FAISS.from_texts(["hoge", "fuga"], OpenAIEmbeddings())
retriever = vectorstore.as_retriever()
# 過去のデフォルトモデルやGPT-4oなどのレガシーモデルは非推奨・廃止の方向です
model = ChatOpenAI() 

# RetrievalQAクラスを使用
# 一見シンプルだが、内部ロジックがブラックボックス化されている
qa_chain = RetrievalQA.from_chain_type(
    llm=model,
    chain_type="stuff",
    retriever=retriever,
    return_source_documents=True,
    # プロンプトのカスタマイズには複雑な引数が必要
    chain_type_kwargs={
        "prompt": SOME_CUSTOM_PROMPT
    }
)

# 実行
result = qa_chain({"query": "AIパイプラインについて教えて"})
print(result['result'])

このアプローチの最大の問題は、RetrievalQAクラスが処理の詳細を隠蔽している点です。「検索結果をリランクしたい」「メタデータをフィルタリングしたい」といった要件変更時に、クラス継承や複雑なコールバックの実装を迫られ、保守性が著しく低下します。また、古いモデル指定のままコードが放置されるリスクも高まります。

After:LCELによる実装(Modern)

次に、同じ処理をLCELで記述します。最新のライブラリ構成(langchain-openaiなど)を採用した現行の標準的な実装であり、OpenAIの最新モデルへの対応が明示的かつ容易です。

# LCELによる実装
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langchain_community.vectorstores import FAISS
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough

# コンポーネントの準備
vectorstore = FAISS.from_texts(["hoge", "fuga"], OpenAIEmbeddings())
retriever = vectorstore.as_retriever()

# 最新の業務標準モデルGPT-5.2を指定(GPT-4o等は順次廃止・統合)
# コーディング特化タスクの場合はGPT-5.3-Codexの利用が推奨されます
model = ChatOpenAI(model="gpt-5.2", temperature=0) 
output_parser = StrOutputParser()

# プロンプト定義
template = """以下のコンテキストに基づいて質問に答えてください:
{context}

質問: {question}
"""
prompt = ChatPromptTemplate.from_template(template)

# --- ここからがLCELのパイプライン定義 ---

chain = (
    # 1. 並列処理で入力を準備(検索と質問のパススルー)
    {"context": retriever, "question": RunnablePassthrough()}
    |
    # 2. プロンプトに流し込む
    prompt
    |
    # 3. LLMで推論(GPT-5.2などの最新モデル)
    model
    |
    # 4. 文字列にパース
    output_parser
)

# 実行
print(chain.invoke("AIパイプラインについて教えて"))

可読性と変更容易性の定量的・定性的評価

このコード比較から、以下の決定的な違いが見て取れます。

  1. データフローの完全な可視化: chain = ... の定義を見るだけで、データの流れが一目瞭然です。「検索結果」と「質問」が「プロンプト」へ渡り、「モデル」を経て「出力」される過程が宣言的に記述されています。
  2. 依存関係の排除と凝集度: 実質的なロジック定義は15行程度で完結しています。RetrievalQAのような約40行に及ぶ巨大なラッパークラスや、難解な設定用辞書への依存がなくなりました。
  3. ストリーミングの標準対応: 追加実装なしで chain.stream(...) を呼び出せ、トークンごとのリアルタイム出力を即座に実現できます。
  4. 最新モデルへの適応と移行の容易さ: langchain_openai などの最新パッケージにより、モデルのアップデートに迅速に対応できます。OpenAIの公式情報によると、2026年2月13日にGPT-4oやOpenAI o4-miniなどのレガシーモデルが提供終了となります。API利用は継続されますが、既存チャットインターフェースはGPT-5.2へ自動移行されます。LCELのパイプラインなら、モデル指定をGPT-5.2などに変更しプロンプトを再テストするだけでスムーズに移行できます。

従来のChainベースのコードをLCELに移行することで、コード行数は約40%〜60%削減され、デバッグ時間も大幅に短縮されます。これはモデルの世代交代が激しいプロダクション環境における保守性と拡張性を担保するための、不可欠な技術的投資です。

堅牢なパイプライン構築のためのLCELベストプラクティス

【実証】命令的記述 vs LCEL:コード比較による生産性の証明 - Section Image

LCELの基本構造を踏まえ、本番環境の厳しい要求に耐えうる、実践的で障害に強いパイプラインを構築する手法を解説します。

RunnableParallelによる並列処理の最適化

RAGにおいて、外部データソースからの検索(Retriever)はボトルネックになりやすい処理です。LCELでは、RunnableParallelを組み込むことで複雑な並列処理を簡潔に実装できます。

from langchain_core.runnables import RunnableParallel

# 例えば、2つの異なるレトリーバーと、質問の要約を同時に走らせる
parallel_chain = RunnableParallel({
    "docs_from_vector_db": vector_retriever,
    "docs_from_web": web_search_retriever,
    "question_summary": summary_chain
})

# これらはPythonのasyncioを活用して並列に実行され、
# 全ての結果が揃った時点で次のステップへ進みます
final_chain = parallel_chain | synthesis_prompt | model

全体の待機時間は「最も時間のかかる単一の処理」に依存する形に抑えられ、直列実行の従来アプローチと比較して応答速度の大幅な向上が見込めます。

RunnableBranchを使わない条件分岐の賢い書き方

質問の意図に応じて後続処理を動的に切り替える際、以前はRunnableBranchが一般的でしたが、現在はルーティングロジックを独立した関数として定義する手法が推奨されています。

from langchain_core.runnables import RunnableLambda

def route(info):
    if "python" in info["topic"].lower():
        return python_chain
    elif "js" in info["topic"].lower():
        return js_chain
    else:
        return general_chain

full_chain = (
    classification_chain # トピックを分類するチェーン
    | RunnableLambda(route) # 動的に次のチェーンを選択
)

Pythonの標準的な制御構文(if/else)をそのまま記述できるため、デバッグ作業が容易になり、コードの可読性も向上します。

本番運用を見据えたフォールバック(.with_fallbacks)戦略

AIシステムはAPI通信エラーやレート制限に直面する可能性があります。また、モデルの世代交代は速く、GPT-3.5の通常チャット提供は終了し、旧モデル(GPT-3.5 Turbo-1106など)は廃止が推奨されています。2026年現在では、GPT-5.2(Instant、Thinking、Pro)が基本モデルとして定着し、コーディング特化型のGPT-5.3-Codexも実運用されています。

本番環境でのシステム停止を防ぐには、最新モデルへの移行と並行して代替モデルへの自動切り替えメカニズムが不可欠です。LCELでは .with_fallbacks() メソッドを活用し、バックアップロジックを宣言的に定義できます。

# メイン:高推論能力を備えた最新モデル
primary_model = ChatOpenAI(model="gpt-5.2-pro")

# フォールバック1:応答速度に優れた軽量モデル(タイムアウト時の保険)
fallback_model_1 = ChatOpenAI(model="gpt-5.2-instant")

# フォールバック2:別プロバイダーの高性能モデル(OpenAI全体の障害対策)
fallback_model_2 = ChatAnthropic(model="claude-3-5-sonnet-20241022")

# 優先順位に従ってフォールバックのチェーンを設定
model_with_fallback = primary_model.with_fallbacks(
    [fallback_model_1, fallback_model_2]
)

chain = prompt | model_with_fallback | output_parser

このフォールバック戦略により、主に3つの利点が得られます。

  1. 可用性の担保: API障害時やモデル廃止時にも、代替モデルがシームレスに応答を引き継ぎます。
  2. コストとパフォーマンスの最適化: エラー発生時のみバックアップを呼び出すため、無駄なリソース消費を抑えつつ確実な動作を保証します。
  3. 運用保守の効率化: モデルのアップデート時も、フォールバックリストを書き換えるだけで対応が完了します。

最新のGPT-5系列やClaude 3.5系列(claude-3-5-sonnet-20241022など)は推論精度と安全性が向上しています。エラーハンドリングをビジネスロジックから切り離すことで、システムの堅牢性を強化できます。

詳細な仕様や最新のモデル情報は、以下の公式ドキュメントを確認してください。

アンチパターン:LCELでやってはいけないこと

LCELは強力ですが、自由度が高いため、新たなスパゲッティコードを生み出すリスクもあります。以下のアンチパターンには注意してください。

ラムダ関数の過剰利用による可読性低下

RunnableLambdaで任意のPython関数を組み込めますが、lambda x: ... の多用は可読性を低下させます。

Bad:

chain = (
    RunnableLambda(lambda x: x["input"])
    | RunnableLambda(lambda x: x.upper())
    | ...
)

Good:
意味のある名前の関数を定義するか、既存のitemgetterなどを使用しましょう。パイプラインは「処理の概要」を示す目次であり、詳細な実装ロジックをインラインで書く場所ではありません。

巨大すぎる単一チェーンの構築

数百行にわたる巨大なLCELチェーンを一つ作るのは避けましょう。デバッグが困難になります。
意味のある単位(例:検索チェーン、要約チェーン、生成チェーン)で小さなRunnableを作成し、それらを最後に結合するように設計してください。

段階的移行ガイド:既存プロジェクトをどうリファクタリングするか

明日からすべてのコードをLCELに書き換える必要はありません。それはリスクが高すぎます。アジャイルなアプローチで、段階的に移行することをお勧めします。

「葉(Leaf)」コンポーネントからの置き換え戦略

システム全体のフロー制御を変えるのではなく、パイプラインの末端(葉)にある小さな処理からLCEL化していきます。

例えば、単純な「プロンプト + LLM」の部分だけをLCELで書き換え、既存コードから呼び出します。LCELのチェーンも.invoke()メソッドを持つため、従来のコードからは単なる関数やオブジェクトのように見えます。まずは動くプロトタイプとして小さな成功体験を積み重ねることが重要です。

チームのLCEL習熟度を測るチェックポイント

移行を進める中で、チームメンバーが以下の概念を理解できているか確認してください。

  1. | 演算子の意味とデータの流れ
  2. Runnableプロトコルの基本メソッド(invoke, stream)
  3. 辞書による並列処理(RunnableParallel)の挙動

これらが浸透すれば、チーム全体の開発速度は加速します。皆さんのチームでは、どの段階まで理解が進んでいるでしょうか?

まとめ:技術的負債を資産に変えるために

実行 - Section Image 3

LCELは、LangChainがたどり着いた「AIアプリケーション構築のためのドメイン固有言語(DSL)」です。

  • 命令的から宣言的へ: 「どうやるか」ではなく「何をするか」を書く。
  • 可読性の向上: パイプライン構造がそのままコードに現れる。
  • 標準機能の享受: ストリーミング、並列処理、フォールバックが容易に。

従来の複雑なChainクラスによる実装は「技術的負債」になりつつあります。LCELへの移行は、将来の変更に強い「資産」としてのコードベースを構築するための投資です。

Unix哲学にあるように、「一つのことをうまくやる」コンポーネントを繋ぎ合わせ、シンプルで強力なAIパイプラインを構築してください。それが、エンジニアがビジネスに提供できる最大の価値の一つです。

「動くが読めない」からの脱却。LCELで構築する保守性の高いAIパイプライン【コード比較付】 - Conclusion Image

参考文献

  1. https://zenn.dev/datajournal1/articles/3bb809ec24e1b7
  2. https://qiita.com/sea_news_yass/items/85e4892f300e0c096459
  3. https://atalupadhyay.wordpress.com/2026/02/25/langchain-vs-langgraph-great-orchestration-debate/
  4. https://www.youtube.com/watch?v=h7pwxFSD_Rc
  5. https://arxiv.org/pdf/2602.21257
  6. https://langfuse.com/guides/cookbook/example_pydantic_ai_mcp_agent_evaluation
  7. https://lobehub.com/ar/skills/hoodini-ai-agents-skills-langchain

コメント

コメントは1週間で消えます
コメントを読み込み中...