CodeGym /コース /ChatGPT Apps /多段プロセス: モデルによる自動オーケストレーションとループ制御

多段プロセス: モデルによる自動オーケストレーションとループ制御

ChatGPT Apps
レベル 12 , レッスン 3
使用可能

1. 多段の run とは何か、そして「単発」のリクエストとどう違うか

ChatGPT App と MCP のツールだけで作業していたときは、全体像はかなり直線的でした。ユーザーのリクエストが来る → GPT が 1 つ以上のツールを呼び出すと判断する → あなたがユーザーに回答を返す。ツール内部で多少複雑なことをしていても、これは依然として「1 つの論理ステップ」と見なせます。

エージェントにおける run は、目的 + 一連のステップです。もはや「1 プロンプト=1 回答」ではなく、タスクをエージェントが最初から最後まで進めるミニプロジェクトとして捉えます。

違いは次のように捉えられます。

インタラクションの種類 モデルが行うこと ロジックの所在
ChatGPT App における通常のツール呼び出し ツールを呼び出すかどうかを判断し、引数を組み立て、結果にもとづいて回答を生成する 主要なビジネスロジックと手順は 1 つのツール内または backend に埋め込まれている
エージェントの run(Agents SDK) 複数のステップを計画し、いつどの tool を呼ぶかを決め、中間結果を分析し、必要に応じて計画を見直す 「目標へどう進むか」のロジックは一部がエージェントの system 指示に記述され、一部はモデルの裁量で生成される

ここが重要です。計画のすべてをモデルに丸投げする必要はありません。一般的にはハイブリッドになります。たとえばシナリオの大枠のフェーズ(「まず要件収集、次にギフト候補選定、その後カード作成」といった具合)は厳密にコード化し、その内側ではエージェントにツールの使い方を比較的自由に任せます。

ミニたとえ

単発のツール呼び出しは、宅配便を呼ぶのに近いイメージです。「書類を 1 通受け取ってオフィスに届けて」。

多段のエージェント run は、パーソナルアシスタントのようなものです。「同僚の誕生日用にプレゼントを用意して。本人の好みを聞き、いくつかの候補を選び、配送を確認し、見栄えの良いプレゼンにまとめて」。アシスタントは道中で何をするか自分で判断します。

この後の講義では、こうした多段の run が Apps SDK → MCP → backend というおなじみのスタックにどのように組み込まれるかも見ていきます。ChatGPT やウィジェットからは、エージェントのロジックが 1 つのきれいな MCP ツールとして見えるようにします。

2. モデルが自律的にステップを計画する仕組み:俯瞰

Agents SDK の用語で言うと、各 run は次の 3 要素として考えると分かりやすいです。

  1. Goal(目的): モデルの system/user 指示に入るタスクのテキスト記述。
  2. Tools: 適切な説明と JSON Schema を備えた利用可能なツール群。
  3. State: ステップの履歴と、外部(DB、Redis など)に保持する構造化状態。

そのうえでおなじみの run サイクルが回り始めます。モデルは目的と利用可能なツールを見て、各ステップで次を判断します。

  • 「今の情報で十分。ユーザーに最終結果を返せる」;
  • または「ツール X をこの引数で呼び出す必要がある」;
  • または「ツールの結果を受け取った。これを解釈・フィルタし、別のツールを呼ぶ必要があるかもしれない」。

擬似コードで表すと次のイメージです(あくまでメンタルモデルであり、実 API ではありません)。

while (!done && steps < MAX_STEPS) {
  const modelResponse = await callModel({
    system: agentPolicy,
    messages: history,
    tools,
  });

  if (modelResponse.type === "tool_call") {
    const toolResult = await callTool(modelResponse.toolName, modelResponse.args);
    history.push({ role: "tool", content: toolResult });
  } else {
    // 最終結果
    done = true;
    return modelResponse.content;
  }

  steps++;
}

実際の Agents SDK では、このループはライブラリ内部に実装・隠蔽されています。あなたはエージェントを宣言的に記述し、SDK が最終回答を得るかステップ数/時間の上限に達するまで、モデルとツールの呼び出しを回してくれます。

アーキテクトの役割は次の通りです。

  • モデルが合理的なステップを計画できるよう、goal と system 指示を定義する;
  • 意味が競合しないようにツール群を設計する;
  • ステップ数と時間の制約を設定する;
  • どのステップを並列化できるかを考える。

目的・ツール・状態の見立てができたら、次の問いは 「どんなステップで目標に向かうか」 です。すべてのステップが同じではありません。厳密に直列なものもあれば、並列化できるものもあります。

