Tool Calling

Tool Calling / Function Calling / Tool Use 呼称は色々あれどやりたいことは同じ。LLMに呼び出し可能な関数のカタログを渡しておいて、LLMが任意のタイミングで関数名と引数を投げてくる。ホストアプリケーションはそれを実行して結果をLLMに返す。

Prompt APIではまだ使えない

ChromeにはPrompt APIというブラウザ組み込みのLanguage Model(組み込みだけど初回実行時はモデルのダウンロードが走る)にプロンプトを投げてアレコレできるAPIが生えている。 仕様書草案にはToolCallingが記載されているが、現状は実行できない。

Tool Callingさせたいじゃない

Prompt APIを使用すれば、日本語の指示から、管理画面の操作を補助できそうだ。 検索フォームというのはUIが複雑になりがちだし、あれを検索したいこれを検索したいとあとからポンポン出てくる。検索クエリを乗せることは意外と容易い。ODataのフィルタクエリやら、KQLやら使えるものは結構ある。 けれどそのクエリを構築するGUIビルダーを作るのは大変だ。大体は過剰になるか、かゆいとこに手が届かないかのどちらかになる。 Prompt APIが十分に賢いなら、GUIビルダーを作る労力をかけずに、日本語から直接検索クエリが作れる。

ただし、Prompt APIは最大コンテキスト長が4k tokenほどでとても少ない。これだと必要な事前知識をシステムプロンプトに詰め込むのは難しい。

また、クエリを正しい形で出力できるほど賢くもないし、検索クエリにつきものの時間関連のプロンプトも時計を見ることができないので学習された年に固定されている。(「先月のデータを見せて」 という指示から2024年のデータを見に行こうとする)

であれば、Tool Callingで提携処理をオフロードできれば、もう少し良い結果をえられるのでは。

だけど、まだ未対応だ。

transformer.js

JSでLLMを実行するライブラリ。Prompt APIのpolyfillでも使われてる。 これに Qwen2.5-1.5B-Instructをロードしてツールを実行させてみた。

結果

だめ。Qwen2.5-1.5B-Instruct は正しくコールできない。

代わりに、LFM2-1.2B-Tool-ONNXというtool実行を重点に学習したモデルは利用できた。OpenAI互換の tool_calls 形式では無いらしく、使うには調整が必要ではあったが。

もしかすると、Qwen2.5にもtoolを使わせる際の作法があるのかもしれないが、いくつか試したプロンプトやtransformerの使い方ではどれもツールを呼ぶという判断をしてくれなかった。

感想

賢さをツールで補いたかったのにツールを使わせるには賢さがいるという現実を知った。 2026年9月現在では、そういうことしたかったら、AIベンダーのAPIを呼ぶしかないということだ。

付録:実験コード

<!DOCTYPE html>

<html lang="ja">

<head>

    <meta charset="UTF-8">

    <meta name="viewport" content="width=device-width, initial-scale=1.0">

    <title>Browser Agent PoC</title>

    <style>
...
    </style>

</head>

<body>

<main>

    <header>

        <div>

            <p class="eyebrow">Local inference / tool use</p>

            <h1>ブラウザの中で、<br>小さなエージェント。</h1>

            <p class="lede">モデルもツール呼び出しも、このページの中で完結します。初回だけモデルのダウンロードが必要です。</p>

        </div>

        <div id="status" class="status">モデル未ロード</div>

    </header>

    <section class="layout">

        <div class="panel"><h2>質問を入力</h2><textarea id="query">今何時?</textarea><div class="controls"><label><input id="webgpu" type="checkbox"> WebGPU を優先</label><button id="run" type="button">エージェントを実行 <span aria-hidden="true">→</span></button></div></div>

        <div class="panel"><h2>回答</h2><div id="answer" class="answer placeholder">ここにエージェントの回答が表示されます。</div><div class="meta"><span id="model">LFM2 · 1.2B Tool</span><span id="elapsed">待機中</span></div></div>

        <div class="log"><div class="panel"><h2>推論ログ</h2><pre id="model-output">まだ実行されていません。</pre></div><div class="panel"><h2>ツール実行結果</h2><pre id="tool-output" class="tool-result">ツールはまだ呼ばれていません。</pre></div></div>

    </section>

