見出し画像

Anthropic API で Claude 3 のツール活用Function Callingを試す

今回は、Anthropic API を使ってGPT-4超えと話題の Claude 3 のツール活用(Function Calling)を試してみます。

Claude 3 では、GPT-4 同様、ツールの定義をすることで LLM にツールの活用をさせることができます。
Anthropic 自体が出しているツール活用用の Python のレポジトリーがありましたので、それをフォークして使ってみます。

今回試すにあたって、Web 検索を試したかったので、同時に Brave Search API も使ってみます。

どちらも API を利用するにあたってサインアップが必要なのでしたが、数分でできました。
Anthropic API の方は今なら$5 分のクレジットがもらえるようでしたので Claude 3 Opus などを API で試すチャンスです。

セットアップ

まずは `.env` ファイルを作成して API_KEY を設定します。

Claude を使うための API_KEY は Anthropic のサイトで取得できます。
https://console.anthropic.com/settings/keys

また、Brave Search API を使うためには一度 Free プランに登録後、API_KEY を取得する必要があります。
https://api.search.brave.com/app/keys

これらを `.env` ファイルに保存します。
`.env` ファイルの中身は以下のようになります。

ANTHROPIC_API_KEY={your_anthropic_api_key}
BRAVE_API_KEY={your_brave_api_key}

これを Python の `dotenv` で読み込みます。

from dotenv import load_dotenv

load_dotenv()

Anthropic が提供しているrepo があったのですが、system_prompt をこちら側で定義できませんでした。そこで、repo をフォークし、system_prompt を設定できるようにしたものを作りました。今回はこの repoをダウンロードします。

!git clone https://github.com/alexweberk/anthropic_tools.git
%cd anthropic_tools
!pip install -r requirements.txt

Brave の Search API を使いやすくしたラッパーライブラリをダウンロードします。

!pip install brave-search -Uqq

準備が整いました。

Anthropic API におけるツール活用

基本概念として用意されているのが `BaseTool` と `ToolUser` です。

  • `BaseTool` は API を叩くための基本的な機能を提供しています。

  • `ToolUser` は `BaseTool` を使うエージェントの概念のようです。

まずはサンプルコード通り試してみます。

BaseTool の定義

import datetime

import zoneinfo
from anthropic_tools.tool_use_package.tools.base_tool import BaseTool


# BaseToolを継承してTimeOfDayToolを作成
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")
# LLMに読み込ませるツールの定義
tool_name = "get_time_of_day"
tool_description = "Retrieve the current time of day in Hour-Minute-Second format for a specified time zone. Time zones should be written in standard formats such as UTC, US/Pacific, Europe/London."
tool_parameters = [
    {
        "name": "time_zone",
        "type": "str",
        "description": "The time zone to get the current time for, such as UTC, US/Pacific, Europe/London.",
    }
]

time_of_day_tool = TimeOfDayTool(tool_name, tool_description, tool_parameters)

ToolUser の定義

次に、BaseTool を使う ToolUser(エージェント)の定義をします。

from anthropic_tools.tool_use_package.tool_user import ToolUser

time_tool_user = ToolUser([time_of_day_tool])

それでは LLM に質問をなげてみます。

messages = [{"role": "user", "content": "What time is it in Tokyo?"}]
time_tool_user.use_tools(messages, execution_mode="automatic")
'\n\nThe current time in Tokyo, Japan is 17:03:15 (5:03:15 PM).'

無事今の時間を取得するツールを活用し、回答ができました。

Brave Search API を試す

今回ウェブを検索してその結果を元に回答するエージェントを作ってみたかったので、ウェブを検索するライブラリとして Brave Search API を試してみました。他にもいろんな API 提供サービスがあるので、この部分は何を使っても OK 可と思います。

Brave Search API の使い勝手を把握するために、まずは簡単な検索を試してみます。
今回は Python で簡易に使えたらいいなと思い、brave-apiというラッパーライブラリを使ってみます。

※Brave Search API の検索で使えるパラメーター一覧はこちらです。

!pip install brave-search
# https://api.search.brave.com/app/documentation/web-search/codes#country-codes

from brave import Brave

brave = Brave()

query = "原宿の歴史"
num_results = 1
country = "JP"
search_lang = "jp"
ui_lang = "ja-JP"