3. 直列ステップと並列ステップ

エージェントの run サイクルの基本が分かったところで、プロセス内部のステップの種類を整理しましょう。エージェントの workflow には大きく 2 種類あります。直列ステップと並列ステップです。

直列ステップ

ステップ A の結果がステップ B に必須な場合です。たとえば学習用の GiftGenius では次のようになります。

  1. まずプレゼントの受取人(同僚か親族か、年齢、興味)を把握する。
  2. 次に tool search_gifts を使って候補を取得する。
  3. その後、予算や制約に合わせてフィルタする。
  4. 続いてウィジェット用にカードをきれいに整形する。
  5. 最後に、必要に応じてチェックアウトへ誘導する。

各ステップが前段のデータに依存するため、実行は厳密に直列になります。

擬似コードとしての「モデルの内的計画」は次のように見えます。

1. ユーザーに受取人と予算をヒアリングする
2. tool search_gifts(profile, budget) を呼び出す
3. tool filter_by_constraints(gifts, constraints) を呼び出す
4. 最終リストと説明を生成する

モデルがこんなリストをコードとして書くわけではありませんが、system 指示、対話例、ツール記述を通じて、このような構造に誘導することはできます。

並列ステップ

ステップを独立に実行できる場合があります。たとえば、3 つのストアのギフト候補を同時に比較したいケースです。

  • search_gifts_amazon
  • search_gifts_etsy
  • search_gifts_local_store

エージェント視点では、これは 3 つの独立したツール呼び出しであり、全体の応答時間を短縮するために並列実行できます。

Agents SDK(および近年のエージェントフレームワーク)には、モデルが 1 ステップで複数の呼び出しを提案した場合の並列実行をサポートする仕組みが備わっていることが多いです。典型的には、モデルが応答内で呼び出しのリストを記述し、SDK がそれらを並行実行して結果を収集し、次のモデルステップに tool メッセージの集合として渡します。

計画の観点では次のように見えます。

// エージェントのステップ: モデルが 3 つのツールを呼び出すと判断した
const calls = [
  { name: "search_gifts_amazon", args: {...} },
  { name: "search_gifts_etsy", args: {...} },
  { name: "search_gifts_local_store", args: {...} },
];

const results = await Promise.all(
  calls.map(c => callTool(c.name, c.args))
);

// 以降、すべての結果が次のモデルステップの前にコンテキストへ追加される

JS/TS でフロントエンドを書いたことがあれば、すでに並列リクエストの考え方に触れているはずです。たとえば Promise.all で複数の fetch() を同時に走らせる場合などです。同じ発想がエージェントの run サイクル内部にも現れますが、ここでは 何を並列化できるか の判断を大きくモデル自身が行います。

4. GiftGenius の workflow 例: ステップ、目的、ツール

直列ステップの章で、GiftGenius の振る舞いを段階に分けて直感的に説明しました。ここではこの多段シナリオをエージェントの workflow としてきちんと形式化します。目的、ステップ、ツールへのひも付け、エージェント設定への落とし込みまでを(特定の Agents SDK API には縛られず)少しの TypeScript 風コードで確認します。

目的(goal)

目的は次のように定義します。

特定の受取人向けに、予算・機会・配送制約を考慮して 3〜5 件のギフト候補を選び、GiftGenius ウィジェット用のギフトカードの構造化リストを返す。

主なステップ

最小構成の 4 ステップを記述します。

  1. 受取人コンテキストの確認
    目的: プレゼントの受取人(年齢・性別・興味・贈り手との関係)、予算、イベント日程を集める。
    ツール: ツールなしでも可。モデル ↔ ユーザーの対話のみ。
  2. ギフトの検索と一次選定
    目的: 「生の」候補集合を得る。
    ツール: search_gifts(profile, budget) — カタログ/検索システムを叩いて候補のリストを返すツール。
  3. フィルタリングとソート
    目的: 不適切な候補(配達不可、予算超過、その他制約に不一致)を除外し、関連度で並べる。
    ツール: filter_and_score_gifts(candidates, constraints) — ピュアで冪等なツール。
  4. ウィジェット向け整形
    目的: UI に都合のよい形式(タイトル、短い説明、画像、価格、CTA)に整える。
    ツール: format_gift_cards(gifts) — 構造生成のコード系ツールにも、テキストを整える LLM 系ツールにもできる。

エージェント設定ではどう書けるか

仮にエージェントのビルダーがあるとします(擬似コード)。

