はじめに
書籍「LangChainとLangGraphによるRAG・AIエージェント[実践]入門」のChapter 12に収録されているエージェントシステムの設計パターンのサンプルコードを読み、動作確認しながら学習しました。
各パターンのコードを追う中で気づいたのは、「どんな設計パターンを採用するか」よりも「LLMの不確実性とどう向き合うか」という横断的なテーマが、品質に大きく影響するということです。
本記事では、コードリーディングと動作確認を通じて得た「LLMエージェント開発における品質担保の原則」をまとめます。
取り上げるデザインパターンの概要
本書籍のChapter 12では、LangGraphを使って以下のエージェント設計パターンが実装されています。それぞれのパターンは独立したモジュールとして実装されており、後述の原則はこれらのコードを横断的に読むことで気づいたものです。
| パターン | 概要 |
|---|---|
| Passive Goal Creator | ユーザーの曖昧な入力を受け取り、明確で実行可能な目標に変換する |
| Prompt Optimizer | 生成された目標をSMART原則(後述)に基づいてさらに精緻化する |
| Response Optimizer | 最終レポートの形式・構造・評価基準を動的に定義する |
| Single Path Plan Generation | 目標をタスクに分解し、順番に実行する基本パターン |
| Multi Path Plan Generation | 各タスクに複数のアプローチを用意し、実行時に最適なものを選ぶ |
| Self Reflection | タスク実行後に結果を自己評価し、不十分であれば再試行する |
| Cross Reflection | 計画と評価を異なるモデルが担うことで単一モデルのバイアスを防ぐ |
| Role Based Cooperation | 各タスクに専門的役割を割り当て、多様な視点から情報収集する |
これらのパターンはそれぞれ独立していますが、共通のパイプライン構造を持っています。
ユーザー入力
→ 目標設定(Passive Goal Creator + Prompt Optimizer)
→ レスポンス仕様定義(Response Optimizer)
→ タスク分解
→ タスク実行(Tavily検索 + ReActエージェント)
→ 結果集約・レポート生成
原則1: 信頼性は加算ではなく乗算で低下する
マルチステップのエージェントパイプラインでは、各ステップの信頼性が掛け算で積み上がります。
エンドツーエンドの成功率 = ステップ1の精度 × ステップ2の精度 × ... × ステップNの精度
各ステップが95%の精度を持つとしても、20ステップ連鎖すると:
0.95^20 ≈ 0.36(36%)
Agent Harness: A Complete Guide でも同様の指摘があります。個々のステップが高精度でも、ステップ数の増加に伴って全体の成功率は指数関数的に低下します。
実践への示唆: パイプラインを設計するとき、「ステップを増やすこと」はそれだけで品質リスクを上げます。ステップを増やすなら、各ステップの精度を上げる手段とセットで考える必要があります。
原則2: LLMのゆらぎは後半に集約する
パイプラインの前半ステップでゆらぎが生じると、そのゆらぎは後続の全ステップに伝播します。
Stage1のゆらぎ → Stage2で増幅 → Stage3でさらに増幅 → 最終出力が元の意図から遠い
たとえば今回のパイプラインでは、最初の Passive Goal Creator がユーザー入力から目標を生成し、次の Prompt Optimizer がその目標をSMART原則(Specific: 具体的、Measurable: 測定可能、Achievable: 達成可能、Relevant: 関連性が高い、Time-bound: 期限がある)に沿って精緻化します。さらに Response Optimizer がその精緻化された目標をもとに最終レポートの仕様を定義します。
この3段構成において、最初の目標生成がズレると、SMART化の段階でそのズレが整理・強化され、レスポンス仕様もそのズレた方向に最適化されます。元のユーザー意図からの距離が3段階で広がるわけです。
加えて、いずれのステージも前ステージの出力しか受け取らず、元のユーザー入力が引き継がれていない点も問題を悪化させます。
実践への示唆: 前半のステップほど決定的な処理で固める。ルールベース・テンプレートベース・ハードコーディングで実装できる部分は積極的にそうする。LLMの創造性や汎化能力が本当に必要な処理は後半に集約する。
原則3: まずハードコーディング、次に動的生成
Response Optimizer は、最終レポートの形式・構造・評価基準といった「レスポンス仕様」をLLMに動的生成させます。クエリの内容に応じてレスポンス仕様を変えることで、多様なクエリに対応できるのが狙いです。
しかし、「レスポンス仕様をハードコーディングしても良いのでは?」という疑問が生まれます。
この疑問は正しい場面が多いです。
| ハードコーディング | 動的生成 | |
|---|---|---|
| 出力の安定性 | 高い | 低い |
| デバッグのしやすさ | 高い(指示が見える) | 低い(指示自体が変わる) |
| 対応できるクエリの幅 | 狭い | 広い |
| LLM呼び出しコスト | 少ない | 多い |
ハードコーディングが適している場合:
- 対応するクエリのカテゴリが限定されている(旅行プランナー、料理レシピ支援など)
- レスポンスの形式が事前に決まっている(「材料・手順・調理時間を含むこと」など)
- 品質のばらつきを最小限にしたい
動的生成が適している場合:
- クエリの種類が広く、事前に全パターンを定義できない汎用ツール
- クエリによってレスポンスの最適な形式が大きく異なる
- 多少のばらつきより多様性を重視する場面
実践への示唆: まずハードコーディングで動かして品質を確認する。動的生成に切り替えるのは「汎化が必要になったとき」でよい。最初から動的生成にすると、問題が生じたときに「プロンプトが悪いのか、動的生成の出力が悪いのか」を切り分けられなくなります。
原則4: 出力構造はスキーマで強制する
LLMへの指示は、プロンプトの文言だけでなく出力スキーマの設計でも行えます。
たとえば Prompt Optimizer は、精緻化された目標を以下のPydanticモデルとして受け取るよう設計されています。
class OptimizedGoal(BaseModel):
description: str # 目標の説明
metrics: str # 目標の達成度を測定する方法
LangChainの with_structured_output(OptimizedGoal) を使うと、LLMはこのスキーマに合致した形式でしか回答できなくなります。
chain = prompt | self.llm.with_structured_output(OptimizedGoal)
プロンプトで「達成度の測定方法も記載してください」と指示するだけでは、LLMが省略する可能性があります。スキーマのフィールドとして定義することで、省略が構造的に不可能になります。
同じ発想が Multi Path Plan Generation の選択肢選択にも使われています。このパターンでは各タスクに2〜3個のアプローチが用意されており、LLMがその中から最適なものを選びます。このとき max_tokens=1 を設定することで、LLMは数字1文字(「1」「2」「3」のいずれか)しか返せなくなります。
chain = choice_prompt | self.llm.with_config(configurable=dict(max_tokens=1)) | StrOutputParser()
「番号のみで答えてください」とプロンプトで指示するより、物理的に1トークンしか出力できない状態にする方が確実です。
実践への示唆: 後続の処理が特定のフィールドに依存するなら、そのフィールドはスキーマで強制する。プロンプトでの指示と構造的な強制は補完関係にあります。
原則5: 制約は具体的に、複数箇所に記述する
エージェントに「インターネット検索とレポート生成しかできない」という行動制約を持たせたい場合、今回のサンプルコードでは次のように書かれています。
"2. あなたが実行可能な行動は以下の行動だけです。\n"
" - インターネットを利用して調査を行う。\n"
" - ユーザーのためのレポートを生成する。\n"
...
"6. REMEMBER: 決して2.以外の行動を取ってはいけません。"
制約をプロンプトの前半に書き、末尾でも REMEMBER: として再掲しています。長いプロンプトでは中盤の制約がモデルの注意から薄れる傾向があるため、末尾で繰り返す(Constraint Anchoring)のは意図的なテクニックです。
一方で、制約の「内容」が不十分な場合もあります。Multi Path Plan Generation の選択肢選択では次のようなプロンプトが使われています。
"与えられたタスクとオプションに基づいて、最適なオプションを選択してください。"
「最適」の基準が定義されていないため、LLMは暗黙の基準で判断します。タスクの種類やモデルの状態によって選択結果がばらつく原因になります。
# 改善案:基準を明示する
"以下の基準で最適なオプションを選択してください:\n"
"1. 全体目標との関連性が高い\n"
"2. インターネット調査で検証可能な情報が得られる\n"
"3. より客観的なデータが期待できる\n"
さらに、このプロンプトには全体目標(optimized_goal)が渡されておらず、タスク単体しか見えていません。「全体目標との関連性」を基準にしたくても、そもそも全体目標を知らない状態で選択させている設計上の問題もあります。
実践への示唆: 「何をしてはいけないか」だけでなく「何を基準に判断するか」も明示する。判断に必要な情報(今回なら全体目標)をプロンプトに渡すことも忘れずに。重要な制約は末尾でも繰り返す。
原則6: 元のクエリをパイプラインの最後まで引き継ぐ
多段パイプラインでは、各ステージが前ステージの出力のみを受け取ることが多いです。しかしこれにより、元のユーザー意図が途中で失われるリスクがあります。
# 元のユーザー入力がどこにも引き継がれていない例
PassiveGoalCreator(query) → Goal
PromptOptimizer(goal.text) → OptimizedGoal # ← 元のqueryを見ていない
ResponseOptimizer(optimized_goal.text) → str # ← 元のqueryを見ていない
各ステージは直前の出力だけを見て処理するため、前ステージのズレを修正する手段がありません。
改善策として、各ステージに original_query を渡す設計があります。
# 改善案
ResponseOptimizer(
optimized_goal=optimized_goal.text,
original_query=state.query # ← ドリフト抑制のため元クエリも渡す
)
実践への示唆: パイプラインが長くなるほど、ユーザーの元の意図を各ステージに明示的に渡す設計を意識する。
原則7: 複雑さと品質はトレードオフである
今回学んだパターンを複雑さの軸で並べると:
シンプル ←──────────────────────────────→ 複雑
Single Path → Multi Path → Self Reflection → Role Based → Cross Reflection
ゆらぎ: 小 ─────────────────────────────→ 大
品質の上限: 低 ──────────────────────────→ 高
制御の難度: 低 ────────────────────────→ 高
複雑なパターンを採用するほど、品質の上限は上がりますが、ゆらぎも大きくなります。「多様な答えを得たい」と「答えがブレてほしくない」は本来相反します。
どこに多様性を許容してどこを固定するかを設計段階で意識的に決めることが、エージェントシステム設計の核心です。
実践への示唆: 複雑なパターンを採用するなら、原則1〜6を意識的に組み合わせて、増えたゆらぎを別の手段で制御する。複雑さは目的ではなく、必要に迫られたときの手段です。
まとめ
| 原則 | 一言まとめ |
|---|---|
| 1. 信頼性の乗算 | ステップを増やすことはリスクを増やすこと |
| 2. ゆらぎの配置 | 前半を固め、後半にLLMの自由度を集約 |
| 3. ハードコード優先 | 動くものを先に作り、複雑さは必要になってから |
| 4. スキーマ強制 | 重要なフィールドはプロンプトでなくスキーマで守る |
| 5. 制約の具体化 | 「何をしてはいけないか」と「何を基準にするか」を明示 |
| 6. 原クエリの引き継ぎ | 元の意図をパイプライン全体で保持する |
| 7. 複雑さのトレードオフ | 複雑にするほど制御コストも上がることを意識する |
エージェント設計の難しさは、LLMの能力を最大限に引き出しながら、同時にその不確実性をコントロールすることにあります。これらの原則は、書籍のサンプルコードを丁寧に読み解くことで気づいた、実装に活かせる実践的な指針かと思い、記事にまとめました。