LoginSignup
7
9

function calling徹底比較(OpenAI vs. langchain vs. llama)

Posted at

function calling徹底比較(OpenAI vs. langchain vs. llama)

function callingは2023年6月にOpen AIによりリリースされた会話の中に関数を入れ込むための機能です。3つの機能を有しており、"1Userの入力に対して関数を呼び出すべきか判断", "2自然言語をAPI呼び出しやSQLクエリなどに変換", "3テキストから必要な構造化データを抽出" の3つがあります。今回は主に1と3を検証し、関数を実行するまでを検証していきます。

目次

以下のそれぞれでfunction callingを実行し比較してみます。

  1. Open AI APIを用いてfunction callingの基本を学ぶ
  2. langchainを用いて色んなfunction callingを活用する
  3. llamaを用いたfuction callingを使ってみる
  4. 最後に

1. Open AI APIを用いてfunction callingの基本を学ぶ

1章のソースコードはgithubに配置しております。

以下の公式ドキュメントを参考に解説します。公式ドキュメントでは今回のデモのように最終的に回答が得られるまでの解説はありませんでした。

前準備を行います。

APIキーを定義、パッケージimport、関数を定義します。

# APIキー定義
$ export OPENAI_API_KEY=???
$ pip install ntplib

chat/completions API を呼び出す関数を定義します。

import json
import requests
import os

GPT_MODEL = "gpt-3.5-turbo-0613"

# Open AI API呼び出し関数
def chat_completion_request(messages, functions=None, function_call=None, model=GPT_MODEL):
    headers = {
        "Content-Type": "application/json",
        "Authorization": "Bearer " + os.environ["OPENAI_API_KEY"],
    }
    json_data = {"model": model, "messages": messages}
    if functions is not None:
        json_data.update({"functions": functions})
    if function_call is not None:
        json_data.update({"function_call": function_call})
    try:
        response = requests.post(
            "https://api.openai.com/v1/chat/completions",
            headers=headers,
            json=json_data,
        )
        return response
    except Exception as e:
        print("Unable to generate ChatCompletion response")
        print(f"Exception: {e}")
        return e

今回function callingのテストに使用する関数を定義します。引数にタイムゾーンを与え、現在の日時を取得する簡単な関数を用います。(openai function-calling guidesを参考に定義しています。公式の例ではenumも活用されています。)

import ntplib
import pytz
from datetime import datetime, timezone

# 今回function callingで実行する関数
def fetch_current_datetime_in_timezone(timezone_str):
    client = ntplib.NTPClient()
    response = client.request('pool.ntp.org')
    utc_now = datetime.fromtimestamp(response.tx_time, timezone.utc)
    try:
        target_timezone = pytz.timezone(timezone_str)
        return utc_now.astimezone(target_timezone).strftime('%Y-%m-%d %H:%M:%S.%f %Z%z')
    except pytz.UnknownTimeZoneError:
        raise ValueError(f"Unsupported timezone: {timezone_str}")

# Open AIに通知するfunctionsパラメータ
functions = [
    {
        "name": "fetch_current_datetime_in_timezone",
        "description": "Enter time zone (e.g., Asia/Tokyo, UTC, America/New_York, Europe/London, etc.). Outputs the current date and time corresponding to the time zone.",
        "parameters": {
            "type": "object",
            "properties": {
                "timezone_str": {
                    "type": "string",
                    "description": "Time zone (e.g., Asia/Tokyo, UTC, America/New_York, Europe/London, etc.). Infer this from the conversation.",
                }
            },
            "required": ["timezone_str"],
        },
    }
]

前準備が終わったので、実際にfunction callingを実行してみます。fucntion callingの基本的な処理フローを示しますので、この手順で実行してみます。(関数を実行するのはUser側であることに注意です。)

funcall.png

①会話 + 関数情報 を通知します。

"What time is it now in tokyo, Japan?" という質問とfunctions JsonパラメータをOpen AI API chat/completions にPOSTします。

