Snowflake経由でClaudeのTool Useを試す

Snowflake Cortex では LLM の Anthropic Claude が提供されており、その一部の機能として外部ツールを実行するための機能 Tool Use という機能があります。この記事ではその機能の動作確認の結果を記します。

最初に断っておきますが、まともに動いてはいないです。

# 1. はじめに

最近は AI エージェントが非常に話題になっています。AI エージェントとは何かというと定義は結構難しいと思っているのですが、

- 複数のタスクからなる複雑なワークを計画できる
- 決められたタスクを決められた順番で実行するだけではなく、タスクの結果を受けてその後に実行すべきタスクを柔軟に選択できる
- 外部から追加の情報を取得したり、外部に影響を与えるアクションを行うということが自律的にできる

という特徴がよく挙げられるかと思います。

このうち、3 番目の特徴を実現するために Anthropic Claude には Tool Use という機能があります。

https://docs.anthropic.com/en/docs/agents-and-tools/tool-use/overview

ユーザーの指示を受けて外部ツールの実行が必要であると判断すると、そのツール（Server Tool）を自動で実行したり、ユーザーにツール（Client Tool）の実行を指示したりする機能になります。

例えば、以下のようなことが実現できます。

- ユーザーの質問に答えるためには LLM の学習データには含まれない最新の情報が必要なため、Web から情報を収集する
- ユーザーの指示に基づいて SaaS などの操作を自動で行う

Snowflake Cortex においても Claude は AWS オレゴン/バージニアのリージョンに限り提供されており、この Tool Use の機能は限定的ながらも利用できます。

https://docs.snowflake.com/en/user-guide/snowflake-cortex/cortex-llm-rest-api#tools-configuration

https://quickstarts.snowflake.com/guide/getting-started-with-tool-use-on-cortex-and-anthropic-claude/index.html#0

今回はこの機能を使い、上で述べた「ユーザーの質問に答えるためには LLM の学習データには含まれない最新の情報が必要なため、Web から情報を収集する」を試してみた結果を記します。

ちなみに、今回は Claude への問い合わせはすべて手動で実行しており AI エージェントっぽくはないので、その点はご容赦ください。また、なぜ Snowflake でやっているかについては完全な趣味です。

# 2. 動作確認

## 2-1. Cortex 呼び出しに必要な JWT の取得

Snowflake 上で Claude の Tool Use 機能を使うには REST API 経由である必要があります（通常の SQL セッションからの ```COMPLETE``` 関数では利用できない）。

REST API を実行するためには、認証として JWT（JSON Web トークン）、OAuth、programmatic access token が利用できますが、今回は JWT を利用します。

JWT の生成方法はいくつかありますが、snowsql コマンドを使うと簡単に生成できます。以下に例を示します。

```sh
snowsql \
    --private-key-path <キーペア認証の秘密鍵ファイル> \
    --generate-jwt \
    -a <アカウント名> \
    -u <ユーザー名>
```

秘密鍵ファイルのパスフレーズが聞かれるので入力すると JWT が出力されるので、それをメモっておきます。この JWT の有効期限はデフォルトでは 90 分です。

## 2-2. Cortex へのリクエストの作成

Cortex に投げるリクエストは大きく以下の構造になります。

```json
{
  "model": "claude-3-5-sonnet",
  "messages": (質問内容),
  "tools": (利用可能な外部ツールの仕様),
  "tool_choice": (外部ツール選択における設定),
  "max_tokens": 4096,
  "top_p": 1,
  "stream": false
}
```

順に具体的な設定内容とともに説明していきます。

### LLM の指定
まずは今回利用する LLM を指定します。

```json
{
  "model": "claude-3-5-sonnet"
}
```

Snowflake Cortex では Calude に関して 3.5 Sonnet と 3.7 Sonnet の 2 つが提供されています。Tool Use は 3.5 しか対応していないらしいため、ここでは 3.5 を指定します。

> Tool use is only supported with Claude 3.5 Sonnet.

https://docs.snowflake.com/en/user-guide/snowflake-cortex/cortex-llm-rest-api#tools-configuration

### 質問内容の指定

次に LLM に投げる質問を指定します。

```json
{
  "messages": [
    {
      "role": "system",
      "content": "ユーザーの質問に対して回答してください。もしあなたが知らないこと、または未来の出来事について質問された場合は、。tavily_searchツールを利用するように回答してください。"
    },
    {
      "role": "user",
      "content": "日本の2025年スーパーフォーミュラ第3戦の優勝者は誰ですか？"
    }
  ]
}
```
本質である質問本文（```"role": "user"``` の直下の「日本の2025年スーパーフォーミュラ第3戦の優勝者は誰ですか？」の部分）を見てほしいのですが、2025 年の出来事についての質問になっています。Claude 3.5 Sonnet は 2024 年 6 月にリリースされているため、3.5 の学習データにはこの質問に答えるための情報が含まれていません。そのため、Web から最新の情報を取得してくる必要がある質問になっています。

