Skip to content
Level 5 · Agentic EngineerLessonPart 27 · page 2 of 530 min
30Minutes
6Sources

Fine-Tuning for Tool Use and Your Codebase

By the end of this lesson you will be able to run Part 13’s supervised fine-tuning recipe on agent trajectories rather than on question-and-answer pairs, say which two settings have to change and why, predict the memory the run needs from arithmetic, and measure the result on three things at once: whether the model calls tools correctly, whether it finishes tasks, and whether anything else got worse.

The previous lesson ended with a rule worth repeating, because it is the one that decides whether a tool-use fine-tune works at all. You do not write tool calls in the model’s wire format. You store a call in the tool_calls key of an assistant message, as an object with a name and an arguments object, and you put the tool schemas in a tools column beside the messages. The chat template turns that into whichever format the model family uses.

The SFT trainer documentation for TRL 1.12.0 · verified 2026-09-08, read on 2026-09-09, states this directly: the trainer fully supports fine-tuning models with tool calling, and each example should include the conversation messages with any tool calls and tool responses, plus the list of available tools in the tools column as JSON schemas.

The model cards say the same thing from the other side. The Qwen3-Coder-30B-A3B-Instruct card, read on 2026-09-09, describes agentic coding support with a specially designed function-call format and shows tool definitions as ordinary JSON schemas in an OpenAI-compatible request, without publishing the wire format itself. That is the normal situation: the family’s format lives in its template, the card tells you the model was trained for tool calling, and the way you honour both is to hand the template structured messages.

The recipe, and the two settings that change

Section titled “The recipe, and the two settings that change”

Everything else is Part 13. An instruct base, a LoRA adapter on the seven linear projections, bfloat16 where the device supports it, batch one with gradient accumulation, a learning rate around 1e-4 rather than the trainer’s default, an evaluation after every epoch, early stopping and the best checkpoint kept. If that sentence is not familiar, read Part 13’s fine-tuning with TRL and PEFT before this page; nothing here re-teaches it.

Two settings change.

The loss mask. Part 13 used completion_only_loss, which is right for a prompt-completion dataset with one answer. A trajectory has many assistant turns interleaved with tool results, and the tool results are the environment speaking, not the model. Training on them teaches the model to write file contents and command output, which is not the job. TRL’s setting for this is assistant_only_loss, documented as computing the loss only on the assistant responses and ignoring user and system messages, and supported only for conversational datasets. Its documentation carries a warning worth reading: the feature needs the chat template to mark the generated spans, and TRL patches the template automatically for known model families such as Qwen3 while other models need checking.

The sequence length. Part 13’s format examples fitted in 1,024 tokens. A five-step agent trajectory with file contents in the tool results is routinely four to six times that. This is the single biggest practical difference between the two parts, and it shows up as memory rather than as an error: a run configured with Part 13’s length silently truncates every episode, and truncation from the end removes the final answer, which is the part you most wanted to train on.

RunnableAll tracks