import { createAgent } from "@acme/agents-sdk";
import { tools } from "./gift-tools";

export const giftAgent = createAgent({
  name: "gift-guru",
  system: `
    あなたは GiftGenius のエージェントで、ギフト選びを支援します。
    目的: 実際に購入可能な候補を 3〜5 件提案すること。
    受取人のプロファイル、予算、配送制約を考慮してください。
    まず重要な詳細を確認し、その後に検索・フィルタのツールを使いなさい。
    予算や主要な興味がまだ分からない場合はツールを呼び出さないでください。
    ギフトカードの明確なリストが揃ったら作業を終了しなさい。
  `,
  tools, // ここに search_gifts, filter_and_score_gifts, format_gift_cards を登録
  maxSteps: 12,
  timeoutMs: 15000,
});

次の点に注意してください。

  • system 指示で、エージェントは最初に詳細を確認してから検索ツールを使うべきだと明示しています。これにより、文脈が曖昧なままツールを乱用するリスクを減らします。
  • maxSteps を制限して、エージェントが無限ループに陥らないようにします。
  • timeoutMs は、run 全体がユーザーの時間を奪い過ぎないために必要です。

5. モデルによる自動オーケストレーション: どこまで任せ、どこを固定するか

エージェントとは、モデルの自由度と、あなたが設計する厳密な構造のバランスです。

モデルに自由を与えすぎて境界を設けないと、「創造的なカオス」が生じます。不要なツール呼び出し、繰り返しのステップ、分かりにくいループなどです。逆にすべてを backend に厳密な状態機械としてハードコードすると、モデルは賢いタスク実行者ではなく、テキストの飾り付け役に落ちてしまいます。

通常、モデルに任せる部分

GiftGenius のようなシナリオでは、次をモデルに任せるのが妥当です。

  • ユーザーへの質問文の作り方(興味の聞き出し方、予算の聞き方など);
  • いつ情報が十分になったと判断して検索を始めるか;
  • フェーズ内でどのツールを使うかの選択(複数の検索ツールがある場合、どれを使うかなど);
  • 説明・比較・サマリなどのテキスト生成。

事前に固定しておくべき部分

一方で、次はあらかじめ固定することをおすすめします。

  • シナリオの大きなフェーズ(「情報収集」→「検索」→「フィルタ」→「整形」→「終了」);
  • ステップ数と時間の上限;
  • タスクが不可能だと正直に告げる条件(例: 予算が 5 ドルで、明日配送の高額ガジェットが必要など);
  • ツールの冪等性ポリシーとリトライ戦略。

ハイブリッド例: フェーズは state として管理、細部はモデルに委任

エージェントの state に phase というフィールドを設け、"collect_profile" | "search" | "filter" | "format" | "done" の値を取るようにします。すると backend(あるいはカスタム state machine をサポートする Agents SDK 自体)が、フェーズごとに利用可能なツールを制御できます。

擬似コード:

type Phase = "collect_profile" | "search" | "filter" | "format" | "done";

interface GiftAgentState {
  phase: Phase;
  profile?: UserProfile;
  candidates?: GiftCandidate[];
  finalGifts?: GiftCard[];
}

エージェントの system 指示にフェーズの簡単な説明を含め、コード側では現在のフェーズに応じてモデルに見せるツールのリストを制限します。これは tool gating の一例で、workflow モジュールでより詳しく扱います。

6. 無限ループと無意味なリトライの抑制

エージェントの run サイクルを無制御にすると、締切前の学生のように「無限に確認と書き直し」を続ける振る舞いが出ます。ハングさせないことが重要です。

無限ループの典型的な原因は 3 つあります。

  1. モデルが自信を持てず、ほぼ同じ引数でツール呼び出しを言い換え続ける。
  2. ツールが安定してエラーや空を返し、エージェントが「もう一度試す」を繰り返す。
  3. 2 つのツールの間で行ったり来たりして、最終回答に向けて前進しない。

ステップ数の上限(maxSteps)

最も単純かつ必須の仕組みは、ステップ数の制限です。多くの Agents SDK 実装では、maxStepsrun 開始時やエージェント設定で指定できます。上限に達したら、SDK は特別なステータス(例: aborted_by_max_steps)で run を終了します。その後のユーザー提示はあなたが決めます。

GiftGenius では、まともなギフト選定は約 10 ステップ(2 回程度の確認、2 回程度の検索、フィルタ、整形)に収まると考えられます。余裕を見て 12〜15 ステップを設定し、上限到達時を丁寧に処理します。

