8月15日、MarkTechPostが「Fine-Tuning Tool-Calling LLMs: A Complete Guide Using XYZ-Aquila-SFT and Qwen3」と題した記事を公開した。この記事では、XYZ-Aquila-SFTデータセットとLoRAを用いてQwen3-0.6BをツールコールLLM向けにファインチューニングするエンドツーエンドのパイプラインが詳細に紹介されている。最大の読みどころは「なぜQwen3の公式テンプレートをあえて使わないのか」という設計判断にあり、この一点を理解するだけで、マルチターンのツールコール訓練に潜む見えにくい落とし穴を回避できる。
なぜ今「ツールコールLLM」のファインチューニングが求められるのか
Function CallingやAIエージェントの文脈で、LLMに外部APIやツールを呼び出させる能力は急速に実用化が進んでいる。OpenAIやAnthropicのホスト型APIであればツールコールは標準機能として提供されるが、オープンウェイトモデルを自前でサービングする場合や、特定ドメインのツールスキーマに特化させたい場合は、ファインチューニングが現実的な選択肢になる。
一方で、ツールコール軌跡のようなマルチターン・多段階の構造を持つデータでLLMを訓練する際には、通常のSFTとは異なる注意が必要だ。本記事が扱うのはまさにその「難しい部分」であり、公式ユーティリティをそのまま使っては訓練信号が欠損するという具体的なケースを題材に、正しい実装を一から組み立てている。
モデルにQwen3-0.6Bを選んだのも意図的だ。0.6Bという超小型モデルはGoogle ColabのT4 GPU(VRAM 15GB前後)でLoRAファインチューニングが成立するサイズであり、手元環境での再現・実験を最優先した選定といえる。エッジ推論やColab制約下での検証を想定する読者にとって、そのままコピー実行できる構成になっている。
apply_chat_template()を使わない理由が肝心
このチュートリアルで最も重要なポイントは、Qwen3のChatMLレンダリングに公式のapply_chat_template()を使わないという設計判断だ。
理由はシンプルかつ致命的で、Qwen3のデフォルトテンプレートは最後のアシスタントターンを除くすべての<think>...</think>ブロックを削除する仕様になっている。XYZ-Aquila-SFTはマルチターンのツールコール軌跡データセットであり、各ターンに推論ブロックが埋め込まれている。テンプレートをそのまま使うと、訓練したいはずの推論監督信号の大半がサイレントに消える。
そのため、記事では手動でChatMLをレンダリングし、アシスタントターンのトークンにのみlossを適用するマスキングを実装している。
def render_and_mask(t: Trajectory, max_len: int, policy: str):
ids, labels = [], []
for m in t.messages:
head = tok(f"{IM_START}{m['role']}{NL}", add_special_tokens=False).input_ids
body = tok(m["content"], add_special_tokens=False).input_ids
tail = tok(f"{IM_END}{NL}", add_special_tokens=False).input_ids
seg = head + body + tail
if m["role"] == "assistant":
lab = [-100] * len(head) + body + tail
else:
lab = [-100] * len(seg)
ids += seg; labels += lab
...
ヘッダー(<|im_start|>role\n)にも-100を割り当て、本文とtailのみをloss対象にする実装だ。この「どのトークンにlossを流すか」という設計は、マルチターン訓練全般において品質を左右する核心的な部分であり、公式テンプレートへの盲目的な依存がなぜ危険かを示す典型例といえる。
パイプライン全体の構成
チュートリアルは以下のフェーズで構成されている。
1. データセットのストリーミングと解析
XYZ-Aquila-SFTは、マルチターンのツールコール軌跡を含むデータセットだ。ストリーミングで400件を取得し、各行の構造(question、answer、number of tool calls、trajectory)を検査する。
2. ネスト安全なJSONパーサーの実装
ツールコールの引数は入れ子構造を持つため、単純な正規表現(r'\{.*?\}')では壊れる。記事ではjson.JSONDecoder.raw_decode()を使ったネスト安全なスキャナーを実装し、<tool_call>タグ内のJSONを確実に抽出している。
def iter_json_objects(text: str, limit: int = 1):
dec, i, n, out = json.JSONDecoder(), 0, len(text), []
while i < n and len(out) < limit:
while i < n and text[i] not in "{[":
i += 1
if i >= n:
break
try:
obj, end = dec.raw_decode(text, i)
except json.JSONDecodeError:
i += 1; continue
out.append(obj); i = end
return out
3. コーパス統計の可視化
パース後、軌跡ごとのツールコール数・メッセージ深さ・文字数の分布をヒストグラムで可視化する。上位10%の長い軌跡が全文字数の大部分を占めるという偏りを確認するステップで、MAX_SEQ_LENの設定判断に使う。訓練前にデータ分布を把握しておくことで、シーケンス長の切り捨てによる情報損失をコントロールできる。
4. ツールスキーマの変換
XYZ-Aquila-SFTはツールスキーマをシステムメッセージ内に埋め込んでいる。記事ではこれを構造化フォーマット({'messages': [...], 'tools': [...]})に分離・再統合するラウンドトリップ変換を実装し、バイト単位の一致を検証している。スキーマの取り扱いを明示的に管理することで、モデルが受け取るコンテキストの一貫性を担保できる。
5. LoRAによるQwen3-0.6Bのファインチューニング
訓練設定は以下のとおりだ。
| パラメータ | 値 |
|---|---|
| モデル | Qwen/Qwen3-0.6B |
| MAX_SEQ_LEN | 2048 |
| MAX_STEPS | 30 |
| GRAD_ACCUM | 8 |
| 学習率 | 1e-4 |
| LORA_R | 16 |
PEFTのLoRAを使い、フルファインチューニングに比べて必要なGPUメモリを大幅に削減している。MAX_STEPS=30という設定は訓練ループのイテレーション数が30であることを意味しており、「手順が30段階ある」という意味ではない。Google Colabでパイプライン全体を通して動作確認することに特化した、最小限の実行回数だ。
6. 訓練前後の評価
評価は「teacher-forced probe」方式で実施する。マルチターン軌跡のうち、アシスタントがツールコールを発行する直前までをプレフィックスとして与え、モデルが正しいツール名・引数を生成できるかを検証する。指標は名前のヒット率と引数のF1スコアだ。この評価設計により、エンドツーエンドの生成精度ではなくツールコール能力にフォーカスした定量比較が可能になる。
実行環境と依存関係
必要なライブラリは以下のとおりで、Google Colabで動作する構成になっている。
datasets>=3.0.0
transformers>=4.51.0
peft>=0.13.0
accelerate>=1.0.0
BF16サポートの有無を自動検出し、CUDA環境であればBF16で訓練する。CPUでも動作するが、実用的な速度にはGPUが必須だ。依存関係がシンプルに整理されており、Colabノートブックをそのまま実行できる再現性の高さも本チュートリアルの特長といえる。
詳細はFine-Tuning Tool-Calling LLMs: A Complete Guide Using XYZ-Aquila-SFT and Qwen3を参照していただきたい。