Agentic Workflow導入記:巨大プロンプトで起こる諸問題を「分割」と「構造化」で解決した話

こんにちは、テックドクターの佐藤です。

今回は、アプリ開発において Agentic Workflow(自律的なエージェントワークフロー)を組み込んだ際に直面した問題と、解決方法について書きます。

その問題とは「巨大プロンプトの限界」。それを解決に導いたのは、マルチエージェントアーキテクチャ化、そしてPydanticAIを用いた型安全な連携でした。

ヘルスケアアプリ「ポポラス」にAgentic Workflowを実装

昨年11月、「ポポラス」というスマートフォンアプリをリリースしました。

populus-app.com

ポポラスは、ユーザーのHealthKitやFitbitのデータに基づき、健康に関するパーソナライズされた受け答えを提供するヘルスケアエージェントアプリです。ユーザーからの「昨日の活動量はどうだった?」といった質問に対し、実際の歩数や睡眠データを参照しながら、自然で適切なアドバイスを行うことを目的としています。

ポポラスはユーザーの質問に対する回答生成に、単一のLLMではなく、複数の専門エージェントが協調して動くAgentic Workflowを採用しています。

AgenticWorkflowとは、AIやプログラムが「自分で考えて動く」ようにする仕組み、あるいはそのための考え方のことです。AIがある目的を達成するために、都度人間の指示を受けるのではなく、自分で手順を考えたり、必要な作業を順番に進めたりできるようにします。

詳しくはこちらのエントリで解説しているので読んでみてください。
techblog.technology-doctor.com

最初のアプローチ:万能な「神エージェント」

開発当初、私たちは全てのタスク(データ取得、分析、応答生成)を一つの大きな「メインAgent」に担わせる設計を採用しました。

当初の実装構成

アーキテクチャ……ユーザー入力と履歴を、一つの巨大なプロンプトを持つエージェントに直接渡す。

ツール群……HealthKitDataFetcherFitbitDataProcessorHealthAnalyzerGetNowなど、あらゆるツールを一度に渡していました。

プロンプト……ひとつのコンテキスト内に全ての機能とガードレール(AIの挙動が意図した範囲から外れないような制御)を詰め込んでいました。

構成図
構成イメージ

最初のアプローチで問題だった点

その結果、この「ひとつの巨大なAgent + 多数のツール」という構成では、期待通りの回答を得られませんでした。具体的には以下の問題が発生しました。

  • 推論の不安定化と無限ループ
    普通の質問をしただけでも、エージェントがツール利用の判断を誤り、データ取得処理のループに入ってしまう現象("An unexpected error occurred")が頻発しました。
  • トークン消費とコストの増大
    本来不要なツールまで呼び出してしまったり、プロンプト自体が巨大であるため入力トークン数が肥大化したりして、APIコストを圧迫しました。
  • コンテキスト理解の失敗(エッジケース)
    「昨日の歩数は?」と質問した後に「じゃあ一昨日は?」と追撃した場合などに、文脈を正しく引き継げず、適切な日付のデータを取得できないケースが多発しました。
  • デバッグの困難さ
    処理がブラックボックス化していたため、意図しない挙動(ハルシネーションなど)が起きた際、原因がプロンプトにあるのかツールの選択ミスにあるのかを特定するのが極めて困難でした。

結論として、「巨大なプロンプトと多数のツールを機械に丸投げして最適な答えを出させる」というやり方ではうまくいかないことが判明しました。

改善したアプローチ:役割分担と構造化

これらの問題を解決するために、「タスクの細分化(Task Decomposition)」と「Pydanticによる入出力の構造化」を行う設計へ移行しました。

協調型Agentic Workflowへの変更

全体の構成図を見てください。単一のAgentではなく、役割を持った専門のAgent群が連携するワークフローに変更しました。

構成図
改善された構成

異なる役割を持った4つのAgentを定義しました。

Agent 役割
RouterAgent ユーザーの意図を汲み取り、適切なエージェントへ振り分ける司令塔。
FetchAgent / AnalysisAgent データ取得と分析に特化した実働部隊。
EmpathyAgent ユーザーに寄り添う会話担当。
SorryAgent 「株価を教えて」や「医療診断して」など、アプリのスコープ外または危険な質問に対して、適切に断りを入れる担当。

これらが連携してユーザーへの応答を生成します。

PydanticAIにより入出力を縛る

Agent間の連携で最も重要なのが、次のAgentが作業しやすいように情報を整形して渡すことでした。
PydanticAI(あるいはPydanticそのもの)を活用し、モデルのフィールドにdescriptionを詳しく書くことで、LLMに対して「この項目には何を入れるべきか」を強く示唆できます。

一例として、司令塔でありユーザーの質問を直接受け取る立場であるRouterAgentの出力定義はこのようにしました。

RoterAgentの出力定義

from pydantic import BaseModel, Field
from typing import Literal

class RouterDecision(BaseModel):
    """
    ユーザーの意図を分析し、次のアクションを決定するためのモデル
    """
    target_agent: Literal["FetchAgent", "EmpathyAgent", "SorryAgent"] = Field(
        description="ユーザーの要求を満たすのに最適なエージェントを選択する。"
        "健康データが必要ならFetchAgent、日常会話ならEmpathyAgent、"
        "スコープ外(株価や医療診断など)ならSorryAgentを選択。"
    )
    
    context_query: str = Field(
        description="次のエージェントに渡すための、具体的かつ自己完結した指示文。"
        "例:ユーザーが『じゃあ一昨日は?』と言った場合、ここには"
        "『2025年10月13日の歩数データを取得してください』のように"
        "日付と目的を補完して正規化したテキストを出力すること。"
    )
    
    reasoning: str = Field(
        description="なぜそのエージェントを選択したかの理由。デバッグ用。"
    )


