🛠️

LLMに社内APIをちゃんと使わせるには — Toolformer論文を手がかりに“ツール利用データ”を自動で増やす

に公開

はじめに

ルミナイR&Dチームの栗原です。

「電卓も検索エンジンもあるのに、LLM がぜんぶ自分で考えようとしてミスる…」

  • 桁の多い計算を自力でがんばって間違える
  • 少し前のニュースをもっともらしく創作する
  • 社内 API があるのに、うまく使うタイミングを選んでくれない

みたいな経験、かなり多いと思います。

Meta の Timo Schick らの論文
“Toolformer: Language Models Can Teach Themselves to Use Tools” は、このギャップに対して

「LLM 自身に、ツールの使い方データを自動で作らせて学習させる」

というアプローチを提案しています。

  • 外部ツール(電卓・検索・翻訳など)への API 呼び出しを
  • どこで、どんな引数で挟めば、次トークン予測が賢くなるか
  • その「有益な API 呼び出し」だけを残して、再学習

することで、ツールをいい感じに使う LLM を作る、というアイデアです。

本記事では、この Toolformer 論文をベースに、

  • 何をやっているのか(ざっくり数式レベル)
  • 実務でどう役立つのか
  • OpenAI API + Python で試せる
    「簡易 Toolformer っぽいツール利用ラッパー」

をコンパクトにまとめます。

この記事で学べること

  • Toolformer 論文が解決しようとしている 「ツール利用の学習」問題 の整理
  • 「LLM 自身に API 呼び出しデータを作らせる」自己教師ありフロー
  • 実務での落としどころ(フル再現ではなく設計の指針としての活かし方)
  • OpenAI API + Python による、タグベースの ミニ Toolformer 風ツール利用実装

1. Toolformer が解決したい問題

論文冒頭では、LLM の限界としてざっくり次のような点が挙げられています。

  • 最新情報にアクセスできない
  • ファクトチェックが甘く、ハルシネーションしやすい
  • 数値計算が苦手
  • 時間の経過を理解していない

これらは、検索エンジン・電卓・カレンダー API などのツールを使えば、比較的簡単に解決できる種類の問題です。

では、なぜ「ツールを使える LLM」は簡単には作れないのか?

  • 人手で「どの場面でどの API を呼ぶか」ラベルを付けるのはつらい
  • 特定タスク用の「ツール付きモデル」は作れても、
    • 横断的にいろんなツールを
    • うまく使い分けられるようにするのは難しい

このギャップに対して Toolformer は、

LLM 自身の文脈理解能力を使って、
「ツール呼び出し付きコーパス」を自己生成 → 再学習

というフローを提案しています。

2. Toolformer のフローをざっくり

論文でやっていることを、実務でイメージしやすい形にざっくり落とすと、次の 3 ステップです。

  1. API 呼び出し位置と内容の候補をサンプリング
  2. 「本当に役に立つ呼び出し」だけをフィルタ
  3. 有益な API 呼び出しを埋め込んだデータで再学習

順番に見ていきます。

2.1 ステップ1:API 呼び出し候補をサンプリング

まず、普通のテキストコーパス(ニュース・会話など)に対して、

「ここで API を呼んだら役に立ちそう」という位置と内容

を、LLM にサンプリングさせます。

  • 事前に少数のデモを用意しておき、
    • 例:
      Pittsburgh is also known as <API>What other name is Pittsburgh known by?</API>
  • コーパスの各位置に対して、
    • <API> トークンを出す確率を見て、
    • 閾値以上の位置を「API 候補位置」として採用
  • その位置ごとに、API の引数(クエリや式)候補を何個かサンプリング

ここでいう API は、例えば:

  • 質問応答(QA)システム
  • Wikipedia 検索
  • 電卓
  • 翻訳
  • カレンダー

などを想定しています。

2.2 ステップ2:「役に立つ」呼び出しだけ残す

すべての API 呼び出し候補が役に立つとは限らないので、