const run = await giftAgent.run({
  input: userGoal,
  maxSteps: 12, // デフォルトを上書き
});

if (run.status === "max_steps_exceeded") {
  // ユーザーに正直なメッセージを表示する
}

時間の上限(timeout)

問題がステップ数ではなく総所要時間にあることもあります。ツールが遅かったり、ネットワークが不安定だったりするためです。そのため、各ツール呼び出しレベルと run 全体に対して timeoutMs を指定するとよいでしょう。

たとえば次のように決められます。

  • 外部 API の各呼び出し(パートナーへのギフト検索)は 3〜5 秒を超えない。
  • ギフト選定の run 全体は 15 秒以内に収める。

タイムアウトが発生したら、run を丁寧に終了し、部分的な結果と「一部のソースが時間内に応答しなかった」旨を正直に示すとよいでしょう。

重複呼び出しの検出

より高度ですが有用なのは、同じ引数でのツール呼び出しの繰り返しを検出するパターンです。もしエージェントが search_gifts(profile, budget) を同じパラメータで 3 回連続呼んでいるなら、行き詰まっているシグナルです。

state に (toolName, argsHash) をキーにした呼び出し回数のカウンタを設け、閾値を超えたら次のいずれかを行います。

  • run を中断し、ユーザーに分かりやすいエラーを返す;
  • またはモデルに追加指示を与える。「同じパラメータでこのツールを 3 回試した。戦略を変えるか、ユーザーに尋ねて」など。

擬似コード:

function shouldAbortToolCall(toolName: string, args: unknown, state: GiftAgentState) {
  const key = `${toolName}:${hashArgs(args)}`;
  const count = state.toolCallCounts[key] ?? 0;

  if (count >= 3) return true;

  state.toolCallCounts[key] = count + 1;
  return false;
}

ここで hashArgs は引数の決定的なシリアライズ関数(例: キーソート付きの JSON.stringify)です。

7. 明確な終了基準

「おもちゃ」エージェントと本番エージェントを分ける鍵の 1 つは、明確な終了基準があることです。これがないと、モデルは早すぎる終了(「とりあえず候補、あとはどうぞ」)か、逆に結果を無限に磨き続けます。

GiftGenius ではシンプルなルールを定められます。

  • 候補が 3〜5 件あり、各候補に idtitleshortDescriptionpriceimageUrlpurchaseUrl が埋まっており、予算と配送のフィルタを通過していれば、エージェントは終了する。
  • 検索とフィルタの試行が最大 N 回に達しても 3 件未満なら、見つからなかった旨を正直に伝え、予算の拡大や制約の緩和を提案する。

これらの基準はエージェントの system 指示に書いてもよいし、run 後の結果検証で実装してもよいでしょう。

run 後の結果検証の例:

if (run.status === "completed") {
  const gifts = run.output.gifts; // たとえばエージェントが構造化 JSON を返すとする

  if (!gifts || gifts.length < 3) {
    // エージェントは「完了」したが、結果が弱い — 次のいずれかを行う:
    // 1) 正直な説明を表示する
    // 2) 条件の変更をユーザーに提案する
  } else {
    // OK — ギフトのウィジェットを表示する
  }
}

モデルにビジネス上の成功を魔法のように理解してもらうことは期待しないでください。開発者であるあなたが「満足できる」条件を明示し、それを検証する必要があります。

8. オーケストレーションはどこで実装するか:エージェント、backend、ウィジェット

以前から述べているように、オーケストレーションは複数レイヤー(エージェント、backend、ウィジェット)のいずれにも住めます。

多段プロセスの観点では、役割はおおむね次の通りです。

エージェント(Agents SDK)は「思考的な」workflow を担います。

  • 目標をどのステップに分解するか;
  • どのツールをどの順番で呼ぶか;
  • ユーザーにどんな追加質問をするか。

Backendは通常、次を担います。

  • ツールの実装(検索、フィルタ、コマース等);
  • 状態とチェックポイントの保存;
  • 厳格なビジネス制約(予算上限、権限、地域の可用性)。

ウィジェット(Apps SDK)は次を管理します。

  • 進捗の表示(ステッパー、プログレスバー、「4 中 2 ステップ目」など);
  • 入力フォーム;
  • UI の細部(必要データが揃うまでボタンを disable する等)。

よい作法は次のように考えることです。エージェントはツールと対話の演出家、UI ウィジェットはユーザー体験の演出家。両者は構造化データ(ToolOutput、agent run output)で手を取り合います。

9. ミニコード例:MCP ツールから多段エージェント GiftGenius の run を起動