messages = []
messages.append({"role": "system", "content": "Don't make assumptions about what values to plug into functions. Ask for clarification if a user request is ambiguous."})
messages.append({"role": "user", "content": "What time is it now in tokyo, Japan?"})
chat_response = chat_completion_request(
    messages, functions=functions
)

②会話から関数の引数情報を抜き出す, ③関数名と引数情報を返却

Open AI API chat/completions は以下の通り結果を返却します。Open AI API側で、"What time is it now in tokyo, Japan?"という文章から、タイムゾーンとして"Asia/Tokyo"を抜き出していることがわかります。

assistant_message = chat_response.json()["choices"][0]["message"]
messages.append(assistant_message)
assistant_message
{
    "role": "assistant",
    "content": null,
    "function_call": {
        "name": "fetch_current_datetime_in_timezone",
        "arguments":  {
            "timezone_str": "Asia/Tokyo"
        }
    }
}

④関数を実行する

得られた引数情報を使って、関数をUser側で実行します。

if assistant_message.get("function_call"):
    function_call_result = assistant_message["function_call"]
    
    # Get function by name from global scope
    func = globals().get(function_call_result["name"])
    if not func:
        raise ValueError(f"No function named '{function_call_result['name']}' found.")
    
    # Parse arguments and call the function
    arguments = json.loads(function_call_result["arguments"])
    response = func(**arguments)
    print("Answer: ", response)    

以下の通り、関数を実行することで現在時刻を出力できました。

Answer: 2023-09-08 11:58:28.184114 JST+0900

⑤関数実行結果をOpen AIに通知する

関数の実行結果を含めたこれまでの会話をOpen AI API chat/completions にPOSTします。

messages.append({"role": "function", "name": "fetch_current_datetime_in_timezone", "content": response})
print(messages)
chat_response = chat_completion_request(
    messages, functions=functions
)

ここでPOSTする会話情報(messages)は以下のようになります。

[
    {
        "role": "system",
        "content": "Don't make assumptions about what values to plug into functions. Ask for clarification if a user request is ambiguous."
    },
    {
        "role": "user",
        "content": "What time is it now in tokyo, Japan?"
    },
    {
        "role": "assistant",
        "content": null,
        "function_call": {
            "name": "fetch_current_datetime_in_timezone",
            "arguments": "{\n  \"timezone_str\": \"Asia/Tokyo\"\n}"
        }
    },
    {
        "role": "function",
        "name": "fetch_current_datetime_in_timezone",
        "content": "2023-09-08 11:58:28.184114 JST+0900"
    }
]

⑥これまでの会話と関数実行結果から回答文を作成

⑤で関数の実行結果を含めた会話情報を通知することにより、関数の実行結果と会話から尤もらしい回答をOpen AI APIが返却してくれます。

assistant_message = chat_response.json()["choices"][0]["message"]
messages.append(assistant_message)
print(assistant_message)

以下が最終的な出力(assistant_message)です。

{
    "role": "assistant",
    "content": "The current time in Tokyo, Japan is 11:58 AM on September 8, 2023."
}

チャットを補完するAPI連携が簡単に実現しました。Userは関数を実行して結果をOpen AI側に教えたのみです。

※ openaiのPythonパッケージを用いる場合は、pip install openaiでパッケージをインストール後、今回のデモでのchat_completion_request関数をopenai.ChatCompletion.createに置き換えるとopenaiのPythonパッケージを利用した実装になると思います。実行される内容はこちらの説明と変わりはありませんので、openaiのPythonパッケージとの動作比較は割愛します。

2. langchainを用いて色んなfunction callingを活用する

2章のソースコードもgithubに配置しております。

langchainはAgentという機能を用いることでfunction callingを実施します。AgentにはいくつかのTypeが準備されており、このlangchain agent typeAgentType.OPENAI_FUNCTIONSを指定すると、1. Open AI APIを用いてfunction callingの基本を学ぶとほぼ同じシーケンスで動作します。異なる箇所としては関数の実行をlangchainが行ってくれるため、User側は関数を呼び出す実装は不要です。

