0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Raspberry pi + ローカルLLMでFunction Calling機能を使ってみる

Posted at

1.開発概要

Raspberry pi 4(4GB)でローカルLLMを動作させて、Funtion Calling機能によってLLMの回答をより豊かにする!

2.はじめに

近年、どこもかしこも生成AIで、仕事でもプライベートでも聞く単語になってきました。そんな中で今回は生成AIの中のLLM、さらに言うとローカルLLMで遊んでいきたいと思います。
ローカルLLMは簡単に説明すると、自分が持っているPCの中で処理を完結できるLLMです。
某大手AIサービスはクラウドシステムに接続し、自分の端末経由でクラウド上に動いているLLMにデータを処理してもらい、自分の端末に結果を返してもらいます。一方でローカルLLMは自分の端末でLLMモデルを動かし、そのLLMモデルに直接入力、処理、出力させるという面で大きく違います。
「LLMなんて大規模なサーバがないと使えないのでは?」と思われがちですが、LLMのモデルにも大小さまざまあり、小さいモデルであればお持ちのノートPCでも動きます。そして今回はRaspberry pi 4でLLMを動かしていきます!

単純にLLMをRaspberry piで動かすことは既に先人の皆様が実証してくださっていますので、参考資料等ご確認ください。

※私が実際に導入するときに参考にさせていただいた記事を以下に載せておきます。

3.Function Calling機能(関数呼び出し機能)について

ローカルLLMの拡張機能として、ユーザーが作成したpythonの関数をLLMが使うかどうか「判断」して、「使用」、その結果をもとに「回答を生成」するような操作を実装してみたいと思います。
すなわち、Function Calling機能です。
私の説明だとイメージがわきづらいと思いますので以下にイメージ図を載せておきます。
image.png

ローカルLLMは基本オフライン環境下で動くので、天気情報などを直接取得することはできません。そこで天気情報を取得する関数を作ってあげて、それをLLMに使わせることで天気情報を得て、回答することができます。ここでポイントなのが関数を使うかどうか、どの関数を使うかはLLMが判断するということです。

4.準備

4.1.H/WとDocker、ollamaのセットアップ

今回LLMを動かすのは前述のようにRaspberry pi4です。その中でも4GBモデルを使います。これよりも低いバージョンまたはRAM値が低いと今回の動作条件では動かない場合があります。 1

ではRaspberry piのセットアップをしていきましょう。
LLMをセットアップするところから説明すると記事が大掛かりになってしまいますので、以下記事のOllamaセットアップまで実施し、ollamaを起動してください($ docker run -d ~~までです)。

また、LLMはpythonで制御しますので、python環境も整えておいてください。デフォルトでRaspberry pi上にはpython環境はあると思いますので特別な操作は必要ないと思います。

4.2.LLMモデルのダウンロード

LLMを使うためには推論モデルが必要です。
また、今回検証するFunction Calling機能が有効なモデルを使用します。今回はその中でも「llama3.2:3b」を使用します。
ちなみにFunction Calling機能に対応しているモデルはollamaサイト上のToolsタブがついているものだと思います。
image.png

image.png

モデルをダウンロードするときは以下コマンドでできると思います。

$ docker exec -it ollama ollama run <モデル名>

今回の場合だと以下ですね。

$ docker exec -it ollama ollama run llama3.2:3b

ダウンロードが完了すると「send message」が出てきますので、終了してください(ctrl + d等)。
この状態では停止している様に見えても裏でプロセスは周り続けていますので、以下コマンドでプロセスを停止してください。

$ docker exec -it ollama ollama stop llama3.2:3b

これで準備完了です!!

5.コード

5.1.全体コード&システム図

LLMの制御はpythonで実施します。
適当にRaspberry pi上にフォルダを作ります。フォルダにpythonファイルを作成してください。今回はhello_ollama.pyファイルとして作成することとします。
また、今回コードを作成するにあたって以下の記事を大変参考にさせていただき、また一部引用させていただいております。本検討の詳細は以下記事も併せてご確認ください。

では結論、以下のコードが完成形です。
コードの解説は後ほど実施していきます。

hello_ollama.py
import ollama
import pandas as pd
import requests
import json

from datetime import datetime

def get_sum(a: int, b: int) -> int:
  """
  二つの数の和を出す関数です

  Args:
    a: 1個目の整数
    b: 2個目の整数

  Returns:
    int: 計算結果の和の出力です
  """
  return a + b
  
def get_weather()-> str:
	"""
	天気情報を取得する関数です
	
	Returns:
		str: 天気予報結果です
	"""
	
	url = 'https://weather.tsukumijima.net/api/forecast'
	payload = {'city': '140010'}
	data = requests.get(url, params=payload).json()
	for weather in data['forecasts']:
		report = '\n' + data['title'] + '\n' + weather['dateLabel'] + ':' + weather['telop']
		if weather['dateLabel'] == "今日":
			today_weather = weather['telop']
	return today_weather

def main():
	
	print("you:")
	user_input = input()
	get_sum_tool = {
		"type": "function",
		"function": {
			"name": "get_sum",
			"description": "二つの数の和を出す",
			"parameters": {
				"type": "object",
				"required": ["a","b"],
				"properties": {
					"a": {"type": "integer", "description": "1個目の整数"},
					"b": {"type": "integer", "description": "2個目の整数"},
				},
			},
		},
	}
	
	get_weather_tool = {
		"type": "function",
		"function": {
			"name": "get_weather",
			"description": "今日の天気を取得する",
			"parameters": {
				"type": "object",
				"required": [],
				"properties": {
				},
			},
		},
	}
	
	messages = [{'role': 'user', 'content': user_input}]
	available_functions: Dict[str, Callable] = {
		"get_sum": get_sum,
		"get_weather": get_weather,
	}
	
	response = ollama.chat(
	'llama3.2:3b',
	messages=messages,
	tools=[get_sum_tool,get_weather_tool], 
	)
	if response["message"].tool_calls:
		print("Use tool!")
		for tool in response["message"].tool_calls:
			if function_to_call := available_functions.get(tool.function.name):
				print(f'関数の呼び出し: {tool.function.name}')
				print(f'引数: {tool.function.arguments}')
				output = function_to_call(**tool.function.arguments)
				print(f'関数の出力: {output}')
			else:
				print(f'関数 {tool.function.name} が見つかりません')
				
		messages.append(response["message"])
		messages.append({
			"role": "tool",
			"name": tool.function.name,
			"content": str(output)
		})
		
		print("最終応答の取得")
		final_response = ollama.chat('llama3.2:3b',messages=messages)
		print(f'最終応答: {final_response.message.content}')
	else:
	    print("not use tool.")
	    print(response['message']['content'])

if __name__ == '__main__':
    main()

そしてこのコードは以下のようなフローで動作しています。
image.png

5.2.ライブラリインポート

今回使用するライブラリのインポートをしています。
インストールしていないライブラリがある場合はインストールしてください。

import ollama
import pandas as pd
import requests
import json

5.3.Function Calling対象の関数

LLMが使用できる関数を実装していきます。今回は「数値の和を計算する関数」と、「天気予報の情報を取得する関数」です。

2つの関数に共通していることはpythonの戻り値のヒント(-> intなど)と、関数内の注釈です。これらの内容はpythonの関数としての動作には意味がありませんが、LLMが関数を理解する際には重要な情報源となるそうです。(まだ作者自身もよくわかっていません)

get_sum関数

この関数は二つの整数の和を計算する関数です。

def get_sum(a: int, b: int) -> int:
  """
  二つの数の和を出す関数です

  Args:
    a: 1個目の整数
    b: 2個目の整数

  Returns:
    int: 計算結果の和の出力です
  """
  return a + b

get_weather関数

