← All content
GuideTool CallingAgentic AI
Conversations are now first-class citizens: two new task types for whole-conversation training

Conversations are now first-class citizens: two new task types for whole-conversation training

Not long ago the typical production task was a single exchange: classify this ticket, extract these fields, answer this question. What you are building now is more likely a conversation: a voice assistant that books an appointment over four turns, an agent that calls a tool, reads the result, and calls another, an assistant that keeps the user engaged while it does something useful in the background.

distil labs has trained small models for multi-turn tool calling since June 2025. What we could not train on was the rest of the conversation: the prose wrapped around the calls. Two new task types close that gap: chat-completion for the conversation itself, chat-completion-agentic for when tool results come back and the assistant keeps going. They take complete conversations, text and tool calls together, and fine-tune a model on the whole interaction. This post introduces both: what they accept, how training works, and the part that is still open research.

Conversations, not columns

When we started, the platform was built around a pair: one user question, one model answer, matching how LLMs are trained and deployed. That is still the true shape of question answering and classification, our most common tasks, and for those it holds up fine.

However, tasks people brought us next did not fit in two columns, so in July, we moved the upload format to the one the rest of your stack already speaks. You now give us OpenAI-style messages lists, one per example, and that is the whole record. Traces, test sets and minimal hand-written datasets upload as-is. The principle behind it: your data should live in the shape your tooling produces, not the shape the model consumes.

Three shapes of a training example: QA pair, conversation, agent loop

The workaround we all got used to

On the training side, multi-turn is not new to us. The tool-calling task trains models on multi-turn interactions: the user requests something, the assistant responds with a tool call, turn after turn. That is the foundation the two new tasks build on. But the multi-turn tool-calling task spoke in functions only. Every assistant turn was exactly one tool call, and content stayed empty. That is a clean contract for a machine-facing agent, and it is where we built up our experience generating and validating synthetic multi-turn data.

It also meant an assistant could not simply talk. To say something to a user, it had to call a function that said it. In the customer support cascade we shipped this June, every assistant reply is wrapped in a respond_to_user call for exactly this reason: it was the only way to make talking fit a tool-calling contract.

Production traces kept showing three shapes that the workaround handled badly.

Text and a tool call in the same turn. A voice assistant that goes silent for the length of a database lookup is a bad product, so real ones say “Let me check that for you” and issue the call together. Under the tool-calling task, that turn had to lose one half or the other.

Several tool calls at once. An assistant that needs three lookups issues three calls in one message. Splitting them into three turns is three round trips instead of one.

The loop. Call a tool, read what came back, decide whether to call another, and only then answer the user. Anything doing multi-step retrieval works this way, and the tool-calling task had no way to feed the result back.

The two new tasks

Two new task types take standard OpenAI conversations directly, with no wrapper function. chat-completion covers the assistant that talks to a user and can fire tool calls in the same turn. chat-completion-agentic adds the loop: read the result, decide whether to call again, then answer. Between them they cover the two things people build on SLMs today: human-facing applications where a turn has to come back fast, like voice AI, and agentic workflows.

Task Accepts Use it when
chat-completion user → assistant(content and/or tool_calls) → user → ... the assistant talks to the user and may emit tool calls, but does not read the results back
chat-completion-agentic user → assistant(content and/or tool_calls) → tool → assistant(...) → ... → assistant(content) the full loop: call, read the result, decide to call again, then summarize for the user

Three examples, from simple to the full loop.

The simplest example of the chat-completion task is a plain conversation - no tool calls, just a multi-turn interaction preserving the whole history.

{"messages": [
  {"role": "user", "content": "Is Paris worth visiting in November?"},
  {"role": "assistant", "content": "Yes, if you pack for it. It's low season, so museums and restaurants are quieter and hotels cheaper, but expect grey skies and rain."},
  {"role": "user", "content": "What would you plan for a weekend?"},
  {"role": "assistant", "content": "Two museum mornings (book the Louvre ahead, it's calm in November), one afternoon in a covered passage, and one evening river cruise. Keep outdoor plans flexible."}
]}

Next, we can involve tools. The assistant talks to the user and issues a call in the same turn if needed. Note what is missing: the tool result never comes back. That is the whole difference between the two tasks, and it is why chat-completion trains the assistant’s side of the conversation rather than its reaction to tool output.

{"messages": [
  {"role": "user", "content": "What's the weather in Paris?"},
  {"role": "assistant", "content": "Let me check that for you.",
   "tool_calls": [{"id": "call_a1", "type": "function",
     "function": {"name": "get_weather", "arguments": {"location": "Paris"}}}]},
  {"role": "user", "content": "Can you check it in 3 weeks?"},
  {"role": "assistant", "content": "I can check only for 2 weeks or less."}
]}

In the chat-completion-agentic case the results come back, and the assistant works with them: here it needs the user’s location before it can look anything up, so one lookup feeds the next:

