1. エージェントとは何か、なぜ必要か
エージェントは ChatGPT App に必須の要素ではありません。LLMエージェントを使わずとも、優れたアプリをいくらでも作れます。それでも、ここでエージェントを紹介するには大きく3つの理由があります。
エージェントは、アプリのバックエンドに知能を加える優れた手段です。たとえば、賢いギフト提案や、ユーザーの希望テキストの解釈。検索・分析・加工・要約などの複雑なシナリオを、LLMエージェントなら容易に実装できます。
ChatGPT は AgentsSDK を TS と Python で提供しています。完成度が高く、エージェントのオーケストレーションが箱から使えます。難しい課題を1つのエージェントではなくチームで解かせることも可能で、とても将来性があります。
そして学習上の狙いもあります。ChatGPT は mcp-tools を、LLMエージェントが自分の tools を呼ぶのと同じ方式で呼び出します。LLMエージェントの仕組みを理解すると、モデル側に状態機械を実装するなどアプリ設計の理解が進みます。また、AgentsSDK を学ぶことで、将来の ChatGPT SDK の動作イメージも掴めます。
それでは始めましょう。
LLMエージェントとは
ChatGPT App が ChatGPT 内であなたのサービスの美しい「フロント」だとすると、MCP サーバーはツールとビジネスロジックを備えた「エンジン」。ではエージェントは何かというと、次のことができる賢いディスパッチャのような存在です。
- 目的を読み取る;
- どのツールをどの順序で呼ぶか自律的に決める;
- 必要に応じて追加データを問い合わせる;
- 失敗時に手順をやり直す;
- 分かりやすい最終結果に到達する。
Agents SDK の公式ドキュメントに近い表現では、エージェントは「プログラム」です。LLM とツール群にアクセスしながら、目標達成のための手順を自律的に計画し、tool‑call を通じてそれらを実行します。
すでにある仕組みと対比すると次のようになります。
- 通常の ChatGPT App では、ChatGPT のモデルが MCP ツールを直接オーケストレーションします。
- バックエンドの LLMエージェントも、課題の説明とツール群を持ち、どの tools を呼ぶか、何ステップ進めるか、いつ止めるか、どんな結果を返すかを自分で決めます。
本講義の GiftGenius 文脈では次のように考えられます。
- エージェントなしのアプリ: モデルが searchGifts、次に filterByBudget、次に getDetails を、その都度考えながら直接呼ぶ。
- エージェントありのアプリ: ChatGPT は mcp‑tool を呼び、バックエンドはエージェントに「このプロフィール向けのトップ5ギフトを見つけよ」といったタスクを与える。 エージェントは複数のステップを踏み、追加情報を集め、複数の検索ツールを呼び、フィルタ・ソートし、最終カードを構築して、構造化された完成品を返す。
ChatGPT とバックエンドの LLMエージェントの関係は、会社の社長と社員のようなものです。ChatGPT の方が自由度は高く、ユーザーと対話し、どの戦略的タスク(mcp tools の呼び出し)を走らせるかを決めます。LLMエージェントはバックエンド専用でユーザーとは直接やり取りしませんが、「考える」力を持ち、自身の tool を呼べます。いわば「小さな ChatGPT」です。
2. エージェントの構成要素:LLM、指示、ツール、状態
エージェントは複数レイヤーから成ると考えると分かりやすいです。
第一に、内部ではやはり LLM が動いています。Agents SDK が使う GPT‑5.1 などのモデルです。テキスト生成、手順の計画、ツール選択といった「思考」を、あなたのオーケストレーションの文脈で行います。
第二に、モデルの上に指示(インストラクション)があります。これはエージェントの system プロンプトで、役割・境界・文体・ツールの使い方を定義します。ChatGPT App でも似たことをしましたが、今回は個別のエージェントに適用されます。
第三に、エージェントのツール群があります。例えば次のようなものです。
- TypeScript の関数(典型的な function calling);
- HTTP/REST ツール;
- あなたの MCP‑tools へのラッパー(ChatGPT App と同じバックエンドに到達できるようにする);
- OpenAI の組み込み「hosted」ツール(例:web‑search。利用を有効にしている場合)。
最後に、状態とステップの扱い規則があります。セッション状態(session state)をどう保持するか、中間結果をどう保存するか、ループをどう制限するか。これについては次回のメモリと状態の講義で詳しく扱いますが、エージェントは「一発の問い合わせ」ではなく、進捗を保存する長いプロセスになり得ることを、今のうちから意識しておくと良いでしょう。
TypeScript 開発者の目線で見ると、頭の中では次のようなオブジェクト(Agents SDK TS に近い擬似コード)のイメージになります。
const giftAgent = new Agent({
model: "gpt-5.1",
systemPrompt: giftAgentPrompt,
tools: { searchGifts, filterGifts, checkoutDraft },
// ここにメモリ、ステップ上限などの設定も置く
});
正確な API には踏み込みませんが、重要なのは「モデル、指示、ツール、挙動の設定が一箇所にまとまる」という像です。
3. メッセージの役割:agent における system / user / assistant / tool
Chat Completions でお馴染みの system、user、assistant、tool の役割は、Agents SDK でも同様ですが、より実務的な意味合いを持ちます。
system 役割はエージェント自身の人格とミッションを定めます。例えば GiftGenius なら「あなたはギフト選定エージェント。最小ステップ数で、受取人のプロフィールと予算に基づき 3〜7 件の妥当な候補を選び、ウィジェット用の構造化 JSON を用意する」といった内容です。ここに、やってはいけないこと(例:明示的なステップなしに実購入しない)や、ツールの使い方の制約も書きます。
user 役割は、必ずしも「生身の人間」とは限りません。多くの場合、それはエージェントへの「タスク」です。つまり、あなたのアプリやサービス、別のエージェントが定義した目的です。例えば、ChatGPT App が user メッセージとして「エンジニアの同僚向けに、予算50ドルでギフトのアイデアを5つ選んで」とエージェントを起動する、といった具合です。
assistant 役割は、エージェント内部のモデルが「話す」内容です。中間的な思考や計画、最終回答が含まれます。これらが有用で、必要に応じてログできるよう、system プロンプトを設計するのがあなたの役目です。
tool 役割(あるいは SDK ごとの同等のもの)は、ツール呼び出しの結果を表現します。例えば「MCP で50件の商品を発見」「API がタイムアウトエラーを返した」「DB がユーザープロファイルを返した」など。これらは assistant メッセージと合わせて、エージェントの run サイクルの履歴を形成します。
これを小さな表にまとめると次の通りです。
| 役割 | 話者 | GiftGenius における例 |
|---|---|---|
|
あなた(エージェントの開発者として) | 「あなたはギフト選定エージェント…」 |
|
外部からの呼び出し(アプリ、別のエージェント) | 「50ドル以内でギフトを5つ選んで…」 |
|
エージェント内部のモデル | 「計画:1) 詳細を尋ねる…」 |
|
呼び出したツールの結果 | 「searchGifts が20件の候補を返した…」 |
この構造が重要なのは、これに基づいてrun サイクル(本講義の主役)が組み立てられるからです。
4. LLM がバックエンドの関数を呼び出す仕組み
Q&A に慣れていると、LLM は単純に「テキストが来る → モデルがテキストで答える」と思えます。実際にはもう少し複雑で、だからこそ function calling が機能します。
モデルが受け取るのは一つの質問ではなく、メッセージのリスト=会話履歴です。そこにはすでに、system の指示(「あなたは誰で、何が可/不可か」)、あなたのメッセージ、過去のモデル応答、ツール結果などが入っています。各ステップでモデルはこの全ログを見て、「この後にどんなメッセージを追記すべきか?」を決めます。
ここが鍵です。LLM は常に一歩=履歴の末尾に次のメッセージを追加します。過去を「書き換える」ことはなく、単にリストを延長します。あなたが質問を書けばモデルが答え、次の質問を足せば、モデルは全履歴(全メッセージ)を考慮して再び答えます。
function calling も同じ原理です。モデルは関数を「直接実行」する代わりに、次のように振る舞います。
- 利用可能なツール/tools の一覧とその説明、そして会話履歴を見る;
- 「今はテキストで答えるより、まずこのツールを呼ぶ方が筋が良い」と判断する;
- そして次のメッセージとして、通常のテキストではなく「この tool を、こういう引数で呼びたい」という特別なメッセージを書き込む。
その後はモデルではなくあなたのバックエンドが、この履歴末尾の新メッセージを読み取り、関数呼び出しの要求だと解釈してツールを実行します。続いて、もう一つメッセージ(ツールの結果)を履歴に追加し、再び全メッセージをモデルに渡します。モデルはまた全ログを見て、次の一手(別の呼び出しか、最終の人間向け回答か)を書き足します。
まとめると:
- 通常の Q&A では、「次のメッセージ」=テキスト回答;
- function calling では、「次のメッセージ」=関数呼び出しの指示 または 関数利用後の回答。
関数を呼ぶ「魔法コマンド」は別にありません。モデルがチェーン末尾に追加する、特別種別の「次のメッセージ」に過ぎないのです。
モデルがあなたのバックエンドの関数を公開 API で呼ぶことはありません。モデルは単に「チャットに書く」のです。「この関数を、こういうパラメータで呼びたい」と。そしてあなたのバックエンドがローカル関数を呼び、その結果をまたチャットに追記します。これを繰り返します。
5. エージェントの run サイクル:どのように段階的に「考える」か
LLMエージェントとは、サーバー上で run サイクルを回すオブジェクト(またはアルゴリズム)です。これは「質問 → 考える → 必要なら行動 → また考える → … → 最終回答」という拡張ループです。OpenAI のドキュメントでは、agent loop や ReAct パターン(Reason + Act + Observe)と呼ばれます。
概念レベルでの一回の run は次のようになります。
- エージェントが入力を受け取る: system の指示、タスク(user メッセージ)、必要に応じて現在の状態。
- モデルが一手を生成: テキスト回答、あるいは計画とツール呼び出しの決定。
- モデルが tool‑call を選んだら、エージェントが該当ツールをコードで呼び出す(ローカル関数、MCP‑tool、HTTP、DB など)。
- ツールの結果は tool メッセージとして履歴に追加。
- ループは新しい文脈でモデルに戻る。モデルは次の手を決める:計画継続、別ツールの呼び出し、または最終回答で完了。
- モデルが明示的に、または停止条件により run を終えると、エージェントは最終結果を呼び出し元へ返す。
簡易な図にすると次のようになります。
flowchart TD
A[run開始: 目標 + system] --> B[モデル呼び出し]
B --> C{モデルは
テキストで回答するか、
それとも tool を呼ぶか?}
C --> D["テキスト応答
(assistant)"]
D --> E{タスクは完了?}
E -->|はい| F[最終結果]
E -->|いいえ| B
C --> G["Tool-call
(呼び出しの記述)"]
G --> H[関数 / MCP / HTTP の呼び出し]
H --> I["Tool の結果
(tool message)"]
I --> B
これを簡略化した TypeScript 擬似コード(実 API とは異なりますが論理は同じ)にすると、次のようになります。
async function runAgent(goal: string) {
let context = buildInitialContext(goal);
while (!isFinished(context)) {
const decision = await callLLM(context); // エージェントのステップ
if (decision.type === "tool_call") { // 関数を呼ぶ?
const toolResult = await callTool(decision.tool, decision.args); // ローカル関数を呼び出す
context = appendToolResult(context, toolResult); // 履歴の末尾に結果を追加
} else {
context = appendAssistantMessage(context, decision.message);
}
enforceLimits(context); // ステップ/時間/ループの制限を適用
}
return extractFinalResult(context);
}
Agents SDK は、履歴管理、tool‑call のマーシャリング、リトライのロジックなど面倒な部分を大半引き受けてくれます。あなたは設定とツール実装に集中できます。
Run と Step
次の2つの概念を区別することが大切です。
- run — ある目的に対してエージェントを一度起動すること(例:「このケースに合うギフトを選んで」)。
- step — run サイクルの1ステップ。具体的なモデル呼び出しで、テキスト応答か tool‑call を生む。
モニタリングでは、1つの run の中に複数のステップが見えます。セキュリティやコストの上限は「run ごと」や「ステップごと」に設定されることが多いです。
run と run サイクルのイメージがつかめたところで、どこにエージェントを使う価値があり、どこは単純な tool で十分かを考えてみましょう。
5. GiftGenius でエージェントが必要な場所と不要な場所
何でもかんでもエージェントを書きたくなる前に、「本当に必要か?」と自問しましょう。
エージェントが向くのは、多段ステップで分岐やリトライがあり、プロンプトだけで管理しにくいロジックがあるタスクです。
GiftGenius では、次のような「賢いギフト選定ウィザード」が該当します。
- 重要な詳細(受取人の性別、趣味、親密度など)を追加質問できる;
- 複数の商品ソース(異なるベンダーの MCP‑tools)にアクセスできる;
- 結果をフィルタ・ランク付けできる;
- ソースのエラー時にリトライや代替経路へフォールバックできる;
- 単なるテキスト一覧ではなく、SKU(product feed)のリンク付きで説明のある構造化リストを返す。
ここではエージェントが「オーケストレーター」として本当に有用です。後に voice/Realtime シナリオや複雑なコマース(ACP)を足したい場合でも活きます。
一方、getGiftDetails(giftId) のような単純な呼び出しにエージェントは要りません。ChatGPT から直接呼ぶ MCP‑tool で十分です。商品説明テキストの要約といった一段階の簡単なシナリオも同様です。
一般論として、「普通のひとつの tool で表現できるなら、たぶんエージェントは不要」。逆に、明らかに多段のワークフローでチェックやリトライが入るなら、エージェントが効果を発揮する可能性が高いです。
6. 決定性:エージェントの挙動を予測可能にするには
LLMエージェントにおける決定性は難物です。理想的には、同じ入力・同じ設定なら同じ行動計画・同じ tool‑call の列を得たいところですが、モデルは本質的に確率的です。とはいえ、予測可能性を高めるレバーはいくつかあります。
第一に定番の、temperature などの生成パラメータ。温度が低いほど創造性は低下し、モデルは「言うことを聞く」傾向に。ギフト選定エージェントなら、ゼロではないが過度でない自由度が望ましいでしょう。そうでないと、同じツールを毎回違う手順で呼ぶ、などが起きがちです。
第二に、明確な system 指示。例えば「好きにツールを呼んで何でもしてよい」と曖昧に書けば、API を渡り歩いたり、根拠なく即答したりと挙動がブレます。いつ、どのツールを、どんなパラメータで呼ぶべきか、エラーをどう解釈するか、どんな場合にタスクを終えるかを、明記した方が良いです。
GiftGenius エージェント向けの system プロンプトの一節例:
受取人のプロフィール(年齢・性別・用途・おおよその予算)が欠けている場合、
まず user-facing チャネルで確認質問をして、回答を待つこと。
その後で profile を満たした上で search_gifts ツールを呼ぶこと。
商品をでっちあげない。常にツールの結果に基づいて回答すること。
このような指示は意思決定の揺らぎを抑え、より決定的な挙動に近づけます。
第三に、ツール設計そのもの。同じ目的の検索ツールが3つあると、モデルは時にA、時にB、と選択が揺れます。責務が重ならないようにツールを設計し、説明にもそれを明記するのがよいです。
最後に、ガードレールを使えます。エージェントの行動やモデル出力を検査する規則やスキーマです。Agents SDK には出力の構造チェックなどの仕組みがあり、スキーマ逸脱時にやんわり修正したり、ステップをやり直すこともできます。
ミニ例:出力フォーマットを固定する
例えば、エージェントが常に gifts フィールドを持つ JSON を返し、その配列要素が id、title、score を含むようにしたいとします。次を行えます。
- このスキーマをエージェント側に定義;
- 最終出力が必ずスキーマに適合すべきと指定;
- 違反時にはステップの再実行や安全なエラー返却。
擬似コード:
const giftResultSchema = z.object({
gifts: z.array(z.object({
id: z.string(),
title: z.string(),
score: z.number().min(0).max(1),
}))
});
// エージェントの設定内
const agent = new Agent({
/* ... */
outputSchema: giftResultSchema,
});
モデルが不適切なものを返そうとすると、runner がバリデーションエラーを報告し、モデルに聞き直すか、インシデントとして記録できます。
7. 冪等性:なぜエージェントはあなたの API を二度呼ぶことがあるのか
決定性が「同じ入力で同じ計画」に関する話だとすると、冪等性は「再試行の安全性」の話です。エージェントの文脈では極めて重要です。
第一に、リトライの層がもう一段増えるからです。HTTP クライアントやロードバランサだけでなく、エージェント自身が、エラーや不完全な結果に対してツール呼び出しを再試行することがあります。第二に、実運用では webhook やキュー、ストリーミングなどが加わり、同じ論理的ステップを誤って複数回処理してしまう可能性が高まります。
MCP‑tools のレベルでも冪等性(重複課金・重複注文を避ける、idempotency key を使う)を議論しました。ここではエージェントの多段性が加わる分、重要性が増します。
GiftGenius に、選んだギフトのリストから ACP/Stripe に draft チェックアウトを作る create_checkout_session というツールがあるとします。ネットワークエラーでエージェントがこの呼び出しを再試行した時、2つの注文と2回の課金は絶対に避けたいはずです。
そこで、次を行います。
- 各論理アクションごとに外部のidempotency key(例:runId + stepIndex や、明示的に生成した checkoutDraftId)を決める;
- そのキーをバックエンド/ACP エンドポイントに渡す;
- バックエンド側で、同じキーをすでに処理していないか確認し、処理済みなら保存済み結果を返す。
TypeScript の擬似例:
async function createCheckoutDraft(runId: string, payload: DraftPayload) {
const key = `gift-checkout-${runId}`;
const existing = await findDraftByKey(key);
if (existing) return existing;
const draft = await stripe.checkout.sessions.create({
/* ... */,
idempotencyKey: key, // もしくは自前のレイヤで管理
});
await saveDraftWithKey(key, draft);
return draft;
}
これで、同じ runId に対してエージェントがこのツールを二度呼んでも、コードは冪等に保たれます。すなわち「同じ論理ステップ → 同じ実結果」です。
「まず確認、次に実行」
もう一つの一般的な冪等パターンは、「実行の前に状態を確認する」ことです。例えば注文を作る前に、同じ clientReferenceId や同一パラメータの注文が既に無いかを確認します。長いワークフローでは、エージェントが前のステップで実行済みであることを「忘れる」可能性があるため、特に有効です。
セーフモード / フェイクモード
開発段階では、危険なツールに「安全モード」を用意すると便利です。実行そのものはせず、「何をするはずだったか」をログに記録し、擬似結果を返します。エージェントの run サイクルを本番環境に近い形で流しつつ、金銭やデータのリスクを避けられます。
8. ミニ実践:GiftGenius のエージェントを人間の言葉で記述する
run サイクル、決定性、ツールの冪等性について話しました。ここで一度コードから離れ、実際のシナリオとしてどう組み上がるかを確認しましょう。
紙や頭の中でできる小さなエクササイズです(コードは不要)。
シンプルなエージェントを記述するとします。
-
: あなたはギフト選定支援のアシスタント。重要な詳細は必ず確認し、商品をでっちあげず、ツールの結果だけを用いる。system -
: 同僚向けのギフトが欲しい。予算は $50。user
このエージェントが踏むべき手順を言葉で説明してください。
典型的には次のようになります。
- まず情報が十分かを確認。不足していれば、同僚のおおまかな職種(デザイナー、開発者、マネージャー)、禁忌(酒類、ジョークグッズ)、配送制約の有無などを質問する。回答は session state かツール呼び出しのパラメータに入る。
- 次に、プロフィールを埋めて search_gifts を呼ぶ。「同僚=開発者、予算=50、カテゴリ=ガジェット/オフィス」。ツールは価格・カテゴリ・商品IDを含む候補リストを返す。
- その後、配送地域の制約が分かった場合は filter_gifts_by_constraints を呼ぶか、プロンプト内で手動フィルタする。関連度や価格で並べ替え、必要ならコメントを付ける(「コーヒー好きに適合」「リモートワーク向け」など)。
- 最後に ChatGPT App 向けの構造化された最終回答を用意する。5〜7 件のギフトと簡潔な説明、使用のヒント、Checkout へのリンク(または次のステップ=draft チェックアウト作成)を含める。
どこで tool‑call が必要か? 明らかに、検索・フィルタ・在庫/配送可否チェック・draft チェックアウト作成です。どのステップを冪等にすべきか? とりわけ注文や課金に関わる操作(draft チェックアウト作成、場合によっては DB への履歴保存)です。
9. 初学者がやりがちなミス
誤り1:制約なしの「第二の ChatGPT」としてのエージェント。
モデルに追加でプロンプトを渡し「エージェント」と呼びたくなることがあります。結果としてテキストを大量生成し、ツールを無秩序に呼び、制御しにくいものができがちです。これを避けるには、system で役割を明確にし、ツール一覧を限定し、「具体的なミッションを持つオーケストレーター」として設計することが重要です。
誤り2:ツールの冪等性がない。
既存の HTTP ハンドラを「そのまま」エージェント用に流用し、runner による自動リトライ可能性を織り込まないケースが多いです。決済や注文では致命的な問題を招きます。最初から、同じ論理キーでの再呼び出しが同じ結果に合流するように設計しましょう。
誤り3:モデル設定が創造性過多。
高い温度はスピーチや詩には向きますが、多段プロセスを堅実にオーケストレーションすべきエージェントでは逆効果です。毎回違うツールを選んだり、計画がばらついたり、そもそも tools の存在を忘れたりします。エージェントは「業務用」の存在と捉え、より厳格なモードで運用しましょう。
誤り4:何でもできる万能ツール。
execute_any_sql や do_anything_with_orders のようなツールを作りたくなるかもしれませんが、LLM の創造性と組み合わさるとセキュリティ上のリスクが高まります。権限と契約が明確な専用ツールを複数用意する方が、何でもできる「神ツール」より安全です。
誤り5:run の明確な終了条件がない。
終了条件がないと、エージェントは「もう一度結果を確認」「もう一度ユーザーに質問」「同じエラーで再試行」など半無限に回り続けることがあります。依存先が不安定なとき負荷下で発覚しがちです。ステップ数・run 時間・同一エラーでの再試行回数に上限を設け、system に「合理的な選択肢を尽くしたら素直に諦める」旨も書いておきましょう。
誤り6:エージェント状態に何でも保存する。
Agents SDK は session state の扱いを簡単にしてくれるため、巨大ドキュメントや生ログ、機微情報まで入れたくなります。これはコンテキストを膨らませコストとリスクを増やします。エージェント状態には本当に継続に必要なものだけを置き、他は DB やログなど別レイヤー(プライバシ配慮付き)に分離しましょう。
誤り7:単純な MCP‑tool で足りるところにエージェントを使う。
タスクが「関数を一度呼んで結果を返すだけ」でも、最初からエージェントにしてしまうことがあります。不要な複雑性(run サイクル、状態、追加ログ、故障点)が増えます。シナリオが単一の tool‑call に収まるなら、そのままにし、本当に多段化したときにエージェントを導入しましょう。
GO TO FULL VERSION