search_results = brave.search(
    q=query, count=num_results, country=country, search_lang=search_lang, ui_lang=ui_lang
)
# web_resultsをアクセスすると検索結果が取得できる
search_results.web_results
[{'title': '原宿の歴史|東京原宿竹下通り観光ガイドマッ...',
  'url': Url('https://www.tour-harajuku.com/history.html'),
  'is_source_local': False,
  'is_source_both': False,
  'description': '江戸時代初期,この付近を千駄ヶ原と称し,かつて相模国から奥州へ行くための鎌倉街道の宿駅があったことから原宿といった地名が起こったといわれる。江戸時代は武家屋敷や寺院が並び,明治時代は華族の屋敷が...',
  'language': 'ja',
  'profile': {'name': 'Tour-harajuku',
   'url': Url('https://www.tour-harajuku.com/history.html'),
   'long_name': 'tour-harajuku.com',
   'img': Url('https://imgs.search.brave.com/efharKI-efqR7XHNY5dWCvf-ALtyQ54814iCMRZi0yI/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvNjMzZjIzMmIx/ODJkMDIzZTNjY2Q0/MDAwYTBkMmFmN2Qw/MDUwMmVmZWRhNzY1/ZTUyOTRlOWJlNTA1/ZjAzY2Q0NC93d3cu/dG91ci1oYXJhanVr/dS5jb20v')},
  'family_friendly': True,
  'meta_url': {'scheme': 'https',
   'netloc': 'tour-harajuku.com',
   'hostname': 'www.tour-harajuku.com',
   'favicon': Url('https://imgs.search.brave.com/efharKI-efqR7XHNY5dWCvf-ALtyQ54814iCMRZi0yI/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvNjMzZjIzMmIx/ODJkMDIzZTNjY2Q0/MDAwYTBkMmFmN2Qw/MDUwMmVmZWRhNzY1/ZTUyOTRlOWJlNTA1/ZjAzY2Q0NC93d3cu/dG91ci1oYXJhanVr/dS5jb20v'),
   'path': '› history.html'}}]

検索結果の各ページの中身はまだ取得できていないので、取得するためには各ページにアクセスし、コンテンツを読み込むなどの処理が必要です。

URL を取得するには下記で行けました。

# その中の`url`を取得する
str(search_results.web_results[0]["url"])
'https://www.tour-harajuku.com/history.html'

以前使ったことのある `trafilatura` というライブラリでメインコンテンツだけを抽出します。

!pip install trafilatura
from trafilatura import extract, fetch_url

url = str(search_results.web.results[0].url)
filename = "textfile.txt"

document = fetch_url(url)
text = extract(document)
print(text[:1000])

with open(filename, "w", encoding="utf-8") as f:
    f.write(text)
江戸時代初期,この付近を千駄ヶ原と称し,かつて相模国から奥州へ行くための鎌倉街道の宿駅があったことから原宿といった地名が起こったといわれる。江戸時代は武家屋敷や寺院が並び,明治時代は華族の屋敷が多かった。1906年(明治39年)の山手線延伸により原宿駅 が開業、1919年(大正8年)には明治神宮創建に合わせて表参道が整備された。終戦後は接収された代々木錬兵場跡地に米空軍の兵舎「ワシントンハイツ」が建設され、表参道沿いにはキディランド、オリエンタルバザー、富士鳥居といった米軍将兵とその家族向けの店が営業を始めるようになった。
1964年(昭和39年)には近隣の代々木体育館などを会場として東京オリンピックが開催。ワシントンハイツの場所に選手村が建設され、外国文化の洗礼を受けた若者たちによって「原宿族」が出現した。1966年(昭和41年)には原宿地区初の本格的ブティックである、マドモアゼルノンノンが開店し、モダンな喫茶店やアクセサリー店なども相次いで開店するように。1972年に地下鉄・明治神宮前駅が開業、1973年のパレフランス、1978年のラフォーレ原宿のオープンや、創刊されたばかりのファッション雑誌「アンアン」や「non-no」により原宿が紹介され、アンノン族が街を闊歩、原宿はファッションの中心地として全国的な名声を手に入れた。
80年代前半、原宿の歩行者天国で独特の派手なファッションでステップダンスを踊る「竹の子族」と呼ばれる若者であふれかえった。竹の子族の由来は、竹下通りにあるブティック竹の子で購入した服を着て踊っていたことが由来の一つと言われている。1978年(昭和53年)にはラフォーレ原宿開業し、この頃になると原宿はファッション・アパレルの中心として広く知られるようになり、流行の発信地になった。
1990年代には表参道に海外有名ファッションブランドの旗艦店が続々とオープン。そのかたわら、NIGOが神宮前四丁目にBAPEをオープンさせる。その界隈やキャットストリートには新たなファッショントレンドの店が並び、「裏原宿(ウラハラ)」と呼ばれる一角が形成された。2006年(平成18年)には表参道ヒルズがオープンし、2008年(平成20年)には東京メトロ副都心線が開業。ハワイ生まれパンケーキやフレイバーポップコーン、クレープといったスイーツ店に行列ができ、低価格帯の雑貨

以上で、Brave Search API を使用して検索結果を取得し、Trafilatura を使用して1つ目の検索結果からテキストを抽出ができました。