train-agent-sft.py
"""Train a LoRA adapter on agent trajectories with TRL's SFT trainer and PEFT.
Purpose: Part 27's supervised fine-tuning run for Tracks S, X and N. Loads the
conversational tool-calling files trajectories-to-sft.py wrote, including the
`tools` column that the chat template turns into the tool section of the prompt,
attaches a LoRA adapter, puts the loss on the assistant turns only, evaluates after
every epoch, keeps the best checkpoint and appends one run record to the lab
notebook. It is Part 13's recipe with two changes: the dataset is multi-turn and
carries tool calls, and the loss mask is assistant-only rather than completion-only.
Platform: spark, strix, nvidia (CUDA, or ROCm which also reports as cuda to PyTorch). It
runs on the CPU too, slowly. Track M uses mlx_lm.lora on the data-mlx layout instead;
PyTorch's MPS backend will run this in float32 if you insist.
Minimum memory: 16 GB for a 1.7B to 4B base at bfloat16 with a rank-16 adapter, batch 1
and a 4,096-token sequence. Agent trajectories are long: the sequence length, not the
parameter count, is what makes this part heavier than Part 13.
Assumes: torch, transformers, trl, peft and datasets installed in the active environment;
trajectories-to-sft.py has been run so that data/train.jsonl and data/valid.jsonl
exist; agentlog.py sits next to this file.
Usage: python3 train-agent-sft.py --model Qwen/Qwen3-1.7B --data-dir data \\
--output-dir runs/agent-qwen3-1.7b --labbook labbook.md
python3 train-agent-sft.py --model Qwen/Qwen3-4B --max-length 6144 --rank 32 \\
--gradient-checkpointing --output-dir runs/agent-qwen3-4b
python3 train-agent-sft.py --model Qwen/Qwen3-4B --inspect-only
--inspect-only renders the first training example through the model's own chat template
and prints it. Do that before every run. It is the only way to see whether the tool
schemas reached the prompt, whether the tool results are inside the turn structure, and
whether the template renders tool calls at all: a template that ignores the tools column
trains the model on a prompt it will never see again.
"""
from __future__ import annotations
import argparse
import json
import time
from pathlib import Path
from typing import Any
import torch
from datasets import Dataset
from peft import LoraConfig
from transformers import AutoModelForCausalLM, AutoTokenizer, EarlyStoppingCallback
from trl import SFTConfig, SFTTrainer
import agentlog
# The seven linear projections of a Qwen3 block. --list-modules prints what your own base
# model has, because a name that does not match attaches nothing and raises no error.
DEFAULT_TARGETS = ["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"]
def pick_device() -> str:
"""CUDA (or ROCm, which reports as cuda), then MPS, then CPU. Part 11's choice."""
if torch.cuda.is_available():
return "cuda"
mps = getattr(torch.backends, "mps", None)
if mps is not None and mps.is_available():
return "mps"
return "cpu"
def use_bf16(device: str, requested: str) -> bool:
if requested == "fp32":
return False
if requested == "bf16":
return True
return device == "cuda" and torch.cuda.is_bf16_supported()
def list_linear_modules(model_id: str) -> None:
model = AutoModelForCausalLM.from_pretrained(model_id, dtype=torch.float32)
names = sorted({name.split(".")[-1] for name, module in model.named_modules()
if isinstance(module, torch.nn.Linear)})
print(f"linear module names in {model_id}:")
for name in names:
print(f" {name}")
print("\nPass the ones you want with --target-modules, or use --target-modules all-linear.")
def read_rows(path: Path) -> list[dict[str, Any]]:
if not path.is_file():
raise SystemExit(f"{path} is missing; run trajectories-to-sft.py first")
rows = [json.loads(line) for line in path.read_text(encoding="utf-8").splitlines()
if line.strip().startswith("{")]
if not rows:
raise SystemExit(f"{path} holds no rows")
return rows
def to_dataset(rows: list[dict[str, Any]]) -> tuple[Dataset, str]:
"""Build a Dataset whose `tools` column survives the trip.
A tool schema is an arbitrary JSON object, so the column cannot be typed as a struct
with fixed fields. Recent versions of `datasets` have a Json() type for exactly this;
older ones do not, and the documented fallback is to store the column as a JSON
string, which the chat template parses. The returned string says which happened, so
the run record can say it too.
"""
try:
from datasets import Features, Json, List, Value
except ImportError:
encoded = [{"messages": r["messages"], "tools": json.dumps(r["tools"])} for r in rows]
return Dataset.from_list(encoded), "tools-as-json-string"
features = Features({
"messages": List({"role": Value("string"), "content": Value("string"),
"name": Value("string"), "tool_calls": List(Json())}),
"tools": List(Json()),
})
normalised = []
for row in rows:
messages = []
for message in row["messages"]:
messages.append({
"role": message["role"],
"content": message.get("content") or "",
"name": message.get("name") or "",
"tool_calls": message.get("tool_calls") or [],
})
normalised.append({"messages": messages, "tools": row["tools"]})
return Dataset.from_list(normalised, features=features), "tools-as-json-objects"
def summarise_history(history: list[dict]) -> dict[str, float | int | None]:
train_losses = [row["loss"] for row in history if "loss" in row]
evals = [(row.get("epoch"), row["eval_loss"]) for row in history if "eval_loss" in row]
best_epoch, best_eval = min(evals, key=lambda pair: pair[1]) if evals else (None, None)
return {
"first_train_loss": round(train_losses[0], 4) if train_losses else None,
"final_train_loss": round(train_losses[-1], 4) if train_losses else None,
"final_eval_loss": round(evals[-1][1], 4) if evals else None,
"best_eval_loss": round(best_eval, 4) if best_eval is not None else None,
"best_epoch": best_epoch,
"evaluations": len(evals),
}
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
parser.add_argument("--model", default="Qwen/Qwen3-1.7B",
help="base model repository id or local path; use an instruct "
"checkpoint, which already carries a chat template")
parser.add_argument("--data-dir", default="data")
parser.add_argument("--output-dir", default="runs/agent-lora")
parser.add_argument("--epochs", type=float, default=3.0)
parser.add_argument("--batch-size", type=int, default=1)
parser.add_argument("--grad-accum", type=int, default=8,
help="batches summed before one optimiser step; only --batch-size "
"costs memory, so this is how you raise the effective batch")
parser.add_argument("--lr", type=float, default=1e-4,
help="adapters take roughly 1e-4, not the SFTConfig default of 2e-5")
parser.add_argument("--max-length", type=int, default=4096,
help="trajectories are long; anything longer than this is truncated "
"from the end, which silently removes the final answer")
parser.add_argument("--rank", type=int, default=16)
parser.add_argument("--alpha", type=int, default=32)
parser.add_argument("--dropout", type=float, default=0.05)
parser.add_argument("--target-modules", nargs="+", default=DEFAULT_TARGETS)
parser.add_argument("--loss", choices=["assistant", "full"], default="assistant",
help="assistant: loss on the assistant turns only, which is what you "
"want, because the tool results are the environment's words")
parser.add_argument("--precision", choices=["auto", "bf16", "fp32"], default="auto")
parser.add_argument("--gradient-checkpointing", action="store_true")
parser.add_argument("--early-stopping-patience", type=int, default=2)
parser.add_argument("--seed", type=int, default=0)
parser.add_argument("--labbook", default=None)
parser.add_argument("--notes", default=None)
parser.add_argument("--list-modules", action="store_true")
parser.add_argument("--inspect-only", action="store_true",
help="render the first example through the chat template and stop")
args = parser.parse_args()
if args.list_modules:
list_linear_modules(args.model)
return
data_dir = Path(args.data_dir)
train_rows = read_rows(data_dir / "train.jsonl")
valid_rows = read_rows(data_dir / "valid.jsonl")
tokenizer = AutoTokenizer.from_pretrained(args.model)
if tokenizer.chat_template is None:
raise SystemExit(
f"{args.model} has no chat template, so it is a base checkpoint rather than an "
"instruct one. Tool calling is a template feature: pick an instruct model."
)
if args.inspect_only:
rendered = tokenizer.apply_chat_template(
train_rows[0]["messages"], tools=train_rows[0]["tools"],
tokenize=False, add_generation_prompt=False)
print(rendered)
length = len(tokenizer(rendered)["input_ids"])
print(f"\n--- {length} token(s) for this example; --max-length is {args.max_length}")
names = [c["function"]["name"] for m in train_rows[0]["messages"]
for c in (m.get("tool_calls") or [])]
for name in sorted(set(names)):
if name not in rendered:
print(f"WARNING: the call to {name!r} does not appear in the rendered text. "
"This template may not render tool calls; check the model card.")
if train_rows[0]["tools"] and train_rows[0]["tools"][0]["function"]["name"] not in rendered:
print("WARNING: the tool list does not appear in the rendered text. Either the "
"template ignores `tools`, or it puts them somewhere this check cannot see. "
"Read the output above before training on it.")
return
device = pick_device()
bf16 = use_bf16(device, args.precision)
dtype = torch.bfloat16 if bf16 else torch.float32
print(f"device: {device} precision: {'bfloat16' if bf16 else 'float32'}")
if device == "mps":
print("note: Track M's supported path is mlx_lm.lora on the data-mlx layout. This "
"will run in float32 and want roughly twice the memory.")
train_dataset, tools_encoding = to_dataset(train_rows)
eval_dataset, _ = to_dataset(valid_rows)
print(f"train rows: {len(train_dataset)} validation rows: {len(eval_dataset)} "
f"tools column: {tools_encoding}")
targets = args.target_modules[0] if args.target_modules == ["all-linear"] else args.target_modules
config = SFTConfig(
output_dir=args.output_dir,
num_train_epochs=args.epochs,
per_device_train_batch_size=args.batch_size,
per_device_eval_batch_size=args.batch_size,
gradient_accumulation_steps=args.grad_accum,
learning_rate=args.lr,
lr_scheduler_type="cosine",
warmup_steps=5,
max_length=args.max_length,
packing=False,
# The tool results are the environment speaking. Training on them teaches the
# model to write file contents and command output, which is not the job.
assistant_only_loss=(args.loss == "assistant"),
gradient_checkpointing=args.gradient_checkpointing,
bf16=bf16,
model_init_kwargs={"dtype": dtype},
eval_strategy="epoch",
save_strategy="epoch",
save_total_limit=2,
load_best_model_at_end=True,
metric_for_best_model="eval_loss",
greater_is_better=False,
logging_steps=5,
report_to="none",
seed=args.seed,
data_seed=args.seed,
)
peft_config = LoraConfig(
r=args.rank,
lora_alpha=args.alpha,
lora_dropout=args.dropout,
target_modules=targets,
bias="none",
task_type="CAUSAL_LM",
)
callbacks = []
if args.early_stopping_patience > 0:
callbacks.append(EarlyStoppingCallback(early_stopping_patience=args.early_stopping_patience))
trainer = SFTTrainer(
model=args.model,
args=config,
train_dataset=train_dataset,
eval_dataset=eval_dataset,
processing_class=tokenizer,
peft_config=peft_config,
callbacks=callbacks or None,
)
# If this percentage is not roughly what the adapter arithmetic predicted, the target
# module names did not match and nothing was attached.
trainer.model.print_trainable_parameters()
started = time.time()
trainer.train()
elapsed = time.time() - started
trainer.save_model(args.output_dir)
# The chat template travels in the tokeniser files and it is the thing that has to
# match at serving time. For a tool-calling fine-tune it is the whole contract.
tokenizer.save_pretrained(args.output_dir)
losses = summarise_history(trainer.state.log_history)
losses["seconds"] = round(elapsed, 1)
print(json.dumps(losses, indent=2))
print(f"adapter saved to {args.output_dir}")
if losses["best_epoch"] is not None and losses["best_epoch"] <= 1:
print("the best epoch was the first: this dataset is small for this many epochs, or "
"the learning rate is too high. Read the two curves before you train again.")
if args.labbook:
train_path = str(data_dir / "train.jsonl")
record = agentlog.record(
labbook=args.labbook,
lab="part-27/train-agent-sft",
model=args.model,
dataset={"path": train_path, "sha256": agentlog.file_sha256(train_path),
"train_examples": len(train_dataset),
"validation_examples": len(eval_dataset)},
data_lineage=agentlog.lineage(trajectories=train_path,
tools_encoding=tools_encoding),
hyperparameters={
"method": "lora", "rank": args.rank, "alpha": args.alpha,
"dropout": args.dropout, "target_modules": targets,
"epochs": args.epochs, "batch_size": args.batch_size,
"grad_accum": args.grad_accum,
"effective_batch": args.batch_size * args.grad_accum,
"learning_rate": args.lr, "max_length": args.max_length,
"loss_mask": args.loss,
"gradient_checkpointing": args.gradient_checkpointing,
"early_stopping_patience": args.early_stopping_patience,
"precision": "bfloat16" if bf16 else "float32",
"output_dir": args.output_dir,
},
seed=args.seed, losses=losses, scores={},
config_path=__file__, notes=args.notes,
)
print(f"recorded run {record['run_id']} in {args.labbook}")
if __name__ == "__main__":
main()