「その API 呼び出しを挟んだときに、
未来のトークン予測がどれだけ楽になるか」

を指標にフィルタします。

もう少しだけ数式っぽく書くと、

  • あるコーパス断片 (x) の位置 (i) に API 呼び出し (c_i) と結果 (r_i) を挿入したとき、
  • その後のトークン列 (x_{i+1:i+L}) に対するクロスエントロピー損失を比較し、

[
\text{loss}(\text{without API}) - \text{loss}(\text{with } c_i, r_i) > \tau
]

となるような呼び出しだけを残す、
という形で「有益さ」を判定します。

  • ざっくり言えば、「API を使った方が次の単語を当てやすくなるなら採用」
  • そうでないもの(ノイズ)は捨てる

というイメージです。

2.3 ステップ3:API 呼び出し付きデータで再学習

最後に、

  • フィルタ後のコーパス(テキスト + API 呼び出しとその結果)を教師データにして
  • 元の言語モデルを再学習

することで、

「どの API を」「いつ」「どんな引数で」「どう結果を組み込むか」

を学ばせます。

実験では、6.7B パラメータの GPT-J をベースに、

  • 電卓・QA・検索・翻訳・カレンダーなど複数ツールを組み合わせ、
  • さまざまなベンチマークで、
    • ツールなしの同サイズモデルよりもかなり良い性能
    • ときには より大きなモデルにも対抗しうる性能

を達成したと報告されています。

3. 実務的にどう活かすか

Toolformer をそのまま再実装しようとすると、

  • 大量のコーパス
  • 自前の LM(微調整可能)
  • トークンレベルの損失計算

などが必要になり、インフラ負荷がそれなりに重いです。

一方で、実務的には次のような観点が参考になります。

  1. ツール呼び出しは、「プロンプト工学」だけでは限界がある
    • いつ・どのツールを使うかを、人間が全部 if 文で書くのはスケールしにくい
  2. API 呼び出しの“ログ”そのものが学習データとして重要
    • どんな問い合わせに対して、どのツールが役立ったか
    • どの引数が「次のトークン予測」を楽にしたか
  3. ツール利用は「別モデル」ではなく「同じ LM の拡張」として学習できる
    • モデル本体の言語能力を保ったまま、ツール利用だけ上乗せする

このあたりを踏まえると、たとえば:

  • 初期フェーズでは
    • プロンプトでツール用タグを出させる
    • 「タグ付きのログをためて分析する」
  • その後、余裕が出たら
    • 自前のオープンソース LM(LLaMA 系など)に
    • よく使われるツールパターンを学習させる

といった段階的アプローチを取りやすくなります。

以下では、その「初期フェーズ」に相当する
タグベースの “簡易 Toolformer” っぽい実装を見ていきます。

4. Python でミニ Toolformer っぽいツール利用ラッパー

ここでは、OpenAI API と Python を使って、

「LLM が文中に <calc>…</calc><faq>…</faq> などのタグを挟むと、
Python 側で実際にツールを呼び出して結果に置き換える」

という簡単なラッパーを実装してみます。

Toolformer のような自己教師あり学習はしていませんが、

  • 「いつツールを使うか」は LLM 側の判断
  • それを Python が実行&置換

という意味で、発想の雰囲気はかなり近いものになります。

4.1 前提

pip install --upgrade openai
export OPENAI_API_KEY="sk-..."  # 自分のキーに置き換え

4.2 コード全体

mini_toolformer_wrapper.py のような名前を想定しています。

"""
mini_toolformer_wrapper.py

タグベースで「LLM がツール呼び出しを提案し、Python 側で実行する」ミニ実装。

- <calc>3 * (25 + 7)</calc>  → Python の簡易電卓で評価
- <faq>請求書の再発行</faq> → 事前に用意したFAQから疑似検索

Toolformer の「いつ・何を呼ぶかはモデルが決める」発想を、
プロンプト+軽いパーサで再現するイメージです。
"""