langchain.png

前準備を行います。

APIキーを定義、パッケージimport、関数を定義します。

# APIキー定義
$ export OPENAI_API_KEY=???
$ pip install openai langchain
import concurrent.futures
import os

import openai
from langchain import LLMChain, PromptTemplate
from langchain.chat_models import ChatOpenAI
from langchain.memory import ConversationBufferMemory

from langchain.agents import AgentType
from langchain.agents import initialize_agent, Tool
from langchain.prompts import MessagesPlaceholder

from typing import Type
from pydantic import BaseModel, Field
from langchain.tools import BaseTool

1OpenAI APIの例と同じく、現在時刻を取得する関数でfunction callingを検証します。langchainでは関数定義の方法はいくつかの方法があります。今回は"BaseToolクラスからサブクラスを作成する方法"で定義しています。

import ntplib
import pytz
from datetime import datetime, timezone

def fetch_current_datetime_in_timezone(timezone_str):
    client = ntplib.NTPClient()
    response = client.request('pool.ntp.org')
    utc_now = datetime.fromtimestamp(response.tx_time, timezone.utc)
    try:
        target_timezone = pytz.timezone(timezone_str)
        return utc_now.astimezone(target_timezone)
    except pytz.UnknownTimeZoneError:
        raise ValueError(f"Unsupported timezone: {timezone_str}")

class TimeZoneDatetimeInput(BaseModel):
  timezone_str: str = Field(description="Time zone (e.g., Asia/Tokyo, UTC, America/New_York, Europe/London, etc.)")

class TimeZoneDatetimeFetcher(BaseTool):
    name = 'fetch_datetime_in_timezone'
    description = "Enter time zone (e.g., Asia/Tokyo, UTC, America/New_York, Europe/London, etc.). Outputs the current date and time corresponding to the time zone."
    args_schema: Type[BaseModel] = TimeZoneDatetimeInput
    
    def _run(self, timezone_str: str):
        return fetch_current_datetime_in_timezone(timezone_str)
    
    def _arun(self, ticker: str):
        raise NotImplementedError("fetch_datetime_in_timezone does not support async")

function callingの対象とする関数は前述の通り定義したTimeZoneDatetimeFetcher()のみとし、initialize_agentでAgent機能を有効化します。(複数のfunctionとする場合、listの順番に関数が呼び出されます。)

tools = [
  TimeZoneDatetimeFetcher(),
]

memory = ConversationBufferMemory(memory_key="memory", return_messages=True)

agent_kwargs = {
  "extra_prompt_messages": [MessagesPlaceholder(variable_name="memory")],
}

prompt = """
Don't make assumptions about what values to plug into functions. Ask for clarification if a user request is ambiguous.
"""

memory.save_context({"input": prompt}, {"output": "I got it!"})

openai.api_key = os.environ["OPENAI_API_KEY"]
llm = ChatOpenAI(model_name="gpt-3.5-turbo")

agent = initialize_agent(
    tools,
    llm,
    agent=AgentType.OPENAI_FUNCTIONS,
    verbose=True,
    agent_kwargs=agent_kwargs,
    memory=memory,
)

前準備が終わったら、あとは質問するのみです。

agent.run(input="現在時刻は何時ですか?")

以下のように出力されます。> Finished chain.まではlangchainのデバッグログになります。デバッグログを見ると、1OpenAI APIの例と同じく、「日本語で質問したので自動的にタイムゾーンは'Asia/Tokyo'と判断」、「関数を呼び出し"2023-09-06 12:36:00.914737+09:00"を出力」、「これまでの会話と関数出力から回答"現在の時刻は、2023年9月6日 12時36分です。"を出力」というように処理されていることがわかります。(実際にブレークポイントを貼ってOpen AIのAPIが想定通り叩かれていることを確認済みです。)

> Entering new AgentExecutor chain...

Invoking: `fetch_datetime_in_timezone` with `{'timezone_str': 'Asia/Tokyo'}`

