Function Callingの強みは気難しいLLMとの付き合いが少し楽になるかもしれないこと。

# ①まえがき
本記事における「ライブラリ」はLangchain, Semantic Kernel, guidanceなどのことを指しています。
ChatGPTやライブラリを扱っているとぶつかる壁を紹介します。（個人的なものです）

- 気分屋
    - 全く同じプロンプト内容でも毎回同じ出力をしてくれるわけではない (temperature=0でも)
    - プロンプト内の指示の順番を変えるだけで出力がガラッと変わったりしますよね、、
    -  出力に応じて関数を発火させる仕組みを実装することを考えると、引数の型・形式などにバラつきがあるととても困ります
- 抽象化
    - ライブラリ側にプロンプトが隠蔽されているケースがある
    - 「気分屋」の影響を受ける可能性が大きいと言える
    - ライブラリ側で少しでも変更されてしまうと出力が変わってしまう
    - 可能な限りユーザ定義のプロンプトを使用したいと思い始めています。




# ②Function Callingとは

- ユーザが定義した関数をOpenAI APIを用いて呼び出すことができます。
    - 正確には、実行すべき関数とその引数をOpenAI(モデル)に動的に決めてもらうことができます
    - 関数を実行する & 結果をどのように使用するかを決めるのはユーザ側です


Function Callingは、「 **ユーザの入力に応じて、実行が必要そうな関数をいい感じに実行する** 」仕組みといったイメージです。このような仕組みは以前から存在しており、Function Callingを使わずとも実現できていました。（自力実装・Langchain・Semantic Kernelなどで）


実現方法としては、「**JSON形式で実行すべき関数と引数を出力しなさい**」といったプロンプトを頑張ってチューニングする、または、ライブラリで提供されているプロンプトを利用するなどです。
そのため、何か新しいことができるようになったというよりは、ユーザ側で頑張る範囲が減ったというイメージです。特に、プロンプトチューニングをしなくてよくなったことが大きいと個人的には思っています。




## 全体イメージ

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/7f54ba0b-363e-a216-3d15-6c11f10b05bd.png)


## 関数の実行イメージ

大きく4ステップです
- ①ユーザの入力
- ②ユーザの入力を受け取り、実行すべき関数を判断
- ③実行すべき関数とその引数を返却
- ④ユーザ側で関数を実行


**①ユーザの入力**
以下二点を満たしたうえで、GPTに入力を投げつけます。

- APIバージョンをFunction Calling用に指定
- 使用したい関数群を指定

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/725bf714-d898-7692-3226-978bf518ff37.png)


**②ユーザの入力を受け取り、実行すべき関数を判断**
ユーザの入力に応じて関数を実行すべきかどうかを判断します。実行すべき関数があるのであれば、関数名と引数を生成してくれます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/7e159c83-b303-8655-9493-47710a5ecf5b.png)


**③実行すべき関数とその引数を返却**

実行すべき関数がある場合は、関数名と引数をユーザに返却します。実行すべき関数があるケースとそうでないケースで出力形式が異なります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/b37ccf77-0139-56e5-c83e-b11519b9f24a.png)


**④ユーザ側で関数を実行**

実行すべき関数があるケースでは、「function_calling」というデータが含まれています。そのデータを抽出し、ユーザ側で関数を実行します。
Function Callingという名前が付いているものの、関数を実行するのはユーザ側です。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/e6d94e36-a326-cba9-7ab8-7ef2e124987c.png)





# ③Function Callingによって解消されること

以下2点が個人的には大きなメリットであると考えています。

- ライブラリを使用せずにエージェント機能を実装しやすくなった
- プロンプトチューニングをしなくていい

これらのメリットによってなにがどう嬉しいのか？といった内容を、これまでのLangchain・Semantic Kernel・guidanceなどを使用した経験を基にした課題と共に見ていきたいと思います。

## a. ライブラリを使用した際に困るケース

ライブラリを使用すると、以下のような場面に遭遇することがあるかと思います。しかし、OpenAIが用意した生のAPIを使用すればAPIの仕様が変わらない限り同じようなことは起きません。

### 例①: Semantic Kernelのプランナーを利用したエージェント機能を実装した時の話

プランナーは、ユーザの入力を基にJSON形式で実行すべき関数を出力してくれる機能です。
バージョンアップに伴い、モデルの出力が少し変わるといった現象に遭遇しました。
具体的には、プランナーの出力末尾に、「 \<**llm_end**\> 」のようなトークンが追加されていました。
プランナーが出力した文字列をJSONにパースするという処理を組み込んでいたため、末尾の\<llm_end\>がJSON形式ではないというエラーが発生するようになりました。