import os
import re
from dataclasses import dataclass
from typing import Dict, Callable

from openai import OpenAI

MODEL = "gpt-4.1-mini"  # 適宜変更可能


# ========= 1. ツール定義 =========

def safe_calc(expr: str) -> str:
    """
    極めて簡易な「電卓」ツール。
    eval は危険なので、本番では math パーサなどに置き換えること。
    ここではデモのために簡略化。
    """
    try:
        # 数字と演算子以外を落とす(雑だけど最低限の防御)
        if not re.fullmatch(r"[0-9+\-*/().\s]+", expr):
            return f"[calc-error: unsafe expression]"
        value = eval(expr, {"__builtins__": {}})
        return str(value)
    except Exception as e:
        return f"[calc-error: {e}]"


FAQ_DB: Dict[str, str] = {
    "請求書の再発行": "請求書の再発行は、管理画面の「請求」→「請求書一覧」から対象の請求書を選び、「再発行」をクリックしてください。",
    "解約手続き": "解約は、サポート窓口へのご連絡が必要です。契約更新日の7営業日前までにお問い合わせください。",
}


def faq_search(query: str) -> str:
    """
    疑似FAQ検索ツール。
    実務ではベクタ検索やBM25に置き換えるイメージ。
    """
    # ここでは雑に「完全一致 or 部分一致」を見るだけ
    for key, answer in FAQ_DB.items():
        if key in query or query in key:
            return answer
    return "[faq-not-found: 該当するFAQが見つかりませんでした]"


TOOLS: Dict[str, Callable[[str], str]] = {
    "calc": safe_calc,
    "faq": faq_search,
}


# ========= 2. タグ検出&ツール実行 =========

TAG_PATTERN = re.compile(r"<(?P<name>\w+)>(?P<body>.*?)</\1>", re.DOTALL)


def apply_tools(text: str) -> str:
    """
    テキスト中の <tool>...</tool> タグを検出し、
    対応する Python ツールで置き換える。
    """

    def replacer(match: re.Match) -> str:
        name = match.group("name")
        body = match.group("body").strip()
        if name not in TOOLS:
            return f"[tool-error: unknown tool '{name}']"
        fn = TOOLS[name]
        result = fn(body)
        return result

    return TAG_PATTERN.sub(replacer, text)


# ========= 3. LLM 呼び出し =========

def create_client() -> OpenAI:
    api_key = os.environ.get("OPENAI_API_KEY")
    if not api_key:
        raise RuntimeError("OPENAI_API_KEY が設定されていません。")
    return OpenAI(api_key=api_key)


SYSTEM_PROMPT = """
あなたは外部ツールを使えるアシスタントです。

次の2種類のツールタグを使えます。

1. 電卓ツール:
   - 形式: <calc>式</calc>
   - 例:   3 * (25 + 7)
   - 数学的な計算が必要なときだけ使ってください。

2. FAQ検索ツール:
   - 形式: <faq>検索したいキーワードや質問</faq>
   - 例:   請求書の再発行
   - 社内FAQから該当しそうな回答を探したいときに使ってください。

ルール:
- 本当に必要なときだけツールタグを使い、不要なときは使わないでください。
- ツールの実行結果は、あなたの回答の一部として自然な文章に組み込まれます。
- ツールを直接説明するのではなく、「普通の回答」の中にタグを挿入してください。
"""


def ask_with_tools(question: str) -> str:
    """
    LLM に質問を投げ、ツールタグを含む回答を生成させる。
    その後 Python 側でツールを実行して、最終回答を返す。
    """
    client = create_client()

    resp = client.responses.create(
        model=MODEL,
        input=[
            {"role": "system", "content": SYSTEM_PROMPT},
            {"role": "user", "content": question},
        ],
        temperature=0.4,
        max_output_tokens=512,
    )

    raw = resp.output_text.strip()
    final = apply_tools(raw)

    print("=== [LLMの生の出力] ===")
    print(raw)
    print("\n=== [ツール適用後の最終回答] ===")
    print(final)

    return final


