Anthropic Tool Use を AWS Bedrock で活用する方法

こんにちは、マネックス証券の INSIGHT(投資情報)で開発をしている倉田です。

最近は AI を使ったプロジェクトの開発を担当しており、ネット上に情報がまとまっていなかった Anthropic の Tool use を AWS SDK (Bedrock) を使った実装を Java と Javascript (TypeScript) でご紹介したいと思います。

Tool Use を簡単に説明すると、OpenAI Function Calling の Anthropic 版です。
Function Calling を知らない人向けに下記にて詳しく解説します。

Function Calling / Tool use とは

OpenAI の GPT モデルや Anthropic の Claude モデルには、通常のチャットプロンプトによる自由度の高いテキスト生成とは異なり、より構造化されプログラムに統合しやすい形で生成ができる機能(API)が提供されています。

Function Calling / Tool use の仕組みをわかりやすく理解するために、AmazonのECサイト向けチャットボットの構築手順を見ていきましょう。

1. 一つ以上の Tool をあらかじめ決まった形式で定義します。
Tool とは…ユーザー入力から実行させたい仮想の関数だと思ってください。

{
  "tools": [
    {
      "name": "buy_book",
      "description": "指定された本を購入する",
      "input_schema": {
        "type": "object",
        "properties": {
          "book_title": {
            "type": "string",
            "description": "購入したい本のタイトル"
          },
          "quantity": {
            "type": "integer",
            "description": "購入する本の数量"
          }
        },
        "required": ["book_title"]
      }
    },
    {
      "name": "get_purchase_history",
      "description": "購入履歴を参照する",
      "input_schema": {
        "type": "object",
        "properties": {
          "year": {
            "type": "integer",
            "description": "購入履歴を参照したい年"
          }
        }
      }
    }
  ]
}

2. 上記で定義した Tools と、処理したいユーザー入力(例:「去年何を買ったか知りたい」)をプロンプトとして、Function Calling / Tool use のAPIリクエストを送信します。

3. 上記リクエスト結果として、あらかじめ定義した Tools から生成AIによって選択された Tool の定義通りの出力が返されます。

{
   "type":"tool_use",
   "id":"toolu_01A09q90qw90lq917835lq9",
   "name":"get_purchase_history",
   "input":{
      "integer":2023
   }
}

ご覧の通り、出力は一貫性があり、JSON形式なのでビジネスロジックへの組み込みが容易です。

まとめ: Function Calling / Tool use とは、AI がユーザーの自然言語の入力を解釈し、そのコンテキストに基づいて事前に定義された Tools から最適なものを選択します。選択されたToolに応じてデータを抽出・生成し、結果をJSON形式で返すAPI機能です。

【OpenAI】Function Calling v.s. Tool Use【Anthropic】

現在、主に担当しているプロジェクトでは Azure OpenAI の Function Calling を使用していますが、別プロジェクトでは AWS Bedrock Anthropic の Tool use を利用しています。これら両方を利用した実体験に基づく見解となります。

では、なぜ今回 Tool use の紹介に絞ったかというと、冒頭でも触れたように、Tool use は 2024年5月リリースされたばかりで、まだ情報が少ないことと、以下の理由から個人的におすすめだからです。

  • AWS上で開発している場合、既存の契約で利用できるため、新たにベンダーとの契約を結ぶ手間を省ける
  • 日本リージョンの Azure が提供するGPTモデルが古い(古いモデルは性能が低く、燃費もわるい)
  • AWS では Claude の最新モデルが使え、アップデートも早い
  • 体感ですが、Claude モデルの方がプロンプトに忠実に動作してくれる傾向がある

既に AWS アカウントをお持ちの場合は、申請だけで簡単に利用を開始できるので、ぜひお試しください。以下に、Java と TypeScript での簡単な実装方法を紹介します。

Tool Use の実装

事前準備

  • JDK (Java開発キット) のインストール【Java用】(java21)
  • Gradle のインストール【Java用】(8.9)
  • Node.js のインストール【TypeScript用】(v20)
  • yarn / npm のインストール【TypeScript用】(v10.1.0)
  • Bedrock で Claude Sonnet 3.5/Haiku モデルをAWSコンソールで有効にする
    • Bedrock > モデルアクセス > Claude 3.5 Sonnet > リクエスト可能 > モデルアクセスをリクエスト
  • AWS Config File (~/.aws/config): に有効なクレデンシャル情報が設定されている