Download train-agent-sft.py326 lines

LoRA on Qwen3-4B, rank 16, batch 1 at 4,096 tokens, on a 16 GB machine

Frozen base weights, BF16
8 GB
Adapter, gradients and optimiser states
0.5 GB
Activations with checkpointing, 4,096 tokens
1.5 GB
Reserved for the operating system
2 GB
Free
4.0 GB
Total
16 GB
Estimate from Part 11's arithmetic, not a measurement. The frozen base is the BF16 size from the course model reference. The adapter is about 33 million trainable parameters, which is rank 16 on the seven projections of 36 layers at this model's dimensions, at sixteen bytes each for the weights, the gradients and the two optimiser moments. The activation figure assumes gradient checkpointing and TRL's default chunked cross-entropy, whose documentation says the projection is computed on non-ignored tokens only and the loss is processed in chunks so that peak activation memory does not scale with the full vocabulary by sequence-length logits tensor; without that, the logits alone would be larger than everything else on this bar except the weights.

At the 16 GB floor the smaller base is the safer choice, and the room saved goes into sequence length rather than into rank. Qwen3-4B and Qwen3-1.7B are both Apache-2.0 according to their cards; the model reference carries the licence for every model this course names.

On Track M the trainer is mlx_lm.lora on the data-mlx layout the converter writes alongside the TRL one. Its documentation, read on 2026-09-09, shows a tools-format example whose tool calls carry the arguments as a JSON string, documents --mask-prompt for computing the loss on the completion only, and gives --num-layers as the memory knob with a default of 16. --mask-prompt is the nearest equivalent to the assistant-only mask and it is not the same thing on a multi-turn trajectory; record which you used.

