AI Engineering Academy · レッスン

分岐と並列Chain

RunnableParallelとRunnableBranchの構造を構築し、複数のChainを同時に実行したり、動的な条件に基づいて入力を異なるChainへ振り分けたりします。

レッスン 3/413 ステップ

「分岐と並列Chain」はCoddyKit上の無料AI Engineering Academyレッスンです。 これはレッスン3/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはAI Engineering Academy学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 AI Engineering Academyコースには全4レッスンが含まれています。

並列処理と分岐が重要な理由

実際のLLMパイプラインでは、複数の処理を同時に行ったり、内容に応じてリクエストを異なる経路へ振り分けたりする必要があります。並列チェーンは複数の分岐を同時に実行し、タスクが独立している場合のレイテンシを削減します。分岐チェーンは、動的な条件に基づいて入力を異なる専門チェーンへ振り分けます。LCELでは、RunnableParallelとRunnableBranchによって、これらのパターンを標準でサポートしています。

RunnableParallelの基本

RunnableParallelは、各キーがRunnableに対応する辞書を受け取ります。呼び出されると、すべての分岐を並行して実行し、各キーに対応する分岐の結果を含む辞書を返します。同じ入力から複数の出力を生成したい場合に適しています。たとえば、要約の生成とキーワードの抽出を同時に行えます。

from langchain_core.runnables import RunnableParallel
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser

model = ChatOpenAI(model='gpt-4o-mini')
parser = StrOutputParser()

parallel = RunnableParallel(
    summary=(
        ChatPromptTemplate.from_template('Summarize: {text}') | model | parser
    ),
    keywords=(
        ChatPromptTemplate.from_template('Extract keywords from: {text}') | model | parser
    )
)

result = parallel.invoke({'text': 'Long document text here...'})
print(result['summary'])
print(result['keywords'])

辞書の省略記法による並列処理

LCELには便利な省略記法があります。パイプチェーンのステップとしてdictをそのまま渡すと、自動的にRunnableParallelでラップされます。これにより、明示的なクラスのインスタンス化が不要になり、並列分岐を自然に記述できます。辞書のキーが結果のキーになり、値が並行して実行される分岐になります。

from langchain_core.runnables import RunnablePassthrough

# Dict shorthand creates RunnableParallel automatically
chain = (
    RunnablePassthrough.assign(
        sentiment=(
            ChatPromptTemplate.from_template('Sentiment of: {review}')
            | model | parser
        ),
        aspects=(
            ChatPromptTemplate.from_template('List aspects mentioned in: {review}')
            | model | parser
        )
    )
)

result = chain.invoke({'review': 'Great battery but poor camera quality.'})
print(result['sentiment'])
print(result['aspects'])

RunnableBranchの仕組み

RunnableBranchは、条件に基づいて入力を異なるチェーンへ振り分けます。(condition, runnable)のペアのリストと、デフォルトのRunnableを指定します。分岐は条件を順番に評価し、最初に一致した分岐を実行します。これにより、意図に基づくルーティングが可能になります。たとえば、カスタマーサービスの問い合わせをサポートチェーンへ、技術的な質問をドキュメントチェーンへ送れます。

from langchain_core.runnables import RunnableBranch

technical_chain = (
    ChatPromptTemplate.from_template('Technical answer: {query}') | model | parser
)
general_chain = (
    ChatPromptTemplate.from_template('General answer: {query}') | model | parser
)

branch = RunnableBranch(
    (lambda x: 'error' in x['query'].lower() or 'bug' in x['query'].lower(),
     technical_chain),
    general_chain  # default branch
)

result = branch.invoke({'query': 'I got a TypeError in my code'})
# Routes to technical_chain because 'error' is in the query

LLM分類による意味的ルーティング

より柔軟なルーティングパターンでは、どの分岐を使うかを判断するために分類器としてのLLM呼び出しを使用します。ルーターはまず小さなモデルを呼び出して入力の意図を分類し、その分類結果に基づいて適切な専門チェーンへルーティングします。API呼び出しが1回増える代わりに、キーワードマッチングでは見逃してしまう微妙なケースにも対応できます。

from langchain_core.output_parsers import StrOutputParser

