ailia LLM Tool Use ガイド

ailia LLM 1.5 の Tool Use API で Gemma 4 の Function Calling を使う方法を説明します。JSON履歴とtool_call_idによる結果の返却、GetResponseJsonによる結果取得を説明します。

Tool Useの流れ

ツールを設定し、JSON履歴を入力して生成します。生成後に応答をJSONで取得し、tool_callsがあればツールを実行して結果を履歴に追加し、次のターンを生成します。最終回答が得られたらツールを解除します。図の関数名はC APIの名前です。各言語での名前は次の「各言語のAPI」を参照してください。

生成結果はSDK内に蓄積されるため、生成中の差分テキストをアプリ側で連結する必要はありません。ailiaLLMGetResponseJsonで取得しても蓄積は消去されず、新しいプロンプトを設定するとリセットされます。

各言語のAPI

言語 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履歴を使用してください。

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メッセージを履歴に追加していきます。

画像・音声

userのcontent配列に、textと並べて画像・音声を指定できます。ツールの設定中はailiaLLMSetMultimodalPromptを使用できないため、画像・音声もJSON履歴で渡します。指定方法はマルチモーダル ガイドを参照してください。

不完全な出力・エラー

状況結果対処
出力に未完了のツール呼び出しやThinkingが含まれる状態でailiaLLMGetResponseJsonを呼ぶPARSE_ERROR失敗したassistantもtoolの結果も履歴に追加せず、再試行を指示するuserメッセージを追加して再生成します。部分的な結果は実行しません。
ailiaLLMSetPromptJsonに不完全なarguments、不正なJSONや型、IDの対応の誤り(不明なID、重複した結果、矛盾するツール名)、空の配列を渡すINVALID_ARGUMENT履歴を修正します。SDKは自動で切り捨てや補完を行いません。
ツールの設定中にailiaLLMSetPrompt / ailiaLLMSetMultimodalPromptを呼ぶINVALID_STATEailiaLLMSetPromptJsonを使用します。ツールを解除すると再び利用できます。
プロンプトの設定前、またはツールやThinkingの設定を変更した後にailiaLLMGetResponseJsonを呼ぶINVALID_STATEailiaLLMSetPromptJsonでプロンプトを設定し直してから生成します。

プロンプトの設定後、生成前にailiaLLMGetResponseJsonを呼んだ場合は、空のassistantが返ります。

C++サンプル

モデルの読み込み、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;
}

Kotlinサンプル

モデル読込済みの 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)
    }
}

Flutterサンプル

モデル読込済みの 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);
  }
}

Pythonサンプル

モデル読込済みの 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)

Unityサンプル

モデル読込済みの 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);
        }
    }
}