def main():
    print("質問例1: 計算が必要なケース\n")
    ask_with_tools("3年分のサブスクリプション料金として、月額 2,980 円を払うと合計はいくらになりますか? 日本語で丁寧に説明してください。")

    print("\n" + "=" * 80 + "\n")

    print("質問例2: FAQ検索が必要なケース\n")
    ask_with_tools("請求書をなくしてしまったのですが、再発行の手順を教えてください。")


if __name__ == "__main__":
    main()

4.3 実行結果イメージ

実際の出力はランダム性やプロンプトによって変わりますが、
ざっくりこんなイメージの動きになります。

例1:計算ツール

  • 質問:
    「3年分のサブスクリプション料金として、月額 2,980 円を払うと合計はいくらになりますか?」

  • LLM の生の出力(例):

    3年間は36か月なので、合計金額は
    <calc>2980 * 36</calc>
    円となります。したがって、3年間のサブスクリプション料金の総額は 107,280 円です。
    
  • ツール適用後の最終回答:

    3年間は36か月なので、合計金額は
    107280
    円となります。したがって、3年間のサブスクリプション料金の総額は 107,280 円です。
    

ここで Toolformer 的なのは、

  • 「計算が必要だ」と判断した LLM が、自分で <calc>2980 * 36</calc> を挿入している
  • Python 側はそれを見て、電卓ツールを実行するだけ

という分業になっている点です。

例2:FAQ 検索ツール

  • 質問:
    「請求書をなくしてしまったのですが、再発行の手順を教えてください。」

  • LLM の生の出力(例):

    請求書の再発行については、社内FAQを参照します。
    <faq>請求書の再発行</faq>
    上記の手順に従って操作していただければ、再発行が可能です。
    
  • ツール適用後の最終回答(FAQ_DB を使った場合):

    請求書の再発行については、社内FAQを参照します。
    請求書の再発行は、管理画面の「請求」→「請求書一覧」から対象の請求書を選び、「再発行」をクリックしてください。
    上記の手順に従って操作していただければ、再発行が可能です。
    

ここでも、

  • LLM 側が「ここで FAQ 検索を挟むとよさそう」と判断
  • Python 側は、そのタグを見て FAQ 検索ツールを呼ぶ

という、Toolformer の構造とよく似た「役割分担」になっています。

5. どこまでやるかの見極め

最後に、実務で「ツール利用 LLM」を設計するときの視点をいくつか。

  • まずは「タグ+ラッパー」から始める
    • 本記事のように、プロンプトでタグを出させて
    • Python 側でツールを実行するだけでも、かなりのことができます
  • 有用なタグ付きログをためる
    • どの問いに対して、どのタグが出たか
    • 実際にツールを使った結果、回答品質がどう変わるか
  • 余裕があれば、自前 LM に「ツール付きコーパス」を食わせる
    • LLaMA 系などオープンモデルを使う場合は、
      • Toolformer や GPT4Tools, ToolAlpaca などの手法を参考に
      • 自前ツールに合わせたデータセットを作る方向に進める

Toolformer 論文のポイントは、

「ツールの使い方は、人間がルールベースで全部決めなくても、
LLM 自身に“どこでどう呼ぶと得か”を学ばせられる」

というところにあります。

本記事で紹介したミニ実装は、そのごく入口ですが、

  • 「LLM 側はタグを出すだけ」
  • 「Python 側はタグを見てツールを実行するだけ」

という構造を一度体験しておくと、
自社の API 群やワークフローに「ツール利用 LLM」を組み込むときの設計イメージが掴みやすくなるはずです。


【現在採用強化中です!】

  • AIエンジニア
  • PM/PdM
  • 戦略投資コンサルタント

▼代表とのカジュアル面談URL
https://pitta.me/matches/VCmKMuMvfBEk

参考文献

ルミナイ - 産業データをLLM Readyにするための技術ブログ

Discussion