本記事は Anthropic Claude Advent Calendar 2023 の 5 日目の記事です。タイトルは 1 日目の記事のタイトルを気に入ったので拝借。
現在開発中のアルファ版の機能について主に言及します。したがって、読まれるタイミングによってはそのまま実行できないこともあるかと思うので、適宜公式ドキュメントや公式リポジトリを参照されてください。
tl;dr
- Claude でも Function Calling が使えるよ!
- 自分で好きな Tools を定義できるよ!
- Anthropic 公式の API で使えるよ!
- Amazon Bedrock の API でも使えるよ!
- Anthropic Tools のリポジトリを翻訳、実行確認したよ!
- Claude は推し!Claude は推し!
- X フォローしてくださいな!
本記事の登場人物
Claude
Anthropic の開発する対話型生成系 AI サービス。OpenAI の ChatGPT と双対をなす対話型 AI。大規模言語モデル(Large Language Model, LLM)を利用して、自然言語に対して自然言語で答えてくれるサービス。
Function Calling
LLM(大規模言語モデル)が外部ツールや API と連携するための機能。LLM の自然言語の入力クエリを API コールに変換し、複雑なクエリに対しても正確に構造化データとして応答を返すためのもの。
詳細は OpenAI の Function calling ページを参照してください。あるいは、Anthropic の Intro to Function Calling の Google Spreadsheet を参照してください。
Function Calling を Claude で使うには?
結論から言うと、現状開発中の公式実装を使いましょう。Anthropic Tools と Google Spreadsheet から Anthropic API を叩く Claude for Sheets というものが開発されています。ただし、どちらもアルファ版ですので本番環境での利用は現状非推奨です。
実は非公式実装であれば以前からいくつか存在します。たとえば、Anthropic with Functions や Bedrock Node などがあります。みんなが待ち望んでいた機能であることが見て取れますね。Claude は推し。
Claude で Function Calling を使ってみよう
Anthropic Tools に準拠して進めていきましょう。基本的に Anthropic Tools の README の翻訳に加えて、適宜情報の追加、コードの補完、修正をしています。動作確認済みですので、この記事の通りに実行すれば動くはず。
Anthropic API Key を準備
方法はふたつ。Anthropic の API 申請の通っている方は 1 を、Bedrock で使いたい方は 2 から API Key を発行してください。
もし Anthropic の API を利用したい方はこちらから申請可能です。
リポジトリのクローンと環境変数の設定
# リポジトリのクローンとディレクトリの変更
git clone https://github.com/anthropics/anthropic-tools.git
cd anthropic-tools
# Anthropic Console をお使いの場合
export ANTHROPIC_API_KEY={your_anthropic_api_key}
# Amazon Bedrock をお使いの場合
export AWS_ACCESS_KEY_ID={your_AWS_access_key_id}
export AWS_SECRET_ACCESS_KEY={your_AWS_secret_access_key}
export AWS_SESSION_TOKEN={your_AWS_session_token}
# 仮想環境の構築
python3 -m venv anthropictools
source anthropictools/bin/activate
# 必要なライブラリの
pip install -r requirements.txt
該当する API Key を環境変数に指定してください。venv で作成されるディレクトリ名に違和感がありますが、README に準拠します。
BaseTool と ToolUser の実装
Anthropic Tools は BaseTool と ToolUser のふたつのクラスから構成されるシンプルなアーキテクチャを採用しています。解説のために部分的にコードを記載しますが後ほど動作確認の取れた全体の実行コードを記載しますので、現状はそのまま読み進めていただいて構いません。
まずは BaseTool の実装から。
import datetime, zoneinfo
from tool_use_package.tools.base_tool import BaseTool
class TimeOfDayTool(BaseTool):
"""現在時刻を取得するツールを定義"""
def use_tool(self, time_zone):
# 現在時刻の取得
now = datetime.datetime.now()
# 指定したタイムゾーンに変換
tz = zoneinfo.ZoneInfo(time_zone)
localized_time = now.astimezone(tz)
return localized_time.strftime("%H:%M:%S")
BaseTool はツールを定義するものです。実装はシンプル。 tool_use_package/tools 配下の base_tool.py を import したのちに、BaseTool を継承したクラスを定義、中に use_tool 関数を定義するのみ。引数に対して想定する戻り値を指定してあげてください。
tool_name = "get_time_of_day"
tool_description = "指定したタイムゾーンの現在時刻を Hour-Minute-Second の形式で取得する。タイムゾーンは、UTC、US/Pacific、Europe/Londonのような標準的なフォーマットで記述する必要がある。"
tool_parameters = [
{"name": "time_zone", "type": "str", "description": "UTC、US/Pacific、Europe/London などの現在時刻を取得するためのタイムゾーン"}
]
time_of_day_tool = TimeOfDayTool(tool_name, tool_description, tool_parameters)
定義した TimeOfDayTool クラスに対して name と description と parameter を与えてインスタンス化するのみ。
次は ToolUser。こちらはもっと記述が短くて済みます。
from tool_use_package.tool_user import ToolUser
time_tool_user = ToolUser([time_of_day_tool])
同様に ToolUser のインポートをして、ToolUser インスタンスを作成。使用したいツール名を指定してあげてください(複数可)。
messages = [{'role': 'human', 'content': 'ロサンゼルスはの現在の時刻は?'}]
time_tool_user.use_tools(messages, execution_mode='automatic')
ただし、Bedrock ユーザは下記のようにfirst_party=False
を指定してください。
time_tool_user = ToolUser([time_of_day_tool], first_party=False)
New Prompt Format
初めて Anthropic API を使う方のびっくりポイントかなと思いますが、Claude では下記のような特殊なフォーマットでプロンプトを定義します。このフォーマットでないとうまく動いてくれません。
Human: Why is the sky blue?
Assistant:
詳しくは公式ドキュメントのプロンプトデザインページを参照してください。
Anthropic Tools では上記のような決まりきったプロンプトを定義するのではなく、下記のような role による tool_inputs / tool_outputs の定義を採用しています。
{
"role": str, # human / assistant / tool_inputs(ツールの使用リクエスト) / tool_outputs(ツールの使用を含む応答) から適切なものを選択
"content": str, # role が tool_outputs である場合は不要で、そうでない場合は必要。
"tool_inputs": list[dict], # role が tool_inputs の時にツールを指定
"tool_outputs": list[dict], # role が tool_outputs の時に tool_outputs か tool_error のいずれかの指定が必要(もう一方は None を指定)
"tool_error": str # 上記と同じくどちらかが tool_outputs でもう一方が None を指定
}
上記のような構造化されたプロンプトの入出力形式を採用しています(OpenAI API も同様ですが)。tool_inputs の具体例を下記に示します。ここでは、perform_addition と perform_subtraction のふたつのツールを指定して、それぞれに引数を渡しています。
{
"role": "tool_inputs",
"content": "",
"tool_inputs": [
{
"tool_name": "perform_addition",
"tool_arguments": { "a": 9, "b": 1 }
},
{
"tool_name": "perform_subtraction",
"tool_arguments": { "a": 6, "b": 4 }
}
]
}
同様に tool_outputs の具体例を下記に示します。perform_addition と perform_subtraction のふたつのツールの result がそれぞれ指定されています。content は上述の通り必須ではありません。
{
"role": "tool_outputs",
"tool_outputs": [
{
"tool_name": "perform_addition",
"tool_result": 10
},
{
"tool_name": "perform_subtraction",
"tool_result": 2
}
],
"tool_error": None
}
エラーメッセージの指定は下記のように行ないます。
{
"role": "tool_outputs",
"tool_outputs": None,
"tool_error": "perform_addition ツールに必要なパラメータ \"b\" が存在しません。"
}
最後に、ツールを使うか否かで応答がどう変わるかを見てみましょう。まずはツールを使わない場合。
human_message = {'role': 'human', 'content': 'C から始まるアメリカの州は?'}
assistant_message = {'role': 'assistant', 'content': 'カリフォルニア州、コロラド州、コネチカット州は C から始まるアメリカの州です。'}
messages = [human_message, assistant_message]
次にツールを使う場合を見てみましょう。
human_message = {'role': 'human', 'content': 'マギーはリンゴを 3 個持っています。そのうち 1 個を食べました。マギーの手元にある残りのリンゴは何個でしょう?'}
tool_inputs_message = {
'role': 'tool_inputs',
'content': " よく考えてみよう。マギーはリンゴを 3 個持っていて、1 個食べた: ",
'tool_inputs': [{'tool_name': 'perform_subtraction', 'tool_arguments': {'a': 3, 'b': 1}}]
}
tool_outputs_message = {
'role': 'tool_outputs',
'tool_outputs': [{"tool_name": 'perform_subtraction', 'tool_result': 2}],
'tool_error': None
}
messages = [human_message, tool_inputs_message, tool_outputs_message]
perform_subtraction を使うことで、減算処理を選び、答えの 2 を弾き出すことができました。では、必要な引数が与えられていない場合はどうなるでしょうか。
human_message = {'role': 'human', 'content': 'マギーはリンゴを 3 個持っています。そのうち 1 個を食べました。マギーの手元にある残りのリンゴは何個でしょう?'}
tool_inputs_message = {
'role': 'tool_inputs',
'content': "よく考えてみよう。マギーはリンゴを 3 個持っていて、1 個食べた: ",
'tool_inputs': [{'tool_name': 'perform_subtraction', 'tool_arguments': {'a': 3}}]
}
tool_outputs_message = {
'role': 'tool_outputs',
'tool_outputs': None,
'tool_error': 'perform_addition ツールに必要なパラメータ \"b\" が存在しません。'
}
messages = [human_message, tool_inputs_message, tool_outputs_message]
この場合 tool_arguments にて 'b' が取得できていないため、エラーを返します。ここまで伝わったでしょうか?安心してください。まずは雰囲気の理解で大丈夫です。
execution_mode の挙動
上記を踏まえてコードを実行します。下記をコピペして main.py として保存してください。
from tool_use_package.tools.base_tool import BaseTool
from tool_use_package.tool_user import ToolUser
# ツールの作成
class AdditionTool(BaseTool):
"""ふたつの数値の和を取るツール"""
def use_tool(self, a, b):
return a + b
class SubtractionTool(BaseTool):
"""ひとつの数値からもうひとつの数値を差し引くツール"""
def use_tool(self, a, b):
return a - b
# それぞれのツールのインスタンス化
addition_tool_name = "perform_addition"
addition_tool_description = "a と b のふたつの数値を足し合わせる。たとえば、add_numbers(a=10, b=12) -> 22。数値は任意の有理数を取り得る。"
addition_tool_parameters = [
{"name": "a", "type": "float", "description": "最初の数値。たとえば 5"},
{"name": "b", "type": "float", "description": "ふたつ目の数値。たとえば 4.6"}
]
subtraction_tool_name = "perform_subtraction"
subtraction_tool_description = "ある数値 b からもう一つの数値 a を差し引く。たとえば、subtract_numbers(a=8, b=5) -> 3。数値は任意の有理数を取り得る。"
subtraction_tool_parameters = [
{"name": "a", "type": "float", "description": "The minuend, such as 5"},
{"name": "b", "type": "float", "description": "The subtrahend, such as 9"}
]
addition_tool = AdditionTool(addition_tool_name, addition_tool_description, addition_tool_parameters)
subtraction_tool = SubtractionTool(subtraction_tool_name, subtraction_tool_description, subtraction_tool_parameters)
# ツールインスタンスを渡して ToolUser をインスタンス化
math_tool_user = ToolUser([addition_tool, subtraction_tool])
# メッセージを作成
human_message = {
"role": "human",
"content": "サリーはリンゴを 17 個持っている。彼女は 9 個をジムにあげます。その日のうちに、ピーターはバナナ 6 本をサリーにあげます。その日の終わりに、サリーが持っている果物の数は何個でしょう?"
}
messages = [human_message]
# execution_mode='automatic' の場合
print(math_tool_user.use_tools(messages, execution_mode='automatic'))
# execution_mode='manual' の場合
print(math_tool_user.use_tools(messages, execution_mode='manual'))
上記のコードを実行してみてください。
python main.py
おそらく下記のような結果が得られるかと思います。現段階では tool_inputs までの処理です。
残ったリンゴ8個に、もらったバナナ6本を足すと、合計 14 個の果物があります。
したがって、その日の終わりにサリーが持っている果物の数は 14 個です。
-----
{'role': 'tool_inputs', 'content': ' はい、わかりました。サリーが最初持っていたリンゴは17個です。\n彼女はジムにリンゴ9個をあげたので、残りは17 - 9 = 8個になります。\nそしてピーターがサリーにバナナ6本をあげたので、サリーが最後に持っている果物の総数は、\nリンゴ8個 + バナナ6本 = 8 + 6 = 14個\nです。\n\nしたがって、その日の終わりにサリーが持っている果物の数は14個です。\n\n', 'tool_inputs': [{'tool_name': 'perform_subtraction', 'tool_arguments': {'a': 17.0, 'b': 9.0}}, {'tool_name': 'perform_addition', 'tool_arguments': {'a': 8.0, 'b': 6.0}}]}
execution_mode を変えていますが、後者ではうまく自然言語を JSON に変換できています。execution_mode を automatic にすると、tool_inputs や入力に対するツールの実行、エラーメッセージなどを自動的に処理する一方で、うまく制御できないことがあります。したがって、カスタマイズ性を求める際は execution_mode を manual に指定してください。
subtraction_tool_parameters は翻訳せず英語のままにしています。引かれる数と引く数で英語では別の英単語が存在しますが、日本語だと該当する単語がなく、無理に日本語訳をあてると制御しにくいため。
全体の実行コード
それでは、tool_outputs まで含めた全体の実行コードを記載します。
from tool_use_package.tools.base_tool import BaseTool
from tool_use_package.tool_user import ToolUser
# ツールの作成
class AdditionTool(BaseTool):
"""ふたつの数値の和を取るツール"""
def use_tool(self, a, b):
return a + b
class SubtractionTool(BaseTool):
"""ひとつの数値からもうひとつの数値を差し引くツール"""
def use_tool(self, a, b):
return a - b
# それぞれのツールのインスタンス化
addition_tool_name = "perform_addition"
addition_tool_description = "a と b のふたつの数値を足し合わせる。たとえば、add_numbers(a=10, b=12) -> 22。数値は任意の有理数を取り得る。"
addition_tool_parameters = [
{"name": "a", "type": "float", "description": "最初の数値。たとえば 5"},
{"name": "b", "type": "float", "description": "ふたつ目の数値。たとえば 4.6"}
]
subtraction_tool_name = "perform_subtraction"
subtraction_tool_description = "ある数値 b からもう一つの数値 a を差し引く。たとえば、subtract_numbers(a=8, b=5) -> 3。数値は任意の有理数を取り得る。"
subtraction_tool_parameters = [
{"name": "a", "type": "float", "description": "The minuend, such as 5"},
{"name": "b", "type": "float", "description": "The subtrahend, such as 9"}
]
addition_tool = AdditionTool(addition_tool_name, addition_tool_description, addition_tool_parameters)
subtraction_tool = SubtractionTool(subtraction_tool_name, subtraction_tool_description, subtraction_tool_parameters)
# ツールインスタンスを渡して ToolUser をインスタンス化
math_tool_user = ToolUser([addition_tool, subtraction_tool])
# メッセージを作成
human_message = {
"role": "human",
"content": "サリーはリンゴを 17 個持っている。彼女は 9 個をジムにあげます。その日のうちに、ピーターはバナナ 6 本をサリーにあげます。その日の終わりに、サリーが持っている果物の数は何個でしょう?"
}
messages = [human_message]
claude_res = math_tool_user.use_tools(messages, execution_mode='manual')
def handle_manual_claude_res(messages, claude_res, tool_user):
"""
- メッセージに claude_res は含まれない
- tool_user は、前のメッセージで使用した ToolUser インスタンスでなければならない。
"""
# Claude の応答をメッセージに追加
messages.append(claude_res)
if claude_res['role'] == "assistant":
# ツールを使わないのであれば、自動で Claude に応答しない。
return {"next_action": "user_input", "messages": messages}
elif claude_res['role'] == "tool_inputs":
# ツールを使うのであれば、ツールと引数をパースし、ツールを使い得られた結果を用いて tool_outputs メッセージを作成、メッセージをmessagesに追加する。
tool_outputs = []
for tool_input in claude_res['tool_inputs']:
tool = next((t for t in tool_user.tools if t.name == tool_input['tool_name']), None)
if tool is None:
messages.append({"role": "tool_outputs", "tool_outputs": None, "tool_error": f"No tool named <tool_name>{tool_name}</tool_name> available."})
return {"next_action": "auto_respond", "messages": messages}
tool_result = tool.use_tool(**tool_input['tool_arguments'])
tool_outputs.append({"tool_name": tool_input['tool_name'], "tool_result": tool_result})
messages.append({"role": "tool_outputs", "tool_outputs": tool_outputs, "tool_error": None})
return {"next_action": "auto_respond", "messages": messages}
else:
raise ValueError(f"Provided role should be assistant or tool_inputs, got {claude_res['role']}")
print(handle_manual_claude_res(messages, claude_res, math_tool_user))
python main.py
上記を実行して、下記のような出力が得られていたら成功しています。自然言語のクエリを定義した各ツールに落とし込んで、それぞれのツールを呼び出すことで得たい出力を得ています。ここでは、17 - 9 + 6
という加減のそれぞれを含む計算をツールごとに計算し、最終的な答えである 8
を返しています。
{'next_action': 'auto_respond', 'messages': [{'role': 'human', 'content': 'サリーはリンゴを 17 個持っている。彼女は 9 個をジムにあげます。その日のうちに、ピーターはバナナ 6 本をサリーにあげます。その日の終わりに、サリーが持っている果物の数は何個でしょう?'}, {'role': 'tool_inputs', 'content': ' はい、わかりました。順を追って計算していきます。\n\n', 'tool_inputs': [{'tool_name': 'perform_subtraction', 'tool_arguments': {'a': 17.0, 'b': 9.0}}]}, {'role': 'tool_outputs', 'tool_outputs': [{'tool_name': 'perform_subtraction', 'tool_result': 8.0}], 'tool_error': None}]}
チュートリアルはここまでです。お疲れ様でした。
Claude は推し
本記事では主に Function Calling に寄った内容になってはいますが、実は Tools の名の通り SQL を叩いたり、検索させたり、かなり拡張性の高い機能になっています。ぜひぜひ遊んでみてください。
100k Context Window の先駆者であり、プロンプトに XML を使うという特殊路線を進む Claude。今は 200k にも対応し、Tools まで使えるときた。個人的には RLHF の捻じ曲げ方もキライじゃない。みなさんも一緒に Claude を推してみませんか?いや、今すぐ推しましょう。
ここまで読んでいただいてありがとうございます。よかったらX フォローしてくださいな。