### 例②: guidanceでif文を使用したプロンプトを使用していた時の話

バージョンアップに伴い、構文（おそらくif文）の書き方が少し変わっていました。
こちらについては、詳細を調査できていませんが、```v0.0.60 → v0.0.63```に変更した際、以前のバージョンで動いていたものが動かなくなりました。

## b. プロンプトチューニングは辛い

「 **ユーザの入力に応じて、実行が必要そうな関数をいい感じに実行する (エージェント機能)** 」仕組みを実装しようと思うと、LangchainのカスタムエージェントやSemantic Kernelのプランナーなどが選択肢となります。これらのライブラリを用いて実装するとなると色々と工夫が必要でした。工夫が必要な理由は以下の通りです。

- ライブラリで用意されているデフォルトのプロンプトでは期待通りの動作を得られない (得られにくい)

上記のような理由から、ライブラリの一部の機能を使いつつ、核となるプロンプトをチューニングしてライブラリに埋め込むといった工夫を行っていました。
ただ、プロンプトチューニングは結構辛いです。(大げさにいうと)一文変わるだけでモデルの出力がガラッと変わってしまいます。


## c. 抽象度の高さがネックになる可能性

「プロンプトチューニングは辛い」に記載したこと少し重複しますが、ライブラリ上で定義されているプロンプトを使用する際は注意が必要です。
上述したように入力に応じて出力が変わるという性質を持っているため、ライブラリで抽象化されているプロンプトがバージョンアップで変更された場合、再度チューニングが必要となることが予想されます。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/b2d570f5-eb81-a9b7-9a3e-502417075ed7.png)

※上記の図は正確ではない部分があります。ユーザ定義のプロンプトを用いることもできます。

可能な限りユーザ側で定義したプロンプトを使用したいところです。



# ④Function Callingではできないこと

現状、ユーザの入力から複数の関数の実行順序を考えさせ、実行結果を連携させるようなことをしたいとなると実装面での工夫が必要そうです。本記事の例で言うと、「**関数A→関数Cを実行する必要がある**」といった出力を得たい場合です。
ライブラリのエージェント機能ではこのような動作を簡単に構築することができます。

少しずつ情報が出始めておりFunction Callingでも実装は可能そうなので、実装コストが大きいというイメージの方が正確かもしれません。（要調査）

**※2023年7月1日追記**
Function callingでも実装できそうです。

https://qiita.com/sakue_103/items/49d894f1edaa7e995b8c

# ⑤まとめ・所感

個人的にはライブラリに任せる範囲を少なくすることで、メンテナンスが楽になると思います。
特に、プロンプト内容がライブラリで抽象化されている場合は注意が必要だと感じています、、

ライブラリに任せる部分とそうではない部分をハッキリと判断基準を持って使い分けるのが現状ベターではないかと思っています。

以下2つのLangchainの機能を例にとると、機能①は安心して使える、機能②は注意が必要と言えます。
理由は機能②を実現するためのプロンプトはライブラリ側で定義されているためです。

- 機能①：PDFファイルを読み込んで文字列を抽出する
- 機能②：ReActエージェント機能 (必要なタスクを考えて自動で実行してくれる機能です)

ただ、機能②のいいところはユーザの入力から必要なタスクをモデル自ら勝手に考え、達成できるまで試行錯誤してくれることなので、ユーザにどのようなシステム/機能を提供したいか？といったことも重要な判断基準となりそうです。


ライブラリとどう付き合っていくのかは実際に進めながら探っていくのがよさそうですね、、
最近は可能な限り生のOpenAI APIを呼び出す方法はないか考えて色々実装してみてます:point_up:

次回は実装面についてまとめようかと思います。




# ⑥おまけ

公式ドキュメントで気になる記載。

https://platform.openai.com/docs/guides/gpt/function-calling

- 潜在的なリスク
    - 関数実行の前に確認フローを入れるべき。

>The latest models (gpt-3.5-turbo-0613 and gpt-4-0613) have been fine-tuned to both detect when a function should to be called (depending on the input) and to respond with JSON that adheres to the function signature. With this capability also comes potential risks. We strongly recommend building in user confirmation flows before taking actions that impact the world on behalf of users (sending an email, posting something online, making a purchase, etc).


- トークン数制限に気を付ける
    - ユーザ定義の関数群はシステムメッセージとして入力される


>Under the hood, functions are injected into the system message in a syntax the model has been trained on. This means functions count against the model's context limit and are billed as input tokens. If running into context limits, we suggest limiting the number of functions or the length of documentation you provide for function parameters.