Tool Use の Java での実装

Javaでの実装例です。 先ほど Function Calling の仮想シナリオとして挙げた Amazon EC サイトのチャットボットを実装します。

1. プロジェクトの作成:

mkdir AmazonBot
cd AmazonBot
gradle init --type java-application

gradle init プロンプト: target Java version [enter] -> Project name [enter] -> Select application structure [Single application project] -> Select build script DSL [Groovy] -> Select test framework [何でも] -> Generate build using new APIs and behavior [enter]

2. /app/build.gradle にてライブラリ追加

dependencies {
    implementation platform('software.amazon.awssdk:bom:2.25.64')//追加
    implementation "software.amazon.awssdk:bedrockruntime"//追加
    implementation 'ch.qos.logback:logback-classic:1.2.10'//awssdkがログで利用
}

3. 以下 Tool use のサービスクラスを作成: /app/src/main/java/org/example/AnthropicToolService.java

package org.example;

import software.amazon.awssdk.auth.credentials.ProfileCredentialsProvider;
import software.amazon.awssdk.services.bedrockruntime.BedrockRuntimeClient;
import software.amazon.awssdk.services.bedrockruntime.model.*;
import software.amazon.awssdk.core.document.Document;
import software.amazon.awssdk.regions.Region;

import java.util.List;
import java.util.Collections;

public class AnthropicToolService {

  // AWS Bedrock Runtimeクライアントを初期化
  private final BedrockRuntimeClient client = BedrockRuntimeClient.builder()
    .region(Region.AP_NORTHEAST_1)// 必要に応じてリージョンを指定
    .credentialsProvider(ProfileCredentialsProvider.create())
    .build();

  // ユーザーの入力に基づいて適切なツールを選択し、Anthropicモデルで実行する
  String executeToolUse(String userMessage) {
  
    InferenceConfiguration inferenceConfig = InferenceConfiguration.builder()
      .maxTokens(100)
      .temperature(1f)
      .build();

    // Tool Use リクエストを作成
    ConverseRequest request = ConverseRequest.builder()
      // .modelId("anthropic.claude-3-haiku-20240307-v1:0") // haikuモデル
      .modelId("anthropic.claude-3-5-sonnet-20240620-v1:0") // sonnetモデル
      .inferenceConfig(inferenceConfig)
      .toolConfig(createToolConfig())
      .messages(createMessage(userMessage))
      .build();

    // Tool use のリクエストを送信
    ConverseResponse response = client.converse(request);
    ToolUseBlock toolUse = response.output().message().content().getLast().toolUse();

    // Tool Use の結果を読みやすいように整形して返す(ビジネスロジックの実装)
    return (toolUse == null) ? "有効なツールの使用が検出されませんでした。" :
      String.format("入力: %s\r\n選択したツール: %s\r\n抽出したパラメータ: %s\r\n", userMessage, toolUse.name(), toolUse.input().toString());
  }

  // ユーザーのメッセージを作成
  private List<Message> createMessage(String userMessage) {
    return List.of(
      Message.builder()
        .role("user")
        .content(List.of(
            ContentBlock.builder()
              .text(userMessage).build()))
      .build());
  }

  // 使用するツールの定義を作成
  private ToolConfiguration createToolConfig() {
    return ToolConfiguration.builder()
      // デフォルトの Auto ではなく Any にする事で、モデルがツールを必ず使用するように強制する
      .toolChoice(ToolChoice.builder().any(AnyToolChoice.builder().build()).build())
      .tools(List.of(
        createTool("buy_book", "指定された本を購入する",
          createToolProperty(List.of(
            createToolFields("book_title", "string", "購入したい本のタイトル"),
            createToolFields("quantity", "integer", "購入する本の数量")), List.of("book_title"))),
        createTool("get_purchase_history", "Amazonでの購入履歴を取得する",
          createToolProperty(List.of(createToolFields("year", "integer", "購入履歴を取得したい年")), Collections.emptyList()))
      ))
      .build();
  }