2023-09-06 12:36:00.914737+09:00

現在の時刻は、2023年9月6日 12時36分です。

> Finished chain.
'現在の時刻は、2023年9月6日 12時36分です。'

実用的な例でも試してみます。

agent.run(input="今週の月曜から日曜まで、それぞれ何月何日が出力してほしい")

出力は以下のようになります。このfunction callingを設けておくことで、通常の会話で「今週まで」といったワードを入れることができるようになります。

> Entering new AgentExecutor chain...

Invoking: `fetch_datetime_in_timezone` with `{'timezone_str': 'Asia/Tokyo'}`


2023-09-06 12:21:35.241683+09:00

現在の日付が2023年9月6日であるため、今週の月曜から日曜までの日付は以下の通りです。

- 月曜日: 2023年9月4日
- 火曜日: 2023年9月5日
- 水曜日: 2023年9月6日
- 木曜日: 2023年9月7日
- 金曜日: 2023年9月8日
- 土曜日: 2023年9月9日
- 日曜日: 2023年9月10日

> Finished chain.
'現在の日付が2023年9月6日であるため、今週の月曜から日曜までの日付は以下の通りです。\n\n- 月曜日: 2023年9月4日\n- 火曜日: 2023年9月5日\n- 水曜日: 2023年9月6日\n- 木曜日: 2023年9月7日\n- 金曜日: 2023年9月8日\n- 土曜日: 2023年9月9日\n- 日曜日: 2023年9月10日'

langchainにはいくつかの非常な便利なfunction calling template(langchain tools, hakky 代表的なツールの一覧)がすでに実装されていますので、いくつかを試してみます。

活用例1: serpapi (検索エンジンをスクレイプするAPI)

from langchain.agents import load_tools
tools = load_tools(["serpapi"], llm=llm)

agent = initialize_agent(
    tools,
    llm,
    agent=AgentType.OPENAI_FUNCTIONS,
    verbose=True,
    agent_kwargs=agent_kwargs,
    memory=memory,
)

agent.run("国分寺市のおすすめのラーメン屋は?")

以下のように出力されます。国分寺に詳しいですが良い線です。

> Entering new AgentExecutor chain...

Invoking: `Search` with `国分寺市 おすすめ ラーメン屋`


1. 紅 · 2. 手作り餃子とラーメンのお店 GYUTON · 3. 燗酒屋がらーじ · 4. つけ麺 紅葉 · 5. 立川マシマシ 国分寺店 · 1. 中華そば ムタヒロ1号店 · 2. 武武 · 3. らいおん亭.国分寺市でおすすめのラーメン屋は以下のようなお店があります:

1. 紅
2. 手作り餃子とラーメンのお店 GYUTON
3. 燗酒屋がらーじ
4. つけ麺 紅葉
5. 立川マシマシ 国分寺店
6. 中華そば ムタヒロ1号店
7. 武武
8. らいおん亭

これらのお店は国分寺市で評判が良く、美味しいラーメンを提供しています。訪れてみる価値があります!ただし、営業時間や定休日などは事前に確認することをおすすめします。

> Finished chain.

活用例2: wikipedia (Wikipedia の記事を検索)

from langchain.agents import load_tools
tools = load_tools(["Wikipedia"], llm=llm)

agent = initialize_agent(
    tools,
    llm,
    agent=AgentType.OPENAI_FUNCTIONS,
    verbose=True,
    agent_kwargs=agent_kwargs,
    memory=memory,
)

agent.run("本田圭佑の誕生日は?")

以下のように出力されます。正解です。

> Entering new AgentExecutor chain...

Invoking: `Wikipedia` with `本田圭佑`


Page: Keisuke Honda
Summary: Keisuke Honda (本田 圭佑, Honda Keisuke, born 13 June 1986) is a Japanese professional football manager and former player.A versatile player, Honda usually played as an attacking midfielder, but could also play as a winger, a false nine or as a deep-lying playmaker, and frequently featured as a right winger for Milan during the 2014–15 season. A quick and creative player, he was also known for his accuracy from bending free-kicks, powerful striking ability from distance, dribbling skills and delivery as a set-piece specialist.
Honda earned over 90 international caps between 2008 and 2018, playing at the 2010, 2014 and 2018 World Cups. He was also part of the squad which won the 2011 Asian Cup, where he was also voted Player of the Tournament.

