NexusRaven-V2でFunction Callingを試す on Databricks

最近試す系が多いんですが、応用にも時間を使いたいこの頃。


# 導入

Huggingfaceのトレンドを見ていると、NexusRaven-V2というfunction callingに特化したモデルがあることを知りました。

https://huggingface.co/Nexusflow/NexusRaven-V2-13B

GPT-4を超えるパフォーマンスということで、少し試してみたいと思います。

# NexusRaven-V2とは

上のサイトより抜粋。

> NexusRavenは、最先端のfunction calling能力を凌駕する、オープンソースで商業的に実行可能なfunction calling LLMです。
>
>💪 **汎用性の高いFunction Calling能力**:NexusRaven-V2は、多くの困難なケースで、単一関数呼び出し、ネストされた呼び出し、および並列呼び出しを生成できます。
>
>🤓 **完全に説明可能**:NexusRaven-V2は、生成する関数呼び出しについて非常に詳細な説明を生成することができます。この動作をオフにして、推論中にトークンを保存できます。
>
>📊 **パフォーマンスハイライト**: NexusRaven-V2は、ネストされた関数と複合関数を含む人間が生成したユースケースにおける関数呼び出しの成功率で、GPT-4を7%上回っています。
>
>🔧 **目に見えないものへの一般化**:NexusRaven-V2は、評価に使用される関数についてトレーニングされたことがありません。
>
>🔥 **商業的に寛容**:NexusRaven-V2のトレーニングには、GPT-4などの独自のLLMによって生成されたデータは含まれません。商用アプリケーションにデプロイすると、モデルを完全に制御できます。

OpenAI社のFunction Callingによく似た機構(LLM部分のみ)を提供するようです。

こちらにデモがあります。日本語でもそれなりに動作します。

https://huggingface.co/spaces/Nexusflow/NexusRaven-V2-Demo

Colab用のサンプルも以下の公開されています。

https://colab.research.google.com/drive/19JYixRPPlanmW5q49WYi_tU8rhHeCEKW?usp=sharing

今回はこちらをベースに、簡易なサンプルを作成してみます。

# Step1. パッケージインストール

いつもの。今回もexllamav2を使って推論します。

```python
%pip install -U -qq transformers accelerate exllamav2 langchain

dbutils.library.restartPython()
```

# Step2. モデルロード

NexusRave-V2モデルをロードします。
オリジナルではなく、以下のGPTQ量子化変換されたモデルを今回利用しました。

https://huggingface.co/TheBloke/NexusRaven-V2-13B-GPTQ

ExLlamaV2を簡易に使うための下記記事のクラスを使ってロードします。
モデルは事前にダウンロードしたものを利用しました。

https://qiita.com/isanakamishiro2/items/c1856c94d4c0227a0d9e

```python
from exllamav2_chat import ChatExllamaV2Model

model_path = "/Volumes/training/llm/model_snapshots/models--TheBloke--NexusRaven-V2-13B-GPTQ"
chat_model = ChatExllamaV2Model.from_model_dir(
    model_path,
    cache_max_seq_len=4096,
    system_message_template="{}",
    human_message_template="{}",
    ai_message_template="{}",
    temperature=0,
    max_new_tokens=1024,
)
```

# Step3. 呼び出すPython関数の定義

Function Callingで実際に実行する関数を定義します。
今回は```get_weather_data```という指定された緯度経度の温度を返す関数と、```get_coordinate_from_city```という都市名を指定すると緯度経度を返す関数をPythonで定義しました。