</main>

<script type="module">

import { pipeline } from 'https://esm.sh/@huggingface/transformers';

  

const $ = (selector) => document.querySelector(selector);

const runButton = $('#run');

const status = $('#status');

let generator;

  

const tools = {

    now: () => {

        return new Date().toLocaleString('ja-JP', { timeZone: 'Asia/Tokyo' });

    },

    dateadd: ({ datetime, days = 0, weeks = 0, months = 0, years = 0, dateOnly = false } = {}) => {

        const date = new Date(datetime);

        date.setFullYear(date.getFullYear() + Number(years));

        date.setMonth(date.getMonth() + Number(months));

        date.setDate(date.getDate() + Number(days) + Number(weeks) * 7);

        if (dateOnly) {

            return date.toLocaleDateString('ja-JP', {

                timeZone: 'Asia/Tokyo',

                year: 'numeric',

                month: 'long',

                day: 'numeric'

            });

        }

        return date.toLocaleString('ja-JP', { timeZone: 'Asia/Tokyo' });

    },

    calc: ({ formula }) => {

        const normalized = String(formula ?? '').replace(/\s+/g, '');

        if (!/^[\d\(\)-+/*].*$/.test(normalized)) {

            throw new Error('計算式は数字、演算子、括弧で始めてください。');

        }

        if (!/^[\d()+\-*/]+$/.test(normalized)) {

            throw new Error('計算式に使用できない文字が含まれています。');

        }

  

        let depth = 0;

        for (const character of normalized) {

            if (character === '(') depth++;

            if (character === ')' && --depth < 0) throw new Error('括弧の対応が正しくありません。');

        }

        if (depth !== 0 || !normalized) throw new Error('計算式の括弧が閉じていません。');

  

        const result = eval(normalized);

        if (!Number.isFinite(result)) throw new Error('計算結果が有限値ではありません。');

        return result;

    }

};

    // 2. デファクトスタンダードなJSON Schemaでのツール定義

    const toolsDefinition = [

        {

            "type": "function",

            "function": {

                "name": "now",

                "description": "現在の日時(日付と時間)を取得する",

                "parameters": {

                    "type": "object",

                    "properties": {},

                    "required": []

                }

            }

        },

        {

            "type": "function",

            "function": {

                "name": "calc",

                "description": "整数を四則演算する。使用可能op: +-/*()",

                "parameters": {

                    "type": "object",

                    "properties": {

                        "formula": {

                            "type": "string"

                        }

                    },

                    "required": ["formula"]

                }

            }

        }

    ];

  

        const systemPrompt = `あなたはツールを使用できるAIアシスタントです。質問に答えるために必要な場合は、提供されたツールを使用してください。ツールが不要な場合は、自然な日本語で回答してください。\nList of tools: <|tool_list_start|>${JSON.stringify(toolsDefinition.map(({ function: tool }) => tool))}<|tool_list_end|>`;

  

function lastContent(output) {

    const generated = output?.[0]?.generated_text;

    if (Array.isArray(generated)) return generated.at(-1)?.content?.trim() || '';

    return String(generated || '').trim();

}

  

function lastMessage(output) {

    const generated = output?.[0]?.generated_text;

    return Array.isArray(generated) ? generated.at(-1) : null;

}

  

function parseToolArguments(argumentsValue) {

    if (!argumentsValue) return {};

    if (typeof argumentsValue === 'string') return JSON.parse(argumentsValue);

    return argumentsValue;

}

  

