コンテンツにスキップ

第05回 学習メモ: エージェントのフォールバック処理

全体像

第04回の Function Calling エージェントに フォールバックチェイン を追加し、BadRequestError(OpenAI からの 400 系エラー、特に context_length_exceeded)を捕捉して ツール無しの素の LLM に「日本語でエラーを説明する」役割を担わせる構成。

04 → 05 の構造変化

flowchart LR
    subgraph T04[第04回: 単一チェイン]
        A04["agent_chain<br/>(llm_with_tools)"] --> Y04["AgentAction or AgentFinish"]
    end

    subgraph T05[第05回: 2つのチェイン]
        A05["agent_chain<br/>(llm_with_tools)"]
        F05["fallback_chain<br/>(素の llm)"]
        A05 -->|通常| Y05["AgentAction or AgentFinish"]
        A05 -.->|BadRequestError| F05
        F05 --> Z05["StrOutputParser<br/>(日本語エラー説明)"]
    end

    T04 -->|追加| T05

エージェントループ(フォールバック分岐込み)

flowchart TD
    Start([ユーザー発話]) --> Loop[エージェントループ]
    Loop --> Invoke["agent_chain.ainvoke<br/>(intermediate_steps を毎回同送)"]

    Invoke -->|正常| Branch{出力の型?}
    Invoke -->|例外| Catch["BadRequestError 捕捉"]

    Branch -->|AgentFinish| Final[最終回答を表示]
    Branch -->|AgentAction のリスト| Exec[ツールを実行]
    Exec --> Append["intermediate_steps に<br/>(action, observation) を append"]
    Append --> Loop

    Catch --> Strip["最後の observation を<br/>空文字に差し替え<br/>(context を縮める)"]
    Strip --> Fallback["fallback_chain.ainvoke<br/>(input = エラー本文)"]
    Fallback --> Explain["日本語のエラー説明を生成"]
    Explain --> Final

    Final --> Save[history に append]
    Save --> End([次のターンへ])

ポイント: - 同じ setup_chain メソッドで2つのチェインを使い分け ている。引数 is_fallback で分岐 - フォールバックは StrOutputParser で締める(通常は OpenAIToolsAgentOutputParser)。出力型が違うので合流地点では is_final=True で扱う - 最後の observation を空文字に差し替えてからフォールバックを呼ぶ: context あふれは多くの場合「最後に読んだファイルが巨大」なので、その本文を捨ててから fallback LLM を呼ぶことで、フォールバック自体が再度 context あふれを起こすのを防ぐ

使用ライブラリ・原理

フォールバックチェインのデザインパターン

「LLM 呼び出しが失敗するかもしれない」というのはエージェント実装の現実的な悩みで、原因はおおよそ:

エラー種別 原因 対策
context_length_exceeded tools schema + chat_history + intermediate_steps の合計が窓を超える 履歴圧縮 / observation 切り詰め / モデル切り替え
rate_limit_exceeded レートリミット超過 指数バックオフ / モデル切り替え
invalid_request_error パラメータ不正、tool_call の引数型違反など プロンプト見直し / ツール側で defensive 化
model_overloaded サーバ過負荷 リトライ

このうち context_length_exceeded だけは「呼び方を変えても直らない」 ので、「諦めて別の LLM 呼び出しでユーザーに状況を伝える」というのが現実解。それがフォールバックチェインのアイデア。

LCEL で2系統のチェインを生やす方法

def setup_chain(self, llm, is_fallback):
    prompt = self.create_prompt(is_fallback)
    assigns = {...}
    if is_fallback:
        return assigns | prompt | llm | StrOutputParser()
    return assigns | prompt | llm | OpenAIToolsAgentOutputParser()
  • __init__llm_with_toolsllm(素)の2つを用意 し、同じ setup_chain に通すだけで2チェイン構築できる
  • assigns 部分(input / agent_scratchpad / chat_history の3キー生成)は両方で共通 → コード重複が少ない
  • 出力パーサーだけ差し替えるのが LCEL の柔軟性の象徴

「最後の observation を空文字に差し替える」工夫

def handle_error(self, error):
    if self.intermediate_steps:
        last_action, _ = self.intermediate_steps[-1]
        self.intermediate_steps[-1] = (last_action, "")
    ...

これは immutability の典型例: タプルなので直接書き換えはできず、(action, "") という新しいタプルで置換する。なぜわざわざ最後だけ空にするかというと: - intermediate_steps は「過去のツール実行履歴」が積まれている - フォールバックを呼ぶ際、これも agent_scratchpad 経由でプロンプトに乗る - 一番容量を食っているのが直前のツール実行結果(例えば巨大ファイルの本文)であることが多い - 全部消すと「何があったかの文脈」が消えてフォールバックも説明できないので、直前の observation だけ捨てる

ファイル別の役割

ファイル 役割
chatbot.py Chainlit エントリーポイント。04 と同形(フォールバックは agent 内部の責務)
conversational_agent.py 2チェイン構成のエージェント。agent_chainfallback_chain__init__ で同時に組み立てる
work/paper1〜3.txt 動作確認用のサンプル英文。連載原典では gpt-4(8K 窓)でこれを全部読ませると context あふれを起こす設計だった
work/paper_huge.txt .gitignore 対象、検証時のみローカル生成)gpt-4o-mini(128K 窓)でフォールバックを発火させるための巨大ダミーファイル
requirements.txt 連載原典の依存固定(langchain==0.0.332)。本リポジトリでは 04 と同じく langchain>=0.3,<1.0 で動かすので参考のみ