冒頭で述べたとおり、新しい概念を Apps SDK → MCP → backend のおなじみのスタックに結び付け、MCP ツールがエージェントの run を呼び出す小例を示します。

app/mcp/route.tsrun_gift_workflow というツールがあるとします。このツールは次を行います。

  • ユーザーのテキストリクエスト(目標)を受け取る;
  • エージェント giftAgent を起動する;
  • ウィジェット用の構造化結果を返す。

コードは簡略化・擬似的ですが、結び付けのイメージはつかめます。

// app/mcp/route.ts
import { server } from "@modelcontextprotocol/sdk/server";
import { z } from "zod";
import { giftAgent } from "@/agents/giftAgent";

server.registerTool(
  "run_gift_workflow",
  {
    title: "ギフトを選ぶ",
    description: "多段のギフト選定エージェントを起動します",
    inputSchema: {
      userGoal: z
        .string()
        .describe("ユーザーのタスク。例: 同僚向けに $50 以下のギフトが欲しい"),
    },
  },
  async ({ userGoal }) => { 		
    const run = await giftAgent.run({		// ここで 12 ステップ・15 秒タイムアウトでエージェントを起動
      input: userGoal,
      maxSteps: 12,
      timeoutMs: 15000,
    });

    return {
      status: run.status,
      gifts: run.output?.gifts ?? [],
      debug: run.debugInfo, // 後で消してよい
    };
  }
);

以降、ChatGPT App はこの MCP ツールを他のツール同様に呼び出せますし、あなたのウィジェット GiftGeniusgifts を基に UI を構築できます。外側からは 1 つのきれいなツールに見えつつ、内部では多段の workflow が走っています。

10. 多段プロセス設計でよくあるミス

エラー 1: 「モデルに任せれば勝手にやってくれる。全部のツールを渡しておこう」。
意味が重複したツールを十数個、明確な system 指示やフェーズなしで渡すと、モデルは右往左往します。同じことを別の手段で呼び、リクエストを重複させ、ループに入ります。時間をかけて設計しましょう。シナリオをフェーズに分け、各フェーズでのツールを制限し、戦略を system プロンプトに明示します。

エラー 2: ステップ数と時間上限の欠如。
maxStepstimeout を設定しないと、本番では資源を食い続ける「迷子の」run がすぐに発生し、ユーザーには何も返りません。上限は「オプション」ではなく衛生要件です。上限超過時を無言の 500 で落とすのではなく、意味のある扱いを設計しましょう。

エラー 3: 終了基準の不在。
モデルは「もう十分」と感じた時点で run を終えますが、その「十分」はビジネス要件とズレがちです。成功基準(候補数、必須フィールド、通過すべきフィルタ)を形式化し、検証しないと UX は不安定になります。今日は素晴らしい 5 件、明日は「微妙」1 件と重複 3 件、といった具合に。

エラー 4: 重複ツール呼び出しの未監視。
エージェントは「エラーを受けた → 2 語だけ言い換えて → 同じツールを再呼び出し」というパターンにはまりがちです。(toolName, args) で重複を監視していないと、ログを見るまでループが見えません。簡単なカウンタと引数のハッシュが非常に役立ちます。

エラー 5: オーケストレーションとビジネスロジック実装の 1 ツール内での混在。
時に、丸ごとの workflow を 1 つの MCP ツールやエージェント関数に押し込めようとします(検索、フィルタ、整形、意思決定まで)。その結果、エージェントの意味は失われます。モデルは段階的にプロセスを制御できず、透明性や再利用性も失います。各段階を独立ツールに切り出し、エージェントに構成させる方がよいです。

エラー 6: 状態とチェックポイントの欠如。
状態やチェックポイントを保存しない多段プロセスは壊れやすいモノリスになります。途中で落ちたらユーザーは最初からやり直し。特に、ユーザーがステップ間を行き来したり、時間をおいて戻るシナリオでは致命的です。state ストアを使い、フェーズ・プロファイル・候補を保存し、所定の地点から再開できるようにしましょう。

エラー 7: UX レイヤーの軽視。
内部の workflow に夢中になるあまり、ユーザーが見るのはウィジェットとチャットメッセージだけであることを忘れがちです。UI に進捗(「ギフト検索中…」「候補をフィルタ中…」)がなければ、エージェントが複雑なプロセスを回していても、ユーザーには「アプリが固まった」「何もしていない」と映ります。多段の run を計画する際は、UI でどう表すかも同時に考えましょう。

コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION