見出し画像

ローカルLLMで動く、簡易版 Function Calling

昨年OpenAIからFunction Callingが発表されてLLMの用途が単なるチャットや質問箱ではなくなり、その後のAgentやAGIなどへの大きな発展に繋がりました。ローカルLLMでもOpenAI互換APIが使えるので試してみますがうまくできません。なので、ローカルLLMのために様々な試みがあります。しかし、多くはFunctionあるいはToolの呼び出しが正確にできないことも多く感じます。そこで、OpenAIの形式にこだわらず、もっと単純な方法で同様な機能が実現できないかと試していたところ、結構、良い結果が得られたので記事にしました。

Function Callingに何を期待するか

これ、多分人それぞれかもしれませんけど、本来の意味は以下に説明があります。

回答形式は、{'function':関数名,'parameter':パラメータ,'parameter1':パラメータ1}

私はAItuberを個人開発していて、AIキャラが色々なことをやってくれるようにフレームワークの開発を進めています。その中で何かを実行するのに人がボタンをクリックするとか、音声入力で単語判定で動かすとかはできますが、曖昧な言葉を汲み取って正しく動いてくれません。そこでFunction Callingが使いたいわけです。AgentでもLangChaineでもいいのですけど。

このような観点から、すごく単純にまとめてみました
1,曖昧な文章から実行する機能を特定する
2,機能(=Function)は自由に定義できること
3,実行する機能がなければ普通に会話すること
この他、機能を実行するためにはパラメータ類も必要なので、そう単純でもないのですけど。

何をやったのか

Userの会話から、上記の通りに出力が得られるようLLMに対してプロンプトとFunctionの定義方法を見直しました。

プロンプト

これで完成でないと思いますが、とりあえず動くプロンプトとして以下を使います。

messages = [{"role": "system", "content": role},{"role": "user", "content": user_msg}]

role="あなたは優秀なaiです。userの質問や指示について、文章から予想される動作や処理を行うために、最適な関数名を以下の語句リストから選び、
適切なパラメータも答えなさい。回答形式は、回答形式は、{'function':関数名,'parameter':パラメータ,'parameter1':パラメータ1}、とします
該当する語句がない場合は、{”function”:'none'}とし、userの会話に回答しなさい。関数リスト:" + str(functions)

そして組み合わせてLLMへのプロンプトにしています。形式はOpenAIのapiで使うプロンプトと同じです。

ここの"role": "system"の"content": roleが上記プロンプトです。

+ str(functions)の部分は関数定義を文字列にして追加しているだけです。このプロンプトでは関数特定部分の形式をJesonで返すよう指示shています。

関数定義

OpenAIの定義よりもずっと単純化しています。
関数名:LLMが返してくれることを期待している関数名です。
parameter:関数を実行するために必要なパラメータの定義です。

functions = [
{"関数名":"get_weather",
"description": "指定された都市の天気を取得する",
"parameter":"cty_name",
"parameter1":"when",
},
・・・同じ形式で必要なだけ
]

使い方

試した環境とモデル
ollama で model="gemma2:latest" なので、
gemma2-9B Q4_0

Ollamaやllama.cppのOpenAI互換サーバを動かしておきます。そこに以下のコードからプロンプトを送信し、LLMのresponceにあるJeson部分には、仕様に合わない形式や文字もあるので、文字や記号を置き換えて、正しいJeson形式でFunction名やパラメータを受け取ることができるように操作しています。更にJeson形式の出力以外はmessageとして分離しています。このコードを動かせば、関数名、parameter、parameter1、messageが得られます。実際の関数は記述していませんが、得られた結果を表示させて正しさを見ています。ここが正しけれは、関数を記述して呼び出せるはずです。

コード

function_call_cli.py
maine()からrun_conversation(user_msg,role)を呼び出して、結果を得ています。

import openai
from   openai import OpenAI
import requests
import json
import math
import re
from  def_function import functions

# OpenAI APIの設定
client = OpenAI(
    base_url="http://127.0.0.1:11434/v1",
    api_key="YOUR_OPENAI_API_KEY",  # このままでOK
    )

role="あなたは優秀なaiです。userの質問や指示について、文章から予想される動作や処理を行うために、最適な関数名を以下の語句リストから選び、\
    適切なパラメータも答えなさい。回答形式は、{'function':関数名,'parameter':パラメータ,'parameter1':パラメータ1}、とします\
    該当する語句がない場合は、{'function':'none'}とし、userの会話に回答しなさい。\
    関数リスト:\
     "+str(functions)

def run_conversation(user_msg,role):
    # ステップ1: ユーザー入力と関数の定義を GPT に送る
    messages = [{"role": "system", "content": role},{"role": "user", "content": user_msg}]
    completion = client.chat.completions.create(
        model="gemma2:latest",
        messages=messages,
    )
    out1=completion.choices[0].message.content
    print("out1=",out1)
    # JSON部分を抽出
    match = re.search(r'\{(.|\s)*?\}', out1)
    if match:
        # 抽出したJSON文字列を取得し、特殊な引用符や文字変換
        json_str = match.group(0)
        json_str = json_str.replace("「", '"').replace("」", '"')  # 日本語のカギカッコを標準のダブルクオートに
        json_str = json_str.replace("'", '"')  # シングルクオートをダブルクオートに変換
        json_str = json_str.replace("“", '"').replace("”", '"')
        json_str = json_str.replace("‘", '"').replace("’", '"')
        json_str = json_str.replace("None", 'null')
        json_str = json_str.replace(",}", '}')
        # JSONデータに変換
        try:
            json_data = json.loads(json_str)
        except json.JSONDecodeError:
            json_data={"function":'none'}
        message=out1.strip()
    else:
        json_data={"function":'none'}
        message=out1.strip()
    if json_data!={"function":'none'}:
        function_name =  json_data["function"]
        try:
            param=json_data["parameter"]
        except:
            param="none"
        try:
            param1=json_data["parameter1"]
        except:
            param1="none"

    message=re.sub(r"\{.*?\}\s*", "", message).strip()
    return json_data,message

def maine():
    content = ""
    while True:
      content = input("User=>")
      # 入力が Enter だけなら終わる
      if not content:
        break

      json_data,message = run_conversation(content,role)
      #print(json_data)
      print("***message=",message)
      if json_data["function"]=='none' or json_data=={} :
        print("***実行できる関数はありません")
      else:
        print("***function=",json_data["function"])
        try:
            print("***parameter=",json_data["parameter"])
            print("***parameter1=",json_data["parameter1"])
        except:
            print("***パラメータが定義されていません")

if __name__ == "__main__": 
    maine() 

run_conversation(user_msg,role)の冒頭は見慣れたOpenAI互換の呼び出しのコードです。

    # ステップ1: ユーザー入力と関数の定義を GPT に送る
    messages = [{"role": "system", "content": role},{"role": "user", "content": user_msg}]
    completion = client.chat.completions.create(
        model="gemma2:latest",
        messages=messages,
    )
    out1=completion.choices[0].message.content

中程、以下は正しいJeson形式になるように文字の置き換えなど行っています。全角文字などをシングルクォーテーションに変換しています。

        # 抽出したJSON文字列を取得し、特殊な引用符や文字変換
        json_str = match.group(0)
        json_str = json_str.replace("「", '"').replace("」", '"')  # 日本語のカギカッコを標準のダブルクオートに
        json_str = json_str.replace("'", '"')  # シングルクオートをダブルクオートに変換
        json_str = json_str.replace("“", '"').replace("”", '"')
        json_str = json_str.replace("‘", '"').replace("’", '"')
        json_str = json_str.replace("None", 'null')
        json_str = json_str.replace(",}", '}')

message=out1.strip()でmessageq部分を分離し、整えて、Jesonとmessageを返しています。

    message=re.sub(r"\{.*?\}\s*", "", message).strip()
    return json_data,message

Functionの定義

def_function.py

functions = [
        {"関数名":"get_weather",
            "description": "指定された都市の天気を取得する",
            "parameter":"cty_name",
            "parameter1":"when",
        },
        { "関数名":"get_current_time",
          "description": "現在時間を取得するための処理",
          "parameter1":"cty_name",
          "parameter":"when",
        },
        { "関数名":"image_analysis",
          "description": "画像の解析処理",
          "parameter":"analisys",
          "parameter1":"object",
        },
        { "関数名":"rotation",
          "description": "その場で回転する処理",
          "parameter":"direction",
          "parameter1":"speed",
        },
        { "関数名":"hand_up",
          "description": "右,左,両手いずれかを上に上げる動作をする",
          "parameter":"select_hand",
          "parameter1":"speed",
        },
        { "関数名":"walk",
          "description": "前に歩いたり,後ろに下がったりする動作をする",
          "parameter":"direction",
          "parameter1":"speed",
        },
    ]

実際の会話と出力の例

ぶつかる!曲がって
このユーザーの指示は、突然危険な状況に陥ったことを示唆しており、回避行動をとるように要求しています。つまり、"回転"する処理を実行し、衝突を避けるのが適切です。右方向への回転が避け場として思いつきました。"slow"という速度設定は、急激な動きを避け、安全な回避を実現することを目指します。
function= rotation
parameter= 90
parameter1= slow

画像の顔がだれなのか判断して
function= image_analisys
parameter= face_recognition
parameter1= person

10月1日の多大阪の天気は?
function= get_weather
parameter= 大阪
parameter1= 2023年10月1日

急いで前に 動いて、 素早く前に動いて、 早く歩いて
のどの命令でも
function= walk
parameter= forward
parameter1= fast

お好み焼き作り方を教えて
普通に以下のような会話が出力されます

***message= お好み焼きの作り方ですか! 私はAIなので、お好み焼きを作ることができませんが、手順を説明できますよ。

材料:

薄力粉: 150g

  • 卵: 2個

  • 水: 70~80ml

  • だし汁または塩水 適量

  • キャベツ: 大さじ2杯 (千切り)

  • 豚肉(またはお好みで、エビなど): 100g

  • teneon_okonomiyakiソース(オクトパシーソースという名前のソースは日本のお好み焼きを表現する際に使われるよく見かけるソースの名前です) 、マヨネーズ、青のり、かつお節: 好みの量

作り方:

  1. ボウルに薄力粉と卵を入れて混ぜ合わせます。少しずつ水を加えながら、ダマにならないようにしっかりと練り混ぜます。

  2. かつお節やお好み焼きソースの材料を入れる場合は、溶かしきれないくらいに固くしてください。

  3. 半分量を流し込んだフライパンは熱して、油をつけるといいでしょう

  4. キャベツを上に敷き詰めたら、半分量の batter をかけるようにしましょう。

  5. 肉などを好きなようにトッピングし、残りの batter をかけていきます (好みでネギも追加)

  6. 表面がカリッとしたら裏返して両面を焼きます (弱火でゆっくり焼き上げてください)

  7. お好みでソース、マヨネーズ、青のり、かつお節をかけて熱々をお召し上がりください!
    ***実行できる関数はありません

まとめ

結構使えると思うんですけど、どうでしょう。プロンプや関数、パラメータ定義を変えて色々と試してください。