はじめに
OpenAI Agents SDKでAgentがFunction Toolを選びながら処理を進める仕組みを試したく、3x3盤面の問題を解く小さなゲームを実装しました。
このゲームでは、プレイヤーが問題とプリセットセリフを選びます。Agentはセリフと問題の状態を受け取り、6つのSkill Toolから必要なものを選択します。Toolの実行結果を読んだ後、次のToolを呼ぶか、最終レポートを返すかもAgentが決めます。
単に「LLMへ質問して答えを表示する」だけではなく、次の点を実装対象にしました。
- Agentが選んだToolとその順番を画面で見えるようにする
- 入力、Tool引数、Tool結果、最終出力をそれぞれ検証する
- 1回の実行を履歴として保存し、ページを再読み込みしても確認できるようにする
- 保存済みの実行履歴を再生し、Agentを再実行せずに判断の流れを振り返れるようにする
3x3盤面から問題やイベントマスを選び、右側でAgentの進行状況を確認する。
ソースコード:
この記事で扱うこと
この記事では、画面の操作に沿って次の内容を紹介します。
- 問題とセリフを選び、Agentへ判断を任せる画面
- OpenAI Agents SDKとFunction ToolでSkill Chainを組む方法
- Agentが扱う4つの境界を検証する安全設計
- Tool Call・Tool Result・最終レポートを履歴として残す方法
- 代表シナリオをテストで評価する方法
まず画面を動かす
操作は2段階です。まず盤面から未解決の問題を1つ選び、次に「直接解決を試す」「条件を整理して解く」などのプリセットセリフを選びます。問題を選ぶまではセリフのボタンを無効にしており、どの問題に対する依頼なのかを必ず確定させます。
盤面には国語・算数・理科の単一教科問題と、複数教科をまたぐ問題があります。経験値ボーナスと休憩はイベントマスです。経験値ボーナスは初回移動時に経験値を増やし、休憩は未解決問題を回復します。どちらも同じゲーム中に1回だけ使えます。
問題を選ぶと、Agentへ渡すプリセットセリフを選択できる。
セリフのクリック後は、画面遷移を待たずにSSEで進行状況を表示します。run.started、tool.selected、tool.started、tool.completed または tool.failed、report.completed を受け取り、現在処理中のToolと完了したToolを順に画面へ追加します。
SSEでToolの選択・開始・完了を受け取り、Agentの実行過程を表示する。
なぜ小さなゲームにしたのか
Toolを呼ぶ順番が常に同じなら、通常のServiceで手順を固定したほうがシンプルです。一方で、入力や途中結果に応じて次の処理を変えたい場合は、手順をアプリケーション側にすべて書くほど分岐が増えます。
このゲームでは、ゲーム固有の状態変化と、Agentによる判断を分けました。
-
GameServiceは問題HP、経験値、盤面イベント、選択状態を担当する -
GameAgentServiceはゲームで使うAgentとToolを構成する -
AgentExecutionServiceはRunnerの実行、ガードレール、Toolの実行履歴を担当する - Agentは利用可能なToolと途中結果を見て、次に呼ぶToolを選ぶ
つまり、ゲームのルールは通常のドメインロジックとして決定的に保ち、どのSkillをどの順番で試すかだけをAgentへ委ねています。
実行の流れ
全体の流れは次のとおりです。
問題・セリフの選択
↓
GameAgentService が Agent と6つのToolを構成
↓
AgentExecutionService が OpenAI Agents SDK の Runner を実行
↓
Tool Call → Tool Result → 必要なら次のTool
↓
AgentRun / Report を作成
↓
SSE表示・セッション保存・履歴リプレイ
OpenAI Agents SDKには、AgentとFunction Toolを渡します。AgentはToolの登録順や画面のボタン順に従うのではなく、Toolの説明と直前の結果を読んで次の操作を選びます。
ゲーム側は、Toolから返った「問題HPをいくつ減らしたか」「経験値をいくつ得たか」を適用するだけです。Agentの判断順を GameService に埋め込まないため、ゲームルールとAgentの判断をそれぞれ独立してテストできます。
6つのSkill Tool
ゲームで使うFunction Toolは次の6つです。
| 教科 | 画面の表示名 | Function Tool名 | 役割 |
|---|---|---|---|
| 国語 | 読解分析 | analyze_reading |
文章の条件や読み取りを整理する |
| 国語 | 表現分析 | analyze_expression |
表現や言い回しを確認する |
| 算数 | 計算 | calculate |
数値計算を行う |
| 算数 | 数量比較 | compare_quantities |
数量や条件を比較する |
| 理科 | 原因推論 | infer_cause |
現象の原因を推論する |
| 理科 | 観察分析 | analyze_observation |
観察結果から特徴を整理する |
各Toolは target_mondai_id を受け取り、成功・失敗、問題へのダメージ、経験値、残りHPを構造化して返します。存在しない問題IDや、選択中ではない問題IDは受け付けません。解決済みの問題へToolを実行した場合も失敗として履歴に残し、ゲーム状態は変更しません。
Skill Chainは「固定シナリオ」ではない
プリセットセリフごとに、代表的なTool Chainを定義しています。例えば国語の問題では、次のケースを用意しています。
- 「直接解決を試す」:
analyze_expression - 「条件を整理して解く」:
analyze_reading - 「別の観点で検証する」:
analyze_reading→analyze_expression
ここで大切なのは、これはAgentへ実行順を強制する設定ではない点です。代表Chainはテストの期待値です。実行結果の ToolCall を見て、期待したToolが期待した順番で選ばれたかを評価します。
「Toolの順番を固定しているだけでは?」という疑問に対しては、実装とテストの役割を分けることで答えています。実装ではAgentがToolを選び、テストでは代表的な入力に対する振る舞いを確認します。
安全性を4つの境界で確認する
LLMの最終出力だけを確認しても、途中のTool引数やTool結果が安全とは限りません。このため、SafetyPolicy で次の4つの境界を検証します。
| 境界 | 主な確認内容 | 違反時の扱い |
|---|---|---|
| 入力 | 文字列、500文字以内、危険な命令やプロンプト操作を含まないこと | Runnerを呼ばず BLOCKED
|
| Tool引数 | JSONオブジェクト、サイズ上限、危険な命令を含まないこと | Toolを呼ばず BLOCKED
|
| Tool結果 | JSON化できること、サイズ上限、危険な内容を含まないこと | 結果を公開せず BLOCKED
|
| 最終出力 | サイズ上限、危険な内容を含まないこと | 出力を公開せず理由を記録 |
ゲーム固有の検証として、Tool引数の問題IDが許可された値か、選択中の問題だけを対象にしているかも確認します。これは、一般的な入力検証とは別に、ゲームの状態を壊さないための検証です。
SDKのGuardrailだけに任せず、決定的なローカル検証を重ねています。外部サービスの判定が一時的に変わっても、ゲームの入力制約や状態制約は変わらないようにするためです。
OpenAI Moderation APIを含む信号機型ガードレールは #310、資料検索を行うRAG Toolは #324 で扱う予定です。これらを追加する場合も、Agentが必要に応じて選ぶToolとして組み込みます。
実行履歴を構造化する
1回の依頼を、単なる表示メッセージではなく AgentRun として保持します。
-
AgentRun: 実行ID、入力、ターン上限、開始・終了時刻、実行状態 -
ToolCall: Agentが選んだTool名、構造化引数、呼び出し順 -
ToolResult: Toolの出力、成功・失敗、結果順 -
Report: 最終出力、Tool履歴、ターン数、エラー
ゲーム画面用の AgentExecutionRecord は、これらに問題名、セリフ、HP変化、経験値変化を加えた表示用の記録です。これにより「成功した」だけで終わらず、どの入力に対して、どのToolが選ばれ、どの結果を経て最終出力に至ったかを後から追えます。
履歴はDjangoセッションへ保存します。ページを再読み込みしても盤面と履歴を復元でき、DBモデルやゲーム状態Cookieへ依存しません。
選ばれたSkill、各Toolの結果、HP・経験値の変化、Agentの説明を実行履歴として確認できる。
履歴リプレイは再実行ではない
実行履歴には「履歴をリプレイ」ボタンがあります。これは保存済みのTool行を順番にハイライトして、実行時の流れを画面上で再生する機能です。
リプレイでAgentやToolを再実行すると、問題HPや経験値がもう一度更新されてしまいます。そのため、リプレイは保存済みの ToolCall と ToolResult を表示するだけにしています。画面の演出とゲーム状態の更新を分けたことで、振り返りを何度行ってもゲーム状態は変化しません。
テストで確認していること
python manage.py test ai_agent
テストでは、次の範囲を確認しています。
- 盤面、問題選択、経験値ボーナス、休憩の状態変化
- 単一Toolと複数Tool Chainの実行
- プリセットセリフごとの代表Chain
- 存在しない問題や選択外の問題を指定したTool引数の拒否
- 入力、Tool引数、Tool結果、最終出力のガードレール
- Tool失敗、タイムアウト、ターン上限、ストリーミング途中失敗
- SSEイベント、セッション復元、履歴の再表示、リプレイ導線
Agentを使う箇所は、実際の外部API呼び出しだけに依存させず、テスト用Runnerでも同じ履歴構造を検証できるようにしています。これにより、Agentの最終文章だけではなく、Toolの選択順やエラー時の状態もテスト対象にできます。
まとめ
OpenAI Agents SDKを使うと、Toolの選択をAgentへ任せながらも、アプリケーション側でゲームルール、安全性、実行履歴を明確に管理できます。
今回の実装で特に意識したのは、Agentの判断を見えない処理にしないことです。画面のSSE表示、構造化した AgentRun、保存済み履歴のリプレイを組み合わせることで、Agentが何をしたかを利用者と開発者の両方が確認できるようにしました。
今後は、#310で外部Moderationを含むガードレールを拡張し、#324でRAG・ベクトル検索をAgentが選べるToolとして追加する予定です。
関連Issueの責務
- #908: 現行Skill Chain型Agentの安全性、実行履歴、代表Chainテスト、記事とREADMEの整合性
- #310: 外部Moderationを含む信号機型ガードレールの拡張
- #324: RAG・ベクトル検索をAgentの選択可能なToolとして導入する検討