  // ツール定義を作成
  private Tool createTool(String name, String description, Document schema) {
    ToolSpecification toolSpec = ToolSpecification.builder()
        .name(name)
        .description(description)
        .inputSchema(ToolInputSchema.builder().json(schema).build())
        .build();
    return Tool.builder().toolSpec(toolSpec).build();
  }

  // ツールのフィールドを作成(任意フィールド、必須フィールド)
  private Document createToolProperty(List<Document> fields, List<String> requiredFields) {
    Document.MapBuilder propertiesBuilder = Document.mapBuilder();
    fields.forEach(field -> propertiesBuilder.putDocument(field.asMap().get("type").asString().toLowerCase(), field));
    return Document.mapBuilder().putString("type", "object")
      .putDocument("properties", propertiesBuilder.build())
      .putList("required", requiredFields.stream().map(Document::fromString).toList())
      .build();
  }

  // ツールのフィールドの詳細を作成
  private Document createToolFields(String name, String type, String description) {
    return Document.mapBuilder().putString("type", type).putString("description", description).build();
  }
}

4. 以下アプリ実行クラスを更新: /app/src/main/java/org/example/App.java

package org.example;

public class App {
    public static void main(String[] args) {
        AnthropicToolService anthropicToolService = new AnthropicToolService();
        String response1 = anthropicToolService.executeToolUse("2023年に買ったものを教えて");
        String response2 = anthropicToolService.executeToolUse("ハリーポッターを2冊買いたい");
        String response3 = anthropicToolService.executeToolUse("お腹すいたな。。");
        System.out.println(response1);
        System.out.println(response2);
        System.out.println(response3);
    }
}

5. アプリ実行 ※AppTest.java のテストコードをコメントアウト ./gradlew build && ./gradlew run

6. 出力結果

入力: 2023年に買ったものを教えて
選択したツール: get_purchase_history
抽出したパラメータ: {"integer": 2023}

入力: ハリーポッターを2冊買いたい
選択したツール: buy_book
抽出したパラメータ: {"string": "ハリーポッター","integer": 2}

入力: お腹すいたな。。
選択したツール: buy_book
抽出したパラメータ: {"book_title": "料理本","integer": 1}

このように、入力に対して適切なツールを使用し、定義通りにデータが抽出されていることが確認できます。最後の出力では、親切に料理本を1冊注文してくれました!

また、デフォルトの Auto ではなく、any(AnyToolChoice) で設定することで、必ずツールを使用するように制約をかけられ、余計なテキスト生成を抑制できます。ユーザー入力に該当するツールがない場合は、ツールでの出力を避けたいケースが多いので、入力プロンプトを工夫したり、ダミーツールを定義することで制御が可能です。

以下の TypeScript 実装では、該当するToolがない場合には、出力を避ける実装になっています。

Tool Use の TypeScript での実装

以下はTypeScriptでの実装手順です。

1. プロジェクトの作成 + ライブラリ追加:

mkdir AmazonBot
cd AmazonBot
npm init # プロンプトは全て[Enter]
npm install typescript --save-dev
npm install @aws-sdk/client-bedrock-runtime
npx tsc --init

2. /index.ts 作成し Tool use を実装:

import {
  Message,
  BedrockRuntimeClient,
  ConverseCommand,
  ToolConfiguration,
} from "@aws-sdk/client-bedrock-runtime";

const client = new BedrockRuntimeClient({
  region: "ap-northeast-1",
});

const toolConfig: ToolConfiguration = {
  tools: [
    {
      toolSpec: {
        name: "buy_book",
        description: "指定された本をAmazonで購入する",
        inputSchema: {
          json: {
            type: "object",
            properties: {
              book_title: {
                type: "string",
                description: "購入したい本のタイトル",
              },
              quantity: {
                type: "integer",
                description: "購入する本の数量",
              }
            },
            required: ["book_title"],
          }
        }
      }
    },
    {
      toolSpec: {
        name: "get_purchase_history",
        description: "Amazonでの購入履歴を取得する",
        inputSchema: {
          json: {
            type: "object",
            properties: {
              year: {
                type: "integer",
                description: "購入履歴を取得したい年",
              }
            }
          }
        }
      }
    }
  ],
  toolChoice: { auto: {} },
};