```python
import requests

def get_weather_data(coordinates):
    """
    指定された緯度と経度にあるOpen-Meteo APIから気象データを取得する。

    Args:
    coordinates (tuple): 場所の緯度

    Returns:
    float: 聞いた場所の現在の温度
    """
    latitude, longitude = coordinates
    base_url = "https://api.open-meteo.com/v1/forecast"
    params = {
        "latitude": latitude,
        "longitude": longitude,
        "current": "temperature_2m,wind_speed_10m",
        "hourly": "temperature_2m,relative_humidity_2m,wind_speed_10m",
    }

    response = requests.get(base_url, params=params)
    if response.status_code == 200:
        return f"""温度(C): {response.json()["current"]["temperature_2m"]}"""
    else:
        return {
            "error": "データ取得に失敗しました、ステータスコード: {}".format(
                response.status_code
            )
        }


def get_coordinates_from_city(city_name):
    """
    Maps.co Geocoding APIを使用して、指定された都市名の緯度と経度を取得する。

    Args:
    city_name (str): 都市名

    Returns:
    tuple: 都市の緯度と経度
    """
    base_url = "https://geocode.maps.co/search"
    params = {"q": city_name}

    response = requests.get(base_url, params=params)
    if response.status_code == 200:
        data = response.json()
        if data:
            # 最初の結果が最も関連性が高いと仮定します
            return data[0]["lat"], data[0]["lon"]
        else:
            return {"error": "指定された都市名のデータが見つかりませんでした。"}
    else:
        return {
            "error": "データの取得に失敗しました、ステータスコード：{}".format(
                response.status_code
            )
        }
```

# Step4. プロンプトテンプレートの準備

LLMに与えるプロンプトのテンプレートを作成します。
見た目がPython関数定義を文字列化したもののように見えますが、使えるようにする関数名とそのdocstringで構成される定義となります。これをLLMに渡すことで、どの関数を実行させるかを判断させる、という構造のようです。

```python
prompt_template = \
'''
Function:
def get_weather_data(coordinates):
    """
    Fetches weather data from the Open-Meteo API for the given latitude and longitude.

    Args:
    coordinates (tuple): The latitude of the location.

    Returns:
    float: The current temperature in the coordinates you've asked for
    """

Function:
def get_coordinates_from_city(city_name):
    """
    Fetches the latitude and longitude of a given city name using the Maps.co Geocoding API.

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

    Returns:
    tuple: The latitude and longitude of the city.
    """

User Query: {query}<human_end>

'''
```

# Step5. とりあえず実行してみる

どのような内容を出力するのか気になるので、上記のテンプレートを利用してLLMにクエリを投げてみます。

```python
from langchain.schema.messages import HumanMessage, SystemMessage

query = "Whats's the weather like in Seattle right now?"
messages = [
    HumanMessage(content=prompt_template.format(query=query))
]

result = chat_model.invoke(messages)

print(result.content)
```

```:出力
Call: get_weather_data(coordinates=get_coordinates_from_city(city_name='Seattle')) 
Thought: The function call `get_weather_data(coordinates=get_coordinates_from_city(city_name='Seattle'))` answers the question "What's the weather like in Seattle right now?" by following these steps:

1. `get_coordinates_from_city(city_name='Seattle')`: This function fetches the latitude and longitude of the city with the given name using the Maps.co Geocoding API. In this case, it returns the coordinates `(47.6062, -122.3321)`.
2. `get_weather_data(coordinates=(47.6062, -122.3321))`: This function fetches the weather data for the given coordinates using the Open-Meteo API. It returns the current temperature in the coordinates you've asked for.

Therefore, the function call `get_weather_data(coordinates=get_coordinates_from_city(city_name='Seattle'))` answers the question "What's the weather like in Seattle right now?" by first getting the coordinates of the city using the Maps.co Geocoding API, and then fetching the weather data for those coordinates using the Open-Meteo API.
```

どうやら、クエリに対して呼び出すべき処理が1行目のCall部に記載され、2行目以降にその処理を選択した理由が出力されるという構成でした。

# Step6. 実行する

実行するべき処理を文字列で受け取ることはできましたが、このままでは処理を実行させられません。
というわけで、LLMから得られた文字列を使って実際の関数呼び出し処理を行ってみましょう。

```python
def parse_command(text:str) -> str:
    return text.split("\n")[0].replace("Call:", "").strip()

eval(parse_command(result.content))
```

```:出力
'温度(C): 5.0'
```