Two hyperparameters deserve a moment’s thought rather than a default. Epochs: a trajectory set of a few hundred episodes is small, and three epochs over it is enough to make the model’s replies read like the training data in every context, not only the agent one. If the run’s best epoch is the first, that is the signal to stop at one and collect more data rather than to train harder on what you have. Rank: sixteen is plenty for a change of habit. Raising it is the wrong first move when the fine-tune underperforms, because the constraint is almost never adapter capacity at this scale; the constraints are the number of episodes, their diversity, and whether the sequence length held them.

There is also a cheap defence against the forgetting described later on this page. Mix a few hundred examples of ordinary, non-agent conversation into the training set, drawn from whatever general data you already trust, and the model has something to hold on to besides trajectories. Part 13’s project uses the same trick against format collapse. Treat the ratio as something to record and vary rather than as a rule: start with roughly one general example for every two trajectories, measure, and change it if the general set still moves.

Part 13’s what fine-tuning changes makes a distinction that matters even more here. Fine-tuning on a few hundred examples reliably changes form: the shape of the output, the conventions it follows, the moves it reaches for first. It does not reliably add knowledge, and it does not make a small model able to reason through something it could not reason through before.

Applied to agents, that maps onto three honest expectations.

Tool discipline improves, often a lot. Calling the right tool, filling in the required arguments, not inventing a tool that was never declared, not emitting the call format as prose, calling the finishing tool instead of trailing off. These are form, they are exactly what a few hundred trajectories demonstrate, and they are where a small model most often fails. Part 24’s reliability test is the instrument that shows it.