```"role": "system"``` の部分はシステムプロンプトです。（おまじない程度で、実際にはあまり効果はありません）

### 外部ツールの仕様の指定

次に、利用可能な外部ツールの仕様を記述します。

```json
{
  "tools": [{
    "tool_spec": {
      "type": "generic",
      "name": "tavily_search",
      "description": "与えられた質問の回答に必要な情報をWebから検索し、結果を返します。",
      "input_schema": {
        "type": "object",
        "properties": {
          "question": {
            "type": "string",
            "description": "Web検索クエリー"
          }
        },
        "required": ["question"]
      }
    }
  }]
}
```
今回は Tavily というサービスを利用して Web から情報を検索する外部ツールを準備しています（内容は後述）。その仕様を記述します。

- ツールの名前とその説明
- ツールの引数

### ツール選択の設定の指定

次に外部ツール選択における設定を指定します。

```json
{
  "tool_choice": {
    "type": "tool",
    "name": ["tavily_search"]
  }
}
```

```name``` に利用可能なツールの一覧を指定することに加え、```type``` でツールを選択する際の戦略を指定します。以下の３つが使えるようです。

| type | 内容 |
|:-:|:-:|
| auto  | ツールを使うかは LLM が自動で判断する  |
| required  | 指定されたツールを１つ以上必ず使う  |
| tool  | 指定されたツールはすべて使う  |

本来は ```auto``` が推奨されるのですが、これを指定した場合にツールが選択されない事象に多く当たったので、今回は ```tool```（指定したツールを必ず使う）を指定しています。

### その他のパラメーターの指定

最後に問い合わせのパラメーターを指定します。

```json
{
  "max_tokens": 4096,
  "top_p": 1,
  "stream": false
}
```

```max_tokens``` は出力トークンの最大値（指定できる最大が 4096）、```top_p``` は生成結果のバリエーションを持たせるかの指定です。

```stream``` に関しては結果をストリーム = 小分けに出力するか、一括で出力するかの指定です。デフォルトは ```true``` です。```true``` にすると出力結果を画面にリアルタイム表示するには都合がいい良いのですが、小分けにされたデータを統合する手間が面倒なので、今回は ```false``` にしています。

## 2-3. リクエストの送信と結果の確認

ここまででリクエストを投げる順義が整ったので、curl コマンドでリクエストを投げます。

```sh
curl --dump-header - \
    -X POST \
    -H "Authorization: Bearer ${SNOWFLAKE_JWT}" \
    -H "Content-Type: application/json" \
    -H "Accept: application/json" \
    -d "${REQUEST_BODY}" \
    https://OEDVIGF-USSTD.snowflakecomputing.com/api/v2/cortex/inference:complete
```

- ```SNOWFLAKE_JWT``` には 2-1. で作成した JWT
- ```REQUEST_BODY``` には 2-2. で作成した JSON

を指定し実行します。

すると、以下のような結果が返ってきます。（JSON を整形しています）

```json
{
  "choices":[{
    "message":{
      "content_list":[
        {
          "type":"text",
          "text":""
        },
        {
          "type":"tool_use",
          "tool_use":{
            "tool_use_id":"tooluse_FYqddJtkRmWjbQI7DzEN7w",
            "name":"tavily_search",
            "input":{"question":"2025年 スーパーフォーミュラ 第3戦 優勝者"}
          }
        }
      ]
    }
  }],
  "usage":{"prompt_tokens":635,"completion_tokens":25,"total_tokens":660}
}
```

ポイントは ```"type":"tool_use"``` と書かれた部分になるのですが、```name``` に実行すべきと判断された外部ツールの名前（```tavily_search```）、```input``` にその外部ツールへ渡すべき引数（```{"question":"2025年 スーパーフォーミュラ 第3戦 優勝者"}```）が含まれています。

また、```tool_use_id``` がありますが、これは外部ツールの結果を LLM へ渡す際に、どの外部ツール利用の指示に対応しているかを追跡するための識別になります。

## 2-4. 外部ツールの実行

外部ツールとして今回は以下のようなスクリプトを用意しています。本家 Claude の Tool Use は Claude 側で Web 検索を実行してくれる（Server Tool）のですが、Snowflake Cortex の Claude にはその機能はないため、Client Tool としてローカルで外部ツールを準備・実行します。

```python
import requests

def tavily_search(param):
    headers = {
        'Authorization': f'Bearer {tavily_token}',
        'Content-Type': 'application/json'
    }
    
    request_body = {
        'query': param["question"],
        'include_raw_content': True
    }
    
    response = requests.post(
        url = 'https://api.tavily.com/search',
        headers = headers,
        json = request_body
    )
    
    return response
```