行っていることは単純で、結果文字列の1行目から実行するコマンドを切り出し、```eval```関数でその内容を実行しているだけです。

他のケースも実行してみましょう。

```python
query = "東京の天気は今どうなってる？"
messages = [
    HumanMessage(content=prompt_template.format(query=query))
]

result = chat_model.invoke(messages)
eval(parse_command(result.content))
```

```:出力
'温度(C): 15.6'
```

日本語もOK。
天気ではない実行可能なことを聞いてみます。

```python
query = "大阪の緯度経度を教えて"
messages = [
    HumanMessage(content=prompt_template.format(query=query))
]

result = chat_model.invoke(messages)
eval(parse_command(result.content))
```

```:出力
('34.7021912', '135.4955866')
```

実行不可能な処理をさせてみましょう。

```python
query = "What is the Databricks?"
messages = [
    HumanMessage(content=prompt_template.format(query=query))
]

result = chat_model.invoke(messages)
# eval(parse_command(result.content)) # 実行はしない
print(result.content)
```

```:出力
Call: get_weather_data(coordinates=get_coordinates_from_city(city_name='Databricks')) 
Thought: The function call `get_weather_data(coordinates=get_coordinates_from_city(city_name='Databricks'))` answers the question "What is the Databricks?" by first fetching the latitude and longitude of the city "Databricks" using the Maps.co Geocoding API, and then passing those coordinates to the `get_weather_data` function to retrieve the current temperature at that location.

Here's a step-by-step breakdown of what happens when this function call is made:

1. The `get_coordinates_from_city` function is called with the argument `'Databricks'`. This function uses the Maps.co Geocoding API to fetch the latitude and longitude of the city "Databricks".
2. The `get_coordinates_from_city` function returns a tuple containing the latitude and longitude of the city.
3. The `get_weather_data` function is called with the coordinates returned by `get_coordinates_from_city` as its argument. This function uses the Open-Meteo API to fetch the current temperature at the specified coordinates.
4. The `get_weather_data` function returns the current temperature at the specified coordinates.

Therefore, the function call `get_weather_data(coordinates=get_coordinates_from_city(city_name='Databricks'))` answers the question "What is the Databricks?" by first fetching the latitude and longitude of the city "Databricks", and then using those coordinates to retrieve the current temperature at that location.
```

どうやら、現状のプロンプトテンプレートだとなんらかの処理に当てはめてしまうようですね。
このあたりはプロンプトエンジニアリングで回避できるのかな。

# Step7. おまけ：Chainにする

Langchainを使って、クエリから実行までの一連を処理するシンプルなChainも作ってみます。

```python
from langchain.prompts import ChatPromptTemplate
from langchain.prompts.chat import (
    HumanMessagePromptTemplate,
)
from langchain.schema.output_parser import StrOutputParser
from langchain.schema.runnable import RunnablePassthrough, RunnableLambda


def call_function(text: str):
    return eval(text)


prompt = ChatPromptTemplate.from_messages(
    [
        HumanMessagePromptTemplate.from_template(prompt_template),
    ]
)

chain = (
    {"query": RunnablePassthrough()}
    | prompt
    | chat_model
    | StrOutputParser()
    | parse_command
    | call_function
)
```

試しに実行。

```python
result = chain.invoke("大阪の緯度経度は？")
print(result)
```

```:出力
('34.7021912', '135.4955866')
```

# まとめ

NexusRaven-V2を使ったFunction Callingの実践を行ってみました。

OpenAIのFunction CallingやLangchainのAgentなど、LLMに処理を実行させる仕組は様々なものが出ていますが、シンプルな記法で関数実行ができるので、面白いなと思いました。
LangchainのAgent機能と連携させるやり方がよくわかっていないのですが、統合されるとなお有難いですね。

また、わずか13BのLLMでこれだけの性能が発揮できるというのも驚きです。

単純に試すだけではなくて、様々なアプリ・サービスにどんどん組み込んでいきたいなあ。