生成AIアプリをより多機能に(Function Calling)

本記事は日本オラクルが運営する下記Meetupで発表予定の内容になります。発表までに今後、内容は予告なく変更される可能性があることをあらかじめご了承ください。

https://oracle-code-tokyo-dev.connpass.com/event/325752/

※本記事は下記記事の補足記事となります。

https://qiita.com/ksonoda/items/f3c703fb7b689fd65868





# Function Calling(Tool Calling)
質問内容からLLMが事前定義済みの「関数(function(tool)」を呼び出して、必要な情報を取得し、その結果を応答テキストに盛り込むことができるようにする機能です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109260/808201d0-23e3-3661-0192-011006d705b0.png)

例えば、プロンプトの内容から必要に応じて下記のような処理を実行し、その結果を応答テキストに反映することが可能になります。

- googlesearchを実行し応答テキスト生成に必要な情報を検索
- ニュースサイトサイトの最新情報を検索
- twitterで今現在話題になっている情報を検索
- データベースやCRMなど業務システムからの情報取得

LLMを使ったアプリケーションを大幅に拡張できる非常に有用な機能なのでアップデートも頻繁にあり現在はTool Calling(Function Callingの上位互換)という名前になっていますが、現在のところ基本的にできることはFunction Callingと変わりません。

Function Callingを使うことで、必要に応じて世の中にある様々なサービスにアクセスして情報を取得することにより、最新で具体的かつ正確な応答テキストを生成できるようになります。


#### Function Callingの仕組み

天気情報を提供しているサイト[OpenWeather](https://openweathermap.org/)に現在の天気情報を確認するfunction(tool)を定義しておけば、ユーザーから「今日の東京は？」というプロンプトが入力された際に、このfunction(tool)で今現在の天気情報を確認し、その結果からテキスト生成を行うことができるようになります。

その際のFunction Callingの処理シーケンス概要は下図のようなものになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109260/9c966209-9b6e-2165-6749-741357990c90.png)

ユーザーアプリ側にtoolを定義しますが、これは一つでも複数でも構いません。上図は複数のtool(get_news_info、get_weather_infoなど複数のtoolが既に定義されている状態です。(上図左上の「Function(Tool)の定義」というところです。この状態からプロンプトを入力した場合の処理シーケンスは下記のようなものになります。


1. プロンプトを入力すると、プロンプトだけでなく定義しているtoolの内容がLLMに渡されます。

2. プロンプトの内容から、toolが必要なのか、不要なのか、必要な場合はどのtoolを使うのか、複数のツールを使う場合はどの順序で使うのかなどが決められます(推論されます)。

3. 使うtoolが決まったのち、そのtool、つまり関数の引数の値を決め、実行する関数を完成させます。

4. 完成した関数(ここではget_weather_info(tokyo))を実行します。これはLLMの仕事ではなくユーザーアプリ側で実行されます。

5. この例の想定ではget_weather_info(tokyo)により、天気情報を提供するサイトOpenWeather.orgが公開するAPIをCallし東京の天気情報を取得します。

6. 取得した天気情報と、元のプロンプトをLLMに入力します。

7. 最終的な応答テキストが生成されます。

特に、function(tool)の処理を実行しているのはあくまでもユーザーアプリ側であり、なんでもLLMで処理しているのではないという点に注意したいですね。

あくまでも、ユーザーアプリとLLMとの連携で処理をこなしていくというイメージです。

#### ユースケース
Function Callingは応用範囲が広く、これにより様々な処理を加えた業務アプリケーションを開発することができるようになります。例示を絞ることはできませんが一例として下記のようなものが挙げられます。特にLLM外部の業務アプリやサービスとの連携が必要になるユースケースは全てが適用対象だと言っていいと思います。


| ユースケース              | 説明                                                                                 |
|-----------------------------|-------------------------------------------------------------------------------------------|
| カスタマーサポートの自動化 | カスタマーサポートチャットボットが、ユーザーの問い合わせに応じてデータベースから情報を取得。顧客満足度の向上と迅速な対応が可能。|
| 営業支援ツール            | 営業担当者が顧客情報を即座に取得するためにCRMシステムに関数呼び出しを行う。効果的な顧客対応と営業プロセスの効率化を実現。|
| 顧客フィードバックの分析  | 顧客からのフィードバックを自動的に処理し、トレンドや感情分析を実施。サービスや製品の改善点を迅速に特定できる。|
| 動的なレポート生成        | 売上データやパフォーマンス指標をリアルタイムで取得し、レポートを生成。即時レポート生成により迅速な意思決定をサポート。|
| 動的な在庫管理            | 在庫状況をリアルタイムで監視し、自動的に再発注を行う。 在庫切れの防止と在庫管理の効率化、販売機会の損失の最小化に貢献。|




# サンプルコードの概説(基本編)

Function Calling(Tool Calling)はOpenAIの機能であり、同社のAPIのみでも実装できますが、ここではLangChain(のAgent/Tools)を使ったパターンをご紹介します。OpenAIのAPIのみと比較して少しコードが簡素化されます。

まずは、LangChainの下記チュートリアルを実行し、Function Callingの基本形を理解してみたいと思います。このコードでは掛け算と乗算と足し算の3つのtoolを定義し、数字の処理がちゃんとプロンプト通りに実行できることを確認するコードです。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109260/64018fe4-17e3-112d-723b-9bcdbbf40fd1.png)





まずは必要なライブラリのimportです。キーとなるのは一番最後のcreate_tool_calling_agent、AgentExecutorの2つです。このコードはLangChainのAgentとTools機能を使いますのでこの2つをimportします。Toolsとはまさにtool、つまり、天気の情報を取得したり、google検索をしたりするtoolを実装するために利用します。そして、ツールをどの順序で利用するのかなどのプランニングをAgentが担当します。


```python
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import ConfigurableField
from langchain_core.tools import tool
from langchain.agents import create_tool_calling_agent, AgentExecutor
```

OpenAIのAPIキーを定義します。オプションですがLangSmithのAPIキーも入力します。

```python
import getpass
import os

def _set_if_undefined(var: str):
    if not os.environ.get(var):
        os.environ[var] = getpass.getpass(f"Please provide your {var}")

_set_if_undefined("OPENAI_API_KEY")
_set_if_undefined("LANGCHAIN_API_KEY")

# Optional, add tracing in LangSmith
os.environ["LANGCHAIN_TRACING_V2"] = "true"
os.environ["LANGCHAIN_PROJECT"] = "Tool Calling"
```

次に、toolを定義します。toolと言っても普通に関数を定義するだけです。が、@toolというLangChain特有のデコレータを付けることにより、この後の処理を簡単にまとめることができるようになっています。下記のコードでは上述した3つのtool(掛け算、乗算、足し算)の関数を定義し@toolのデコレータを付けています。

```python
@tool
def multiply(x: float, y: float) -> float:
    """Multiply 'x' times 'y'."""
    return x * y

@tool
def exponentiate(x: float, y: float) -> float:
    """Raise 'x' to the 'y'."""
    return x**y

@tool
def add(x: float, y: float) -> float:
    """Add 'x' and 'y'."""
    return x + y
```

次に、これら3つのtoolをtoolsとしてリストに纏めます。


```python
tools = [multiply, exponentiate, add]
```
これでfunction(tool)は完成です。

プロンプトテンプレートを定義します。

```python
prompt = ChatPromptTemplate.from_messages([
    ("system", "you're a helpful assistant"), 
    ("human", "{input}"), 
    ("placeholder", "{agent_scratchpad}"),
])
```

LLMを定義します。

```python
from langchain_openai import ChatOpenAI

llm = ChatOpenAI(model="gpt-4")
```

最後にTool Calling用のAgentをcreate_tool_calling_agentで作成します。もちろんこのAgentの対象はここまで定義したLLM、prompt、toolsの3つです。そして、AgentExecutorでこのエージェントを実行する環境を構築し、Agentがタスクを実行できるようにします。

```python
agent = create_tool_calling_agent(llm, tools, prompt)
agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)
```

これで準備完了です。
実行してみます。

```python
agent_executor.invoke({"input": "3 に 5 を足して 2 乗すると何になりますか？また、17.24 + 918.1241 はいくつになりますか？", })
```

出力は下記の通り。

```python
> Entering new AgentExecutor chain...

Invoking: `add` with `{'x': 3, 'y': 5}`


8.0
Invoking: `exponentiate` with `{'x': 8, 'y': 2}`


64.0
Invoking: `add` with `{'x': 17.24, 'y': 918.1241}`


935.36413に5を足して2乗すると64になります。また、17.24と918.1241を足すと935.3641になります。

> Finished chain.
{'input': '3 に 5 を足して 2 乗すると何になりますか？また、17.24 + 918.1241 はいくつになりますか？',
 'output': '3に5を足して2乗すると64になります。また、17.24と918.1241を足すと935.3641になります。'}
```

Agentによって、始めに足し算のtool(add)が実行され、その次に乗算(exponentiate)が実行され、最後にまた足し算が実行されています。各toolの引数に何が入っているかもわかるようになっています。

こちらがLangChain Agentを利用したFunction Callingの基本形です。纏めると下記のような3ステップの手順です。

①必要な関数をtoolとして定義
②LLMとPromptTemplateを定義
③tool、LLM、PromptTemplateを纏めてAgentとして定義






# サンプルコード概説(実用例編)

上述した基本形を踏まえ、もう少し実用的なサンプルにしたものが下記コードです。

シナリオとしては、「東京のおすすめの観光地は？」というプロンプトに対して天気情報を取得したのち、その結果にふさわしい観光地をお勧めするというものにしてみました。快晴の場合は屋外でもいいですがそれ以外の天気の場合は屋内の施設を推薦するというシナリオです。

作成するtoolとしては下記2つ

- GoogleSearchサービスにアクセスし観光施設を調べる
- [openweathermap](https://openweathermap.org/)にアクセスし現在の天気情報を取得する

上述した基本形の「①必要な関数をtoolとして定義」の関数を入れ替えているだけです。


importするクラスは上述した内容と同じです。

```python
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import ConfigurableField
from langchain_core.tools import tool
from langchain.agents import create_tool_calling_agent, AgentExecutor
```

OpenAI以外に、OpenWeatherとGoogleSearchサービスを利用しますのでそのAPIキーの定義が増えています。

[OpenWeatherのapiキー取得はこちら](https://openweathermap.org/api)
[GoogleSearchのapiキー取得はこちら](https://developers.google.com/custom-search/v1/overview?hl=ja)


```python
import getpass
import os

def _set_if_undefined(var: str):
    if not os.environ.get(var):
        os.environ[var] = getpass.getpass(f"Please provide your {var}")

_set_if_undefined("OPENAI_API_KEY")
_set_if_undefined("LANGCHAIN_API_KEY")
_set_if_undefined("OPENWEATHERMAP_API_KEY")
_set_if_undefined("GOOGLE_CSE_ID")
_set_if_undefined("GOOGLE_API_KEY")

# Optional, add tracing in LangSmith
os.environ["LANGCHAIN_TRACING_V2"] = "true"
os.environ["LANGCHAIN_PROJECT"] = "Tool Calling"
```

まずは、天気確認用の関数を定義します。

```python
import requests

def get_weather(city_name):
    api_key = os.getenv("OPENWEATHERMAP_API_KEY") 
    if not api_key:
        return "API key not set"

    base_url = "http://api.openweathermap.org/data/2.5/weather"
    params = {
        'q': city_name,
        'appid': api_key,
        'units': 'metric'  # Use 'imperial' for Fahrenheit
    }

    response = requests.get(base_url, params=params)
    
    if response.status_code == 200:
        data = response.json()
        weather = {
            'city': data['name'],
            'temperature': data['main']['temp'],
            'description': data['weather'][0]['description'],
            'humidity': data['main']['humidity'],
            'wind_speed': data['wind']['speed']
        }
        return weather
    else:
        return f"Error: {response.status_code}, {response.text}"
```

動作確認のため実行してみます。

```python
city_name = "tokyo"
weather_info = get_weather(city_name)
print(weather_info)
```

取得した情報は以下の通りで、東京の現在の天気情報が取得できています。

```python
{'city': 'Tokyo', 'temperature': 31.06, 'description': 'broken clouds', 'humidity': 67, 'wind_speed': 7.72}
```

関数が完成したのでこれをtoolにします。
基本は定義した関数の頭に@toolを付けるだけなのですが、docstringが必須になりますのでご注意ください。このdocstringに記載した文章はLLMから読み込まれ、推論に必要な情報になります。

※下記のコードでは戻り値のうち'description': 'broken clouds'だけが欲しいので絞っています。

```python
@tool
def get_weather_info(city_name: str) -> dict:
    """
    Get the current weather information for a given city.

    Args:
        city_name (str): The name of the city.

    Returns:
        dict: A dictionary containing the status, weather description, and any error messages if applicable.
    """
    api_key = os.getenv("OPENWEATHERMAP_API_KEY")
    if not api_key:
        return {"status": "error", "message": "API key not set"}
    
    base_url = "http://api.openweathermap.org/data/2.5/weather"
    params = {
        'q': city_name,
        'appid': api_key,
        'units': 'metric'
    }

    response = requests.get(base_url, params=params)
    
    if response.status_code == 200:
        data = response.json()
        weather_description = data['weather'][0]['description'] if 'weather' in data and len(data['weather']) > 0 else "No weather description available"
        return {
            "weather_info": weather_description
        }
    else:
        return {
            "status": "error",
            "message": f"Unable to fetch weather information. HTTP Status Code: {response.status_code}"
        }
```

これで天気情報取得のtoolは完成です。

続いてgooglesearchで観光地を検索するtoolを定義します。


```python
from langchain_core.tools import Tool
from langchain_google_community import GoogleSearchAPIWrapper

search_tool = GoogleSearchAPIWrapper(k=10)

@tool
def recommend_location(city_name: str, weather_info: str) -> dict:
    """
    Recommends a location in the specified city based on the weather.

    Args:
        city_name (str): The name of the city.
        weather_info (str): The current weather description.

    Returns:
        dict: A dictionary containing the city name, weather_info, and recommended locations.
    """
    if weather_info and weather_info.lower() == "clear sky":
        recommendations = search_tool.run(f"{city_name} 快晴時の観光地")
    else:
        recommendations = search_tool.run(f"{city_name} 屋内の観光施設")
    
    return {
        "city_name": city_name,
        "weather_info": weather_info,
        "recommended_location": recommendations
    }
```

これで2つのtoolが作成できました。
この2つのtoolをリストに纏めます。

```python
tools = [get_weather_info, recommend_location]
```

プロンプトを定義します。

```python
from langchain_core.prompts import ChatPromptTemplate

prompt = ChatPromptTemplate.from_messages([
    ("system", "you are a helpful assistant."), 
    ("human", "{input}"), 
    ("placeholder", "{agent_scratchpad}"),
])
```

LLMを定義します。

```python
from langchain_openai import ChatOpenAI

llm = ChatOpenAI(model="gpt-4")
```

Agentを定義します。

```python
from langchain.agents import create_tool_calling_agent, AgentExecutor

agent = create_tool_calling_agent(llm, tools, prompt)
agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)
```

以上、基本形とほぼかわらないことが見て取れると思います。
以下のプロンプトを入力してみます。

```python
# Tool Calling エージェントの実行
agent_executor.invoke({"input": "東京のおすすめの観光地を教えてください。", })
```

出力は下記の通りです。
シナリオ通り、まず、天気情報を取得し「小雨」ということから屋内の施設をお勧めしている文章になっていることがわかります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109260/480c6c4f-f88c-2473-8c10-049e1c9cb3a3.png)


# OpenAI以外のモデルも試してみる

本記事はOpenAIのモデルを利用しましたが、他のLLMでも実装可能です。例えば、OracleのGenerative AI ServiceをLLMとしてFunction Callingを構成したわかりやすい記事がありましたのでこちらも是非参考にされてください。

https://qiita.com/msasakaw/items/21ce7f48f60ae97ed778

https://qiita.com/shukawam/items/14ee6efc69f75ace0a0e

https://qiita.com/tkote/items/2fed24cf25172e05ea13