Azure AI Studio の Prompt flow によるテストと評価のハンズオン第四回目では Function calling の評価をしていきたいと思います。
まずはテンプレートチャットフローから始めましょう。Azure AI Studio の「プロンプトフロー」メニューからプロンプトフローの「+作成」ボタンをクリックして、ギャラリーから「Use GPT Function Calling」を探して「クローン」ボタンをクリックして名前を入力して複製します。
デフォルトの Function calling フローは以下のような処理フローとなっています。
-
use_functions_with_chat_models
: ユーザーの質問から呼ぶべき関数を選んでパラメータを抽出する -
run_function
: 選択した関数を実行して結果を返す
1. カスタマイズ
今回は以前紹介した出張申請アシスタントの例を使用して、レストランとホテルを検索する関数を実装していきたいと思います。
1.1. プロンプトエンジニアリング
use_functions_with_chat_models
ノードのシステムプロンプトを以下のように書き換えます。その下のチャット履歴の部分はそのままにしておきます。
# system:
あなたは Contoso 社の社員の出張を支援するためのアシスタントです。あなたは以下の業務を遂行します。
- 旅程を作成します
- ホテルを検索したり予約します
- 交通機関を検索します
- 出張で行くべきレストランや居酒屋を提案します
- 出張にかかる概算費用を計算します
#制約事項
- ユーザーからのメッセージは日本語で入力されます
- ユーザーからのメッセージから忠実に情報を抽出し、それに基づいて応答を生成します。抽出できない場合は必要な項目を聞き返します。
- ユーザーからのメッセージに勝手に情報を追加したり、不要な改行文字を追加してはいけません
1.2. 関数呼び出しの追加
プロンプトボックスの上にある「関数呼び出し」部分をクリックして開き、functions
項目のテキストボックス右側の編集ボタンをクリックして以下の JSON を貼り付けて保存します。
スキーマ定義
このスキーマに沿うようにパラメータを抽出せよというモデルへの指示です。
[
{
"name": "search_hotpepper_shops",
"description": "ホットペッパーグルメAPIを利用し、キーワードや個室の有無などのオプションフィルターで飲食店を検索できます。",
"parameters": {
"type": "object",
"properties": {
"keyword": {
"type": "string",
"description": "飲食店を検索するためのキーワード。店名、住所、駅名、お店ジャンルなどを指定できる。ユーザーメッセージから検索キーワードとなる文字を抽出して検索クエリーにしてください。例: ###大阪駅 和食###"
},
"private_room": {
"type": "integer",
"description": "個室ありの店舗のみを検索, 0:絞り込まない, 1:絞り込む。オプション",
"enum": [
0,
1
]
}
},
"required": [
"keyword"
]
}
},
{
"name": "search_vacant_hotels",
"description": "楽天トラベルのAPIを使って、場所、チェックイン日、チェックアウト日、予算、大人の人数など、さまざまなフィルターで空室ホテルを検索できます。",
"parameters": {
"type": "object",
"properties": {
"latitude": {
"type": "number",
"description": "ホテル検索場所の緯度(WGS84), ex:35.6065914"
},
"longitude": {
"type": "number",
"description": "ホテル検索場所の経度(WGS84), ex:139.7513225"
},
"searchRadius": {
"type": "number",
"description": "緯度経度検索時の検索半径(単位km), 0.1 to 3.0"
},
"checkinDate": {
"type": "string",
"description": "yyyy-MM-dd 形式のチェックイン日。年の指定がない場合は2024年を指定する。"
},
"checkoutDate": {
"type": "string",
"description": "yyyy-MM-dd 形式のチェックアウト日。年の指定がない場合は2024年を指定する。"
},
"maxCharge": {
"type": "integer",
"description": "上限金額, int 0 to 999999999"
},
"adultNum": {
"type": "integer",
"description": "宿泊者数, int 1 to 99"
}
},
"required": [
"latitude",
"longitude",
"searchRadius",
"checkinDate",
"checkoutDate"
]
}
}
]
あと、現状の Function calling ノード、バージョン古いですね。Azure OpenAI API の 2023-12-01-preview
バージョンのリリースに伴い、functions
および function_call
パラメーターは非推奨になりました。functions
に置き換わるのは tools
パラメーターで、function_call
に置き換わるのは tool_choice
パラメーターですが、まだ反映されていません。今後のアップデートに期待です。
注意
今後 Structured Outputs の機能も載ってくる可能性がありますので、この記事はすぐ古くなるかもしれません。
これで use_functions_with_chat_models
ノードを実行すると、必要な関数の名前とパラメーターが返るようになります。ここでフローを保存して、チャットボックスからテストしてみましょう。
有楽町近辺でおすすめのイタリアンを探しています
今は実行すべき関数が無いので run_function
ノードで KeyError が発生しますが、use_functions_with_chat_models
ノードの出力を見ると、ちゃんと function_call
に呼ぶべき関数の名前と引数が出力されていることが分かりますね。事前に functions
で定義したスキーマ通りの結果が返ってきています。
{
"arguments":"{\"keyword\":\"有楽町 イタリアン\"}",
"name":"search_hotpepper_shops"
}
1.3. 関数の追加
呼ぶべき関数とパラメーターの取得ができるようになったので、次の処理へ移ります。run_function
ノードの中に呼ばれる関数を実装します。デフォルトでは天気を返す関数が入っているのでこれを以下のサンプルデータを返すスタブ関数で置き換えます。run_function
関数はそのままにしておきます。
実際はここで Web API をコールする処理を実装します。
def search_hotpepper_shops(keyword=None, private_room=0, start=1, count=3):
"""ホットペッパーグルメAPIを利用し、キーワードや個室の有無などのオプションフィルターで飲食店を検索できます。"""
params = {
"keyword": keyword,
"start": start,
"count": count,
"private_room": private_room, # 個室ありの店舗のみを検索, 0:絞り込まない, 1:絞り込む
}
shops = [
{
"name": "トラットリア・AI 有楽町店",
"address": "東京都千代田区有楽町2-4-1 有楽町プラザ3F",
"station_name": "有楽町",
"access": "JR有楽町駅から徒歩2分/地下鉄日比谷駅A3出口から徒歩3分",
"genre": {
"name": "イタリアン・レストラン",
"catch": "本格イタリアンとワインが楽しめるトラットリア",
"code": "G008"
},
"budgetAverage": "2500円(通常平均)/1200円(ランチ平均)",
"open": "月~日、祝日、祝前日: 11:00~23:00 (料理L.O. 22:00 ドリンクL.O. 22:30)",
"close": "無"
},
{
"name": "グリル&バー サンシャイン 有楽町",
"address": "東京都千代田区有楽町2-8-4 サンシャインビル5F",
"station_name": "有楽町",
"access": "JR有楽町駅徒歩3分/地下鉄銀座駅徒歩5分",
"genre": {
"name": "ステーキ・グリル",
"catch": "ステーキとワインが楽しめる",
"code": "G008"
},
"budgetAverage": "4500円(通常平均)/1000円(ランチ平均)",
"open": "月~日、祝日、祝前日: 11:00~23:00 (料理L.O. 22:00 ドリンクL.O. 22:30)",
"close": "不定休"
},
{
"name": "スシバー 銀座 有楽町店",
"address": "東京都千代田区有楽町2-2-3 銀座ビル1F",
"station_name": "有楽町",
"access": "JR有楽町駅徒歩1分/地下鉄銀座駅徒歩2分",
"genre": {
"name": "寿司",
"catch": "新鮮なネタを使った寿司",
"code": "G009"
},
"budgetAverage": "3000円(通常平均)/1200円(ランチ平均)",
"open": "月~日、祝日、祝前日: 11:00~22:00 (料理L.O. 21:30 ドリンクL.O. 21:45)",
"close": "無"
}]
return {"shops": shops, "params": params}
def search_vacant_hotels(latitude, longitude, searchRadius, checkinDate, checkoutDate, maxCharge=50000, adultNum=1, page=1, hits=2):
"""楽天トラベルのAPIを使って、場所、チェックイン日、チェックアウト日、予算、大人の人数など、さまざまなフィルターで空室ホテルを検索できます。"""
params = {
"page": page,
"hits": hits,
"latitude": latitude, # ex:35.6065914
"longitude": longitude, # ex:139.7513225
"searchRadius": searchRadius, #緯度経度検索時の検索半径(単位km), 0.1 to 3.0
"datumType": 1, # WGS84
"checkinDate": checkinDate, # yyyy-MM-dd
"checkoutDate": checkoutDate, # yyyy-MM-dd
"maxCharge": maxCharge, # 上限金額, int 0 to 999999999
"adultNum": adultNum # 宿泊者数, int 1 to 99
}
hotels = [
{
"hotelName": "シティホテル 東京ベイ",
"address": "東京都江東区豊洲2-3-12",
"checkInDate": checkinDate,
"checkOutDate": checkoutDate,
"price": maxCharge,
"numberOfAdults": adultNum,
"roomType": "ダブルルーム",
"rating": "4.5",
"amenities": [
"無料Wi-Fi",
"朝食バイキング",
"フィットネスジム"
],
"access": "地下鉄有楽町線 豊洲駅から徒歩5分"
},
{
"hotelName": "新宿グランデホテル",
"address": "東京都新宿区西新宿7-10-1",
"checkInDate": checkinDate,
"checkOutDate": checkoutDate,
"price": maxCharge,
"numberOfAdults": adultNum,
"roomType": "ツインルーム",
"rating": "4.2",
"amenities": [
"無料Wi-Fi",
"大浴場",
"レストラン"
],
"access": "JR新宿駅から徒歩8分"
},
{
"hotelName": "横浜ベイフロントホテル",
"address": "神奈川県横浜市中区海岸通2-15-4",
"checkInDate": checkinDate,
"checkOutDate": checkoutDate,
"price": maxCharge,
"numberOfAdults": adultNum,
"roomType": "スイートルーム",
"rating": "4.8",
"amenities": [
"無料Wi-Fi",
"朝食サービス",
"プール",
"スパ"
],
"access": "みなとみらい線 元町・中華街駅から徒歩3分"
}]
return {"hotels": hotels, "params": params}
run_function
関数(コメント付き)
パット見何をやっているか分かりづらいのでコメントつけておきましたが、use_functions_with_chat_models
からの入力値から呼ぶべき関数名を取り出して、以下のコードで実行まで行います。ナルホド、データでの関数のやり取りはこーやってやってるんだ。
# Text to 関数実行
globals()[function_name](**function_args)
ではこの状態でフローを保存し、チャットのテストを実施します。
有楽町近辺でおすすめのイタリアンを探しています
run_function
ノードからの出力がレストランの配列であることが確認できます。これは実際に search_hotpepper_shops
関数が実行された結果です。ただし最終出力が JSON になってしまっているので、以下のように整形処理を追加します。
1.4. 整形処理の追加
デフォルトのフローだと関数の実行結果(JSON)を受けてチャットアシスタントの回答として整形する処理が入っていないので、これを追加します。整形処理には gpt-4o
を使用したいと思います。
ツールバーから「+LLM」ボタンをクリックして「MakeResponse」と名付けて追加します。以下のような与えられたリストに基づいて質問に回答する指示を含むプロンプトを貼り付けます。JSON を #list
の下に直接貼り付けて整形は gpt-4o
に任せてしまおうという考えです。
# system:
与えられたお店やホテルのリストを使用してユーザーからの質問に回答します。
#list が空の場合やデータが含まれない場合は、足りない部分を聞き返します。
# list:
{{list}}
# user:
{{question}}
その後、「入力の検証と解析」をクリックすると、list
と question
変数が抽出されます。これに対して以下のように紐づけを行うと、右のようなフローが完成します。
- list:
${run_function.output}
- question:
${inputs.question}
出力の設定変更
チャットの回答に MakeResponse
ノードからの出力を使用するには、一番上の「出力」項目を編集します。llm_output
は使用しないので消しておきます。
OK ですね。
足りない部分を聞き返す
COOL😎
2. 専用 LLM の追加と条件分岐の実装
もしも旅行プランを作成する能力に特化した LLM があった場合、あえて gpt-4o
に答えさせずに Function calling でそちらへ振り分けることもできます。ここでは独自にトレーニングしたモデルやファインチューニングしたカスタマイズモデルを呼ぶことを想定しています。また、このタイミングで条件分岐も試してしまいましょう。
2.1. スキーマ定義を追加
use_functions_with_chat_models
に以下のスキーマを追加します。
{
"name": "make_travel_plan",
"description": "ユーザーのキーワードから旅行プランを作成します。",
"parameters": {
"type": "object",
"properties": {
"keyword": {
"type": "string",
"description": "旅行プランを作成するためのキーワード。ユーザーメッセージから検索キーワードとなる文字を抽出して検索クエリーにしてください。例: ###大阪旅行###"
}
},
"required": [
"keyword"
]
}
}
2.2. 関数の追加
run_function
に以下の関数を追加します。Python コード内で LLM をコールしてもよいのですが、今回はあえて LLM ツールを利用したいと思います。ですので、関数内では処理をせずに後段処理に旅行プラン作成用のキーワードとフラグを引き渡します。
def make_travel_plan(keyword):
return {"keyword": keyword, "plan": 1}
2.3. LLM ツールの追加
ツールバーから「+LLM」をクリックして「MakeTravelPlan」と名付けます。接続設定には MaaS でデプロイしたでサーバーレス API をセットします。ここでは、Phi-3-medium-4k-instruct
を使用しています。
例えば以下のようなプロンプトを記述します。
# system:
ユーザーのニーズに合わせた旅行プランを作成します
# user:
{{question.keyword}}
そして、question
パラメーターには ${run_function.output}
をマッピングします。
when ステートメントの利用
「構成のアクティブ化」項目で指定した条件を満たす時に MakeTravelPlan
ノードが実行されるように設定することができます。run_function
ノードから渡された値${run_function.output.plan}
が 1
の時、つまり make_travel_plan
関数が実行された時に MakeTravelPlan
ノードが動きます。それ以外の場合はバイパスされます。
この when を実装するために、run_function
ノードの他の関数の戻り値にも plan
を入れておかないと KeyError になっちゃうのがちょっとカッコ悪いです。もっとスマートなやり方ある気がする。。。
2.4. 最終回答の作成
MakeResponse
のプロンプトを以下のように修正します。プロンプトテンプレートに用いられている Jinja テンプレートで if 文を記述しています。plan
項目がある場合のみプロンプトに挿入するようにしてみました。
# system:
与えられたお店やホテルのリストを使用してユーザーからの質問に回答します。
旅行プランの場合はリストは使いません。
"#list" が空の場合やデータが含まれない場合は、足りない部分を聞き返します。
# list:
{{list}}
{% if plan %}
# plan:
{{plan}}}}
{% endif %}
# user:
{{question}}
2.5. トレースの確認
プロンプトフローのノードが増えてくると各処理ごとのデバッグが面倒臭くなりますよね。その場合はトレース機能が便利です。フローにおける各関数の実行結果や LLM ツールの詳細なリクエスト/レスポンスを確認できます。
MakeTravelPlan
use_functions_with_chat_models
3. フローの評価
評価方法については RAG の評価を参照ください。
シリーズ