検索結果をもとに回答するエージェントを作成

サンプルコードで大まかな流れはわかりましたので、Anthropic API と Brave Search API を使って、検索結果を元に回答する簡単なエージェントを作成してみます。

import re

from anthropic_tools import BaseTool, ToolUser  # noqa F401
from brave import Brave
from trafilatura import extract, fetch_url


# ウェブをリサーチするメインの関数を先に定義してしまいます。
def scrape_page(url: str) -> str:
    """指定されたURLからテキストを取得する。"""
    document = fetch_url(url)
    text = extract(document)
    print(url)
    print("-" * 80)
    print(text[:1000], "..." if len(text) > 1000 else "")
    print("-" * 80)

    return text


def research_web(query: str, max_doc_len: int = 10000) -> str:
    """ウェブから検索結果を取得し、最初の`max_doc_len`文字を返す。"""
    print("### 検索を開始 > 検索語句:", query)  # 確認用
    brave = Brave()

    # 検索条件
    num_results = 1
    country = "JP"
    search_lang = "jp"
    ui_lang = "ja-JP"

    # brave-searchを使ってwebから検索結果を取得
    search_results = brave.search(
        q=query,
        count=num_results,
        country=country,
        search_lang=search_lang,
        ui_lang=ui_lang,
    )
    url = str(search_results.web.results[0].url)
    filename = re.sub(r"[^a-zA-Z0-9_]", "_", url) + ".txt"  # URLからファイル名を作成

    # URLからテキストを取得
    text = scrape_page(url)

    with open(filename, "w", encoding="utf-8") as f:
        f.write(text)

    return text[:max_doc_len]  # 長くなりすぎないように最初のmax_doc_len文字だけ返す


# BaseToolを継承してResearchWebToolを作成
class ResearchWebTool(BaseTool):
    """Tool to search the web for a query."""

    def use_tool(self, query):
        return research_web(query, max_doc_len=10000)


tool_name = "research_web"
tool_description = "Research the web for a query."
tool_parameters = [{"name": "query", "type": "str", "description": "The query to search for."}]

research_web_tool = ResearchWebTool(tool_name, tool_description, tool_parameters)

質問しやすいように簡単な関数を定義します。

# [{'role': 'user' or 'assistant', 'content': str}]
ConversationHistory = list[dict[str, str]]


def ask(
    agent: ToolUser,
    question: str,
    history: ConversationHistory = [],
    verbose: float = 0.0,
) -> tuple[str, ConversationHistory]:
    """質問を受け取り、回答と会話履歴を返す。"""
    history.append({"role": "user", "content": question})
    response = agent.use_tools(
        history,
        execution_mode="automatic",
        verbose=verbose,
        temperature=0.3,
    )
    history.append({"role": "assistant", "content": response})
    return response, history

プロンプトを作るうえでは Anthropic 自体が出しているガイドがとても参考になりそうです。特に、XML タグでの定義がおすすめされているのが特徴的でした。

system_promptでもXMLタグを使ってみます。

system_prompt = """<role>あなたは日本の歴史に大変詳しいAIアシスタントです。
ユーザーの質問に対し、ウェブから情報を検索し、事実に基づく回答を返します。</role>

<task>
フレンドリーな関西弁の口語体で返答してください。
必ず下記のワークフローに従って回答をしてください。
1. これまでの会話履歴を踏まえ、ユーザーの質問を言い換え、<question>として記録する
2. 質問を回答するのに必要な情報を得るのに最適な検索語句を考える
3. その検索語句を使ってウェブ検索を行う
4. 検索結果で得られたテキストに答えがない場合は、検索語句を変えて再度検索を行う。2回だめだったら諦めてユーザーに謝る。
5. 検索結果で得られたテキストを元に、質問に対する回答を作成して<answer>として回答する。
</task>
"""

# エージェントを定義
agent = ToolUser(
    [research_web_tool],
    max_retries=3,
    model="default",
    system_prompt=system_prompt,
    temperature=0.3,
    verbose=1.0,
)

conversation_history = []

question = "原宿の歴史について教えて下さい。"

response, conversation_history = ask(agent, question, conversation_history, verbose=0.0)

verbose=1とすることで生成される system_prompt がプリントされるようにしてあります。与えた system_prompt とfunction calling用に生成される部分のsystem_promptの合計なので割と長くなってきます。

----------SYSTEM_PROMPT----------
<role>あなたは日本の歴史に大変詳しいAIアシスタントです。
ユーザーの質問に対し、ウェブから情報を検索し、事実に基づく回答を返します。</role>