Page: Keisuke
Summary: Keisuke (written: 京佑, 圭佑, 惠佑, 佳祐, 慶祐, 圭祐, 敬佑, 馨祐, 敬輔, 恵輔, 圭輔, 敬典, 恵介, 啓介, 啓祐, 啓輔, 啓左 慶介, 健介, 敬介, 圭介, 銈介, 敬介, 敬助, 蛍介, 景介 or 圭亮) is a masculine Japanese given name. Notable people with the name include:

Keisuke Endo (遠藤 敬佑, born 1989), Japanese footballer
Keisuke Fujie (藤江 恵輔, 1885–1969), Japanese general
Keisuke Fujiwara (藤原 敬典, born 1982), Japanese mixed martial artist
Keisuke Funatani (船谷 圭祐, born 1986), Japanese footballer
Keisuke Hada (羽田 敬介, born 1978), Japanese footballer
Keisuke Harada (原田 圭輔, born 1988), Japanese footballer
Keisuke Hayasaka (早坂 圭介, born 1984), Japanese baseball player
Keisuke Hayashi (林 佳祐, born 1988), Japanese footballer
Keisuke Hoashi (born 1967), American actor
Keisuke Honda (本田 圭佑, born 1986), Japanese footballer
Keisuke Hoshino (星野 圭佑, born 1985), Japanese footballer
Keisuke Ishida (石田 圭祐, born 1955), Japanese actor and voice actor
Keisuke Itagaki (板垣 恵介, born 1957), Japanese manga artist
Itai Keisuke (板井 圭介, born 1956), Japanese sumo wrestler
Keisuke Ito (伊藤 圭介, 1803–1901), Japanese physician and biologist
Keisuke Ito (swimmer) (伊藤 圭祐, 1943–2006), Japanese swimmer
Keisuke Iwashita (岩下 敬輔, born 1986), Japanese footballer
Keisuke Izumi (泉 圭輔, born 1997), Japanese baseball player
Keisuke Kaneko (金子 圭輔, born 1985), Japanese baseball player
Keisuke Kato (加藤 慶祐, born 1988), Japanese actor
Keisuke Katto (甲藤 啓介, born 1983), Japanese baseball player
Keisuke Kihara (木原 敬介, born 1939), Japanese politician
Keisuke Kimoto (木本 敬介, born 1984), Japanese footballer
Keisuke Kinoshita (木下 恵介, 1912–1998), Japanese film director
Keisuke Koide (小出 恵介, born 1984), Japanese actor
Keisuke Kumakiri (熊切 圭介, born 1934), Japanese photographer
Keisuke Kumazawa (熊澤 圭祐, born 1989), Japanese footballer
Keisuke Kunimoto (国本 京佑, born 1989), Japanese-Korean racing driver
Keisuke Kurihara (栗原 圭介, born 1973), Japanese footballer
Keisuke Kurokawa (黒川 圭介, born 1997), Japanese footballer
Keisuke Kuwata (桑田 佳祐, born 1956), Japanese musician and singer-songwriter
Keisuke Makino (born 1969), Japanese footballer
Keisuke Matsumoto (松本 圭介, born 1988), Japanese footballer
Keisuke Minami (南 圭介, born 1985), Japanese actor and singer
Keisuke Mori (森 惠佑, born 1980), Japanese footballer
Keisuke Moriya (森谷 佳祐, born 1986), Japanese footballer
Keisuke Murai (村井 啓介, born 1973), Japanese rower
Keisuke Naito (内藤 圭佑, born 1987), Japanese footballer
Keisuke Ogawa (小川 啓介, born 1986), Japanese footballer
Keisuke Ogihara (荻原 イルマリ 恵介, born 1975), Japanese musician
Keisuke Okada (岡田 啓介, 1868–1952), Japanese admiral, politician and Prime Minister of Japan
Keisuke Okuno (奥野 景介, born 1965), Japanese swimmer
Keisuke Osako (大迫 敬介, born 1999), Japanese footballer
Keisuke Ota (太田 恵介, born 1979), Japanese footballer
Keisuke Ota (footballer born 1981) (太田 圭輔), Japanese footballer
Ōtori Keisuke (大鳥 圭介, 1833–1911), Japanese military commander
Keisuke Saka (坂 圭祐, born 1995), Japanese footballer
Keisuke Satsuki (皐月 啓左, born 1941), Japanese water polo player
Keisuke Sawaki (born 1943), Japanese long-distance runner
Keisuke Sekiguchi (関口 圭亮, born 1986), Japanese footballer
Keisuke Serizawa (芹沢 銈介, 1895–1984), Japanese textile designer
Keisuke Shimizu (清水 圭介, born 1988), Japanese footballer