export async function excecAnthropicTools(userInput: string) {
  const conversation = [{
    role: "user",
    content: [{
      text: `ツールのみで応答してください。入力が明らかに書籍や購入履歴に関係ない場合は応答しないでください:${userInput}`,
    }]
  }];

  const command = new ConverseCommand({
    modelId: "anthropic.claude-3-haiku-20240307-v1:0",
    messages: conversation as Message[],
    toolConfig: toolConfig,
  });

  const response = await client.send(command);
  const toolUse = response?.output?.message?.content?.[0]?.toolUse;

  return !toolUse?.name 
    ? "該当するツールが見つかりませんでした"
    : `入力: ${userInput}
選択したツール: ${toolUse?.name}
抽出したパラメータ: ${JSON.stringify(toolUse?.input)}
`;  
}

(async () => {
  const messages = [
    "2023年に買ったものを教えて",
    "ハリーポッターを2冊買いたい",
    "お腹すいたな。。"
  ];
  const responses = await Promise.all(messages.map(excecAnthropicTools));
  console.log(responses.join("\r\n"));
})();

3. 実行

npx tsc && node index.js

4. 出力結果

入力: 2023年に買ったものを教えて
選択したツール: get_purchase_history
抽出したパラメータ: {"year": 2023}

入力: ハリーポッターを2冊買いたい
選択したツール: buy_book
抽出したパラメータ: {"book_title": "ハリーポッター", "quantity": 2}

該当するツールが見つかりませんでした

TypeScript 実装でも、適切なツールで定義通りのデータが抽出されました。 今回は「お腹すいたな。。」の入力で「該当するツールが見つかりませんでした」を出力してくれました。デフォルトの toolChoice: auto で実装しており、61行目の入力プロンプトの工夫と合わせることで、該当する tool がない場合に出力を抑えることができます。

また、TypeScript では JSON 形式で Tools の定義ができ、型チェックも入るため安全かつ少ないコード量で効率よく開発できるので、個人的にオススメです。 特に、Node.js 環境での JavaScript は非同期処理に強く、CPU集約型ではなく、I/O集約型のアプリに適しています。(Tool use リクエストがアプリのボトルネックになる場合は特に)。

ただし、最終的なサーバーコストは、実装する仕組みやシステムの規模によって大きく異なるため、一概には言えません。プロジェクトの要件やシステムの規模、将来の拡張性、チームのスキルセットなどを総合的に考慮して言語を選択すことが重要です。

Tool use(Function Calling)・ Agent

上記の実装では、コンソールへの出力までしかしておりませんが、実際のアプリでは JSON 形式の出力結果に基づく後続の処理や関数の実行は各自で実装する必要があります。ただし、LangChain ライブラリの Agent 機能 や Bedrock の Agent サービス を利用することで、ビジネスロジックの呼び出しや目的の結果が得られるまでの反復処理などの実装を省くことができます。

あくまで個人的な意見ですが、以下の理由から、Agent のライブラリやサービスに依存せずに、 今回のように Converse API を使って LLM提供元の API を直接利用することに多くのメリットがあると考えています。

  • 頻繁にリリースされる最新モデルへのアップグレードや調整が容易
  • 新機能への迅速な対応が可能(例: 2024年8月リリースの OpenAI Structured Outputs)
  • トークン数を節約する工夫や、 プロジェクト固有の要件に合わせたカスタマイズ等が容易
  • サードパーティライブラリに起因するリスクや制約を回避

とはいえ、Agentのライブラリやサービスを使うことで、開発時間を大幅に削減できる場合もありますので、社内ツールやPOCなど、特定のシーンでは活用を検討してみて下さい。

最後に

今回は、AWS Bedrock を使った Anthropic の Tool use の実装方法を Java と TypeScript で紹介しました。コピペで簡単に試せるように構成していますので、ぜひ実際に動かしてみてください。

Function Calling / Tool use はさまざまな場面で応用が可能です。以下にいくつかのユースケース例を紹介しますので、参考にしてみてください。

  • 自然言語や画像の解析・分析・情報抽出、からのプログラム処理
  • プログラムでの実現が難しいような複雑な分岐、選択、分類処理

今回の内容が、皆様のAIを活用したプロジェクトや開発におけるヒントになれば幸いです。

参考リンク:

システム開発二部 第二プロダクトグループ
MONEX INSIGHT Engineer
倉田 賢