<task>
フレンドリーな関西弁の口語体で返答してください。
必ず下記のワークフローに従って回答をしてください。
1. これまでの会話履歴を踏まえ、ユーザーの質問を言い換え、<question>として記録する
2. 質問を回答するのに必要な情報を得るのに最適な検索語句を考える
3. その検索語句を使ってウェブ検索を行う
4. 検索結果で得られたテキストに答えがない場合は、検索語句を変えて再度検索を行う。2回だめだったら諦めてユーザーに謝る。
5. 検索結果で得られたテキストを元に、質問に対する回答を作成して<answer>として回答する。
</task>


In this environment you have access to a set of tools you can use to answer the user's question.

You may call them like this:
<function_calls>
<invoke>
<tool_name>$TOOL_NAME</tool_name>
<parameters>
<$PARAMETER_NAME>$PARAMETER_VALUE</$PARAMETER_NAME>
...
</parameters>
</invoke>
</function_calls>

Here are the tools available:
<tools>
<tool_description>
<tool_name>research_web</tool_name>
<description>
Research the web for a query.
</description>
<parameters>
<parameter>
<name>query</name>
<type>str</type>
<description>The query to search for.</description>
</parameter>
</parameters>
</tool_description>
</tools>

回答内容を見てみましょう。

print(response)
<search_quality_reflection>
検索結果は原宿の歴史について、江戸時代から現代までの変遷を詳しく説明しており、質問に対する十分な情報が得られています。
</search_quality_reflection>

<search_quality_score>5</search_quality_score>

<answer>
原宿の歴史についてまとめたで!

江戸時代は千駄ヶ原言うて、鎌倉街道の宿場町やったんや。明治になって山手線の原宿駅ができて、明治神宮の表参道も整備されたんやって。

戦後はアメリカ軍の施設ができて、そこ向けの店がたくさんできたんや。東京オリンピックの時は選手村にもなって、外国文化の影響受けた若者が「原宿族」言うて現れたんやって。

そのあと、ファッションの店がどんどんできて、アンノン族とか竹の子族とか新しいファッションが生まれて、原宿は流行の発信地になっていったんやな。

90年代からは表参道に有名ブランドの店が並んで、裏原宿言うとこも人気になって。最近はスイーツの店とか雑貨屋さんも増えてきとるみたいやわ。

これからもどんどん賑やかになりそうやな!
</answer>

続いて2個目の質問をしてみます。

question = "なるほど。「竹の子族」って何?名前の由来は?"
response, conversation_history = ask(agent, question, conversation_history, verbose=0.0)

print(response)
<search_quality_reflection>
検索結果から、竹の子族の概要や名前の由来について詳しく説明されていることがわかりました。
竹の子族とは1980年代前半に原宿の歩行者天国で派手な衣装を着て踊っていた若者グループのことで、
名前の由来は衣装を購入していた「ブティック竹の子」に由来するという説明がありました。
これらの情報から、ユーザーの質問に十分答えられると思います。
</search_quality_reflection>

<search_quality_score>5</search_quality_score>

<answer>
なるほどな、竹の子族っちゅうんは1980年代前半に原宿の歩行者天国で踊ってた若者グループのことやねんて。
派手な衣装着て、ラジカセからディスコミュージック流しながらステップダンス踊ってたらしいわ。
ピーク時には2000人以上おったんやて!めっちゃ人気やったんやなぁ。

で、名前の由来なんやけど、竹の子族が着てた衣装を売ってた店が「ブティック竹の子」っちゅう店やってん。
そこで衣装買うてたから「竹の子族」って呼ばれるようになったんちゃうかなぁ、って言われとるみたいやわ。
まぁ諸説あるみたいやけどな。

ほんで竹の子族のファッションは、原色の生地使って奇抜なデザインのもんが多かってん。
化粧もド派手にしてたみたいやし、目立つの大好きやったんやろなぁ。
東洋っぽさもあったらしいで。

そんな感じで竹の子族は80年代を代表する若者文化のひとつやったんよ。
ほんまに独特のカルチャーやったみたいやなぁ。
</answer>

いい感じに生成できました。

終わりに

以上、Anthropic API と Brave Search API を使って、検索結果を元に回答するエージェントを作成してみました。Function callingやsystem_promptが長くなってくると生成時間も長くなってしまい、実際の利用場面に対してのチューニングが必要そうです。ただ、指示通りの返答をしっかりしてくるところが Claude 3の優秀さですね。

ここまでお読みいただきありがとうございます。少しでも参考になればと思います。

もし似たようなコンテンツに興味があれば、フォローしていただけると嬉しいです:

https://twitter.com/alexweberk

後日談ですが、よく見ると `anthropic-tools` には `brave_search_tool.py` というツールがすでに用意されていました(README では見つけられなかったのですが...)。もしかしたら今回やったことがもう少し簡単にできるかもしれません。

そのまま使える(はず)今回使った Notebook の Gistも公開します:



この記事が気に入ったらサポートをしてみませんか?