このスクリプトは [Tavily](https://www.tavily.com/) というサービスを利用して Web 検索を実施しています。

このスクリプトを 2-4. で指定された引数で実行すると、以下の結果が返ってきます。（検索結果本文も ```content``` に含みますが、記事上では省略しています）

```json
{
  "query":"2025年 スーパーフォーミュラ 第3戦 優勝者",
  "follow_up_questions":null,
  "answer":null,
  "images":[],
  "results":[
    {
      "title":"牧野任祐、完勝で2連勝達成。岩佐歩夢はまさかのトラブルでポイントリーダー陥落｜スーパーフォーミュラ第3戦もてぎ",
      "url":"https://jp.motorsport.com/super-formula/news/2025-sf-r3-race-report/10714835/",
      "content":"..."
     },
     {
       "title":"2025 Super Formula Championship - Wikipedia",
       "url":"https://en.wikipedia.org/wiki/2025_Super_Formula_Championship",
       "content":"..."
     },
     {
       "title":"2025年のスーパーフォーミュラ - Wikipedia",
       "url":"https://ja.wikipedia.org/wiki/2025年のスーパーフォーミュラ",
       "content":"..."
     },
     {
       "title":"2025年のf1第3戦・日本グランプリは鈴鹿サーキットに26万6000人が来場! 王者の意地を見せたマックス・フェルスタッペン選手がポール ...",
       "url":"https://motorsports.jaf.or.jp/enjoy/topics/2025/20250414-02",
       "content":"..."
     },
     {
       "title":"【正式結果】2025スーパーフォーミュラ第3戦もてぎ 決勝（オートスポーツweb） - Yahoo!ニュース",
       "url":"https://news.yahoo.co.jp/articles/a14793d2bfd6379e8678dd2d1e13cbfc3fedb0e5",
       "content":"..."
     }
   ],
   "response_time":1.22
   }
```

4 番目の検索結果は無関係（スーパーフォーミュラではなく F1 に関する記事）ですが、その他は質問内容の答えが載っていそうな Web ページを拾ってこれていますね。

## 2-5. 外部ツールの結果を含めた再度の Cortex へのリクエスト

外部ツールの結果を含めて再度 LLM に問い合わせを投げます。（外部ツールの結果は長いので表記上 XXXXX に置き換えています）

```sh
curl --dump-header - \
    -X POST \
    -H "Authorization: Bearer ${SNOWFLAKE_JWT}" \
    -H "Content-Type: application/json" \
    -H "Accept: application/json" \
    -d '{
        "model": "claude-3-5-sonnet",
        "messages": [
            {
                "role": "user",
                "content": "日本の2025年スーパーフォーミュラ第3戦の優勝者は誰ですか？以下の情報に基づいて答えてください。XXXXX"
            }
        ],
        "max_tokens": 4096,
        "top_p": 1,
        "stream": false
    }' \
    https://OEDVIGF-USSTD.snowflakecomputing.com/api/v2/cortex/inference:complete
```

本来は 2-2. で作成した JSON の ```messages``` に 2-3. の LLM の回答と 2-4. の外部ツールの結果を追加して送るべきなのですが、そうすると「わかりません」という回答か、もう一度ツールを使うよう回答するかしかしないため、シンプルな質問に書き直しています。

結果は以下になります。

```json
{
    "choices":[{
        "message":{
            "content":"提供された情報のタイトルから、牧野任祐が2025年スーパーフォーミュラ第3戦もてぎで優勝し、2連勝を達成したことがわかります。",
            "content_list":[{
                "type":"text",
                "text":"提供された情報のタイトルから、牧野任祐が2025年スーパーフォーミュラ第3戦もてぎで優勝し、2連勝を達成したことがわかります。"
            }]
        }
    }],
    "usage":{"prompt_tokens":456,"completion_tokens":65,"total_tokens":521}
}
```

一応、正しい結果にはなっているようです。

# 3. さいごに

今回は Snowflake Cortex 経由で Claude の Tool Use を使ってみましたが、

- tool_choice を ```auto```（LLM に外部ツール実行の要否を判断させる）では、外部ツールを実行する旨の回答にならなかった
    - 英語の質問をするとうまく動くケースもある
    - 日本語で質問をすると全滅
- 外部ツールの結果を ```messages``` に追記しても、その結果に基づいて回答してくれなかった

という状況で、まともに動いているとは言い難いです。

ここまで試して & 書いておいてあれですが、実は

- Snowflake のドキュメント/クイックスタートと Claude 本家のドキュメントを見比べると、リクエストのフォーマとなどが異なっている部分が複数個所ある
- Claude 本家ではあるはずの Server Tool (web serarch) の機能が Snowflake では存在しない

などの違いがあり、Snowflake Cortex の Tool Use と Claude 本家の Tool Use は別物ではないかと思い始めています。いずれサポートに確認した方がよいかな。