Building with the Claude API 第3章 — Tool Use
Lesson: Introducing tool use(tool useの導入)
このモジュールでは tool use(ツール使用)を扱う。tools は Claude が外部世界の情報にアクセスできるようにする仕組みである。tool use の理解は少しチャレンジングなので、このレッスンでは「ソフトな導入」として、tool use の全体フローを一通りウォークスルーする。
Claude が限界にぶつかる例: Claude はデフォルトでは、トレーニング時点で学習した情報にしかアクセスできない。つまり、一般的には最近の出来事に関する情報を持っていない。例えば、チャットボットのユーザーが「サンフランシスコ、カリフォルニアの今の天気は?」のような質問をしたとする。それをそのまま Claude に送ると、「申し訳ありませんが、そのような最新の気象情報にはアクセスできません」といった応答が返ってくるだろう。
これを解決してユーザーに良い応答を返すために、tools を使う。
tools の全体フロー(一般化した図):
- Claude に初期リクエストを送る。質問やタスクを与えるとともに、「Claude が外部世界から追加データを取得する方法」についての instructions(指示)を含める。
- Claude はその質問・タスクを検討し、追加データが必要だと判断することがある。その場合、追加データを求める応答を返し、どんな情報が必要かの詳細を教えてくれる。
- こちらのサーバー側で、Claude が求めた情報を取得する少量のコードを実行する。
- その情報を持って、Claude へのフォローアップリクエストで応答する。
- Claude は理論上、応答に必要な情報がすべて揃った状態になるので、その追加データによって拡張・改善された最終応答を生成する。
天気の例に当てはめたフロー:
- ユーザーが天気について尋ねるプロンプトを含む初期クエリを Claude に送る。このリクエストには「現在の天気データをどうやって取得するか」の詳細を含める。
- Claude はプロンプトを見て、「この質問に答えるには現在の天気データが必要だ」と判断し、その旨の応答を返す。
- こちらのサーバー側で、サードパーティの天気 API などにアクセスして、特定の場所の実際の最新の天気の詳細を取得するコードを実行する。
- その外部 API から得た詳細を持って、Claude へのフォローアップリクエストを行う。
- Claude は元のプロンプトと最新の天気データの両方を手にしているので、最終応答を生成して返す。
Lesson: Project overview(プロジェクトの概要)
tools についてさらに学ぶために、小さな目標を設定する。Jupyter notebook の中で実装する小規模プロジェクトとして、Claude に「将来のある時点でリマインダーを設定する」方法を教えることに取り組む。これには複数の異なる tools の実装が必要になる。最初は一度に1つの tool に集中するが、最終的には複数の tools を扱うことになる。
最終的なゴールは、例えば Claude に「歯医者の予約のリマインダーを設定して、それは木曜日から1週間後です」のようなメッセージを送り、Claude が「わかりました、その時点でお知らせします」のように応答してくれることである。
一見すると簡単そうに見えるが、実際にはいくつかの課題があり、それらをすべて tools を通じて解決していく。
直面する3つの課題:
- Claude は現在時刻を正確に把握していない。 Claude は現在の日付は知っている(今すぐプロンプトで「今日の日付は?」と聞けば正確に答えてくれる)が、一日の中の正確な時刻は必ずしも把握していない。そのため「今から24時間後にリマインダーを設定して」と頼んでも、Claude は現在時刻を知らないため「今から24時間後」が正確に何時なのかわからない。
- Claude は時間の加算を常に完璧にこなせるわけではない。 例えば「1973年1月13日から379日後は何日か」と尋ねると、Claude はほとんどの場合正しく答えるが、時々加算を間違えることがある。
- Claude には「リマインダーを設定する」という概念そのものがない。 リマインダーを設定するとはどういうことか概念的には理解しているが、実際に将来リマインダーを設定する仕組みはClaude の内部には一切存在しない。
これら3つの課題を解決するために、専用の tool を1つずつ作っていく:
- 現在の日時を取得する tool — 最もシンプルな tool で、tool calling がどういうものかを理解するための第一歩として使う。現在の日付と時刻を取得するだけの役割。
- 日時に期間を加算する tool — 「現在の日時に20日を加えたら何日になるか」のような計算を可能にする。
- リマインダーを設定する tool — 実際にリマインダーを設定する機能を提供する。
Lesson: Tool functions(tool functionsを書く)
最初の tool、「現在の日時を取得する」tool の実装から始める。この notebook は "001 Tools" という名前でレクチャーに添付されている。この notebook には既にコースで書いてきたコードの多くが含まれているほか、「Tools and Schemas」という新しいセルが追加されている。このセルには、後で使う add_duration_to_datetime 関数を含む大量のボイラープレートコードが用意済みである。
この最初の tool(get current date time)の実装では、あえて add_user_message や add_assistant_message といったこれまでのヘルパー関数は使わない。理由は、これらのヘルパー関数を少しリファクタリングして tools 向けに対応させる必要があるため、「ヘルパー関数の更新」と「tool の学習」を同時に行うと混乱するからである。そのため、まずはヘルパー関数を使わずに手作業で tool call を行うことに集中する。
tool 実装の全体プロセスをステップに分解:
ステップ1: tool function を書く
tool function とは、Claude がユーザーを助けるために追加情報の取得が必要だと判断したときに、自動的に実行されるプレーンな Python 関数である。例として get_weather という tool function が挙げられている。Claude はこの tool function を使って、世界のある特定の場所の現在の天気を取得できる。
tool functions のベストプラクティス(3点):
- 十分に名前が付けられた、説明的な引数を使うこと。 関数自体とその引数は、それが何についてのものかのヒントを与える程度にきちんと名前が付けられているべきである。
- 入力を検証し、何か問題があればエラーを発生させること。 例えば、
locationを受け取れなかった場合やlocationが空文字列だった場合は、直ちにエラーを発生させるべきである。 - エラーを発生させる際は、必ず意味のあるエラーメッセージを含めること。 Claude が tool function を呼び出してエラーになった場合、Claude はそのエラーメッセージを目にすることになる。Claude はそれを見て、少し違うやり方で tool function を再度呼び出し、エラーを修正しようとすることがある。例えば
get_weatherに空文字列を渡すと最初のバリデーションチェックで失敗しエラーが発生するが、Claude はエラーメッセージ「location cannot be empty」を見て、今度は空でない文字列を渡して再度呼び出そうとするかもしれない。
実装(get_current_date_time 関数):
date_formatという引数を受け取り、デフォルト値として"%Y-%m-%d %H:%M:%S"を設定する。- 関数内では
datetime.now().strftime(date_format)を返す。 - 呼び出し例: 引数なしで呼べば「年-月-日 時:分:秒」形式の日時が返る。カスタムのフォーマット文字列(例:
"%H:%M")を渡せば、現在の時と分だけを出力できる。 - 改善点として、
date_format引数のバリデーションを追加する。 正確にstrftimeのフォーマット文字列として妥当かどうかを検証するのは簡単ではないが、少なくとも空文字列が渡されていないかはチェックできる。if not date_format:として、条件を満たさなければValueError("date format cannot be empty")を発生させる。Claude がこのミス(空文字列を渡すこと)を犯す可能性は正直低いが、万が一そうなった場合にでも、Claude にエラーを修正するためのシグナルを返せるようにしておく。
Lesson: Tool schemas(tool schemasを書く)
tool function ができたところで、ステップ2として JSON schema を書く。これは最終的に Claude に送信する設定情報であり、Claude はこれを使って、利用可能な tool functions と、それらに渡すべき引数を理解する。
JSON schema とは何か: スキーマオブジェクト全体(name、description、input_schema を含むもの)そのものは厳密には JSON schema ではない。トップに name と description があり、その下の input_schema キーに割り当てられた辞書部分こそが技術的な意味での JSON schema にあたる。JSON schema という考え方自体は言語モデルや tool calling に特有のものではなく、あらゆる JSON データを検証するためのルール一式であるデータ検証仕様である。言語モデルのコミュニティが、tool calls を配線するのに便利な技術としてこれを採用した、という経緯であり、JSON schema 自体は何年も前から存在する広く理解された技術である。
スキーマの構造:
name: tool の名前(例:GetWeather)。description: tool が何をするか、いつ使うべきか、どんなデータを返すかを Claude に伝える。ベストプラクティスとして、3〜4文程度の長さにすることが推奨される(デモでは簡略化して短い説明にしているが、実際にはもっと長い description を用意すべき)。input_schema: 関数に渡すべき引数を記述する実際の JSON schema 仕様。例えばlocationを受け取るget_weatherなら、input_schemaの中でlocationはstring型であり、その目的を説明する description を持つ。この引数の description も同様に3〜4文程度にして、Claude がこの引数が何を制御し、関数呼び出し全体にどう影響するかを正確に理解できるようにする。
JSON schemaをほぼ完璧に書くための「裏技」:
- エディタに戻って、対象の tool function(ここでは
get_current_date_time)を見つける。 - Claude.ai のウィンドウを開き、「tool calling のための妥当な JSON schema spec を書いて」「添付のドキュメントに記載されているベストプラクティスに従って」と依頼するシンプルなプロンプトを書く。
- 対象の tool function をそのプロンプトに貼り付ける。
- ここが本当の裏技: Anthropic API ドキュメントの User Guide セクションにある「Tool Use with Claude」のページ全体をコピーし、Claude.ai のウィンドウに添付として貼り付ける。 このページには tool use のベストプラクティスや、良い tool description・悪い tool description の例が多数含まれている。
- これを実行すると、Claude はおそらく非常に強力な JSON schema spec で応答してくれる。それをコピーしてエディタに戻り、
get_current_date_time関数のすぐ下に貼り付ける。
命名パターン: tool function には好きな名前を付け、対応するスキーマにはその名前に _schema を付けたもの(例: get_current_date_time_schema)を使う。こうすることで、どのスキーマがどの関数に対応するかを追跡しやすくなる。
型エラー対策: セルの先頭で from anthropic.types import ToolParam をインポートし、スキーマの辞書全体を ToolParam(...) でラップする、というやり方もある。ただし ToolParam は実行時のバリデーションを行わない TypedDict にすぎず、あくまで mypy や pyright のような静的型チェッカー向けのヒントである点には注意したい。公式ドキュメントのサンプルコードはいずれもプレーンな dict をそのまま tools=[{...}] に渡す形で示しており、ToolParam(...) でのラップは必須でも公式に推奨されているわけでもない。
Lesson: Handling message blocks(メッセージブロックを扱う)
ステップ3として、この JSON schema とユーザーメッセージを使って Claude を呼び出す。サーバーから Claude へのリクエストはこれまでと同様に行うが、今回はこの tool schema も含める。これにより、Claude は「利用可能な tool がある」ことを理解する。
手作業でのリクエスト構築:
- 空のメッセージリストを作り、
role: "user"、content: "What is the exact time formatted as hour minutes seconds?"のメッセージを手動で追加する。 client.messages.create()を呼び出す際、model・max_tokens・messagesに加えて、toolsキーワード引数を含める。これはリストであり、これまでに作成した JSON schema すべてを含む(現時点ではget_current_date_time_schemaのみ)。- 応答を出力すると、これまでとは異なる構造のメッセージが返ってくる。これまでの応答メッセージは常に
contentフィールドがリストで、その中にtextブロックが1つだけ入っていたが、tools を使うようになるとcontentリストの中にtool_useブロックという2つ目のブロックが現れる。
マルチブロックメッセージの理解:
- メッセージは
assistantメッセージかuserメッセージのいずれかであり、多くの場合これまで見てきたようにテキストが格納されているが、テキスト以外にも他の種類のデータをメッセージの中に格納できる。 - Claude が tool を使うことを決めた場合、
textブロックとtool_useブロックの両方を含むassistantメッセージを返してくることが多い。textブロック: ユーザーに表示するためのテキスト(例: 「現在時刻を調べますね。少々お待ちください」)。tool_useブロック: Claude が tool を使いたいという開発者への合図。呼び出したい tool function の名前(この例ではget_current_date_time)と、その関数に渡すべき入力(引数)が含まれる。
会話履歴の維持: Claude はステートレスであり、会話履歴やメッセージ履歴をセッションとして自動的には保持しない。会話や履歴を維持したい場合は手動で管理する必要がある。つまり、tool_use ブロックを処理して実際に関数を呼び出した後、Claude に応答を返す際には、これまでと同様に会話の全履歴を含める必要がある。唯一の違いは、複数のブロックを持つメッセージを扱う必要があるという点である。
実装: レスポンスの content(ブロックのリスト)をそのまま role: "assistant" のメッセージとしてメッセージリストに追加する。すなわち messages.append({"role": "assistant", "content": response.content})。これにより、会話履歴には user メッセージ、そして text ブロックと tool_use ブロックを含む assistant メッセージが正しく積み上がっていく。この先、add_user_message と add_assistant_message の2つのヘルパー関数を、こうした複数ブロックのメッセージに対応できるように更新する必要がある。
Lesson: Sending tool results(tool resultsを送信する)
ステップ4では、Claude が要求した tool function を実際に実行する。
tool_use ブロックへのアクセス: response.content の中で2番目のブロック(response.content[1])が tool_use ブロックである。この中の input フィールドが、Claude が get_current_date_time に渡してほしいと要求している引数の辞書である(response.content[1].input)。これにアクセスすると型エラーが出ることがあるが、このレッスンの時点では一旦無視してよい(後で修正する)。
tool functionの呼び出し: get_current_date_time は辞書ではなくキーワード引数 date_format を受け取る関数なので、辞書をキーワード引数に展開して渡す。get_current_date_time(**response.content[1].input) のように呼び出す。これを実行すると、実世界の現在の日時(デモでは 14:41 のような値)が返ってくる。
ステップ5: フォローアップリクエストを送る: 会話履歴全体(元の user メッセージ、tool_use ブロックを含む assistant メッセージ)に加えて、新しいタイプのブロックである tool_result ブロックを含む user メッセージを追加する。
tool_result ブロックの詳細:
tool_resultブロックはuserメッセージの中に置かれ、tool を実行した結果を格納する。つまり、呼び出した tool function から得られた結果を Claude に返すためのものである。tool_use_id: 最も理解しにくいキー。これを説明するために、2つの数式を同時に計算するcalculatorという架空の tool の例が使われる。ユーザーが「10 + 10 は?あと 30 + 30 は?」と尋ねた場合、Claude はcalculatortool に対して2回の呼び出しを要求する可能性がある(tool_useブロックが2つ含まれる assistant メッセージが返る)。それぞれに ID(例:AB3、PO9)が付与されており、calculatortool を2回実行した後、フォローアップリクエストで送り返すtool_resultブロックにも、同じ ID を正確に対応させる必要がある(AB3→ 10+10 の結果である 20、PO9→ 30+30 の結果である 60)。Claude はブロックの並び順だけに依存するのではなく、この ID を使って「どのリクエストにどの結果が対応するか」を正確に紐づける。content: tool function から得られた出力。数値・辞書・リストなど何であっても、通常は plain JSON に変換して文字列にする。is_error(オプション): tool function の実行中に何か問題が発生した場合にtrueに設定する。デフォルトは常にfalse。
実装: メッセージリストに新しい user メッセージを追加する。content はリストで、1つの tool_result ブロックを含む。type は "tool_result"、tool_use_id は response.content[1].id(ここでも型エラーが出るが今は無視)、content は get_current_date_time() を呼んだ結果を result に代入し、それを渡す。実行にエラーがなかったので is_error は false(デフォルトなので厳密には不要だが明示的に書く)。
このメッセージリストを client.messages.create() に送る際、元の tool schema も再度含める必要がある。これから tool を使わなかったとしても、tool_use ブロックや tool_result ブロックの中でこの tool を参照しているため、Claude にはその tool の存在を伝え続ける必要がある。
これを実行すると、Claude から「The current time is 1504」のような text ブロックのみを含む最終応答が返ってくる。tool call は成功である。
全体フローのおさらい:
- tool function を書き、それを説明する tool schema を書く。tool schema の目的は、Claude に利用可能な tools と、それらを実際に呼び出す方法を理解させること。この tool schema は以降すべてのリクエストに含める必要がある。
- Claude からの応答は
textブロックとtool_useブロックの2つを含むassistantメッセージになる。textブロックはユーザーに表示するためのもの、tool_useブロックは呼び出したい tool の名前と入力引数の情報を含む。 - サーバー側で tool function を実行し、Claude へのフォローアップリクエストを送る。このリクエストには会話履歴全体と tool schemas のリストの両方を含める。
- リクエストの最後のメッセージは、
tool_resultブロックを含むuserメッセージ。このtool_resultブロックには、tool function 実行の結果(この例では現在時刻)を格納する。 - Claude はこの
tool_resultブロックからの入力を使って、textブロックのみを含む最終応答を送り返す。
Lesson: Multi-turn conversations with tools(複数tool呼び出しのマルチターン会話)
ここまでで、Claude に1つの tool を配線する例を見てきた。しかし、このプロジェクトの目標は複数の tools を配線することであり、最終的に3つの tools を揃える必要がある。複数の tools を配線したとき、コードの中で何が起きるかを考える。
シナリオ: ユーザーメッセージ「今日から103日後は何日?」を Claude に送るとする。これに答えるには、Claude は2つの tools を順に使う必要がある。まず Get Current Date Time で現在の日付を調べ、次に Add duration to date time でそこに103日を加算する。
裏側で起きること:
- Claude はまず
Get Current Date Timeの呼び出しを求めるtool_useブロックとともに応答を返す。 - その関数を呼び出し、現在の日付を Claude に伝える。
- Claude はまだ元の質問に答えるには情報が不足していると判断し、
Add duration to date timeの呼び出しを求める別のtool_useブロックで応答する。 - それを呼び出して結果を Claude に渡す。
- これで Claude は実際のクエリに答えるのに十分な情報を得る。
なぜこの例が重要か: 実際のユーザーからの入力を受け取る場合、ユーザーが Claude に何を求めるかを常に正確に予測することはできない。ユーザーは複数の異なる tool call を必要とするような予測不能な質問をしてくるかもしれない。そのため、tool calling をアプリケーションに組み込む際には、Claude が連続して複数の tools を使いたがる可能性を常に想定する必要がある。Claude に何かクエリを送るたびに、Claude が tool を使いたがっているかもしれないと考え、応答を確認する必要がある。tool を求めていなければ、それはユーザーに配信できる最終応答が得られたというサインである。
擬似コード(run_conversation 関数のイメージ):
- 初期のメッセージリストを受け取る
run_conversationのような関数を作る。 whileループの中で Claude に問い合わせて応答を得る。- Claude が tool use を求めていなければ、ユーザーに送り返す準備が整った応答があると判断する。
- そうでなければ、tool を実行して結果の
tool_resultブロックを得て、それをuserメッセージに追加し、whileループの中で Claude を再度呼び出す。
この関数を実装するための準備(リファクタリング計画): まずステップ1として、add_user_message と add_assistant_message の両ヘルパー関数を、複数のメッセージブロックをより良く扱えるようにアップグレードする。現状ではこれらの関数は常に単純な text ブロックのみを想定しているため、対応させる必要がある。
add_user_message / add_assistant_message の改修:
- セルの先頭に
from anthropic.types import Messageを追加する。 - 第2引数の名前を
textからmessageに変更する。 - 辞書を展開し、
contentをmessage.content if isinstance(message, Message) else messageとする(add_assistant_messageも同様に更新する)。
これにより、add_assistant_message(messages, response) のように応答メッセージ全体をそのまま渡すことも、response.content(ブロックのリスト)を渡すことも、単なるプレーン文字列を渡すことも、すべて正しく処理できる、非常に柔軟なヘルパー関数になる。これは今後、tool_use ブロックを含むメッセージを扱う際に大いに役立つ。
ステップ2: chat 関数の更新:
tools引数(デフォルトNone)を追加し、systemと同様の方法で、渡された場合のみtoolsパラメータとして渡す。- 戻り値を変更する。これまでは
chat関数は assistant メッセージの最初のブロックからプレーンテキストだけを返していたが、これは「常に1つの text ブロックだけが返ってくる」という前提に立っていた。tools を使うようになった今、その前提はもはや成り立たないため、メッセージ全体を返すように変更する。これは少し不便になるが(テキストへのアクセスに一手間かかる)、現実をより正確に反映しており、より安全である。 - 新しいヘルパー関数
text_from_messageを追加する。メッセージを受け取り、その中のすべてのtextタイプのブロックからテキストを抽出して連結する。実装は"\n".join(block.text for block in message.content if block.type == "text")のような内包表記になる。これは、削除した「テキストだけを取り出す」機能を代替するもので、あるメッセージからすべてのテキストを取り出すのを容易にする。
この後、複数の tool call をひとつの会話の中でサポートする実装(run_conversation に相当する関数)に進む。
Lesson: Implementing multiple turns(複数ターンの実装)
リファクタリングが終わったので、run_conversation のような関数を実装していく。実際の実装は先ほど示した擬似コードとほぼ同じものになる。この関数の目標は、Claude が tool を使いたがらなくなるまで Claude を呼び続けることであり、tool を求めなくなったときが、ユーザーに送り返せる最終応答が得られたというサインになる。
Claude が tool を使いたいかどうかの判定方法: 応答メッセージの中に tool_use ブロックがあるかを見る方法もあるが、もっと便利な方法がある。応答メッセージには stop_reason というフィールドがあり、これが Claude がなぜテキスト生成を止めたのかを教えてくれる。値が文字列 "tool_use" になっている場合、それは Claude が tool を呼び出す必要があると判断したという明確な合図である。stop_reason には他の値もあり得るが、最もよくチェックすることになるのはこの "tool_use" である。
run_conversation 関数の実装:
messagesのリストを受け取り、whileループに入る。- ループの中で、アップグレードした
chat関数(toolsに対応済み)を呼び出して応答を取得する。この時点ではget_current_date_time_schemaのみを渡す。 - 得られた応答を
add_assistant_messageで会話履歴に追加する。 text_from_message(response)を使って現在 Claude が何をしているかを把握するために出力する。response.stop_reason != "tool_use"であれば、Claude はもう tool を必要としないというサインなのでwhileループを直ちにbreakする。- そこを通過したら Claude が tool を呼びたがっているということなので、
run_toolsという新しい関数に、直前に得たメッセージを渡して呼び出す。
run_tools 関数の設計: この関数は、メッセージの中のすべての tool_use ブロックを見て、それぞれに対応する tool を実行するのが目的である。複数の tool call が1つのメッセージに含まれる可能性を前提に書く必要がある(前述の calculator の10+10・30+30の例と同様)。
run_tools の処理フロー:
- メッセージの
contentプロパティ(ブロックのリスト)を見る。textブロックは無視し、tool_useブロックだけを抽出する:tool_requests = [block for block in message.content if block.type == "tool_use"](tool_useではなくtool_requestと呼ぶのは、これが Claude からの「tool を使ってほしい」というリクエストだから)。 - 空リスト
tool_result_blocksを用意する。 - 各
tool_requestについてループし、tool_request.nameを見て、どの tool 関数を実行すべきかを判定する。 - 対応する tool function を
tool_request.inputを展開して呼び出し(**tool_request.input)、出力を得る。 - その出力から新しい
tool_resultブロックを組み立てる。tool_use_idはtool_request.idと厳密に一致させる必要がある(tool_useブロック側はidというプロパティ名だが、tool_resultブロック側はtool_use_idという別名になっている点に注意)。contentは tool 実行結果をjson.dumps()で文字列化したもの。is_errorは現時点ではfalseにしておく(後でより堅牢なエラーハンドリングを追加する)。typeは"tool_result"。 - 組み立てた
tool_result_blockをtool_result_blocksに追加する。 - ループの外側で
tool_result_blocksを返す。
改善1: エラーハンドリング: tool function の実行を try / except で囲む。except 節でも tool_result_block を組み立てるが、is_error を true にし、content にはエラーメッセージを f-string(f"Error: {e}" のような形)で埋め込む。Claude はこのエラー内容を見て、tool を別の引数でもう一度呼び直そうとすることがある。
改善2: 複数tool対応のためのディスパッチ: 現状では get_current_date_time 1つだけを想定した if 文になっているが、今後 add_duration_to_date_time や set_reminder など tools が増えることを見越し、この if を並べていくやり方はスケールしない。そこで run_tools の上に新しいヘルパー関数 run_tool(tool_name, tool_input) を作る。この関数の中で、tool_name を見てどの tool function を実行すべきか判定し、実行して結果を返す。if tool_name == "get_current_date_time": return get_current_date_time(**tool_input) のように書く。tool が増えた場合はこの if チェックを追加していくだけでよい。run_tools 側は run_tool(tool_request.name, tool_request.input) を呼ぶように書き換える。
run_conversation の完成: run_tools から返ってきた tool_result_blocks を add_user_message(messages, tool_results) として会話履歴に追加する。while ループの外側で messages を返す。
これにより run_conversation は「Claude を呼ぶ → tool use を求めていれば tools を実行して結果を user メッセージとして追加し、ループの先頭に戻って Claude を再度呼ぶ → tool use を求めなくなるまで繰り返す」という全体ループを捉えたものになる。
テスト: 「現在時刻を時:分形式で、そして秒形式でも教えて」のような、2回の tool call を要求しそうなクエリを送って run_conversation を実行する。結果として:
- 最初の応答は
textブロックとtool_useブロック(時:分形式の現在時刻を求める)を含む。 tool_resultを返す。- Claude は2回目の tool call を要求する(このときは
textパートを含まずtool_useブロックのみ)。今度は秒形式でのget_current_date_timeを求める。 - 結果を送ると、最終的に「ここに元のクエリへの答えがあります」という
textブロックのみの最終応答が返る。
これで、Claude が tool use を求めなくなるまで会話を実行し続ける処理全体、そして run_tools の中ですべてのブロックを確認して tool_use ブロックだけを抜き出し、それぞれに対して tool を実行し、結果を tool_result として組み立て、まとめて Claude に送り返す処理の重要性が実例で示された。次はこのプロジェクトの最後のステップとして、複数の異なる tools(Add duration to date time と Set reminder)へのサポートを追加する。
Lesson: Using multiple tools(複数tool対応の仕上げ)
最後に残っているのは Add Duration to DateTime tool と Set Reminder tool の追加である。前のレッスンは難しかったが、こちらは非常に簡単で短く、あっけないほど straightforward である。
必要なコードの多くは、既に "Tools and Schemas" セルの中に用意されている。具体的には add_duration_to_datetime 関数の実装、set_reminder 関数の実装(実際には何もリマインダーを設定するわけではなく、与えられた時刻と内容を出力するだけ)、add_duration_to_datetime_schema と set_reminder_schema の2つのスキーマも既に提供されている。
やるべきことは2つだけ:
- この2つのスキーマを Claude に渡す。
- Claude がどちらかの tool を求めてきたら、対応する tool function を呼び出す。
実装手順:
run_conversation関数の中の tools のリストにadd_duration_to_date_time_schemaとset_reminder_schemaを追加する。run_tool関数にifケースを追加する。tool_name == "add_duration_to_date_time"ならadd_duration_to_date_time(**tool_input)を返し、tool_name == "set_reminder"ならset_reminder(**tool_input)を返す。
これだけで完了である。最初の run_tool と run_conversation さえ整えておけば、tool を追加していくのは非常に簡単になる。run_tool を更新し、tool schema を追加し、tool function 自体の実装を追加するだけでよい。
テスト: 「2050年1月1日の177日後にリマインダーを設定して」のようなクエリを送る。これは複数回の tool call を要求する — まず add_duration_to_date_time で日付を計算し、その後 set_reminder を呼ぶ必要がある。
実行結果:
- Claude はまず「1月1日から177日後」を計算する必要があると告げる。
- 計算後、リマインダーの設定を試みる。
set_reminder関数からのログ出力(引数をそのまま出力するだけの関数)が表示される。- 最終的に Claude から「2050年6月27日月曜日という正しい日付に予約が設定されました」という応答が返る。
会話履歴を確認すると、user メッセージ、text ブロックと tool_use ブロックを含む assistant メッセージ、tool_result を含む応答、フォローアップという流れが確認できる。これで複数の異なる tools を notebook に配線できたことになる。
Lesson: Fine grained tool calling(fine grained tool calling)
このレッスンでは、tool use と streaming(ストリーミング)を組み合わせる方法を扱う。streaming を有効にして Claude にリクエストを送ると、まず初期応答が返り、その後一連の異なる event(イベント)を受信する。それぞれの event にはユーザーに表示したいテキストの断片が含まれる。これまでにも扱ってきたように、アプリケーションで処理すべき event の種類は多数あり、よく目にするものとして content_block_delta がある。
tools と streaming を組み合わせると新しい種類の event が追加される。それが input_json event である。streaming を有効にして tools を使うと、Claude からこの新しいタイプの event が送られてくることがある。このオブジェクトには2つの重要なプロパティがある。
partial_json: Claude が tool に送りたい引数の一部を表す JSON の断片。snapshot: これまでに受信したすべてのpartial_jsonの断片を累積合計したもの。
デモ用notebook: "003 tool streaming" という新しい notebook がレクチャーに添付されている。この notebook には、改修版の run_conversation 関数が含まれており、Anthropic API からのレスポンスストリームを開くための新しい関数 chat_stream を使う。ストリームの各チャンクについて、そのタイプに応じて処理する。input_json チャンクを受信した場合、今のところは単にそれを出力するだけの実装になっている。
notebook の下部には、既に用意されたプロンプトと save_article_schema という新しい tool がある。この notebook を実行すると、応答の各チャンクが逐次届き、tool call の引数も含めて表示される。この notebook は学術論文(scholarly article)を生成するもので、これまでと少しプロパティ構成が異なる。
streamingの基本はここまで: tool streaming 自体には、これ以上あまり多くのことはない。すでに構築済みの streaming パイプラインに、この追加の event タイプを処理する対応を組み込むだけでよい。
もう一段深い話題: fine grained tool calling(誰もが必要とするわけではない、かなり特化した機能)。
この notebook を再実行すると、streaming の挙動自体は確認できる。テキストはチャンクごとに現れてくる。しかし、tool call の引数に関しては、数秒間待たされた後、突然大きなテキストの塊が一度に現れるという挙動になる。この大きな遅延とその後のまとまった出現は、多くの場合気にならないかもしれないが、ユーザーに可能な限り早く精緻な更新を見せたいUIを作っている場合や、tool call の一部をできるだけ早く受け取って処理を始めたい場合には、少し煩わしく感じられるだろう。
なぜこの遅延が起きるのか: この notebook に追加されている tool は、以前扱っていた「学術論文を生成する」tool を少し調整したものである。引数の構造が少し異なり、トップレベルのキーとして abstract(論文の要約)と meta(オブジェクト。word_count と review を持つ)の2つを持つ。
Anthropic API の内部で streaming モードにおいて tool call の引数を生成する際に何が起きているかというと、想定通りには振る舞わない。Claude はリクエストを受け取ると、tool call の入力となる JSON を生成し始める。この JSON は一度に全部生成されるのではなく、チャンクごとに作られる。ここで少し意外なことが起きる。Anthropic API はこれらのチャンクを即座にサーバーへ送り返すのではなく、しばらく手元に保持する。 その理由は、Claude が時折無効な JSON を生成してしまうことがあるためであり、API は各チャンクを即座に送信するのではなく、バリデーションのステップを実行し、有効な JSON を送っていることを確認しようとする。
バリデーションの仕組み:
- API は、オブジェクト全体が生成されるのを待つのではなく、1つのトップレベルのキーバリューペアが生成されるまで待つ。
abstractキーの場合、API はabstractの値の文字列に対する閉じクォートが現れるまで待つ。それが現れた時点(例のチャンク4番目)で、API は少なくとも1つのキーバリューペア(abstractのキーバリューペア)が生成されたと認識する。- API はそのキーバリューペア単体を、提供された JSON schema と照合してバリデーションする。有効であれば、それを構成していた元のすべてのチャンクがサーバーへ送信される。個々のチャンクはそのまま受け取れる(キーバリューペア丸ごと1つのチャンクとしてではない)。実際に受信するすべての個々のチャンクをログに出せば、大量の個別チャンクを受け取っていることが確認できるが、それらはすべて、このバリデーションステップのためにAPI側でバッファリングされていたことにより、ほぼ同時にサーバーに到着する。
- この生成プロセスは、次のトップレベルキーである
metaについても同様に続く。API はmetaのキーバリューペア全体が生成されるまで待ち、それをバリデーションしてから、個々のチャンクをサーバーに送り返す。
これが、streaming を有効にしていても大きな一時停止とその後の大きなチャンクの出現が起きる理由である。
この挙動が問題になる場合の解決策 — Fine-Grained Tool Calling: Anthropic API にはこの機能(現在の正式名称は fine-grained tool streaming で、eager_input_streaming パラメータで有効化する)があり、本質的にやることは1つだけ、JSON バリデーションのステップを無効化することである。fine-grained tool calling を有効にすると、API はサーバー側でのバッファリングをやめ、Claude が生成し始めた瞬間からチャンクを逐次的(インクリメンタル)にそのまま送るようになる。これにより、tool の入力を生成する際により伝統的な streaming の出力体験が得られる(先ほど説明した「トップレベルのキーバリューペアが完成するまで待ってからまとめて送る」というバッファリング挙動は、この機能を有効にしないデフォルトの場合の話である)。
重要な注意点: fine-grained tool calling を有効にすると、API 側での JSON バリデーションは無効化される。つまり、サーバー側のコードは、無効な JSON が渡される可能性を前提として、適切なエラーハンドリングを実装すべきである。
実演: notebook の run_conversation の呼び出しに fine_grained_tool = True のような引数を追加して実行する。すると、tool call の部分でもより「クラシックな」streaming 体験(少しずつテキストが届く)が得られるようになる。例えば word_count の値が重要な場合、fine-grained tool calling なしでは、その前にある大量のテキストがすべて生成されるまで word_count にアクセスできないが、fine-grained tool calling を使えばその値をより早く取得できる。
無効なJSONが生成された場合の挙動確認: 講師は、ほぼ確実に無効な JSON を生成させるプロンプトを用意している。それを notebook に貼り付け、さらに run_conversation 関数に force_tool_call という追加の引数を渡して、モデルに必ず save_article tool を呼び出させるようにする。これを実行すると、最初はうまくいくように見えるが、すぐにエラーが発生する。
原因は、先ほどの word_count の値にある。word_count は本来数値であるべきだが、このプロンプトは word_count の値を undefined にするよう仕向けるものである。JSON の仕様では undefined は有効な値ではない(JSON における相当の値は null である)。JSON の値が undefined になると、パースエラーが発生する。実際にエラーメッセージには、無効な JSON であること、具体的には word_count が undefined になっていることが原因である旨が表示される。
fine-grained tool callingを使わない場合はどうなるか: force_tool_call をコメントアウトして再実行すると、バリデーションのステップが発生する。この場合、API は meta オブジェクトを含めはするが、実際にはオブジェクトとしてではなく、オブジェクト全体を文字列としてラップする。結果として、技術的には JSON schema の仕様に従っていないレスポンスになる。提供した JSON schema では meta は word_count(数値)と review(文字列)を持つオブジェクトであるべきと定義していたが、実際には meta はオブジェクトではなく文字列になってしまう。
まとめ: tool streaming 自体はそれほど複雑なものではなく、既存の streaming パイプラインに容易に組み込める。デフォルトの tool streaming では、大きなトップレベルのキーバリューペアを生成している場合、API 側のバリデーションステップによって生成に遅延が生じる。これが問題になる場合は、fine-grained tool streaming を有効にすることで、より伝統的な streaming 体験を得られるが、その代償としてバリデーションステップが失われる。
Lesson: The text edit tool(text editor tool)
このモジュールでこれまで見てきたように、通常は開発者自身がClaude に渡すすべての tools を作成する。しかし、Claude にデフォルトで組み込まれている tool が1つ存在する。それが text editor tool である。
この tool は、標準的なテキストエディタでできることに関連する幅広い能力を Claude に与える。具体的には、ファイルやディレクトリを開いて内容を読む、ファイル内の特定範囲のテキストを見る、ファイル内のテキストを追加・置換する、新しいファイルを作る、undo を行う、など、通常のテキストエディタでできることはほぼすべて可能になる。これにより Claude の能力は劇的に拡張され、ほぼそのままソフトウェアエンジニアとして振る舞う能力を Claude に与えることになる。
text editor tool の理解を助ける図解: tools を使う際、通常は2つのものを自分で書く必要がある。1つは JSON schema spec(Claude に、利用可能な tool とそれが必要とする引数について伝えるもの)、もう1つは、その JSON schema と対になる tool function の実装(実際にコードベースの中で、Claude が tool を使いたいときに呼び出される関数)である。
text editor tool の場合、Claude に組み込まれているのは JSON schema の部分だけである。 Claude が「新しいファイルを作りたい」といった tool_use パートを送ってきたときに、実際にハードドライブ上に新しいファイルを作る関数の実装は存在しない。それは自分たちのコードベースの中に書き出す必要がある。つまり、text editor tool の利用は「無料」ではなく、開発者側でいくつかの関数を書く必要がある。
デモ用notebook: "005 Text Editor Tool" という新しい notebook。3番目のセル(コメント「implementation of the text editor tool」)に、text editor tool を使うために必要なすべての関数を含む、あらかじめ用意されたクラスがある。これは前述の「開発者が書く必要のある実装」の部分を提供するものである。このクラスには、ファイルやディレクトリの内容を見るための view、ファイル内の文字列を置換する string replace 関数、ファイルを作成する関数などのメソッドが含まれている。すべてあらかじめ用意されている。
モデルバージョンに応じたスキーマの生成: 「make the text edit schema based upon the model version being used」というコメントが付いたセルがある。text editor tool のスキーマは基本的に Claude に組み込まれており、開発者側で用意する必要はないというのはおおむね正しいが、少しだけややこしい点がある。Claude へのリクエストでこの tool を使いたい場合、非常に小さなスキーマだけは送信する必要がある。 そのスキーマの中の正確な type 文字列(日付が含まれる)は、使用している Claude のバージョンによって異なる。notebook 内の関数では、Claude 3.7 Sonnet を使っているかをチェックし、対応する日付のスキーマを返す。Claude 3.5 を使っている場合は、少し異なる日付のスキーマになる(これは収録当時の話。Claude 3.5 Sonnet・Claude 3.7 Sonnet はいずれも2026年2月19日付でAPI提供が終了しており、現行モデル(Opus 4.8 や Sonnet 5 などのClaude 4系以降)はすべて統一的に text_editor_20250728/str_replace_based_edit_tool を使うため、バージョン分岐ロジックは現在は不要になっている)。
この非常に小さなスキーマを Claude に送ると、それは自動的にはるかに大きなスキーマへと展開される。Claude はこの小さなスタブスキーマ(name が str_replace_editor(旧バージョン)や str_replace_based_edit_tool(現行版)のようなもので、特定の type を持つ)を見ると、裏側でこのスキーマがはるかに大きなもの(text editor tool の使い方をClaude に詳しく伝える情報を大量に含むもの)に置き換わっていると考えることができる。
デモ1: ファイルの内容を要約させる:
notebook と同じディレクトリに main.py という新しいファイルを作り、greeting という「Hi there」を出力するだけの簡単な関数を書いて保存する。notebook の一番下に空の user メッセージを追加し、run_conversation に渡すセルを用意し、Claude に「./main.py ファイルを開いて内容を要約して」と依頼する。実行すると、応答の中で Claude が実際にそのファイルの内容を取得し、内容の要約を返してくることが確認できる。メッセージリストを見ると、Claude が main.py の内容を見ようとする tool_use ブロックがあり、先述の text editor tool クラスがそのコマンドを受け取って自動的にファイルを開き、その内容を Claude に送り返している。その内容自体もメッセージリストの中で確認できる。
text editor tool の意義: text editor tool がそもそも何のために存在するのか、ファイルシステム上のファイルを扱えるという明白な事実以外にどんな機能を提供してくれるのか。多くの開発者は既にAIアシスタントが組み込まれたコードエディタを使っているだろうが、実はこの text edit tool を使うだけで、そうした高機能なコードエディタの機能の多くを大部分再現できる。
デモ2: 関数の実装とテストの作成:
プロンプトを更新し、Claude に「そのファイルを開いて、円周率を小数点5桁まで計算する関数を書いて」と依頼し、その後「実装をテストするための ./test.py ファイルを作成して」と続けて依頼する。これを実行すると、メッセージのやり取りは次のようになる。
- 最初の
userメッセージ。 - Claude の
assistantメッセージで、text edit tool を使うことを決定し、具体的にはmain.pyの内容を見ようとする。 - ファイルの内容を送り返す。
- Claude は「わかった、このファイルの中身がわかった」と言い、内容を新しいコードで置き換えようとする(円周率計算の実際の実装がここに現れる)。
- ファイルの更新が成功したことを応答で伝える。
- Claude は
test.pyファイルを作成し、そこにテキスト(作った実装をテストするためのテスト)を書き込もうとする。
実際に main.py を確認すると円周率計算の実装が追加されており、test.py にはテストが書かれていることが確認できる。この tool を使うことで、かなり高機能なコードエディタを容易に近似できる。
それでも自分のコードエディタを使えばいいのでは、という疑問について: この tool を使いたくなる場面として、ネイティブでフル機能なコードエディタにアクセスできない何らかのアプリケーションで、ファイルシステム上のファイルを編集する必要があるようなシナリオが挙げられる。そうした場面こそが text edit tool を使うべき状況である。
Lesson: The web search tool(web search tool)
Claude にはもう1つ組み込みの tool があり、web search tool という。名前の通り、この tool は Claude がユーザーの質問に答えるために最新の情報や専門的な情報をウェブ上で検索できるようにするものである。例えば量子コンピューティングの最新の話題について尋ねられた場合、Claude はこの tool を使って量子コンピューティングに関連する最新の記事を見つけ、その内容を使って回答を組み立てることができる。
text editor tool との違い: text editor tool とは異なり、検索を実際に実行するための実装を自分で用意する必要はない。検索は完全に Claude 側で行われるため、この tool は非常に扱いやすい。
デモ用notebook: "006 web search" という新しい notebook。下部に Claude へのリクエストを行う典型的なコードのセルがあり、その1つ上のセルで web_search_schema という新しい変数を作成する。これは自分たちで書いてリクエストに含め、tool として指定するスキーマである。text editor tool と同様、この非常に小さなスキーマは裏側でより大きなスキーマに展開される。
web_search_schemaのフィールド:
type:"web_search_20250305"(ハイフンなしのYYYYMMDD形式。現行ではweb_search_20260209(動的フィルタリング対応)やweb_search_20260318(response_inclusion対応)といった新しいバージョンも利用できる)name:"web_search"max_uses:5
max_uses は、Claude が検索を実行できる最大回数である。1回の検索で複数の異なる検索結果が返ることがあり、その検索結果の内容次第で Claude はフォローアップの検索を行うことがある。このプロセスは何度か繰り返されうるため、検索できる回数の合計を5回に制限している。
デモ: Claude に「脚の筋肉をつけるのに最適な運動は?」と尋ね、web_search_schema を含めてリクエストする。応答はかなり大きくなるため、内容を大幅に削った縮小版が示される。
レスポンスの content リストの構造:
- 最初に
textブロックが返り、Claude がこれから何をするかを説明する(この質問に答えるためにウェブ検索を行う、という旨)。 - 次に
server_tool_useブロックが現れ、その中に検索 tool への入力(Claude が使う実際の検索クエリ)が入っている。 - 続いて
web_search_tool_resultブロックの一覧が現れ、その中に複数のweb_search_resultブロックが含まれる。これらが、その初期クエリに対して Claude が受け取った異なる検索結果である。実際のクエリ応答にはもっと多くの検索結果が含まれるはずだが、デモでは1つを残してすべて取り除いている。1件の実際の web search result には、Claude が取得したページのタイトルと実際の URL が含まれる(この段階ではまだコンテンツ自体は含まれておらず、Claude が検索で何を見つけたかを伝えるだけのもの)。 - その後、Claude はユーザーの質問に答え始める。回答は複数の
textブロックで構成され、その中には citations(引用)のリストが含まれることがある。citations は、Claude が主張している内容を裏付けるためのテキストである。例では、Claude が特定のウェブページを引用し、その特定のテキストを使って自分の論点・主張を裏付けている。
allowed_domains によるドメイン制限: web_search_schema を定義する際に指定できるフィールドの中で特に推奨されるのが、ユーザーが何を尋ねてくるかについてよく理解できている場合に検討すべきフィールドである。運動に関するアドバイスをオンラインで検索すると、AIが生成したと思われるコンテンツを含む大量のブログがヒットする可能性があり、そこで得られるアドバイスは必ずしも正確・最良のものとは限らない。一方で、米国政府が運営する PubMed(NIH.gov)のような、医学関連の学術論文を集めたウェブサイトも存在する。このようなサイトだけを検索するように Claude に指示できれば、ユーザーに提供する情報の質を担保できる。
これを実現するには、対象ドメイン(nih.gov)を見つけ、web_search_schema に allowed_domains フィールドを追加し、["nih.gov"] のようなリストを設定する。これにより Claude の検索はそのドメインだけに制約され、他のドメインを検索しようとしなくなる。これを再度実行してリクエストを送ると、応答の中の URL がすべて nih.gov ドメインに属するものだけになっていることを確認できる。これにより、少なくとも科学的根拠のあるアドバイスをユーザーに提供できるようになる。
UIでの見せ方: 最後に、この tool を使って得られる多種多様なブロックのリストを、実際にはどのように活用することを意図しているかが示される。基本的な考え方は、すべての text ブロックはプレーンテキストとしてレンダリングし、web_search_result ブロックや citation の web_search_result_location に出会ったら、それらをユーザーが「情報が何らかの形で裏付けられている」とわかるようなUIでレンダリングするというものである。
講師は、レスポンスメッセージ内のさまざまなブロックを受け取ってレンダリングする小さなページを例として書いている。ページの最上部には web_search_tool_result ブロックの一覧(Claude がウェブ検索で見つけたページ群)を表示する。そして、すべてのブロックのリストを反復処理し、各 text ブロックのテキストを表示する。citation の web_search_result_location を持つテキストブロックに出会った場合(先ほどのその情報源を示す長い用語のもの)、そこにはドメイン、見つけたページのタイトル、正確なアドレス、そして引用された正確なテキストとともに、小さな citation としてレンダリングする。こうすることで、ユーザーは Claude が実際にどうやってその情報を得たのかをより良く理解できるようになる。
章末まとめ
- tool use の基本フロー: Claude にリクエスト(+ tool schema)を送る → Claude が
tool_useブロックで tool 呼び出しを要求する → こちら側で tool function を実行する → 結果をtool_resultブロックとして会話履歴に含めてフォローアップリクエストを送る → Claude が最終応答を返す。この一連のやり取りは Claude 側では一切状態を保持しないため、会話履歴は毎回すべて自前で構築・送信する必要がある。 - tool を実装するときの2点セット: (1) 実際に処理を行う plain Python の tool function、(2) Claude にその tool の存在・引数を伝える JSON schema(
name・3〜4文のdescription・input_schema)。tool function は「引数の妥当性検証+意味のあるエラーメッセージ」を徹底すると、Claude 自身がエラーを見て呼び出しを修正してくれる。 - JSON schema はドキュメントを添付してClaudeに書かせるのが実用的なコツ(Anthropic API ドキュメントの Tool Use ページを添付して生成させる)。
- メッセージは複数ブロックを持ちうる(
textブロックとtool_useブロック、あるいはtool_resultブロックなど)。この前提に立ってadd_user_message/add_assistant_message/chatを汎用的に書き直す必要がある。 tool_use_id/tool_use.idの対応関係が、複数の tool call が同時に発生した場合にどの結果がどのリクエストに対応するかを一意に決める。stop_reason == "tool_use"が「Claude がまだ tool を必要としている」かどうかを判定する最も簡単な方法であり、これがwhileループで Claude を何度も呼び直すrun_conversation実装の核になる。- tool が増えても構造は壊れない設計にする(
run_toolにifを追加するだけで済むディスパッチ構成)。 - Fine-grained tool calling: streaming + tools を組み合わせると、デフォルトでは API 側が JSON の妥当性検証のために各トップレベルキーの完成を待ってからチャンクをまとめて送るため、大きな「間」の後にテキストがどさっと出現する。fine-grained tool calling はこの検証ステップを無効化し、より細かい単位でのストリーミング体験を得られる代わりに、無効な JSON(例:
undefinedのような非JSON値)が返ってくる可能性を自前でハンドリングする必要が生じる。 - Claude 組み込みの tools が2つ存在する: text editor tool(JSON schema のみ組み込み済みで実装は自前で用意する必要がある。ファイル閲覧・編集・作成が可能でコードエディタ的な体験を再現できる)と web search tool(実装まで含めて完全に Claude 側で完結し、
allowed_domainsで検索先を信頼できるドメインに絞り込める。応答にはserver_tool_use・web_search_tool_result・citations 付きtextブロックが含まれる)。
情報源は動画の公式英語字幕(トランスクリプト)。具体例・数値・デモ手順・コード例・比喩はできる限り漏らさず反映している。技術用語(RAG, chunking, embedding, BM25, lexical search, semantic search 等)は英語表記のまま用いる。