Conventions specific to your repositories transfer. If your agent should look in docs/runbooks before README.md, prefer a search over a directory listing, or always run the test command before reporting, those are habits, and habits are form. This is the sense in which a fine-tune is “adapted to your codebase”.

Knowledge of your codebase does not transfer, and should not be expected to. A model that has read your repository in a few hundred trajectories has not memorised it and cannot answer questions about a file it was never shown. If what you want is for your codebase to be available to the model at answer time, the instrument is retrieval from Part 10 and a longer context, not an adapter. This is the most common wrong reason to run the training in this part, and it is worth deciding which one you are buying before you spend the evening.

Three instruments, all of which already exist, and all of which have to be run against both models at the same settings.

Part 24’s tool-call-reliability.py sends a fixed prompt set with the same tool list every time and reports six rates: call rate, parse rate, right-tool rate, schema validity, argument correctness and false-call rate. It is the closest thing to a unit test for tool use, it needs no workspace and it runs in a couple of minutes. Run it first, before you train anything: the per-case failures tell you what to put in the collection set.

Part 26’s agent-eval.py runs the task suite end to end and reports success rate, mean steps, mean tokens and mean seconds. It is the measurement that matters, because it is the one whose units are “tasks finished”. It is also noisy on a suite of fifteen tasks, which is why it takes --repeats and why three attempts per task is the minimum that can distinguish a real change from a sampling one.

