Fine-Tuning Qwen3-0.6B for Tool Calling with XYZ-Aquila-SFT
In this article
Supervised fine-tuning LLMs for reliable tool-calling remains one of the more finicky corners of applied ML — existing chat templates silently mutate training data, and naive JSON extraction breaks on nested argument objects. A new end-to-end pipeline walks through every failure mode explicitly, using the XYZAILab/XYZ-Aquila-SFT dataset and Qwen/Qwen3-0.6B as concrete targets. The result is a reproducible workflow a practitioner can clone, adapt, and scale. Given that agentic deployment patterns are reshaping how models interact with external systems, getting tool-calling supervision right at the data-preparation stage has compounding downstream value.
Dataset Streaming and Trajectory Parsing
The pipeline streams 400 rows from XYZAILab/XYZ-Aquila-SFT (English split) using Hugging Face datasets>=3.0.0 with streaming=True, avoiding a full download. Each row exposes a question, answer, number of tool calls integer, and a trajectory list of role-tagged messages.
A critical implementation decision is the JSON extractor. The pipeline explicitly rejects re pattern matching — citing breakage on nested arguments objects present in every real tool call — in favour of a nesting-safe scanner that walks the raw string character by character using json.JSONDecoder.raw_decode. The parse_row function assembles a Trajectory dataclass, splitting the system message on a # Tools header, extracting embedded tool schemas with a <tools>...</tools> regex, harvesting all <tool_call> blocks from assistant turns, and separately counting <think> reasoning blocks and <tool_response> observations. Parser agreement with the dataset's declared number of tool calls field is logged at inference time as a direct validation check.
ChatML Rendering and Loss Masking
Rather than calling tok.apply_chat_template(), the pipeline manually renders every trajectory into ChatML using <|im_start|>, <|im_end|>, and newline delimiters. The rationale is explicit: Qwen3's built-in template deletes <think>...</think> blocks from every assistant turn except the last, silently destroying the reasoning supervision signal for multi-step trajectories.
The manual renderer applies label = -100 to all header and non-assistant tokens, preserving loss only on assistant-generated body and tail tokens. Sequences exceeding MAX_SEQ_LEN = 2048 are truncated by default (LENGTH_POLICY = "truncate"), with a drop option available. Examples where all labels remain -100 after truncation are filtered out. The supervised-token ratio (mean, p10, p90) is reported across the encoded corpus as a dataset-health diagnostic — a low ratio indicates most sequence budget is consumed by prompt context rather than learnable output.
LoRA Configuration and Training Loop
The model is loaded as Qwen/Qwen3-0.6B, initialised with attn_implementation="sdpa" and BF16 dtype where the GPU supports it. Gradient checkpointing is enabled before LoRA attachment. The LoRA config sets r=16, lora_alpha=32 (2×r), lora_dropout=0.05, bias="none", and task_type="CAUSAL_LM". Training runs for MAX_STEPS=30 with batch size 1 and GRAD_ACCUM=8, yielding an effective 8 trajectories per step. The optimiser is AdamW with lr=1e-4, weight_decay=0.0, and betas=(0.9, 0.95). A cosine schedule with 5 warmup steps governs the learning rate. Loss, EMA loss, learning rate, and perplexity are logged every 5 steps. The tutorial frames this 30-step run explicitly as a smoke test on roughly 350 trajectories, not a publishable result, and recommends scaling N_STREAM and MAX_STEPS for production use.
Evaluation Protocol
Post-training evaluation uses teacher-forced probes: the trajectory is cut immediately before an assistant turn containing a tool call, the prefix is fed to the model with max_new_tokens=160 and greedy decoding (do_sample=False), and the generation is parsed for tool calls. Three metrics are reported: parseable rate, tool-name accuracy, and argument-key F1. The pipeline runs evaluation both before and after LoRA fine-tuning to produce a delta, using up to 24 probes (N_EVAL_PROBES=24) constructed from the held-out split.
| Parameter | Value | Role in Pipeline |
|---|---|---|
| Model | Qwen/Qwen3-0.6B | Base causal LM for SFT |
| Dataset rows streamed | 400 | Training corpus size (smoke test) |
| Max sequence length | 2048 tokens | Truncation ceiling |
| LoRA rank (r) | 16 | Adapter parameter budget |
| LoRA alpha | 32 | Scaling factor (2 × r) |
| LoRA dropout | 0.05 | Regularisation |
| Learning rate | 1e-4 | AdamW base LR |
| Gradient accumulation | 8 | Effective batch = 8 trajectories/step |
| Max training steps | 30 | Smoke-test run length |
| Warmup steps | 5 | Cosine schedule ramp |
| Evaluation probes | 24 | Teacher-forced tool-call assessment |
| Max new tokens (eval) | 160 | Generation budget per probe |
The most consequential decisions in this pipeline — preserving <think> blocks by bypassing apply_chat_template, using nesting-safe JSON extraction, and verifying parser agreement against ground-truth counts — are exactly the steps that get skipped when teams move fast. As tool-calling models become standard infrastructure for AI agents interacting with external data sources, these low-level correctness guarantees become load-bearing. The XYZ-Aquila-SFT + Qwen3-0.6B pairing is Colab-accessible; the architecture of the data pipeline is what scales to larger models and datasets.