学んだこと(要点)

  • エラー処理 = 「諦め方の設計」: try/except で潰すのではなく、「失敗したらどう振る舞うか」を別チェインで明示的に作る。これはエージェントだけでなく LLM パイプライン全般に応用できる
  • is_fallback フラグで分岐させる対称デザイン: 通常用と例外用の2チェインを共通の setup_chain から生やすことで、プロンプトと出力パーサ以外の構造を揃えられる
  • BadRequestErrormessage 属性: error.response.json()["error"]["message"] は旧 openai SDK の書き方。現行 SDK は error.message でアクセスできる
  • 「観測を捨てる」操作: フォールバックが連鎖的に context あふれを起こさないよう、intermediate_steps[-1] の observation を空に差し替える。immutability を守るためタプルを丸ごと差し替え している
  • gpt-4o-mini では現実的にあふれない: 128K 窓は連載執筆当時の gpt-4 (8K) と桁違いに大きいので、フォールバック検証には意図的に巨大ファイルを用意する必要がある(学習目的)

拡張アイデア

  • エラー種別ごとに違うフォールバック挙動: BadRequestError のサブコード(context_length_exceeded vs invalid_request_error)で振る舞いを分岐させる
  • リトライ + フォールバックのハイブリッド: rate_limit_exceeded はリトライ、context_length_exceeded はフォールバック、というように責任分担する
  • intermediate_steps の自動要約: あふれる前に「過去の observation を要約して縮める」処理を入れて、フォールバックに頼らず継続できるようにする(自前の context compression)
  • モデル切り替えフォールバック: gpt-4o-mini で失敗したら、より大きな context 窓を持つモデル(例: gpt-4.1)で再試行する
  • fallback_chain にも tools を生やす: 「エラー説明 + 自動回復アクション」(例: write_file でユーザーに代替案を出力する)

連載原典からの移植で行った変更

第04回と完全に同じ移植を施したうえで、フォールバック関連の handle_error を非同期化fallback_chain.invokefallback_chain.ainvoke)してエージェントループ全体の async 性を揃えた。

箇所 原典 本フォルダ
LangChain バージョン 0.0.332 固定 >=0.3,<1.0(1.x は agent API 廃止)
Tools 渡し llm.bind(functions=[format_tool_to_openai_function(t) for t in tools]) llm.bind_tools(self.tools)
出力パーサ OpenAIFunctionsAgentOutputParser OpenAIToolsAgentOutputParser
思考の足跡フォーマッタ format_to_openai_functions format_to_openai_tool_messages
ChatOpenAI langchain.chat_models.ChatOpenAI langchain_openai.ChatOpenAI
Toolkit langchain.agents.agent_toolkits.FileManagementToolkit langchain_community.agent_toolkits.FileManagementToolkit
メモリ ConversationBufferMemory(memory_key=, return_messages=True) _SimpleHistoryHumanMessage/AIMessage を list で管理)
working_directory working_directory.name(カレント依存) working_directory.resolve()(絶対パス)
エラーメッセージ取得 error.response.json()["error"]["message"] getattr(error, "message", str(error))
tool_execute 3要素のタプル分解(順序依存) 名前→Tool の辞書
モデル gpt-4 (8K) gpt-4o-mini(128K, 安価。フォールバック検証は意図的な巨大ファイルで再現)
fallback_chain 呼び出し .invoke(...) .ainvoke(...)(async 化)
APIキー取得 os.environ["OPENAI_API_KEY"] = "sk-..." ハードコード 1Password CLI(op read op://Personal/openAI_API/credential
関連情報の表示 cl.Message(author="tool", content=..., indent=1) cl.Step(name="ツール実行", type="tool")

フォールバック検証の工夫(連載原典との違い)

原典は gpt-4(8K 窓)+ paper1〜3 を全部読ませれば自然に context_length_exceeded が踏めた。本リポジトリは gpt-4o-mini(128K 窓)で動かすので、paper1〜3 を全部読ませても窓に余裕がある。意図的に 5MB の英文ダミーファイル paper_huge.txt を生成してフォールバックを発火させる手順を README.md に追記 している(.gitignore 登録済み)。

起動方法

詳細は README.md を参照。サマリ:

cd 05
uv run --no-project \
  --with "openai>=1.0" \
  --with "langchain>=0.3,<1.0" \
  --with "langchain-openai>=0.2,<1.0" \
  --with "langchain-community>=0.3,<1.0" \
  --with "chainlit>=2.0" \
  chainlit run chatbot.py -w

フォールバック発火検証:

python -c "open('05/work/paper_huge.txt','w').write('Lorem ipsum dolor sit amet. ' * 200000)"
# → Chainlit で「work の paper_huge.txt を読んで要約して」と話しかける

記事参照

  • Software Design 2023年〜の連載 第05回

作成: 2026-05-19 / 最終更新: 2026-06-10