AIエージェント開発ガイド——Claude APIで最初の一体を作る

「AIエージェント」という言葉はよく聞くものの、実際に何を実装すれば「エージェント」と呼べるのか、イメージが掴みにくい方も多いはずです。この記事では、Claude APIを使って最小構成のAIエージェントを実際に組み立てながら、その構造を理解していきます。難しい概念は出てきません。API呼び出し1回で完結する「チャットボット」と、複数ステップで自律的にタスクを進める「エージェント」の違いが、コードレベルで見えるようになることがゴールです。

エージェントとチャットボットの違い

単純なチャットボットは、ユーザーの入力をAPIに渡し、返ってきたテキストをそのまま表示するだけです。一方でAIエージェントは、以下のようなループを持ちます。

  1. ユーザーの指示を受け取る
  2. モデルが「次に何をすべきか」を判断する(回答を返す、あるいはツールを呼び出す)
  3. ツール呼び出しが必要なら実際に実行し、結果をモデルに返す
  4. モデルが結果を踏まえて次のアクションを判断する(2に戻る、または最終回答を返す)

つまりエージェントの正体は、「モデルにツールを与え、判断結果を実行してまた渡す」というループの実装そのものです。これから作る最小構成でも、この骨格は変わりません。

準備:APIキーとSDK

Anthropicのコンソールから取得したAPIキーを使います。APIキーはクライアント側のコードに埋め込まず、サーバー側の環境変数として扱ってください。

bun add @anthropic-ai/sdk
# .dev.vars(ローカル開発用・gitignore対象)
ANTHROPIC_API_KEY=sk-ant-xxxx

ステップ1:まずは1回だけ呼び出す

最初にツールなしの単純な呼び出しを確認します。

import Anthropic from '@anthropic-ai/sdk';

const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });

const response = await client.messages.create({
  model: 'claude-sonnet-5',
  max_tokens: 1024,
  system: 'あなたは業務効率化を支援するアシスタントです。簡潔に回答してください。',
  messages: [{ role: 'user', content: '在庫管理システムを作る際の注意点を3つ教えて' }],
});

console.log(response.content);

systemパラメータでエージェントの役割を定義し、messagesに会話履歴を渡す、という構造はどのモデル呼び出しでも共通です。ここまではチャットボットと変わりません。

ステップ2:ツールを定義する

エージェントらしさが出てくるのはここからです。モデルに「使えるツール」を宣言し、モデル自身に「今このツールを呼ぶべきか」を判断させます。例として、社内の在庫DBを検索するツールを定義します。

const tools = [
  {
    name: 'search_inventory',
    description: '商品名またはSKUで在庫データベースを検索し、在庫数と保管場所を返す',
    input_schema: {
      type: 'object',
      properties: {
        query: { type: 'string', description: '検索したい商品名またはSKU' },
      },
      required: ['query'],
    },
  },
];

descriptionはモデルがツールの用途を理解するための唯一の手がかりです。曖昧な説明にすると、モデルが不要な場面でツールを呼んだり、逆に呼ぶべき場面で呼ばなかったりするため、「いつ使うべきか」まで書き込むのがコツです。

ステップ3:実行ループを組む

モデルがツール呼び出しを要求したら、実際にコードでそれを実行し、結果を会話履歴に追加してモデルに返します。この往復こそがエージェントの中核です。

async function searchInventory(query: string) {
  // 実際には社内DBへのクエリに置き換える
  return { query, stock: 42, location: 'A-3棚' };
}

async function runAgent(userMessage: string) {
  const messages: Anthropic.MessageParam[] = [{ role: 'user', content: userMessage }];

  while (true) {
    const response = await client.messages.create({
      model: 'claude-sonnet-5',
      max_tokens: 1024,
      system: 'あなたは在庫管理を支援するアシスタントです。必要に応じてsearch_inventoryツールを使ってください。',
      tools,
      messages,
    });

    messages.push({ role: 'assistant', content: response.content });

    if (response.stop_reason !== 'tool_use') {
      // ツール呼び出しが不要 = 最終回答が出た
      return response.content;
    }

    // ツール呼び出しブロックを取り出して実行する
    const toolResults = [];
    for (const block of response.content) {
      if (block.type === 'tool_use' && block.name === 'search_inventory') {
        const result = await searchInventory(block.input.query as string);
        toolResults.push({
          type: 'tool_result' as const,
          tool_use_id: block.id,
          content: JSON.stringify(result),
        });
      }
    }

    messages.push({ role: 'user', content: toolResults });
    // ループの先頭に戻り、ツール結果を踏まえた次の判断をモデルに求める
  }
}

このループには終了条件が2つ埋め込まれています。stop_reasontool_use以外になった時点でループを抜ける、という条件です。実運用ではここに加えて、無限ループを避けるための最大反復回数(例えば10回)も必ず設定してください。モデルがツール呼び出しを繰り返し続けるケースはまれに発生します。

実運用で気をつけること

  • ツールの副作用を最小化する: 検索のような読み取り専用のツールはリスクが低いですが、DBの更新や外部APIへの送信を行うツールをエージェントに持たせる場合は、実行前に確認ステップを挟む、あるいは影響範囲を限定した権限のトークンを使うといった対策が必要です。
  • 入力のバリデーションを省略しない: input_schemaはモデルへのヒントであって、実行時の型安全性を保証するものではありません。ツール関数の内部では、受け取った引数を通常のAPIエンドポイントと同じように検証してください。
  • 反復回数とタイムアウトの上限を設ける: ループが想定外に長引くと、APIコストが線形に増加します。ステップ数の上限とタイムアウトを必ずコードレベルで持たせます。
  • ログを残す: どのツールが、どんな引数で、何回呼ばれたかを記録しておくと、期待通りに動かなかった際の原因調査が格段に楽になります。

ここまでで、ツール呼び出しを含む最小構成のエージェントが動く状態になりました。次のステップとしては、ツールを複数用意して使い分けさせる、あるいはツールの実装を外部サーバーに切り出して再利用可能にする、という方向があります。後者の仕組みが、次に紹介するMCP(Model Context Protocol)です。