# Step 1: classify intent
classify_prompt = ChatPromptTemplate.from_template(
    'Classify this query as exactly one of: billing, technical, general.\nQuery: {query}'
)
classifier = classify_prompt | model | StrOutputParser()

# Step 2: route based on classification
def route(classification_result: dict):
    topic = classification_result['topic'].strip().lower()
    if topic == 'billing':
        return billing_chain
    elif topic == 'technical':
        return technical_chain
    return general_chain

full_chain = (
    RunnablePassthrough.assign(topic=lambda x: classifier.invoke(x))
    | RunnableLambda(route)
)

並列RAG:複数のRetriever

高度なRAGシステムでは、複数のデータソースから同時に検索し、結果を統合することがあります。RunnableParallelを使うと、商品データベース、FAQストア、ドキュメントインデックスに同時にクエリを送れます。続く統合ステップで上位の結果をまとめてからLLMにコンテキストを渡すことで、モデルにより豊富な情報基盤を提供できます。

from langchain_core.runnables import RunnableParallel, RunnablePassthrough

# Assume these retrievers are already set up
faq_retriever = faq_vectorstore.as_retriever(search_kwargs={'k': 3})
doc_retriever = doc_vectorstore.as_retriever(search_kwargs={'k': 3})

retrieval = RunnableParallel(
    faq_results=faq_retriever,
    doc_results=doc_retriever
)

def merge_docs(retrieved: dict) -> str:
    all_docs = retrieved['faq_results'] + retrieved['doc_results']
    return '\n\n'.join(d.page_content for d in all_docs)

pipeline = (
    retrieval
    | RunnableLambda(merge_docs)
    | ChatPromptTemplate.from_template('Context: {context}\nAnswer: {question}')
    | model | parser
)

itemgetterを使った条件付きチェーン

並列処理の結果から特定のキーを選択したり、コンテキストの一部だけを次のステップに渡したりする必要がある場合、Python の operator.itemgetter は軽量な Runnable セレクターとして機能します。これは、異なる分岐がそれぞれ異なるキーを生成する並列ステップの後に、次の処理段階に必要なものだけを取り出す場合に便利です。

from operator import itemgetter
from langchain_core.runnables import RunnablePassthrough

# After a parallel step, extract just the summary
chain = (
    RunnableParallel(
        summary=summary_chain,
        sentiment=sentiment_chain
    )
    | itemgetter('summary')  # pass only the summary onward
    | translate_chain
)

# itemgetter works because dict.__getitem__ is a valid transform

並列処理による高速化の測定

RunnableParallel の主なメリットは、実時間の短縮です。2 秒かかる LLM 呼び出しを 3 回順番に実行すると、合計で 6 秒かかります。並列実行すれば、最も遅い分岐の所要時間である約 2 秒で完了します。ただし、並列呼び出しでは一度に使用するトークン数が増えるため、レート制限に注意してください。必要に応じて、batch() の max_concurrency を使用するか、キーごとにレート制限を設定してください。

import time

# Measure sequential time
start = time.time()
result1 = chain_a.invoke(input_data)
result2 = chain_b.invoke(input_data)
result3 = chain_c.invoke(input_data)
seq_time = time.time() - start
print(f'Sequential: {seq_time:.2f}s')

# Measure parallel time
start = time.time()
results = RunnableParallel(a=chain_a, b=chain_b, c=chain_c).invoke(input_data)
par_time = time.time() - start
print(f'Parallel: {par_time:.2f}s')
print(f'Speedup: {seq_time/par_time:.1f}x')

非同期の並列チェーン

非同期アプリケーションで真にノンブロッキングな並列実行を行うには、RunnableParallel.ainvoke() を使用してください。内部では、LCEL が asyncio.gather() を使用して、イベントループ上で分岐を同時に実行します。これは、各リクエストハンドラーがコルーチンである FastAPI サービスで特に重要です。非同期インターフェースを使うことで、複数の LLM 呼び出しを同時に実行してもイベントループがブロックされません。

import asyncio