Part 10’s run-eval.py over your own general task set is the regression check, and it is the one people leave out.

Pending validationBefore and after, one model, three instruments
InstrumentMeasureBaseFine-tuneChange
Part 24 reliabilityparse rate
Part 24 reliabilityright-tool rate
Part 24 reliabilityschema-valid rate
Part 24 reliabilityfalse-call rate
Part 26 agent suitesuccess rate, all tasks
Part 26 agent suitemean steps per task
Part 10 general setpass rate per category

the machine serving both models: track, chip and memory, your operating system and version · llama.cpp behind the Part 9 gateway, or mlx-lm on Track M the build the gateway is running · the base and the fine-tune, as two aliases on the same server, the same quantisation for both, or the comparison measures the quantiser · 16,384 tokens of context · the date you ran it

The lab in this part fills this in with compare-agent-models.py, which runs all three instruments against both aliases and prints the differences. Repeats per attempt and the spread across them belong in the same table: a change smaller than the spread is not a result.

Two failures are specific enough to name.

The general set gets worse. A model trained for several epochs on a few hundred trajectories over three documents has been told, in effect, that this is what conversation is now. Part 13’s project already showed this on formats; it is stronger here because the trajectories are longer and more distinctive. Part 10’s harness over your own general set is what catches it, and the fix is usually fewer epochs, a lower rank, or mixing in general examples rather than abandoning the run.

The false-call rate rises while everything else improves. This is the one that hides, because the headline success rate can go up at the same time. A model trained on trajectories that all begin with a tool call learns that a tool call is always the right first move, and then calls a tool when it is asked a question it could simply answer. Part 24’s prompt set includes cases that should produce no call at all, precisely so that this shows up as a number. A rise there is a regression even when the suite improves, and it belongs in your report on its own line.

Train the action contract without teaching repository secrets

Section titled “Train the action contract without teaching repository secrets”

Tool-use examples should include valid calls, tool errors, denied operations and recovery when the user supplies missing information. The model needs to learn when to stop or ask, not merely how to emit a schema. Render the complete conversation with the serving template and inspect assistant/tool boundaries before training.

For repository-specific behaviour, prefer examples of conventions and workflows whose labels you can verify. Current code facts often belong in retrieval or file tools, since a fine-tune becomes stale as the repository changes. Remove credentials and private data that are unnecessary to the task.

