第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_toolsとllm(素)の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_chain と fallback_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から生やすことで、プロンプトと出力パーサ以外の構造を揃えられるBadRequestErrorのmessage属性: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_exceededvsinvalid_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.invoke → fallback_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) |
_SimpleHistory(HumanMessage/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