会話型AIエージェントでFunction Callingを使いこなす!

どうも、tacomsのエンジニアのMorixです。
現在、飲食店向け電話注文受付AIエージェント「Camel AI Call」を開発しております! 先日プレスリリースも出ました!

prtimes.jp

このプロダクトではLLMを使用していますが、その中でもFunction Callingを多用しています。 使っていく中で様々な課題にぶつかってきたので、その紹介と解決策をいくつか紹介したいと思います!

Function Callingとは

Function Callingとは、LLMが外部システムと連携するための機能です。APIを実行したりDBにアクセスしたり・・・
OpenAIのFunction Callingのページに具体例があるので紹介します。

引用元:https://platform.openai.com/docs/guides/function-calling

  1. Developer(アプリケーション)はプロンプトと使えるツール情報をLLMに送る
  2. LLMはコード実行要求をDeveloperに返す
  3. Developerはコード実行をする
  4. Developerは結果をLLMに送る
  5. LLMは最終結果をDeveloperに返す

この例だと、天気情報を取得するツールを用意しておくとLLMがそれを使って答えを返してくれるというものです。
この仕組みがFunction Callingです。

似たようなものにMCPがありますが、LLMにやらせたいことが明確な場合はFunction Callingを使用するほうが良いと思います。理由は速度と正確性です。もちろんどのような業務システムを提供するかでこの観点の重要度は変わります。
tacomsでは飲食店の電話注文AIエージェントを提供していますが、このプロダクトではレスポンスの速さと正確性が重要になってきます。この2つの観点だとFunction Callingのほうに優位性があると考えています。

Function Callingを使うときの課題と解決策

ここからはFunction Callingを会話型AIエージェントで使っていく中でぶつかった課題と解決策を紹介します!

期待するツールを呼んでくれない

LLMに実行してほしいツールが複数あったときに、「ユーザーのこういう発話のときはこのツールを呼んでほしいのに呼んでくれない」「別のツールを呼んでしまう」ということがありました。
LLMに対し使えるツールリストを提示することができますが、会話の内容を元にそのツールリストを適切に変更をすることは難しいです。
よってLLMに伝えるツールリストには、使えるツールをすべて列挙することになります。

ではどのようにすると適切な場面で適切なツールを使用してくれるようにできるのか?それはシステムプロンプト動的プロンプトによって解決しています。

システムプロンプトはLLMを使うときに一番初めに与えるプロンプトです。ここでツールを呼ぶタイミングを具体的に指示します。

ユーザーが天気のことを聞いてきた場合、`get_weather()` を呼び出してください。
ユーザーがニュースのことを聞いてきた場合、`get_news()` を呼び出してください。

しかしシステムプロンプトの内容が複雑・長大になってくると、LLMはこのコンテキストを忘れることがあります。
それを回避するために動的プロンプトを使用します。動的プロンプトとは私が便宜上つけた名前です。
これはLLMにユーザーの発話内容を送る際に、動的に追加情報を付与することです。

ユーザーが「今日のパリの天気は?」と聞いたら、この文章をそのままLLMに送るのではなく、次の形式にしてLLMに送ります。

ユーザーの発話「今日のパリの天気は?」
ツールの呼び出しルール:
- ユーザーが天気のことを聞いてきた場合: get_weather()
- ユーザーがニュースのことを聞いてきた場合: get_news()

動的プロンプトと言ってるのは、ここに会話の状態に応じた情報を付与できるからです。会話で得られた情報からLLMに絶対に忘れないでほしいことをここで挿入します。例えばこんな感じ↓

ユーザーの発話「今日のパリの天気は?」
ツールの呼び出しルール:
- ユーザーが天気のことを聞いてきた場合: get_weather()
- ユーザーがニュースのことを聞いてきた場合: get_news()

天気情報:
- パリ: 晴れ

このように大事な情報を都度LLMに伝えることで、LLMのツール実行精度や情報理解度が上がります。

ツール実行後のLLMの動作を制御できない