Evaluate schema validity, correct tool selection, argument correctness, permission adherence and final task success separately. Include a regression set of ordinary coding tasks and an unseen repository or task family where appropriate. A gain on familiar trajectories can coexist with overfitting to filenames or habitual call sequences. Promote the adapter only after the deployed export passes the same tool contract and independent task checks as the training-time model.

  • Store tool calls as structured messages with a tools column and let the chat template render them. The template is part of the contract; serve the fine-tune with the same one.
  • The recipe is Part 13’s. Two settings change: the loss mask becomes assistant-only, and the sequence length grows to hold a whole trajectory. Truncation removes the final answer, which is silent and expensive.
  • Expect form to change and knowledge not to. Tool discipline and house conventions transfer; a model that saw your repository in three hundred trajectories has not memorised it.
  • Measure with three instruments against both models at identical settings: Part 24’s reliability test, Part 26’s agent suite with repeats, and Part 10’s general set.
  • A rising false-call rate is a regression even when the success rate rises with it.

Check your understanding

Question 1. Why is assistant-only loss the right mask for a trajectory dataset, where completion-only was right in Part 13?
Show the answer and why

Answer: A trajectory has many assistant turns interleaved with tool results, and the tool results are the environment’s output rather than the model’s

Completion-only assumes one prompt and one answer. In a trajectory the assistant speaks several times and the tool speaks between those turns. Training on the tool messages teaches the model to generate file contents and command output, which the environment supplies at serving time.

Question 2. You keep Part 13’s sequence length of 1,024 tokens for a trajectory dataset. What happens?
Show the answer and why

Answer: Episodes are truncated from the end, so the final answer and the finishing tool call are removed from most examples

Truncation is silent and it takes the end of the sequence, which is where the answer is. The loss then lands mostly on the early tool calls, and the model learns to start a trajectory and never finish one.

Question 3. Which of these should you expect a fine-tune on three hundred of your own trajectories to improve?
Show the answer and why

Answer: The rate at which the model emits a well-formed call to a declared tool, Whether the model calls the finishing tool instead of trailing off, Whether the model reaches for your house search tool before listing a directory

The first, third and fourth are form: shapes and habits that a few hundred demonstrations teach well. The second is knowledge, and an adapter trained on a handful of trajectories is not how knowledge of a repository gets in. Retrieval and context are.

Question 4. After fine-tuning, the agent suite success rate rises and the false-call rate on Part 24’s prompt set also rises. What is the honest reading?
Show the answer and why

Answer: The model has learned that calling a tool is always the right first move, which is a regression that the success rate hides

Part 24’s prompt set deliberately includes cases that should produce no call. A model trained on trajectories that all open with a call generalises that opening move to questions it could just answer, which costs latency, tokens and trust. Report it on its own line rather than netting it off against the gain.

Question 5. True or false: it is fine to serve the base model at Q8_0 and the fine-tune at Q4_K_M when comparing them, because both are quantised.
Show the answer and why

Answer: False

False. The difference you measure would be the sum of the fine-tune and the quantisation change, with no way to separate them. Serve both at the same type, engine, context length and sampling settings, and record all four alongside the numbers.

Sources for this lesson

6 verified · checked 2026-09-09

  1. 01TRL documentation — SFT Trainer§ Train on assistant messages only; Tool Calling with SFT; Train adapters with PEFThuggingface.co/docs/trl/en/sft_trainer2026-09-09
  2. 02TRL documentation — Dataset formats and types§ Tool Callinghuggingface.co/docs/trl/dataset_formats2026-09-09
  3. 03Transformers documentation — Chat templates§ Model training; add_generation_prompthuggingface.co/docs/transformers/chat_templating2026-09-09
  4. 04Qwen3-4B model card§ Agentic use; licence; best practiceshuggingface.co/Qwen/Qwen3-4B2026-09-09
  5. 05Qwen3-Coder-30B-A3B-Instruct model card§ Agentic coding; function calling; context lengthhuggingface.co/Qwen/Qwen3-Coder-30B-A3B-Instruct2026-09-09
  6. 06mlx-lm — LoRA and QLoRA fine-tuning§ Data; tools format; mask-prompt; num-layersgithub.com/ml-explore/mlx-lm/blob/main/mlx_lm/LORA.md2026-09-09

Every technical claim on this page was checked against the official documentation of the tool, vendor or model publisher on the date shown, at the version pinned for the course. Where the course disagrees with folklore, the source is how you can tell which one to trust.