はじめに
ガイド: ツール呼び出しエージェント
ツール呼び出しエージェントを作るガイドです。ツール定義、ステップ上限付きのループ、並列ツール呼び出し、構造化された最終出力、モデルの選び方、コスト管理を、openai パッケージのコードで順に示します。コードは公式の openai パッケージを使います。
エージェントとはループです。モデルが会話を読み、自分のツールを 1 つ以上呼び出すことを決め、あなたがそれを実行して結果を返し、モデルが回答できるようになるまで続けます。api14 は、ツールに対応するすべてのモデルで OpenAI の tools 形式をサポートしているため、モデル ID を変えるだけで、OpenAI、Anthropic、Google などのプロバイダーで同じコードが動作します。
このガイドでは、ステップ数の上限、並列のツール実行、構造化された最終回答、コスト管理を備えた小さなエージェントを構築します。コードは公式の openai パッケージを、ベース URL https://api14.com/api/v1 で使います。
ツールを定義する
各ツールは、名前、モデルが読む説明、引数の JSON Schema で構成されます。説明は具体的に書いてください。それがモデルにとって唯一のドキュメントになります。
import json, os
from openai import OpenAI
client = OpenAI(base_url="https://api14.com/api/v1", api_key=os.environ["API14_API_KEY"])
def search_orders(customer_email: str) -> list:
return [{"id": "A-1001", "status": "shipped"}] # データベースへのクエリ
def get_shipment(order_id: str) -> dict:
return {"order_id": order_id, "carrier": "UPS", "eta": "2026-10-09"} # API 呼び出し
TOOLS = {"search_orders": search_orders, "get_shipment": get_shipment}
tools = [
{"type": "function", "function": {
"name": "search_orders",
"description": "List a customer's orders with their status.",
"parameters": {"type": "object", "properties": {"customer_email": {"type": "string"}},
"required": ["customer_email"]},
}},
{"type": "function", "function": {
"name": "get_shipment",
"description": "Carrier and estimated delivery date for one order.",
"parameters": {"type": "object", "properties": {"order_id": {"type": "string"}},
"required": ["order_id"]},
}},
]
ステップ上限付きのループ
各反復は別々に課金されるリクエストなので、必ずステップ数に上限を設けてください。ツールのエラーはクラッシュさせず、コンテンツとしてモデルに返します。モデルは通常、引数を修正して回復します。
MAX_STEPS = 8
def run_tool(call):
try:
args = json.loads(call.function.arguments)
result = TOOLS[call.function.name](**args)
except Exception as e: # 不正な JSON、未知のツール、または関数の失敗
result = {"error": str(e)}
return {"role": "tool", "tool_call_id": call.id, "content": json.dumps(result)}
def agent(task, model="openai/gpt-5-mini"):
messages = [
{"role": "system", "content": "You are a support agent. Use tools to look things up. Be brief."},
{"role": "user", "content": task},
]
spent = 0.0
for step in range(MAX_STEPS):
r = client.chat.completions.create(model=model, messages=messages, tools=tools, max_tokens=1000)
spent += r.usage.cost
msg = r.choices[0].message
if not msg.tool_calls:
return msg.content, spent
messages.append(msg)
messages.extend(run_tool(call) for call in msg.tool_calls)
raise RuntimeError(f"No answer after {MAX_STEPS} steps (spent ${spent:.4f})")
print(agent("Where is my order? My email is ada@example.com"))
ループ内のすべてのリクエストで tools を送ってください。多くのプロバイダーは、リクエストにツール定義がないと tool メッセージを拒否します。
並列ツール呼び出し
parallel_tool_calls(対応するモデルではデフォルトで有効)を使うと、モデルは 1 ターンで複数の呼び出しを要求できます。たとえば、3 件の注文に対する get_shipment です。それらを並行して実行し、呼び出しごとに 1 つの tool メッセージを任意の順序で追加します。tool_call_id が各結果を呼び出しに結び付けます。Node では次のようになります。
import OpenAI from "openai";
const client = new OpenAI({ baseURL: "https://api14.com/api/v1", apiKey: process.env.API14_API_KEY });
const MAX_STEPS = 8;
async function agent(messages, tools, impl, model = "openai/gpt-5-mini") {
for (let step = 0; step < MAX_STEPS; step++) {
const res = await client.chat.completions.create({ model, messages, tools, max_tokens: 1000 });
const msg = res.choices[0].message;
if (!msg.tool_calls?.length) return msg.content;
messages.push(msg);
const results = await Promise.all(msg.tool_calls.map(async (call) => {
let content;
try {
content = JSON.stringify(await impl[call.function.name](JSON.parse(call.function.arguments)));
} catch (err) {
content = JSON.stringify({ error: String(err) });
}
return { role: "tool", tool_call_id: call.id, content };
}));
messages.push(...results);
}
throw new Error(`No answer after ${MAX_STEPS} steps`);
}
ツールに順序が重要な副作用(作成してから更新するなど)がある場合は、parallel_tool_calls: false を設定して、モデルが 1 ターンに 1 つの呼び出しだけを要求するようにします。
構造化された最終出力
エージェントの回答を別のコードに渡す場合は、スキーマに一致する JSON を要求します。シンプルで移植性の高いパターンは、ループで事実を集めた後に、追加の呼び出しを 1 回行う方法です(ここでの messages はループの会話です。agent() から回答とともに返してください)。
schema = {
"type": "object",
"properties": {
"order_id": {"type": "string"},
"status": {"type": "string", "enum": ["processing", "shipped", "delivered", "unknown"]},
"eta": {"type": ["string", "null"]},
"reply": {"type": "string"},
},
"required": ["order_id", "status", "eta", "reply"],
"additionalProperties": False,
}
final = client.chat.completions.create(
model="openai/gpt-5-mini",
messages=messages + [{"role": "user", "content": "Summarize the result for our ticket system."}],
tools=tools,
tool_choice="none",
response_format={"type": "json_schema", "json_schema": {"name": "ticket", "strict": True, "schema": schema}},
)
ticket = json.loads(final.choices[0].message.content)
tool_choice: "none" を指定すると、履歴に tool メッセージが含まれるため必要な tools をリクエストに残したまま、それ以上の呼び出しを止められます。解析した JSON は必ず検証してください。構造化出力を参照してください。
モデルの選び方
- 機能: モデルの
supported_parametersにtoolsが含まれている必要があります。GET /modelsで確認してください。モデルを参照してください。最終ステップでは、structured_outputsまたはresponse_formatを探します。 - ルーティング: 送信するすべてのパラメーターに対応するプロバイダーにのみリクエストが送られるように、
provider: { "require_parameters": true }を追加します(Python ではextra_body経由)。プロバイダールーティングを参照してください。 - サイズ:
openai/gpt-5-miniのような小さなモデルは、適切に説明された少数のツールを、確実かつ低コストで扱えます。ツールが多い、複数ステップの長い計画がある、または誤ったツールを選び続ける場合は、より大きなモデルに移行してください。 - 自分のツールでテストする: 同じ 20〜50 件の実際のタスクを 2〜3 個の候補モデルで実行し、成功率、タスクあたりのステップ数、
usage.costの合計を比較します。
curl -s "https://api14.com/api/v1/models?category=text" \
| jq -r '.data[] | select(.supported_parameters | index("tools")) | "\(.id) \(.pricing.prompt)"'
コストの管理
エージェントは 1 つのタスクに多数のリクエストを行うことがあり、各ステップで会話全体が再送されます。複数のレベルで支出を制限してください。
- 上記のとおり、ステップ上限(
MAX_STEPS)とリクエストごとのmax_tokens。 - タスクごとの予算: ステップ全体で
usage.costを合計し、しきい値を超えたら停止します。 - 短いツール結果: モデルが必要とするフィールドだけを返します。大きなツール出力は、その後のステップごとに入力トークンとして課金されます。
- キーごとの 1 日の USD 上限: API キーでエージェント専用のキーを作成し、1 日の上限を設定します。上限に達すると、次の UTC 日までリクエストは
429 daily_limit_reachedを返します。このコードは再試行ではなく、エージェントを停止して処理してください。 - モデル許可リスト: キーを、テストしたモデルに制限します。それ以外は
403 model_not_allowedを返します。 - プロンプトキャッシュ: キャッシュされたプレフィックスの割引が適用されるように、システムプロンプトとツール定義を、すべてのリクエストの先頭で同一に保ちます。プロンプトキャッシュを参照してください。