ストーリー
ミッション概要
| 項目 | 内容 |
|---|---|
| 目標 | カスタマーサポートエージェント用の業務ツールを設計・実装する |
| 所要時間 | 90分 |
| ミッション数 | 3つ |
| 使用知識 | Tool設計原則 / ファンクションコーリング / エラーハンドリング |
| 評価観点 | スキーマ設計の品質、説明文の明確さ、エラー処理の網羅性 |
Mission 1: 注文管理ツールセットの設計
要件
NetShop社のカスタマーサポートで必要な注文管理ツールを設計してください。
必要なツール:
- 注文検索(複数条件で注文を検索)
- 注文詳細取得(特定の注文の完全な情報)
- 注文キャンセル(注文のキャンセル処理)
- 注文ステータス更新(配送状況等の更新)
設計要件:
- OpenAI Function Calling 形式のスキーマ定義(JSON Schema)
- 各ツールの説明文は、類似ツールとの使い分けを含むこと
- パラメータには型、必須/任意、enumを適切に設定すること
解答例
const orderTools: OpenAI.ChatCompletionTool[] = [
{
type: "function",
function: {
name: "search_orders",
description:
"注文を条件で検索し、該当する注文の一覧を返します。" +
"顧客ID、注文日の範囲、ステータスでフィルタリング可能です。" +
"最大50件を返却します。" +
"特定の注文の詳細情報が必要な場合は get_order_details を使ってください。",
parameters: {
type: "object",
properties: {
customer_id: {
type: "string",
description: "顧客ID(例: CUST-001)"
},
status: {
type: "string",
enum: ["pending", "confirmed", "processing", "shipped", "delivered", "cancelled"],
description: "注文ステータスでフィルタ"
},
date_from: {
type: "string",
format: "date",
description: "検索開始日(YYYY-MM-DD)"
},
date_to: {
type: "string",
format: "date",
description: "検索終了日(YYYY-MM-DD)"
},
limit: {
type: "integer",
description: "取得件数の上限(デフォルト: 20, 最大: 50)"
}
}
}
}
},
{
type: "function",
function: {
name: "get_order_details",
description:
"注文番号を指定して1件の注文の完全な詳細情報を取得します。" +
"商品一覧、合計金額、配送先、支払い方法、ステータス履歴が含まれます。" +
"複数注文の検索には search_orders を使ってください。",
parameters: {
type: "object",
properties: {
order_id: {
type: "string",
description: "注文番号(例: ORD-12345)"
}
},
required: ["order_id"]
}
}
},
{
type: "function",
function: {
name: "cancel_order",
description:
"注文をキャンセルします。" +
"ステータスが pending または confirmed の注文のみキャンセル可能です。" +
"shipped 以降の注文には return_order(返品処理)を使ってください。" +
"キャンセル理由は必須です。",
parameters: {
type: "object",
properties: {
order_id: {
type: "string",
description: "キャンセルする注文番号"
},
reason: {
type: "string",
enum: ["customer_request", "out_of_stock", "payment_failed", "other"],
description: "キャンセル理由"
},
reason_detail: {
type: "string",
description: "キャンセル理由の詳細(reason が other の場合は必須)"
}
},
required: ["order_id", "reason"]
}
}
},
{
type: "function",
function: {
name: "update_order_status",
description:
"注文のステータスを更新します。管理者権限が必要です。" +
"ステータスの遷移ルール: pending→confirmed→processing→shipped→delivered。" +
"逆方向への遷移はできません。キャンセルには cancel_order を使ってください。",
parameters: {
type: "object",
properties: {
order_id: {
type: "string",
description: "更新する注文番号"
},
new_status: {
type: "string",
enum: ["confirmed", "processing", "shipped", "delivered"],
description: "新しいステータス"
},
note: {
type: "string",
description: "ステータス変更の備考"
}
},
required: ["order_id", "new_status"]
}
}
}
];
ポイント:
- 各ツールの説明文で他ツールとの使い分けを明記
- cancel_orderでは対象外のステータスと代替手段を明示
- update_order_statusでは遷移ルールを説明文に含めている
Mission 2: バリデーション付きツール実行の実装
要件
Mission 1で設計したツールに対して、Zodを使ったパラメータバリデーション付きのツール実行関数を実装してください。
実装要件:
- Zodスキーマでパラメータを検証
- バリデーションエラー時はLLMが理解できるエラーメッセージを返す
- 各ツールのモック実装(実際のDB呼び出しの代わりにダミーデータを返す)
解答例
import { z } from "zod";
// バリデーションスキーマ
const SearchOrdersSchema = z.object({
customer_id: z.string().regex(/^CUST-\d+$/, "顧客IDはCUST-数字の形式").optional(),
status: z.enum(["pending", "confirmed", "processing", "shipped", "delivered", "cancelled"]).optional(),
date_from: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "日付はYYYY-MM-DD形式").optional(),
date_to: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "日付はYYYY-MM-DD形式").optional(),
limit: z.number().int().min(1).max(50).default(20).optional()
});
const GetOrderDetailsSchema = z.object({
order_id: z.string().regex(/^ORD-\d+$/, "注文番号はORD-数字の形式")
});
const CancelOrderSchema = z.object({
order_id: z.string().regex(/^ORD-\d+$/, "注文番号はORD-数字の形式"),
reason: z.enum(["customer_request", "out_of_stock", "payment_failed", "other"]),
reason_detail: z.string().optional()
}).refine(
(data) => data.reason !== "other" || (data.reason_detail && data.reason_detail.length > 0),
{ message: "reason が other の場合、reason_detail は必須です" }
);
// ツール実行関数
interface ToolResponse {
success: boolean;
data?: unknown;
error?: { code: string; message: string; suggestion?: string };
}
async function executeTool(
name: string,
args: Record<string, unknown>
): Promise<ToolResponse> {
try {
switch (name) {
case "search_orders": {
const params = SearchOrdersSchema.parse(args);
return {
success: true,
data: {
orders: [
{ order_id: "ORD-12345", status: "shipped", total: 15800, ordered_at: "2026-02-28" },
{ order_id: "ORD-12346", status: "delivered", total: 3200, ordered_at: "2026-02-25" }
],
total_count: 2
}
};
}
case "get_order_details": {
const params = GetOrderDetailsSchema.parse(args);
return {
success: true,
data: {
order_id: params.order_id,
status: "shipped",
items: [{ product_name: "ワイヤレスイヤホン", quantity: 1, price: 15800 }],
total: 15800,
shipping_address: "東京都渋谷区...",
payment_method: "credit_card",
ordered_at: "2026-02-28T10:30:00Z"
}
};
}
case "cancel_order": {
const params = CancelOrderSchema.parse(args);
return {
success: true,
data: {
order_id: params.order_id,
previous_status: "confirmed",
new_status: "cancelled",
cancelled_at: new Date().toISOString(),
refund_status: "processing"
}
};
}
default:
return {
success: false,
error: { code: "UNKNOWN_TOOL", message: `不明なツール: ${name}` }
};
}
} catch (error) {
if (error instanceof z.ZodError) {
return {
success: false,
error: {
code: "VALIDATION_ERROR",
message: error.errors.map(e => `${e.path.join(".")}: ${e.message}`).join("; "),
suggestion: "パラメータの形式を確認して再試行してください"
}
};
}
return {
success: false,
error: {
code: "INTERNAL_ERROR",
message: "ツールの実行中にエラーが発生しました",
suggestion: "しばらく時間をおいて再試行してください"
}
};
}
}
ポイント:
- Zodの
.refine()で複合バリデーション(reasonがotherならreason_detail必須) - エラー時にフィールド名とメッセージを含めてLLMの再試行を支援
- モック実装でも実際のレスポンス構造を再現
Mission 3: リトライ付きエージェントループの実装
要件
Mission 1-2のツールを使い、リトライとフォールバック付きのエージェントループを実装してください。
実装要件:
- Exponential Backoffによるリトライ(最大3回)
- リトライ可能/不可能なエラーの判別
- 全ツール失敗時のエスカレーションメッセージ
- 最大10ステップのループ制御
解答例
const RETRYABLE_ERRORS = ["TIMEOUT", "RATE_LIMITED", "SERVICE_UNAVAILABLE"];
async function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function executeWithRetry(
name: string,
args: Record<string, unknown>,
maxRetries: number = 3
): Promise<ToolResponse> {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
const result = await executeTool(name, args);
if (result.success) return result;
// リトライ不可能なエラーは即座に返す
if (!RETRYABLE_ERRORS.includes(result.error?.code ?? "")) {
return result;
}
// 最後の試行なら失敗を返す
if (attempt === maxRetries) {
return {
success: false,
error: {
code: "MAX_RETRIES_EXCEEDED",
message: `${name} が ${maxRetries} 回のリトライ後も失敗しました: ${result.error?.message}`,
suggestion: "別の方法を試すか、人間のオペレーターにエスカレーションしてください"
}
};
}
// Exponential Backoff
await sleep(Math.pow(2, attempt) * 1000);
}
// ここには到達しないが型安全のため
return { success: false, error: { code: "UNEXPECTED", message: "予期しないエラー" } };
}
async function agentLoop(userMessage: string): Promise<string> {
const MAX_STEPS = 10;
const messages: Message[] = [
{ role: "system", content: "あなたはNetShop社のカスタマーサポートエージェントです。ツールを使って顧客の問題を解決してください。" },
{ role: "user", content: userMessage }
];
for (let step = 0; step < MAX_STEPS; step++) {
const response = await callLLM(messages, orderTools);
if (response.type === "text") {
return response.content;
}
if (response.type === "tool_call") {
const result = await executeWithRetry(
response.toolName,
response.toolArgs
);
messages.push(
{ role: "assistant", content: response.rawContent },
{ role: "tool", content: JSON.stringify(result) }
);
// エスカレーション判定
if (!result.success && result.error?.code === "MAX_RETRIES_EXCEEDED") {
messages.push({
role: "system",
content: "ツールの実行に失敗しました。エラー内容を踏まえて、顧客に状況を説明し、必要に応じて手動対応を案内してください。"
});
}
}
}
return "申し訳ございません。処理が複雑なため、カスタマーサポートチームに引き継ぎます。担当者から改めてご連絡いたします。";
}
ポイント:
- RETRYABLE_ERRORSで一時的エラーのみリトライ対象に
- MAX_RETRIES_EXCEEDED時にLLMに追加コンテキストを与えて適切な回答を促す
- 最大ステップ数到達時の人間へのエスカレーションメッセージ
達成度チェック
- Mission 1: 4つの注文管理ツールのスキーマを適切に設計できた
- Mission 1: 各ツールの説明文で類似ツールとの使い分けを明記できた
- Mission 2: Zodによるパラメータバリデーションを実装できた
- Mission 2: LLMが理解できるエラーレスポンスを設計できた
- Mission 3: リトライとフォールバック付きのエージェントループを実装できた
推定所要時間: 90分