この関数は天気予報APIを使用して横浜市の今日の天気を取得する関数です。詳細はAPIの公式 (https://weather.tsukumijima.net/) を参照してください。少しだけ解説しますと、{'city': '140010'}で横浜市を指定して、if weather['dateLabel'] == "今日":で今日の天気を指定して取得しています。
ここで別の条件分岐をすれば別の場所や別の日時の天気予報を取得できます。

def get_weather()-> str:
	"""
	天気情報を取得する関数です
	
	Returns:
		str: 天気予報結果です
	"""
	
	url = 'https://weather.tsukumijima.net/api/forecast'
	payload = {'city': '140010'}
	data = requests.get(url, params=payload).json()
	for weather in data['forecasts']:
		report = '\n' + data['title'] + '\n' + weather['dateLabel'] + ':' + weather['telop']
		if weather['dateLabel'] == "今日":
			today_weather = weather['telop']
	return today_weather

5.4.メイン処理 関数の仕様書

次に実際に実行される順序を記述したmain関数の中身に入っていきます。
冒頭はユーザーがテキスト文書を入れられるようにしています。
その次に以下の表の下にあるJSON形式の記述を行います。これは「関数の仕様書」となるものです。動作するLLMが関数を呼び出すかどうか判断する際の一つの情報源となります。今回2つの関数を実装しているので2つの仕様書を作成しています。参考としてget_sum関数の仕様書の項目と内容、説明を以下の表にまとめています。

項目 内容 説明
type function LLMにこれは関数ですと伝える識別子
function.name get_sum LLMが呼び出す関数名(pythonのget_sumと対応)
function.description 二つの数の和を出す LLMがこの関数を使うべき状況を判断するための説明
parameters.type object 引数がオブジェクト(辞書)形式で渡されることを示す
parameters.required ["a","b"] 必須の引数。使用する場合には必ずaとbを渡す必要がある
parameters.a.type integer aは整数である
parameters.a.description 1個目の整数 aの意味を説明
parameters.b. --- bもaと同様
get_sum_tool = {
		"type": "function",
		"function": {
			"name": "get_sum",
			"description": "二つの数の和を出す",
			"parameters": {
				"type": "object",
				"required": ["a","b"],
				"properties": {
					"a": {"type": "integer", "description": "1個目の整数"},
					"b": {"type": "integer", "description": "2個目の整数"},
				},
			},
		},
	}
	
	get_weather_tool = {
		"type": "function",
		"function": {
			"name": "get_weather",
			"description": "今日の天気を取得する",
			"parameters": {
				"type": "object",
				"required": [],
				"properties": {
				},
			},
		},
	}

5.5.初期メッセージ設定&関数辞書登録

次にLLMに渡す最初の「メッセージ」を作成します。システム図でいうところの一番上のブロックとなります。
下の1行目がその初期メッセージです。
2行目は使用したい関数を辞書として登録しています。これにより、LLMがget_weatherを呼んでといったらpythonがget_weatherを実行できるようになります。

    messages = [{'role': 'user', 'content': user_input}]
	available_functions: Dict[str, Callable] = {
		"get_sum": get_sum,
		"get_weather": get_weather,
	}

5.6.LLMに質問と関数情報を渡す

次に先ほど作成したmessages変数をLLMに渡し、思考させます。ollama.chat関数がその操作で、引数には動作させるモデル名や、messages変数、ツールの情報を渡します。得られた回答は、ここで新規に作られたresponse変数に格納されます。

	response = ollama.chat(
	'llama3.2:3b',
	messages=messages,
	tools=[get_sum_tool,get_weather_tool], 
	)

5.7.関数を使う場合の処理

次のif文はFunction Calling機能が必要と判断した場合の処理です。
LLMがFunction Calling機能が必要とした場合には、response変数にその情報が含まれ、下の1行目の最初のif文がTrueとなります。
その次にfor文でLLMが関数を探します。(複数の場合でも対応可能です。)
if文内で関数を実行し、関数の結果をoutput変数に格納します。
このコードは参考文献とさせていただいております記事の全体コードの114行目から126行目を引用させていただいております(print関数以外)。

	if response["message"].tool_calls:
		print("Use tool!")
		for tool in response["message"].tool_calls:
			if function_to_call := available_functions.get(tool.function.name):
				print(f'関数の呼び出し: {tool.function.name}')
				print(f'引数: {tool.function.arguments}')
				output = function_to_call(**tool.function.arguments)
				print(f'関数の出力: {output}')
			else:
				print(f'関数 {tool.function.name} が見つかりません')

ちなみに関数を使用しないと判断されればこのif文は無視され、通常のLLMの返答となります。
その場合は後半のコードの以下が実行される形となります。

    else:
        print("not use tool.")
        print(response['message']['content'])

5.8.ツールの実行結果をLLMに渡す&最終回答を得る

次に「LLMが関数を使った」ということを会話履歴として保存します。それが以下のコード1行目です。
2行目以降では関数を実行した結果をLLMに渡すため、1行目と同様にmessages変数に関数の出力結果を追加します。
最後に、会話履歴(ユーザーの質問~ツール出力結果)を全てLLMに渡し最終回答を生成します。この時、LLMは関数の出力を踏まえた回答を生成させます。回答はfinal_response変数に格納され、次のprint関数で出力されます。

        messages.append(response["message"])
    	messages.append({
    		"role": "tool",
    		"name": tool.function.name,
    		"content": str(output)
    	})
		
    	print("最終応答の取得")
    	final_response = ollama.chat('llama3.2:3b',messages=messages)
    	print(f'最終応答: {final_response.message.content}')

6.実行結果

では、実際に5章で解説したコードの動作結果の一例を以下に紹介していきます。

6.1.get_sum関数を使う場合

初めにget_sum関数を使った場合の実行結果を下に示します。ちなみにRaspberry piの標準Terminal上で実行しています。
you:の後の行は作者が入れた質問文です。その後、関数が呼び出され、引数が設定され、関数の出力を得ています。最後にLLMの最終応答が得られていることが分かります。
ただ、これだけですと本当に関数の結果が反映されて使われているかわかりません。LLM自体が計算していることも否定できません。ですので、次のget_weather関数で確かめていきましょう。

image.png

6.2.get_weather関数を使う場合

こちらもget_sum関数と同様に、質問に対し関数が実行され、その結果をもとに最終応答が生成されていることが分かります。
そしてローカルLLMではインターネット上から直接情報は取得できないにも関わらず、最終応答で今日の天気は「晴のち曇」です。と回答しています。これは紛れもなく、関数の出力結果を反映して回答を生成していると言えます。
image.png

6.3.関数を使わない場合

最後に関数を使わない場合の出力結果を見ていきましょう。
はい、このようにnullとなってしまいました。
image.png

また、別のタイミングで同じ入力をした場合の結果です。今度はツールが使われてしまっています。最終応答の部分だけを見ると、普通の回答に近いものが得られていますが、ツールを使わない場合はツールを使わずに回答を生成してほしいです。
image.png

7.今後に向けて

さて、最後の実行結果で示したように関数を使わない場合にも関数を使ってしまったり、関数は使わないけども回答がnullになってしまうなど課題があります。
今回の検証での課題を以下に列挙しておきます。

  • 関数が不要と思われる場面でも関数を使ってしまう
  • 関数を使わない場合にnullが最終回答となる場合がある
  • 入力から最終回答まで3~5分程度かかる
  • 同じ入力を与えていても結果が大きく変わる

課題については今後改善するとして、Function Calling機能が実装できたことでいろんな応用が考えられます。例えば今は天気予報を今日の横浜市のみ取得していますが、日付や場所を引数として定義することができれば、自由な場所の自由な時間帯の天気情報を取得することもできます!
また、このAPIでは気温や湿度なども取得できるので、これらの情報を取得して今日の服装の提案をしたりなんかもできるかもしれません!
他にも、いろいろなAPIを用いることでLLMに様々な機能を持たせられると考えます!
APIだけじゃなく、H/Wを動かしたりする関数があれば家電や家庭菜園なんかにも活かせるかもしれません!

8.まとめ

説明は以上です。
少しだけ感想を述べておきます。課題はありつつも、一世代前のRaspberry pi上でLLMを動かし、Function Calling機能を実装できたのはとても進歩していると感じました。LLMの軽量化が進み、今後はエッジ側のデバイスにもLLMが普及してくる可能性があるかもしれません。

感想は以上ですが、少し自語りを...今回の記事が2026年の初記事となりました。昨年は仕事であまり出せなかったので、今年は多めに出せたらいいなと思っています。勿論、いいネタがあれば即座にアウトプットしたいです!また、社会人と技術者としてもレベルアップしていきたいですね!

LLMについては今後社会全体として活発に使われていくであろう存在です。
それらの技術をいち早く身に着けていきたいですね!
では、よい電子工作ライフを~!!

参考/引用文献

1.本記事のコーディングで大変参考にさせていただき、また引用もさせていただいております。

2.ollama公式のFunction Calling機能についての説明です。

3.Raspberry pi上のLLMの動かし方についての説明です。

4.天気予報のAPI公式サイトです

5.基本的なpythonでのLLM制御についての説明です。

  1. 今回の検証とは関係ないかもしれませんが、私の動作させているRaspberry pi4はUSB bootを有効にし、USB接続のSSD経由で起動しています。そのため、もしかしたらLLMの動作時間などは影響があるかもしれません。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?