実際のLLM出力例
※文脈:昨日の歩数の話の続き

# User: 「じゃあ一昨日は?」
# Result:
# {
#   "target_agent": "FetchAgent",
#   "context_query": "2025-10-13の歩数データを取得",
#   "reasoning": "直前の会話履歴から歩数の話題と判断し、日付を一昨日に特定したため"
# }

このように、Field(description=...) に「日付を補完して正規化せよ」といった具体的な指示を埋め込むことで、プロンプトの一部として機能させます。

これにより、後続のFetchAgentには曖昧な「一昨日」という言葉ではなく「2025-10-13」といった確定情報が渡されることになります。FetchAgentはこれを使ってAPIを叩くだけで済みます。

マルチエージェント化によるメリット

マルチエージェント化したことにより、多くのメリットが得られました。

Agentに特化したプロンプトが書けるようになった

マルチエージェント構成では、「そのエージェントに関係ない禁止事項」を書く必要がなくなります。

例えば、

  • RouterAgent には「ツールを直接呼び出してはいけない」「最終回答を生成してはいけない」という制約だけを書けばよい
  • FetchAgent には「ユーザーに話しかけてはいけない」「指定されたツールだけを呼ぶ」「推論をしない(※API経由で正確なデータを取得させるため)」という制約だけを書けばよい
  • SummarizeAgent には「取得済みデータを要約することだけに集中する」「新しいデータ取得はしない」という制約だけを書けばよい

というように、「その役割の逸脱を防ぐためのガードレール」だけを、最小限かつ強い言葉で書けるようになります。

結果として、

  • プロンプト1つあたりの制約の数が激減
  • 各制約の意味が曖昧にならない
  • モデルが「何をしてはいけないか」をほぼ確実に守る

という状態を作ることができました。

出力が安定した

もう一つの非常に大きなメリットは、出力フォーマットが安定したことです。

巨大プロンプトで全タスクをやらせていた頃は、

  • たまにJSONが壊れる
  • たまに説明文が混ざる
  • たまにキーが変わる
  • たまに謎の自然文が出る

といった「機械処理するには致命的だが、人間が見ると一見それっぽい」出力が頻発していました。

マルチエージェント化すると、

  • RouterAgent……{ intent: "...", normalized_text: "..." } しか返さない
  • FetchAgent……{ tool_results: [...] } しか返さない
  • SummarizeAgent……{ summary: "...", insight: "..." } しか返さない

というように、各エージェントに「一種類のJSON構造しか返させない」設計にできます。

これにより、

  • パースエラーがほぼゼロになる
  • 後段の処理が例外処理だらけにならない
  • ログの可読性とデバッグ効率が爆上がりする

という、「正しい状態」に一気に寄せられました。

デバッグ可能性が劇的に向上した

マルチエージェント構成にして一番「これは正解だった」と感じたのは、デバッグのしやすさです。

巨大プロンプト時代は、なにかを失敗した際に「どこで?」「なぜ?」「どの指示を誤解した?」「モデルが悪い?プロンプトが悪い?設計が悪い?」が完全にブラックボックスでした。

マルチエージェント化すると,

  • RouterAgent のログを見る → 意図分類が間違っていることがわかる
  • FetchAgent のログを見る → ツール引数の組み立てが間違っていることがわかる
  • SummarizeAgent のログを見る → 要約ロジックが微妙であることがわかる

というように、壊れている責務の場所が一瞬で特定できるようになります。

最初のアプローチとの比較

最初の「巨大プロンプト方式」との違いを一覧表にまとめました。

項目 初期設計(失敗) 改善後の設計(成功)
Agent構成 1つの巨大なAgent 複数Agentが連携
(Router, Fetch, Analysis, Empathy, Sorry)
プロンプト 全機能を網羅した巨大プロンプト タスクごとに特化した
小規模プロンプト
データ連携 自然言語(曖昧) Pydanticモデル
(型定義+Descriptionによる指示)
エラー対応 プロンプト内で禁止事項を羅列 SorryAgentへ遷移させて分離
安定性 低い(ループ、ハルシネーション多発) 高い(責任分界点が明確)
デバッグ容易性 低い(原因が分からない) 高い(原因が明確化した)
ガードレール 巨大なプロンプトに内包 最後の出力プロンプトの
Agentのみに適応

まとめ

ヘルスケアアプリのような複雑な要件をAgentic Workflowで実現するためには、「何でもできる神エージェント」を作ろうとせず、「専門家のチーム」を作ることが成功の鍵でした。

特に、PydanticモデルのDescriptionを活用して文脈(Context)を構造化データに変換するテクニックは、LLMの推論精度を劇的に向上させます。また、SorryAgentのような「断り担当」を設けることで、他のエージェントが余計なノイズに惑わされず、本来の性能を発揮できるようになりました。

今後は、この構造を活かしつつ、RAG Agent なども追加して「ポポラスって名前の由来は?」などより幅広い質問に答えられるようにしていく予定です。

また、今後のエントリではLLMのデバッグを劇的に楽にしたLangFuseの導入などにも触れたいと思います。


似顔絵
書いた人:佐藤