本田圭佑の誕生日は1986年6月13日です。

> Finished chain.

活用例3: terminal (ターミナルでコマンドを実行)

tools = load_tools(["terminal"], llm=llm)

agent = initialize_agent(
    tools,
    llm,
    agent=AgentType.OPENAI_FUNCTIONS,
    verbose=True,
    agent_kwargs=agent_kwargs,
    memory=memory,
)

agent.run("googleへのpingの応答速度は?")

以下のように出力されます。コマンドを打ち込み内容を確認するより速いです。

> Entering new AgentExecutor chain...

Invoking: `terminal` with `{'commands': 'ping -c 5 google.com'}`


PING google.com (172.217.161.78): 56 data bytes
64 bytes from 172.217.161.78: icmp_seq=0 ttl=117 time=7.179 ms
64 bytes from 172.217.161.78: icmp_seq=1 ttl=117 time=9.638 ms
64 bytes from 172.217.161.78: icmp_seq=2 ttl=117 time=9.868 ms
64 bytes from 172.217.161.78: icmp_seq=3 ttl=117 time=10.218 ms
64 bytes from 172.217.161.78: icmp_seq=4 ttl=117 time=9.278 ms

--- google.com ping statistics ---
5 packets transmitted, 5 packets received, 0.0% packet loss
round-trip min/avg/max/stddev = 7.179/9.236/10.218/1.073 ms
Googleへのpingの応答速度は、平均で9.236 msです。ただし、ネットワークの状況や接続先の距離によって速度は変動する可能性があります。

> Finished chain.

3. llamaを用いたfuction callingを使ってみる

これまではOpen AIのAPIを用いたfunction callingを試してきました。langchain agentもOpenAIのAPIを叩いてfunction callingを行なっているので、これまで紹介したfunction callingはOpen AIのAPIで行われているものです。Open AI APIの中身はブラックボックス化されており、どのように引数を抜き出しているのかは不明です。(何かしら論文などで公開されていることをご存知の方は是非教えてください!)Open AIのAPI以外でfunction callingを実行するのは以下のような方法があると思います。

  1. フルスクラッチでfunction calling機能を実装する
  2. llama2がホストされたllama APIを使用する
  3. llama2派生のfunction callingモデルを使用する(Llama-2-7b-chat-hf-function-callingなど)

ここまで書いていて随分長くなってしまったので、上記の検証は次回にします・・!

4. 最後に

色々使ってみて、以下の2点が重要と感じています。

  • "1Userの入力に対して関数を呼び出すべきか判断", "2自然言語をAPI呼び出しやSQLクエリなどに変換", "3テキストから必要な構造化データを抽出" それぞれ意図した100%の精度で動作するわけではないので、system promptで注意や補足しておくことで意図した精度に近づけるように調整する。
  • function callingで外部APIにPOSTやDBにINSERTする場合、実行する前にUserに内容の確認するフェーズを設けることで上記のようにfunction callingのHallucinationへの対策となる。
7
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
9