function extractToolCalls(assistantMessage) {

    if (assistantMessage?.tool_calls?.length) return assistantMessage.tool_calls;

  

    const content = assistantMessage?.content?.trim() || '';

    const legacyCall = content.match(/^\[([a-zA-Z_]\w*)\((.*?)\)\]$/s);

    if (!legacyCall) return [];

  

    const [, name, rawArguments] = legacyCall;

    let argumentsValue = {};

    if (rawArguments.trim()) {

        try {

            argumentsValue = JSON.parse(rawArguments);

        } catch {

            return [];

        }

    }

  

    return [{

        id: `legacy-${name}`,

        function: { name, arguments: argumentsValue }

    }];

}

  

async function loadModel() {

    if (generator) return generator;

    status.textContent = 'モデルをロード中...';

    status.classList.remove('ready');

    // const modelId = 'onnx-community/Qwen2.5-1.5B-Instruct';

    const modelId = 'onnx-community/LFM2-1.2B-Tool-ONNX';

    const deviceOptions = $('#webgpu').checked ? { device: 'webgpu' } : {};

    generator = await pipeline('text-generation', modelId, { ...deviceOptions });

    status.textContent = 'モデル準備完了';

    status.classList.add('ready');

    return generator;

}

  

async function main() {

    const userQuery = $('#query').value.trim();

    if (!userQuery) return;

    const startedAt = performance.now();

    runButton.disabled = true;

    $('#answer').classList.remove('placeholder');

    $('#answer').textContent = '考えています...';

    $('#model-output').textContent = 'モデルを準備しています...';

    $('#tool-output').textContent = 'ツールを確認しています...';

  

    const messages = [

        { role: "system", content: systemPrompt },

        { role: "user", content: userQuery }

    ];

    try {

        const model = await loadModel();

        const output = await model(messages, {

            max_new_tokens: 256,

            temperature: 0,

            do_sample: false,

            return_full_text: false,

            tools: toolsDefinition

        });

        const assistantMessage = lastMessage(output);

        const responseText = assistantMessage?.content?.trim() || lastContent(output);

        console.log(assistantMessage)

        $('#model-output').textContent = responseText;

        const toolCalls = extractToolCalls(assistantMessage);

        if (toolCalls.length > 0) {

            const followUpMessages = [...messages, assistantMessage];

            const toolResults = [];

  

            for (const toolCall of toolCalls) {

                const toolName = toolCall.function?.name;

                const toolArguments = parseToolArguments(toolCall.function?.arguments);

                if (!toolName || !tools[toolName]) continue;

  

                const toolResult = tools[toolName](toolArguments);

                toolResults.push(`${toolName}() → ${toolResult}`);

                followUpMessages.push({

                    role: 'tool',

                    tool_call_id: toolCall.id,

                    name: toolName,

                    content: String(toolResult)

                });

            }

  

            if (toolResults.length > 0) {

                $('#tool-output').textContent = toolResults.join('\n');

                const finalOutput = await model(followUpMessages, {

                    max_new_tokens: 256,

                    temperature: 0,

                    do_sample: false,

                    return_full_text: false

                });

                $('#answer').textContent = lastContent(finalOutput);

                return;

            }

        }

        $('#tool-output').textContent = 'ツールは使われませんでした。';

        $('#answer').textContent = responseText;

    } catch (e) {

        status.textContent = 'エラーが発生しました';

        const errorDetails = [e?.name, e?.message, e?.stack].filter(Boolean).join('\n');

        $('#answer').textContent = `実行できませんでした。${errorDetails || String(e)}`;

        $('#model-output').textContent = errorDetails || String(e);

    } finally {

        $('#elapsed').textContent = `${((performance.now() - startedAt) / 1000).toFixed(1)}s`;

        runButton.disabled = false;

    }

}

  

runButton.addEventListener('click', main);

$('#query').addEventListener('keydown', (event) => {

    if ((event.ctrlKey || event.metaKey) && event.key === 'Enter') main();

});

</script>

</body>

</html>