LLMはツールを呼び出した結果を元に返答内容を生成しますが、違うアクションをしてほしい場合があります。
たとえば天気の情報を答える電話応対AIエージェントがあったとして、使えるツールに「天気情報を取る(get_weather) 」と「電話を切る(end_call)」があったとします。
get_weatherが成功したらその結果を元に回答してほしいが、失敗したらユーザーに謝罪し電話を切ってほしいとします。

失敗時の期待動作:

  1. get_weather実行
  2. 失敗したので謝罪文発話
  3. end_call実行

上述の通りシステムプロンプトにも具体的なツール実行指示をしていたとします。
しかしLLMは2で満足してしまい、3を実行してくれないことがあります。   前述の動的プロンプトはユーザーの発話に対する挙動を指示しているので、ツール実行後のことまではフォローできません。

そこでツールの返却値に仕込みをします。ツールの返却値はJSONで実装すると思いますが、そこに ai_instruction というフィールドを追加します。
get_weatherツールでは次のようなJSONを返却するようにします。

# 成功時
{
  "result": "success",
  "result": {
    "天気": "晴れ"
  },
  "ai_instruction": "resultフィールドの値を使って回答を生成せよ"
}

# 失敗時
{
  "result": "error",
  "result": null,
  "ai_instruction": "謝罪文を生成し、その後end_callを実行する"
}

システムプロンプトの冒頭部分に「function callingのツールを実行したときは、ai_instructionの指示に必ず従うこと」と記載します。(動的プロンプトにもこの指示を入れておくと効果的です)
このようにすると意図した挙動をするようになりました。システムプロンプトだけに書いておいてもLLMは忘れるので、一番新しいコンテキストに指示を込めるのが大事だと思ってます。

これを使うと次のようにAIの生成内容にも口を出せます。

{
  "result": "success",
  "result": {
    "天気": "晴れ"
  },
  "ai_instruction": "「パリの天気は晴れです」とそのまま発話してください。余計なことは言わないこと。"
}

実行が遅い

通常であれば会話の流れとして次の2ステップで済みます。

  1. ユーザーがなにか言う
  2. LLMがそれを解釈して回答する

しかしFunction Callingを使うと次の4ステップかかります。

  1. ユーザーがなにか言う
  2. LLMはツールの実行が必要であることを判断する
  3. LLMはツール実行を行う
  4. LLMはツール実行結果を元に回答する

さらにツール実行はLLM呼び出し元のサーバー上で実行されるため、そのネットワーク経路やプログラム実行速度もレイテンシーに加味されます。

実行が遅いことが課題である場合、どこで時間がかかっているかの計測が必要です。大まかに時間がかかる要因としては次の4点があると思います。

  1. LLMがツールの実行が必要だと判断するまでの時間
  2. 1の判断がおわりそこからツールが実行するまでの時間(≒LLMからサーバーへのネットワークレイテンシー
  3. ツールの実行速度
  4. ツールの実行結果を元に回答を生成するまでの時間

2が主要因の場合、LLMとサーバーの距離を小さくしましょう。
3が主要因の場合は、ツール(プログラム)を改善しましょう。
1と4はLLMモデルを応答速度の速いモデルにすることで改善可能です。ただツールの呼び出しや結果の解釈にモデルの性能が求められる場合があるので、改善しにくいポイントだったりします。
一番効くのはツールの呼び出しを減らすことです。つまりLLM単独でやれそうなことはLLMに移譲するということです。なにかのバリデーション目的でFunction Callingを使うのであれば、それを思い切ってLLMに任せても意外と精度が出ます。これは速度と品質のトレードオフなのをご留意ください!

まとめ

  • 期待するツールを呼んでくれない
    • => システムプロンプト・動的プロンプトにツール実行方法を指示する!
  • ツール実行後のLLMの動作を制御できない
    • => ツールの実行結果に ai_instruction を追加し、LLMにツールの結果をどう使うかを指示する!
  • 実行が遅い
    • => どこに時間がかかってるのかを計測する!可能であればツールの実行頻度を減らす!

LLMを思い通り動かすためには、一番新しいコンテキストに指示を込めるのが大事だと思ってます。
これを意識してFunction Callingを使いこなしていきましょう!