ailia LLM 1.5 の Tool Use API で Gemma 4 の Function Calling を使う方法を説明します。JSON履歴とtool_call_idによる結果の返却、GetResponseJsonによる結果取得を説明します。
ツールを設定し、JSON履歴を入力して生成します。生成後に応答をJSONで取得し、tool_callsがあればツールを実行して結果を履歴に追加し、次のターンを生成します。最終回答が得られたらツールを解除します。図の関数名はC APIの名前です。各言語での名前は次の「各言語のAPI」を参照してください。
生成結果はSDK内に蓄積されるため、生成中の差分テキストをアプリ側で連結する必要はありません。ailiaLLMGetResponseJsonで取得しても蓄積は消去されず、新しいプロンプトを設定するとリセットされます。
| 言語 | JSON履歴の設定・生成 | SDK内の生成結果を取得 |
|---|---|---|
| C / C++ | ailiaLLMSetPromptJson → ailiaLLMGenerate |
ailiaLLMGetResponseJsonSize / ailiaLLMGetResponseJson |
| Python | generate_json(messages) |
get_response_json()(dict) |
| Unity | SetPromptJson(messagesJson) → Generate |
GetResponseJson()(JSON文字列、失敗時は空文字列) |
| Flutter | setPromptJson(messages) → generate |
getResponseJson()(Map) |
| Kotlin | setPromptJson(messagesJson) → generate |
getResponseJson()(JSON文字列) |
Python・Flutter・Kotlinはエラー時に例外を返します。UnityのSetPromptJsonはboolを返します。 Tool Useでは、最初のターンからJSON履歴を使用してください。
[
{"role":"user","content":"東京の天気は?"},
{"role":"assistant","content":"","tool_calls":[
{"id":"call_0","type":"function","function":{
"name":"get_weather","arguments":"{\"city\":\"東京\"}"}}
]},
{"role":"tool","tool_call_id":"call_0","content":"雪、-3度"}
]
messages配列はOpenAI互換の形式です。tool_callsを含むassistantと、その結果のtoolメッセージを履歴に追加していきます。
ailiaLLMGetResponseJsonで取得したオブジェクトをそのまま追加します。Thinkingはreasoning_contentに指定します。contentの本文はツール呼び出しとして解析し直されないため、ツール呼び出しはtool_callsに指定します。idとfunction.nameは空にできません。function.argumentsはJSONオブジェクトを表す文字列です。IDは同じassistant内で一意にします。別のターンでは同じIDを再利用できます。tool_call_idで直前のassistantの呼び出しに対応付けます。結果の順序は問いません。contentは文字列で指定し、オブジェクトはJSON文字列に変換します。ailiaLLMSetToolsでツールを解除した後も、過去のtool_callsとtoolの結果を含む履歴をailiaLLMSetPromptJsonに渡して、要約や最終回答を生成できます。IDの対応は引き続き検証されます。解除すると出力パーサーが無効になるため、応答のJSONは解除前に取得してください。userのcontent配列に、textと並べて画像・音声を指定できます。ツールの設定中はailiaLLMSetMultimodalPromptを使用できないため、画像・音声もJSON履歴で渡します。指定方法はマルチモーダル ガイドを参照してください。
| 状況 | 結果 | 対処 |
|---|---|---|
出力に未完了のツール呼び出しやThinkingが含まれる状態でailiaLLMGetResponseJsonを呼ぶ | PARSE_ERROR | 失敗したassistantもtoolの結果も履歴に追加せず、再試行を指示するuserメッセージを追加して再生成します。部分的な結果は実行しません。 |
ailiaLLMSetPromptJsonに不完全なarguments、不正なJSONや型、IDの対応の誤り(不明なID、重複した結果、矛盾するツール名)、空の配列を渡す | INVALID_ARGUMENT | 履歴を修正します。SDKは自動で切り捨てや補完を行いません。 |
ツールの設定中にailiaLLMSetPrompt / ailiaLLMSetMultimodalPromptを呼ぶ | INVALID_STATE | ailiaLLMSetPromptJsonを使用します。ツールを解除すると再び利用できます。 |
プロンプトの設定前、またはツールやThinkingの設定を変更した後にailiaLLMGetResponseJsonを呼ぶ | INVALID_STATE | ailiaLLMSetPromptJsonでプロンプトを設定し直してから生成します。 |
プロンプトの設定後、生成前にailiaLLMGetResponseJsonを呼んだ場合は、空のassistantが返ります。
モデルの読み込み、ailiaLLMSetToolsによるツール設定から、生成、ツール結果の返却、ツールの解除までの一連の流れです。履歴の編集にはnlohmann/jsonを使用します。天気は動作例のモック値で、実際の天気情報ではありません。
#include "ailia_llm.h"
#include <nlohmann/json.hpp>
#include <cstdio>
#include <string>
#include <vector>
using json = nlohmann::json;
// SetPromptJson → Generate → GetResponseJsonを1ターン分実行します。
static int generate_turn(AILIALLM* llm, const json& messages, json& response) {
std::string input = messages.dump();
int status = ailiaLLMSetPromptJson(llm, input.c_str());
if (status != AILIA_LLM_STATUS_SUCCESS) return status;
unsigned int done = 0;
while (!done) {
status = ailiaLLMGenerate(llm, &done);
if (status != AILIA_LLM_STATUS_SUCCESS) return status;
// 必要な場合だけGetDeltaTextでプレビュー。履歴用の連結は不要。
}
unsigned int size = 0;
status = ailiaLLMGetResponseJsonSize(llm, &size);
if (status != AILIA_LLM_STATUS_SUCCESS) return status;
std::vector<char> output(size);
status = ailiaLLMGetResponseJson(llm, output.data(), size);
if (status != AILIA_LLM_STATUS_SUCCESS) return status;
try {
response = json::parse(output.data());
} catch (const json::exception&) {
return AILIA_LLM_STATUS_INVALID_ARGUMENT;
}
return AILIA_LLM_STATUS_SUCCESS;
}
static int run_tool_use(AILIALLM* llm) {
const char* tools = R"([{"type":"function","function":{
"name":"get_weather","description":"現在の天気を取得します。",
"parameters":{"type":"object","properties":{"city":{"type":"string"}},
"required":["city"]}}}])";
int status = ailiaLLMSetTools(llm, tools);
if (status != AILIA_LLM_STATUS_SUCCESS) return status;
json messages = json::array({{{"role", "user"}, {"content", "東京の天気は?"}}});
for (int turn = 0; turn < 8; turn++) {
json response;
status = generate_turn(llm, messages, response);
// 不完全な出力の場合はPARSE_ERROR。ツールを実行せず再試行を判断します。
if (status != AILIA_LLM_STATUS_SUCCESS) return status;
try {
if (!response.is_object() || response.at("role") != "assistant") {
return AILIA_LLM_STATUS_INVALID_ARGUMENT;
}
messages.push_back(response);
if (!response.contains("tool_calls") || response["tool_calls"].is_null()) {
if (!response.contains("content") || !response["content"].is_string() ||
response["content"].get<std::string>().empty()) {
return AILIA_LLM_STATUS_INVALID_ARGUMENT;
}
printf("%s\n", response["content"].get<std::string>().c_str());
return AILIA_LLM_STATUS_SUCCESS;
}
if (!response["tool_calls"].is_array() || response["tool_calls"].empty()) {
return AILIA_LLM_STATUS_INVALID_ARGUMENT;
}
for (const auto& call : response["tool_calls"]) {
const std::string id = call.at("id").get<std::string>();
const auto& function = call.at("function");
if (id.empty() || function.at("name") != "get_weather") {
return AILIA_LLM_STATUS_INVALID_ARGUMENT;
}
json args = json::parse(function.at("arguments").get<std::string>());
if (!args.is_object() || !args.contains("city") || !args["city"].is_string() ||
args["city"].get<std::string>().empty()) {
return AILIA_LLM_STATUS_INVALID_ARGUMENT;
}
// 実際には検証済みの引数でツールを実行します。以下はモック結果。
json result = {{"city", args["city"]}, {"weather", "sunny"}};
messages.push_back({{"role", "tool"}, {"tool_call_id", id},
{"content", result.dump()}}); // 結果はJSON文字列
}
} catch (const json::exception&) {
return AILIA_LLM_STATUS_INVALID_ARGUMENT;
}
}
// ツール呼び出しのまま上限に達した場合、最終回答はまだ得られていません。
return AILIA_LLM_STATUS_OTHER_ERROR;
}
int main() {
AILIALLM* llm = nullptr;
if (ailiaLLMCreate(&llm) != AILIA_LLM_STATUS_SUCCESS) return 1;
int status = ailiaLLMOpenModelFileA(llm, "gemma-4-E2B-it-Q4_K_M.gguf", 4096);
if (status == AILIA_LLM_STATUS_SUCCESS) {
status = run_tool_use(llm);
ailiaLLMSetTools(llm, nullptr); // エラー時もツール設定を解除します。
}
if (status != AILIA_LLM_STATUS_SUCCESS) {
fprintf(stderr, "error %d: %s\n", status, ailiaLLMGetErrorDetail(llm));
}
ailiaLLMDestroy(llm);
return status == AILIA_LLM_STATUS_SUCCESS ? 0 : 1;
}
モデル読込済みの AiliaLLM を渡します。Androidの org.json を使用します。Android以外のJVM環境では対応するJSONライブラリを追加してください。
import axip.ailia_llm.AiliaLLM
import org.json.JSONArray
import org.json.JSONObject
// llmはモデル読込済み。JSON操作にはAndroidのorg.jsonを使用します。
fun runToolUse(llm: AiliaLLM) {
try {
llm.setTools("""[{"type":"function","function":{
"name":"get_weather","description":"現在の天気を取得します。",
"parameters":{"type":"object","properties":{"city":{"type":"string"}},
"required":["city"]}
}}]""")
val messages = JSONArray().put(
JSONObject().put("role", "user").put("content", "東京の天気は?")
)
for (turn in 0 until 8) {
llm.setPromptJson(messages.toString())
while (!llm.generate()) {
print(llm.getDeltaText()) // 任意のプレビュー。連結は不要。
}
// 不完全な出力の場合は例外。ツールを実行せず再試行を判断します。
val response = JSONObject(llm.getResponseJson())
messages.put(response)
val calls = response.optJSONArray("tool_calls")
if (calls == null || calls.length() == 0) {
println(response.optString("content"))
return
}
for (i in 0 until calls.length()) {
val call = calls.getJSONObject(i)
val function = call.getJSONObject("function")
require(function.getString("name") == "get_weather")
val args = JSONObject(function.getString("arguments"))
val city = args.getString("city")
require(city.isNotBlank())
// 実際には引数を検証してツールを実行します。以下はモック結果。
val result = JSONObject().put("city", city).put("weather", "sunny")
messages.put(JSONObject()
.put("role", "tool")
.put("tool_call_id", call.getString("id"))
.put("content", result.toString())) // 結果はJSON文字列
}
}
error("8ターン以内に最終回答が得られませんでした")
} finally {
llm.setTools(null)
}
}
モデル読込済みの AiliaLLMModel を渡します。tool結果の content はMapのままではなく、JSON文字列に変換します。
import 'dart:convert';
import 'dart:io';
import 'package:ailia_llm/ailia_llm_model.dart';
// llmはモデル読込済み。
void runToolUse(AiliaLLMModel llm) {
try {
llm.setTools([{
'type': 'function',
'function': {
'name': 'get_weather',
'description': '現在の天気を取得します。',
'parameters': {
'type': 'object',
'properties': {'city': {'type': 'string'}},
'required': ['city'],
},
},
}]);
final messages = <Map<String, dynamic>>[
{'role': 'user', 'content': '東京の天気は?'},
];
for (var turn = 0; turn < 8; turn++) {
llm.setPromptJson(messages);
String? delta;
while ((delta = llm.generate()) != null) {
stdout.write(delta); // 任意のプレビュー。連結は不要。
}
// 不完全な出力の場合は例外。ツールを実行せず再試行を判断します。
final response = llm.getResponseJson();
messages.add(response);
final calls = response['tool_calls'] as List? ?? [];
if (calls.isEmpty) {
print(response['content']);
return;
}
for (final call in calls) {
final function = call['function'];
if (function['name'] != 'get_weather') {
throw StateError('Unknown tool: ${function['name']}');
}
final args = jsonDecode(function['arguments'] as String);
final city = args['city'] as String;
if (city.isEmpty) throw FormatException('city must not be empty');
// 実際には引数を検証してツールを実行します。以下はモック結果。
final result = {'city': city, 'weather': 'sunny'};
messages.add({
'role': 'tool',
'tool_call_id': call['id'],
'content': jsonEncode(result), // 結果はJSON文字列
});
}
}
throw StateError('No final answer within 8 turns');
} finally {
llm.setTools(null);
}
}
モデル読込済みの AiliaLLM を渡します。各サンプルでは、例外時もfinallyでツール設定を解除します。
import json
# modelはモデル読込済みのailia_llm.AiliaLLM。
def run_tool_use(model):
try:
model.set_tools([{"type": "function", "function": {
"name": "get_weather", "description": "現在の天気を取得します。",
"parameters": {"type": "object", "properties": {"city": {"type": "string"}},
"required": ["city"]}}}])
messages = [{"role": "user", "content": "東京の天気は?"}]
for turn in range(8):
for delta in model.generate_json(messages):
print(delta, end="", flush=True) # 任意のプレビュー。連結は不要。
# 不完全な出力の場合は例外。ツールを実行せず再試行を判断します。
response = model.get_response_json()
messages.append(response)
calls = response.get("tool_calls") or []
if not calls:
print(response["content"])
return
for call in calls:
function = call["function"]
if function["name"] != "get_weather":
raise ValueError("Unknown tool: " + function["name"])
args = json.loads(function["arguments"])
city = args.get("city") if isinstance(args, dict) else None
if not isinstance(city, str) or not city:
raise ValueError("Invalid city argument")
# 実際には引数を検証してツールを実行します。以下はモック結果。
result = {"city": city, "weather": "sunny"}
messages.append({"role": "tool", "tool_call_id": call["id"],
"content": json.dumps(result)}) # 結果はJSON文字列
raise RuntimeError("8ターン以内に最終回答が得られませんでした")
finally:
model.set_tools(None)
モデル読込済みの AiliaLLMModel を渡します。JSONの操作にはNewtonsoft.Json(Unityパッケージ com.unity.nuget.newtonsoft-json)を使用します。UnityのAPIは失敗時にfalseまたは空文字列を返すため、GetErrorDetail() で詳細を取得します。
using System;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using ailiaLLM;
public static class ToolUseSample
{
// llmはモデル読込済み。JSON操作にはNewtonsoft.Jsonを使用します。
public static string RunToolUse(AiliaLLMModel llm)
{
try
{
var tools = new JArray(new JObject
{
["type"] = "function",
["function"] = new JObject
{
["name"] = "get_weather",
["description"] = "現在の天気を取得します。",
["parameters"] = new JObject
{
["type"] = "object",
["properties"] = new JObject { ["city"] = new JObject { ["type"] = "string" } },
["required"] = new JArray("city")
}
}
});
if (!llm.SetTools(tools.ToString(Formatting.None))) throw new Exception(llm.GetErrorDetail());
var messages = new JArray(new JObject { ["role"] = "user", ["content"] = "東京の天気は?" });
for (int turn = 0; turn < 8; turn++)
{
if (!llm.SetPromptJson(messages.ToString(Formatting.None))) throw new Exception(llm.GetErrorDetail());
bool done = false;
while (true)
{
if (!llm.Generate(ref done)) throw new Exception(llm.GetErrorDetail());
if (done) break;
// GetDeltaText()で途中経過を表示できます(任意)。連結は不要。
}
// 不完全な出力などのエラー時は空文字列。ツールを実行せず再試行を判断します。
string responseJson = llm.GetResponseJson();
if (string.IsNullOrEmpty(responseJson)) throw new Exception(llm.GetErrorDetail());
var response = JObject.Parse(responseJson);
messages.Add(response);
var calls = response["tool_calls"] as JArray;
if (calls == null || calls.Count == 0)
{
return (string)response["content"];
}
foreach (JObject call in calls)
{
var function = (JObject)call["function"];
if ((string)function["name"] != "get_weather") throw new Exception("Unknown tool");
var args = JObject.Parse((string)function["arguments"]);
string city = (string)args["city"];
if (string.IsNullOrEmpty(city)) throw new Exception("Invalid city argument");
// 実際には引数を検証してツールを実行します。以下はモック結果。
var result = new JObject { ["city"] = city, ["weather"] = "sunny" };
messages.Add(new JObject
{
["role"] = "tool",
["tool_call_id"] = (string)call["id"],
["content"] = result.ToString(Formatting.None) // 結果はJSON文字列
});
}
}
throw new Exception("8ターン以内に最終回答が得られませんでした");
}
finally
{
llm.SetTools(null);
}
}
}