{"messages": [
  {"role": "user", "content": "What's the weather at my location?"},
  {"role": "assistant", "content": "Let me check that for you.",
   "tool_calls": [{"id": "call_a1", "type": "function",
     "function": {"name": "get_location", "arguments": {}}}]},
  {"role": "tool", "tool_call_id": "call_a1", "content": "Paris, FR"},
  {"role": "assistant", "tool_calls": [{"id": "call_b2", "type": "function",
     "function": {"name": "get_weather", "arguments": {"location": "Paris"}}}]},
  {"role": "tool", "tool_call_id": "call_b2", "content": "34°C, sunny"},
  {"role": "assistant", "content": "You're in Paris, where it's 34°C and sunny. Very hot for the season, so plan for shade."}
]}

Results

To benchmark the chat-completion task we used a dataset built from the Schema-Guided Dialogue (SGD) dataset from Google Research, over 20,000 multi-turn, task-oriented dialogues, which we also used in our post on training from traces. It contains both content-only turns, where the assistant just talks, and turns where a tool call is requested.

The setup follows that earlier post: GLM-5 as the teacher, generating 2,000 chat-completion examples, and Qwen3-1.7B as the student. Because most turns are free text, we score with an LLM judge given the reference answer, run over content and tool-calling turns alike, and report the mean across the 359 turns of the held-out test set. GLM-5 is also the judge, so the teacher row is scored by the model that produced it, which if anything tilts the table in the teacher’s favour.

Model All (359) Content (287) Tool calls (72)
GLM-5 (teacher) 0.869 0.937 0.597
Qwen3.5-397B-A17B 0.780 0.777 0.792
Qwen3-235B-A22B 0.813 0.885 0.528
kimi-k3 0.794 0.878 0.458
gpt-oss-120b 0.708 0.774 0.444
Qwen3-1.7B (base) 0.524 0.523 0.528
Qwen3-1.7B (tuned) 0.886 0.906 0.806

The tuned 1.7B tops the table overall at 0.886, above its own teacher at 0.869, and it wins tool calls outright at 0.806 against 0.792 for Qwen3.5-397B-A17B, a model roughly 230 times its parameter count. Fine-tuning moved the same 1.7B from 0.524 to 0.886. The one column it does not win is content-only turns, where the teacher holds 0.937 against the student’s 0.906. This is one task on one dataset, and chat-completion-agentic is not in the table, so nothing here measures the full loop.

The fine-tuned Qwen3-1.7B against its GLM-5 teacher and five larger models, on all turns and on tool-call turns

How we train it, and what had to change

The pipeline is the one we use for every task. You give us a plain-English description of the job, your tools array, and a small set of seed conversations, usually somewhere between 10 and 50. From there:

  1. We check that the base model fails at the task, so there is a gap worth closing.
  2. We set a teacher model as the ceiling, currently GLM-5.2 for most work (the run above used GLM-5).
  3. The teacher generates in-domain synthetic examples.
  4. Rule-based validators throw out the ones that are malformed, off-schema or too similar to what we already have.
  5. The student is fine-tuned on your seed data plus what survived, then evaluated on a held-out test set against both the teacher and the untuned base model, so you see the comparison before you switch anything.

The student can end up beating its teacher, as it did in the table above: validation strips out the teacher’s mistakes before the student ever sees them, and the student spends all of its capacity on one task instead of holding the whole world in its weights.

Two parts of that loop had to grow for conversations.

Generation now produces whole conversations. Not one labeled output, but a coherent exchange where the user has a reason to say what they say and the assistant’s tool calls follow from it.

Evaluation is harder when a turn is half prose and half structured call, so the two are scored together. Six metrics run on every conversation: rouge on the text, tool_call_equivalence, binary_tool_call and staged_tool_call on the calls, and an LLM judge in two forms, with a reference answer and without one. A turn that correctly makes no tool call counts as correct, so the model is not penalized for staying quiet when quiet is right.

What’s next

Generating multi-turn conversations was already the hard part of multi-turn tool calling. Free text and several calls per turn raise the bar again, and that is where most of our current work sits.

Better synthetic conversations. The generator has to cover the tool-call space thoroughly and keep the free-text content sounding like a person. Push on coverage and the prose stiffens. The task already clears our quality bar, and improving it is a continuous effort we are not stopping.

Reasoning traces support. A conversation that carries the assistant’s reasoning content is rejected at validation at the moment. Full reasoning support is something we are looking forward to releasing soon.

Try it on a trace you already have

Take one real interaction out of your logs, the ugliest multi-turn one you can find, and upload it as a messages list. Pick chat-completion-agentic if the assistant reads tool results, chat-completion if it does not. The dataset docs show the expected data format for every task type, and you can sign up and run it today.

If a dataset does not map cleanly, tell us. The format is new, the tasks are newer, and the rough edges are the part we want to hear about.


distil labs trains task-specific small language models that are cheaper and faster per request than general-purpose LLMs, with equal-or-better accuracy on bounded tasks. Docs · GitHub · Slack