async def analyze_document(text: str) -> dict:
    parallel = RunnableParallel(
        summary=summary_chain,
        keywords=keyword_chain,
        sentiment=sentiment_chain
    )
    # All three chains run concurrently with asyncio.gather internally
    result = await parallel.ainvoke({'text': text})
    return result

# In FastAPI:
from fastapi import FastAPI
app = FastAPI()

@app.post('/analyze')
async def analyze(request: dict):
    return await analyze_document(request['text'])

並列チェーンと逐次チェーンのネスト

複雑なパイプラインでは、逐次ステップと並列ステップを組み合わせることがよくあります。RunnableParallel を逐次パイプ内にネストすることも、その逆も可能です。たとえば、まずインテントを分類し(逐次)、次に検索とコンテキストの整形を並列で実行し、その後に最終レスポンスを生成します(逐次)。LangChain はネスト構造を正しく評価するため、コールバックだらけの複雑なコードにせず、洗練されたパイプラインを読みやすく構築できます。

# Full pipeline: classify → parallel retrieval → generate
pipeline = (
    RunnablePassthrough.assign(
        intent=classify_chain  # sequential: classify first
    )
    | RunnablePassthrough.assign(
        context=RunnableParallel(  # parallel: retrieve from both sources
            faq=faq_retriever,
            docs=doc_retriever
        )
    )
    | format_context_chain  # sequential: format merged context
    | generate_answer_chain  # sequential: call LLM
)

分岐でのエラー処理

RunnableParallel の分岐の 1 つが失敗すると、デフォルトでは並列呼び出し全体が例外を発生させます。分岐単位の失敗を適切に処理するには、個々の分岐 runnable に .with_fallbacks() を使用してください。RunnableBranch では、各分岐を RunnableLambda 内の try-except でラップするか、デフォルトのフォールバックチェーンを使用してルーティングエラーを処理します。

from langchain_core.runnables import RunnableParallel

# Wrap each branch with a fallback
safe_summary = summary_chain.with_fallbacks([
    RunnableLambda(lambda x: 'Summary unavailable')
])
safe_sentiment = sentiment_chain.with_fallbacks([
    RunnableLambda(lambda x: 'Sentiment unavailable')
])

robust_parallel = RunnableParallel(
    summary=safe_summary,
    sentiment=safe_sentiment
)

# Now one branch failing won't kill the entire parallel call

理解度チェック

LCEL における分岐と並列チェーンについての理解度を確認しましょう。

レッスンのまとめ

このレッスンでは、RunnableParallel が複数のチェーンを同時に実行して結果の辞書を返し、独立した LLM 呼び出しのレイテンシを短縮できること、RunnableBranch が条件や LLM による分類に基づいて入力を異なる専門チェーンに振り分けること、そして並列ステップと逐次ステップをネストすることで、読みやすさを保ったまま高度なマルチパスパイプラインを構築できることを学びました。次は、LangChain における出力のストリーミングについて学びます。

無料で開始

AI チューターと学ぶ Python — 無料

ブラウザでリアルコードを書いて実行し、24/7 の AI チューターから瞬時にサポートを受け、ウェブまたはアプリで続きから学習できます。

コース
30
レッスン
120

よくある質問

「分岐と並列Chain」レッスンは無料ですか?

はい。「分岐と並列Chain」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、AI Engineering Academyコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 AI Engineering Academyコースには全4レッスンが含まれています。

「分岐と並列Chain」で何を学びますか?

RunnableParallelとRunnableBranchの構造を構築し、複数のChainを同時に実行したり、動的な条件に基づいて入力を異なるChainへ振り分けたりします。 ブラウザで直接実行するハンズオンコードでAI Engineering Academyを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。

AI Engineering Academyを始めるのに経験は必要ですか?

事前経験は必要ありません。CoddyKitのAI Engineering Academyは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン3/4です。

「分岐と並列Chain」レッスンにはどのくらい時間がかかりますか?

ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。

このAI Engineering Academyレッスンでコードを書いて実行できますか?

はい。すべてのAI Engineering Academyレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。

このコースのすべてのレッスン

  1. LangChainのアーキテクチャとコア抽象化
  2. LCELでChainを構築する
  3. 分岐と並列Chain
  4. LangChainでの出力ストリーミング
← AI Engineering Academyに戻る