はじめに
こんにちは! yu-Matsu と申します。
皆さん、楽しいChatGPTライフをお過ごしでしょうか。昨今の生成AIを取り巻く環境の変化は目まぐるしく、私もついていくのが大変です... そんな中、ChatGPT界隈では先日、Function callingというものが発表されて話題になりましたね!
Function calling and other API updates
発表されてから10日以上経ってしまったため、既にバリバリ使いこなしている方には釈迦に説法かと思いますが、今回はこの Function calling について、LangChainと比較しながら試した内容を記事にしたいと思います。これから Function calling に触れる方の参考になれば幸いです。
本記事で紹介しているコードについて、Google Colaboratory を用意していますので、実際に動かしながら読んでみて下さい!
そもそもFunction callingって?
まずはざっくりと Function calling について触れたいと思います。詳しい解説等は以下の記事が参考になりますので、ご覧ください。
例えば以下のようなコードがあったとしましょう。単純に ChatGPT のAPIを実行するコードです。
def chat_gpt_without_functions(text):
response = openai.ChatCompletion.create(
model="gpt-3.5-turbo-0613",
messages=[
{"role": "user", "content": text},
]
)
return response["choices"][0]["message"]["content"]
「こんにちは」と挨拶をすると、もちろん挨拶が返ってきます。
chat_gpt_without_functions("こんにちは")
> 'こんにちは!どのようにお手伝いできますか?'
しかし、例えば天気について尋ねると、望んだ答えが返ってきません...
chat_gpt_without_functions("東京の天気を教えて")
> '申し訳ありませんが、私は情報を提供できません。天気情報はリアルタイムで変動するため、正確な情報を取得するには天気予報サイトや天気アプリを利用することをおすすめします。'
それもそのはず、ChatGPTは膨大な学習データを元に回答を生成しているため、学習データにない情報の回答や、情報の検索などを行うことが出来ません。まあ、情報の検索であれば、GPT-4のWeb browsingを使えばその限りではありませんが、例えばChatGPTに「TODOリストを管理させたい」など、単なる会話だけではなく様々な機能を持たせたい、と感じる人は多かったと思います。
そういった要望に対応したのがFunction callingという訳です。詳細は後ほどになりますが、Function calling を利用することで、ChatGPTが回答を生成する際に、ユーザーが用意した機能(Function) を活用することが出来るようになりました!
例えば天気を調べる機能を追加したい場合は、天気予報のAPIを叩くFunctionを、TODOリストを管理する機能を追加したい場合は、TODOリストにアイテムを追加するFunctionやDoneリストにアイテムを移動するFunctionをユーザーで定義することが出来ます。
LangChainを利用されている方は、「あれ? これって Agent に似ているような...」と感じたと思います。(私もそうでした) 以降ではLangChainのAgentと比較しながら試してみた内容を記載していきます。ちなみに、LangChainに関しての説明は論旨から外れるため、今回は割愛します。以下の記事が参考になるため、ご覧下さい。
Function calling を動かしてみた
それでは、実際にFunction callingを動かしてみたいと思います。今回は以下のような、場所(東京、大阪、北海道)に応じて固定で天気を返す簡単なFunctionを用意しました。
def weather_function(location):
match location:
case "東京" | "Tokyo":
weather = "晴れ"
case "大阪" | "Osaka":
weather = "曇り"
case "北海道" | "Hokkaido":
weather = "雪"
case _ :
weather = "不明"
weather_answer = [
{"天気": weather}
]
return json.dumps(weather_answer)
上記のFunctionを利用した Function calling の実装例が以下のようになります。
def chat_gpt_with_function(text):
# AIが呼び出せる関数の定義
functions = [
# 何をする関数かについて記述
{
"name": "weather",
"description": "天気を調べる",
"parameters": {
"type": "object",
"properties": {
# location引数についての情報を記述
"location": {
"type": "string",
"description": "天気を知りたい場所を入力。例: 東京",
},
},
"required": ["location"],
},
}
]
# ユーザーの入力から、Functions Callingが必要かどうか判断する
response = openai.ChatCompletion.create(
model="gpt-3.5-turbo-0613",
messages=[
{"role": "user", "content": text},
],
functions=functions,
function_call="auto",
)
# Function Callingが必要なければ、そのままAIの回答になる
message = response["choices"][0]["message"]
# Functions Callingが必要な場合は、messageに関数名と引数を格納されている
if message.get("function_call"):
# messageから実行する関数と引数を取得
function_name = message["function_call"]["name"]
arguments = json.loads(message["function_call"]["arguments"])
# 関数を実行
if function_name == "weather":
function_response = weather_function(
location=arguments.get("location"),
)
# 関数の実行結果を元にAIの回答を生成
second_response = openai.ChatCompletion.create(
model="gpt-3.5-turbo-0613",
messages=[
{"role": "user", "content": text},
message,
{
"role": "function",
"name": function_name,
"content": function_response,
},
],
)
return "AIの回答: " + second_response.choices[0]["message"]["content"]
else:
return "AIの回答: " + message["content"]
実際に実行してみましょう。まず、「こんにちは」と挨拶をしてみます。すると、いつも通りAIから挨拶が返ってきます。
chat_gpt_with_function("こんにちは")
> 'AIの回答: こんにちは!どのようにお手伝いできますか?'
では、「東京の天気を教えて」と聞いてみると...
いい感じで東京の天気を答えてくれました!!
chat_gpt_with_function("東京の天気を教えて")
> 'AIの回答: 東京の天気は「晴れ」です。'
動きを少しだけ見てみる
簡単にどういう動きなのかを見てみたいと思います。
まず、こちらの1回目のAIへの質問で、ユーザーの入力からFunction callが必要かどうかを判断しています。
response = openai.ChatCompletion.create(
model="gpt-3.5-turbo-0613",
messages=[
{"role": "user", "content": text},
],
# 通常のAPI呼び出し時のパラメータに加え、以下2行を追加
functions=functions,
function_call="auto",
)
Function callが必要な場合は、レスポンスに以下のような function_call という情報が含まれます。実行するFunction名と、その引数の情報があることが分かります。
# 東京の天気について質問した場合
{
"arguments": "{\n \"location\": \"\u6771\u4eac\"\n}",
"name": "weather"
}
次に、function_callから情報を取り出し、Functionを プログラムが 実行します。
function_name = message["function_call"]["name"]
arguments = json.loads(message["function_call"]["arguments"])
if function_name == "weather":
function_response = weather_function(
location=arguments.get("location"),
)
最後に、Functionの実行結果を元に、FunctionがAIに質問するという形で回答を作成します。
second_response = openai.ChatCompletion.create(
model="gpt-3.5-turbo-0613",
messages=[
{"role": "user", "content": text},
message,
{
# roleが「function」であることに注目!!
"role": "function",
"name": function_name,
"content": function_response,
},
],
)
このように、AIがFunctionを直接実行するのではなく、あくまで 実行するFunctionの情報をプログラムに渡し、Functionの実行はプログラムが実行するという点に注意して下さい。
LangChainのAgentだとどうなのか
今回の比較対象にしているLangChainの Agent で実装した場合も見てみたいと思います。実際のコードが以下になります。用いる関数(LangChain Agentでは Tool と呼ばれます)は同じく weather_function になります。
from langchain.llms import OpenAI
from langchain.agents import initialize_agent, Tool
from langchain.agents.mrkl import prompt
def lang_chain_agent(text):
llm = OpenAI(model_name='gpt-3.5-turbo-0613')
# toolsに利用したいToolを格納する。LangChainで用意されたToolを利用することも出来る。
# 代表的なTool一覧:https://book.st-hakky.com/docs/agents-of-langchain/
tools = [
Tool(
name = "Weather",
func=weather_function,
description="天気を知りたい場所を入力。例: 東京",
)
]
# エージェントの準備
agent = initialize_agent(
tools,
llm,
agent="zero-shot-react-description",
# AIの回答を日本語にするために必要
agent_kwargs=dict(suffix='Answer should be in Japanese.' + prompt.SUFFIX),
# 以下2行を有効にしておくことで、agentの動きを確認しやすい
# AIの回答のみ欲しい場合はFalseにする
verbose=True,
return_intermediate_steps=True)
response = agent({"input": text})
return response
では実行してみましょう。
lang_chain_agent("東京の天気を教えて")
> Entering new chain...
どのツールを使えばいいか考える必要があります
Action: 天気
Action Input: 東京
Observation: 天気 is not a valid tool, try another one.
Thought:天気ツールは無効です。他のツールを試してみてください。
Action: Weather
Action Input: 東京
Observation: [{"\u5929\u6c17": "\u6674\u308c"}]
Thought:東京の天気は晴れです。
Final Answer: 東京の天気は晴れです。
> Finished chain.
{'input': '東京の天気を教えて',
'output': '東京の天気は晴れです。',
'intermediate_steps': [(AgentAction(tool='天気', tool_input='東京', log='どのツールを使えばいいか考える必要があります\nAction: 天気\nAction Input: 東京'),
'天気 is not a valid tool, try another one.'),
(AgentAction(tool='Weather', tool_input='東京', log='天気ツールは無効です。他のツールを試してみてください。\nAction: Weather\nAction Input: 東京'),
'[{"\\u5929\\u6c17": "\\u6674\\u308c"}]')]}
Final Answer で天気について回答されていることが分かります。
動きについて見てみると、Tool(Function callingで言うところのFunction)の利用が必要な場合、ユーザーの入力から Action と input を抽出しています。Actionが定義されたToolの中になければ、繰り返し探索します。(今回だと2回目にWeatherツールを発見出来ていますが、何回も探索が必要な場合もあります...) 無事Actionが見つかると、AgentがActionを実行し、その結果を元に回答を生成しています。
ライブラリを用いた実装になりますので、随分とシンプルになりました。
Function calling と LangChain Agent の比較
ここまでで、Function calling と LangChain Agent を実際に動かしてみました。両者とも動きは似ていることが分かりますが、違いとしては、Function callingの場合はFunctionを実行するのはAIではなく プログラム であり、LangChain Agent では、Agent がToolを実行しているところになります。
基本的に、今までAgentを使ってきた方はそのままでもいいのでは、と思いつつ、やはり OpenAI公式から機能が提供された というのが大きいのかなと考えています。私がLangChainを勉強する際によく参考にさせていただいた以下の記事にもある通り、LangChainにもし破壊的変更があった場合、コードが動かなくなってしまいます。ライブラリのメンテナンスを少なくしたいと考えると、出来るだけOpenAI API内で完結する方が望ましいです。
しかし、LangChainがめちゃくちゃ便利なのも事実で、AgentのToolに関してだと、なんとよく使われそうなToolはLangChain側から提供されていたりします。
また、Agentの他にも、Memory機能 や Index機能 が提供されていて、簡単にChatGPTを用いた高度なアプリケーションを作成することが出来ます。
まだまだこれからOpenAI APIも機能が充実してくると思いますので、現状はLangChain等のライブラリとの住み分けを上手く考えて実装していくのが良いのかと考えています。実際に、 LangChainのバージョン0.0.203から Function calling がサポートされました ので、次はその例を見ていきたいと思います
LangChain Agent で Function Calling を利用してみる
いきなりですが、実装例は以下のようになります。
from langchain.schema import (
AIMessage,
HumanMessage,
FunctionMessage
)
from langchain.chat_models import ChatOpenAI
def lang_chain_with_function_calling(text):
# AIが利用する関数の定義
functions = [
# 何をする関数かについて記述
{
"name": "weather",
"description": "天気を調べる",
"parameters": {
"type": "object",
"properties": {
# location引数についての情報を記述
"location": {
"type": "string",
"description": "天気を知りたい場所を入力。例: 東京",
},
},
"required": ["location"],
},
}
]
# ユーザーの入力をmessagesに格納
messages=[HumanMessage(content=text)]
llm=ChatOpenAI(model_name='gpt-3.5-turbo-0613')
# ユーザーの入力内容から、Functions Callingが必要かどうか判断する
# Function Callingが必要なければ、こちらの結果がAIの回答になる
message = llm.predict_messages(
messages, functions=functions
)
# Functions Callingが必要な場合は、additional_kwargsに関数名と引数を格納されている
if message.additional_kwargs:
# messageから実行する関数と引数を取得
function_name = message.additional_kwargs["function_call"]["name"]
arguments = json.loads(message.additional_kwargs["function_call"]["arguments"])
# 関数を実行
function_response = weather_function(
location=arguments.get("location"),
)
# 実行結果をFunctionMessageとしてmessagesに追加
function_message = FunctionMessage(name=function_name, content=function_response)
messages.append(function_message)
# FuncitonMessageを元に、AIの回答を取得
second_response = llm.predict_messages(
messages=messages, functions=functions
)
return "AIの回答: " + second_response.content
else:
return "AIの回答: " + message.content
天気について質問した結果は以下になります。
lang_chain_with_function_calling("東京の天気を教えて")
> 'AIの回答: 東京の天気は晴れです。'
問題なく回答が返ってきていますね! コードを見ていただいたら分かる通り、LangChainを利用しない場合とかなり似ています。LangChainの場合は、1回目のAIへの質問のレスポンスに、additional_kwargsとして実行するFunctionの情報が含まれます。
print(message)
> content='' # contentは空
additional_kwargs={'function_call': {'name': 'weather', 'arguments': '{\n "location": "東京"\n}'}}
また、Functionの実行結果は、FunctionMessage として、2回目のAIへの質問に渡されます。
実はAgentでも対応していました
LangChainでのFunction callingについて試している間に、なんと バージョン0.0.205 にてAgentにも対応していました! アップデートが早すぎる...
こちらも実装例を掲載します。
from langchain.agents import AgentType
def lang_chain_agent_with_function_calling(text):
llm = ChatOpenAI(model_name='gpt-3.5-turbo-0613')
# カスタムツールの登録は従来のAgentと同様
tools = [
Tool(
name = "Weather",
func=weather_function,
description="天気を知りたい場所を入力。例: 東京"
)
]
# エージェントの準備
agent = initialize_agent(
tools,
llm,
agent=AgentType.OPENAI_FUNCTIONS, # ここで、AgentType.OPENAI_FUNCTIONSを指定する
verbose=True,
return_intermediate_steps=True)
response = agent({"input": text})
return response
変化点として、今までは agent に zero-shot-react-description などを指定していたところを、代わりにAgentType.OPENAI_FUNCTIONS を指定しています。これで、Agent として OpenAIのFunction callingを利用することが出来ます。
実際に実行してみましょう。
> Entering new chain...
Invoking: `Weather` with `東京`
[{"\u5929\u6c17": "\u6674\u308c"}]今日の東京の天気は晴れです。
> Finished chain.
{'input': '今日の東京の天気を教えて',
'output': '今日の東京の天気は晴れです。',
'intermediate_steps': [(_FunctionsAgentAction(tool='Weather', tool_input='東京', log='\nInvoking: `Weather` with `東京`\n\n\n', message_log=[AIMessage(content='', additional_kwargs={'function_call': {'name': 'Weather', 'arguments': '{\n "__arg1": "東京"\n}'}}, example=False)]),
'[{"\\u5929\\u6c17": "\\u6674\\u308c"}]')]}
「今日の東京の天気は晴れです。」という回答が返ってきています!実行の流れを見てみると、intermediate_stepsのmessage_logで
[AIMessage(content='', additional_kwargs={'function_call': {'name': 'Weather', 'arguments': '{\n "__arg1": "東京"\n}'}}, example=False)]),
'[{"\\u5929\\u6c17": "\\u6674\\u308c"}]')]
という処理が FunctionsAgentAction で実行されたことが分かります。この処理は、Agentを用いない場合に自分で記述した処理と同じ内容になっています。これからはこちらを使うのが良さそう...。
まとめ
Function callingに関して、実際に動かして試した結果をLangChain Agentと比較しつつご紹介しました。比較結果でも述べた通り、現状はライブラリの使い所を上手く考えて付き合っていく必要があるかと思いますが、OpenAI側もLangChain等のライブラリ側もお互いを追いながらアップデートし続けている状況ですので、これからも注意深く動向を見続け、新しい機能が追加されたら取り敢えず使ってみるのが大事なのかなと思った次第です。(どうやら4日前に LangChainにて OpenAI Multi Functions Agent なる機能が追加されたようです...)
今回はこれで以上とさせていただきたいと思います!ありがとうございました!!