GPTのAPIのアップデートによりFunction Callingという機能が追加されました。今回は、自社データを利用するシステムを想定して、ベクトルデータ検索と組み合わせて、Function Callingを使ってみます。
以下の会話例のように、学習データにない質問への回答を検索したデータに基づいて行います。
[会話例]
ユーザー:
スティービーの武道館公演の日程と回数を教えてください。
AI:
スティービー・ワンダーの日本武道館での公演は以下の通りです:
- 1975年:1月29日、30日
- 1981年:3月31日、4月1日
- 1982年:11月4日、5日
- 1988年:4月25日、26日、27日
- 1995年:2月21日、22日
- 1996年:9月18日、19日、20日
以上の情報から、スティーヴィー・ワンダーは日本武道館で合計11回の公演を行っています。
Embeddingsを準備する
上記の記事内で作成した、Wikipedia(スティーヴィー・ワンダー)を元にしたベクトルデータベース使用し、またそのデータベースへのクエリを行う関数をfunction callingから呼び出します。本ページに記載のコードだけでは動作しません。
### query_collectionは、chromaを検索して、queryと近い文章を探し出し、テキストと関連度を返却する関数
# OpenAIのAPIを使用したEmbeddingの作成から、Chromaへの保存まで以下の記事に記載している
# https://qiita.com/kuromiya123/items/01c2807288c055f017b9
def query_collection(
query: str,
collection: chromadb.api.models.Collection.Collection,
max_results: int = 100)-> tuple[list[str], list[float]]:
results = collection.query(query_texts=query, n_results=max_results, include=['documents', 'distances'])
strings = results['documents'][0]
relatednesses = [1 - x for x in results['distances'][0]]
return strings, relatednesses
strings, relatednesses = query_collection(
collection=stevie_collection,
query="スティーヴィーは日本武道館で何回公演している?",
max_results=3,
)
for string, relatedness in zip(strings, relatednesses):
print(f"{relatedness=:.3f}")
display(string)
この関数の実行結果は以下のようになります。ユーザーのスティーヴィーは日本武道館で何回公演している?
という質問に対して、関連度の高い順にテキストを返却しています。
それでは、このようにベクトル検索を行う関数をGPTにfunctionとして渡して実行してみましょう。
Function Callingを使って、ベクトルデータベースを検索する
全体のコードは以下の通りです。OpenAIのドキュメントに掲載されているサンプルコードと、npaka氏のnoteを元に改変しています。
次の項で流れを追って、解説をします。
import tiktoken
import json
def num_tokens(text: str, model: str = "gpt-3.5-turbo") -> int:
encoding = tiktoken.encoding_for_model(model)
return len(encoding.encode(text))
def run_conversation(question):
# STEP1: モデルにユーザー入力と関数の情報を送る
first_response = openai.ChatCompletion.create(
model="gpt-3.5-turbo-0613",
messages=[{"role": "user", "content": question}],
functions=[
{
"name": "query_collection",
"description": "スティーヴィー・ワンダーに関する記事をベクターストアから検索する",
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "ユーザーの質問文から知りたいと考えている内容を補完してテキストにする",
},
"collection": {
"type": "string",
"description": "検索対象のchromaコレクションを指定"},
},
"required": ["query"],
},
}
],
function_call="auto",
)
message = first_response["choices"][0]["message"]
print("message>>\n", message, "\n\n")
# STEP2: モデルが関数を呼び出したいかどうかを確認
if message.get("function_call"):
function_name = message["function_call"]["name"] #呼び出す関数名
arguments = json.loads(message["function_call"]["arguments"]) # その時の引数dict
# STEP3: 関数を呼び出す
query=arguments.get("query")
strings, relatedness = query_collection(question, collection=stevie_collection, max_results=5)
token_budget=4096-500
text = "" #token budgetまでレスポンスを切り詰める
for string in strings:
next_article = f'{string}\n---\n'
if (
num_tokens(text + next_article)
> token_budget
):
break
else:
text += next_article
print(f"APIが生成した引数:{query}")
print(f"データベースの検索結果:{text}")
# STEP4: モデルにユーザー入力と関数の実行結果を送る
second_response = openai.ChatCompletion.create(
model="gpt-4-0613",
messages=[
{"role": "user", "content": question},
message,
{
"role": "function",
"name": function_name,
"content": text,
},
],
temperature=0,
)
return second_response["choices"][0]["message"]["content"]
else:
#関数を呼び出さなかった場合、ファーストレンポンスの内容をそのまま戻す
return first_response["choices"][0]["message"]["content"]
実行
それでは、質問に対する答えが生成されるまでの流れを追っていきましょう。
question = "スティービーの武道館公演の日程と回数を教えてください。"
print("response>>\n", run_conversation(question))
1回目のAPI呼び出し
1回目のAPIコールでユーザーの入力と関数の情報を渡します。
first_response = openai.ChatCompletion.create(
model="gpt-3.5-turbo-0613",
messages=[{"role": "user", "content": question}],
functions=[
{
"name": "query_collection",
"description": "スティーヴィー・ワンダーに関する記事をベクターストアから検索する",
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "ユーザーの質問文から知りたいと考えている内容を補完してテキストにする",
},
"collection": {
"type": "string",
"description": "検索対象のchromaコレクションを指定"},
},
"required": ["query"],
},
}
],
function_call="auto",
)
1回目のAPIコールで、GPTは質問に対し与えられたfunctionを使用するかどうか、また使用する場合、どのような引数を設定するかを決めます。
今回は、スティービーの武道館公演の日程と回数を教えてください。
というユーザーの質問に対して、functionを使用して回答を生成することを決定。そして、引数であるqueryの値を生成します。以下のようになりました。
APIが生成した引数:スティービー・ワンダー 武道館 公演 日程
次に、この値を使って、関数を呼び出します。
query=arguments.get("query")
strings, relatedness = query_collection(question, collection=stevie_collection, max_results=5)
token_budget=4096-500
text = "" #token budgetまでレスポンスを切り詰める
for string in strings:
next_article = f'{string}\n---\n'
if (
num_tokens(text + next_article)
> token_budget
):
break
else:
text += next_article
データベースからは以下の結果が得られました。処理できるテキストの限界量は、token_budgetにあるため、バジェットいっぱいまで変数textに入れています。
データベースの検索結果:日本公演
~~~~~
1975年
1月29日、30日 日本武道館、31日、2月3日 大阪厚生年金会館、2月1日 静岡駿府会館
1981年
3月31日,4月1日 日本武道館
1982年
~~~~
2回目のAPI呼び出し
ユーザーからの質問と、データベースの検索結果を与えて、2回目のAPIコールを行います。
second_response = openai.ChatCompletion.create(
model="gpt-4-0613",
messages=[
{"role": "user", "content": question},
message,
{
"role": "function",
"name": function_name,
"content": text,
},
],
temperature=0,
)
return second_response["choices"][0]["message"]["content"]
もし、GPTが1回目のAPIコールでfunctionを使わないと判断した場合、1回目のレスポンスに含まれる、first_response["choices"][0]["message"]["content"]
が最終的なGPTの返答になります。2回目のAPIコールは行われません。
結果の確認
最終的に以下のような結果が得られました。
スティービー・ワンダーの日本武道館での公演は以下の通りです:
- 1975年:1月29日、30日
- 1981年:3月31日、4月1日
- 1982年:11月4日、5日
- 1988年:4月25日、26日、27日
- 1995年:2月21日、22日
- 1996年:9月18日、19日、20日
以上の情報から、スティービー・ワンダーは日本武道館で合計11回の公演を行っています。
ん、11回?
何が良いのか
LangchainではLLMを使った処理を行うtoolを用意して、toolを実行するかどうかをまたLLMが判断していました。そのようなライブラリで実装していたようなことがオフィシャルで実装されて、より安定して動くようになったという印象です。LLMによる処理を多段階で重ねて、結果を正規表現で抜き出し、次の処理に繋げて・・・ということをしていくと、どこかで意図しない回答が発生し、全体の処理が不安定化するのは避けられませんでした。今回のアップデートでオフィシャルに処理を連結できるようになったのはとても大きな変化だと思います。
また、embeddingで独自データを利用する手法では、これまでIn-Context Learningと呼ばれるように検索したテキストを最終的にはプロンプトに詰め込んでLLMに渡していました。今回のアップデートで、role:userによる発言ではなく、role:functionによる発言(?)として、messageのリストに入れることができるようになりました。ユーザーの発言とシステム上も区別できるという点から、こちらの方が無理矢理感が薄くていいですね。
messages=[
{"role": "user", "content": question},
message,
{
"role": "function",
"name": function_name,
"content": text,
},
],
functionを使うかどうかをAPIが判断してくれるという点も効いています。スティーヴィー・ワンダーについての質問にとどまらず、ユーザーが他のアーティストについて問い合わせたり、世間話をしても、返答につまらず答えてくれるでしょう。(ノーマルのGPTとして)
ユーザー:スポンジボブに登場するクラリネットの名手の名前は?
AI: スポンジボブに登場するクラリネットの名手の名前はスクイッドワード・テンタクルズです。
※ 日本語版での役名はイカルド、吹替は納谷六朗さんでした。余談。