EXERCISE 90分

ストーリー

田中VPoE
ツール設計の理論を学んだところで、実際にNetShop社の業務ツールを設計・実装してみよう
あなた
どんなツールを作るんですか?
田中VPoE
カスタマーサポートエージェント用のツールセットだ。注文管理、在庫確認、配送追跡の3つの業務領域をカバーするツールを設計してもらう
あなた
ファンクションコーリングのスキーマ定義から、エラーハンドリングまで一通り実装するんですね
田中VPoE
そうだ。特に重要なのは、LLMがツールを正しく選択できる説明文を書くこと。曖昧な説明文では実運用で使い物にならない

ミッション概要

項目内容
目標カスタマーサポートエージェント用の業務ツールを設計・実装する
所要時間90分
ミッション数3つ
使用知識Tool設計原則 / ファンクションコーリング / エラーハンドリング
評価観点スキーマ設計の品質、説明文の明確さ、エラー処理の網羅性

Mission 1: 注文管理ツールセットの設計

要件

NetShop社のカスタマーサポートで必要な注文管理ツールを設計してください。

必要なツール:

  1. 注文検索(複数条件で注文を検索)
  2. 注文詳細取得(特定の注文の完全な情報)
  3. 注文キャンセル(注文のキャンセル処理)
  4. 注文ステータス更新(配送状況等の更新)

設計要件:

  • 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分