{"course":{"slug":"fine-tuning","title":"Fine-Tuning, and When Not To","url":"https://addaly.com/learn/fine-tuning","level":"advanced","track":"deep"},"lesson":{"slug":"instruction-data-formats","title":"The three formats, and the conversion trap","url":"https://addaly.com/learn/fine-tuning/instruction-data-formats","position":30,"of":84,"minutes":8,"answer":"Convert everything to messages format once on the way in, use the same formatting function for training and inference, and print one templated example as a string before every run.","keyIdea":"Convert everything to messages format once on the way in, use the same formatting function for training and inference, and print one templated example as a string before every run, because the commonest data bugs are a changed separator, a half-present system prompt, and truncation that removes the end-of-turn token."},"body":{"format":"markdown","text":"## Three shapes you will meet\n\nInstruction data comes in three formats. You will have to convert between them, and the conversion is where errors enter.\n\n**Alpaca style.** Three fields, one of which is optional:\n\n```json\n{\"instruction\": \"Summarise the complaint in one sentence.\",\n \"input\": \"I ordered on the 3rd and it still hasn't...\",\n \"output\": \"Customer reports a delayed order placed on the 3rd.\"}\n```\n\n**ShareGPT style.** A conversation list with its own vocabulary:\n\n```json\n{\"conversations\": [{\"from\": \"human\", \"value\": \"...\"},\n                    {\"from\": \"gpt\",   \"value\": \"...\"}]}\n```\n\n**Messages style.** What the OpenAI API popularised and what has become the default:\n\n```json\n{\"messages\": [{\"role\": \"system\", \"content\": \"You are a triage assistant.\"},\n              {\"role\": \"user\", \"content\": \"...\"},\n              {\"role\": \"assistant\", \"content\": \"...\"}]}\n```\n\nUse messages style. TRL, Axolotl and Llama-Factory all accept it natively, it represents multi-turn and tool calls without extension, and it maps cleanly onto every model's chat template. Convert everything to it on the way in, once, and never think about the others again.\n\n## The trap in Alpaca's `input` field\n\nAlpaca's two-part prompt has to become one user message. Almost everybody concatenates them, and the question is *how*.\n\nThe original Alpaca template inserted specific headers: an instruction block, then an `### Input:` block, then `### Response:`. If you concatenate with a newline instead, you have changed the prompt format. That is fine, as long as you do it identically at inference time. It is a disaster if training used one separator and serving uses another, because the model learned to respond to a pattern that never arrives.\n\n```python\ndef to_messages(row):\n    user = row[\"instruction\"]\n    if row.get(\"input\"):\n        user += \"\\n\\n\" + row[\"input\"]        # decide once, apply everywhere\n    return {\"messages\": [{\"role\": \"user\", \"content\": user},\n                          {\"role\": \"assistant\", \"content\": row[\"output\"]}]}\n```\n\nThe rule that prevents the whole class of bug: **the code that formats training examples and the code that formats inference requests must be the same function.** Not similar. The same function, imported from one module, used by both.\n\n## The system prompt decision\n\nThree options, and they have different consequences:\n\n> Figure: Three formats in, one format used. Steps: 1. Alpaca: instruction, input, output (Two fields that have to become one user message. The separator you choose is part of the prompt format.); 2. ShareGPT: conversations, from, value (A conversation list with its own vocabulary for the roles.); 3. Convert on the way in, once (One function, used by training and by serving. Then count how many examples exceed your maximum length before the tokenizer decides for you.); 4. Messages: role and content (What TRL, Axolotl and Llama-Factory accept natively, what represents tool calls without extension, and what every chat template maps onto.). The rule that prevents a whole class of bug: the code that formats training examples and the code that formats inference requests must be the same function, imported from one module.\n\n1. **The same system prompt on every example, and at inference.** The model learns to rely on it. Fine, and it is what most production systems do. Do not then change the system prompt after training without re-evaluating.\n2. **No system prompt anywhere.** Also fine, and the simplest. The model's behaviour is entirely in its weights.\n3. **Varied system prompts.** Genuinely useful when you want the model to remain steerable: different tones, different output formats, selected at request time. Costs more data, because each variant needs examples.\n\nThe failure is mixing them by accident: half your examples carrying a system prompt because they came from a log that included one, half not. The model then learns that the system prompt is optional and roughly ignorable, which is the opposite of what you wanted.\n\n## Validate before you train\n\nFour checks, worth the ten minutes:\n\n```python\nfor i, row in enumerate(data):\n    m = row[\"messages\"]\n    assert m[-1][\"role\"] == \"assistant\", f\"{i}: must end with assistant\"\n    assert all(x[\"content\"].strip() for x in m), f\"{i}: empty content\"\n    roles = [x[\"role\"] for x in m if x[\"role\"] != \"system\"]\n    assert roles == [\"user\", \"assistant\"] * (len(roles)//2), f\"{i}: bad alternation\"\n    assert len(tok.apply_chat_template(m)) <= MAX_LEN, f\"{i}: too long\"\n```\n\nThe last one matters more than it looks. Examples over your maximum sequence length are silently truncated, and truncation removes the *end* of the assistant's answer, including the end-of-turn token. Train on a few hundred of those and you have taught the model not to stop. Count how many examples exceed the limit before you train, and if it is more than a couple of percent, either raise the limit or drop those examples deliberately rather than letting the tokenizer decide.\n\n## The one-line test that catches most of it\n\nBefore any run, take one example, apply the chat template, and print the string:\n\n```python\nprint(tok.apply_chat_template(data[0][\"messages\"], tokenize=False))\n```\n\nRead it with your eyes. Every special token should be where you expect, the roles should be marked correctly, and there should be exactly one end-of-turn marker at the end. Five seconds, and it catches format bugs that would otherwise cost you a run and a day of confusion."},"check":{"question":"A team trains on Alpaca-format data converted with a newline separator, then serves with the original Alpaca template's '### Input:' headers. Quality is far worse in production than in evaluation. Why?","options":[{"text":"The '### Input:' header tokens consume context, leaving less room for the actual input at inference time.","correct":false,"tempting":"Identifies a genuine but trivial cost; a few header tokens cannot account for a large quality gap."},{"text":"Evaluation used the training format and production uses another, so the prompt pattern was never learned.","correct":true},{"text":"The headers contain '#' characters, which some tokenizers treat as markdown and split differently from plain text.","correct":false,"tempting":"Focuses on a tokenisation detail of the separator rather than the fact that the model never saw this prompt structure in training."},{"text":"Alpaca's template was designed for a different base model, so its headers conflict with the chat template's special tokens.","correct":false,"tempting":"Blames an incompatibility between templates; the same mismatch causes the failure even when the headers contain no special tokens at all."}]},"prev":{"slug":"where-the-data-comes-from","title":"Where the data legitimately comes from","url":"https://addaly.com/learn/fine-tuning/where-the-data-comes-from"},"next":{"slug":"cleaning-at-scale","title":"Cleaning at scale","url":"https://addaly.com/learn/fine-tuning/cleaning-at-scale"},"authorship":"Founder and series editor: Dr Mustafa Mun. Written by AI models to a written standard. Exam keys checked blind by a second model: 3,665 of 3,666 agreed. Academic review invited.","review":{"status":"Draft, open for academic review"},"cite":"Addaly (2026) 'The three formats, and the conversion trap', in Fine-Tuning, and When Not To. Edited by M. Mun. Addaly. Available at: https://addaly.com/learn/fine-tuning/instruction-data-formats (Accessed: 13 October 2026).","licence":{"name":"CC BY-NC-SA 4.0","url":"https://creativecommons.org/licenses/by-nc-sa/4.0/"},"reader":{"schema":1,"title":"The three formats, and the conversion trap","courseTitle":"Fine-Tuning, and When Not To","module":{"index":4,"slug":"building-the-dataset","title":"Building the dataset"},"answer":null,"keyIdea":"Convert everything to messages format once on the way in, use the same formatting function for training and inference, and print one templated example as a string before every run, because the commonest data bugs are a changed separator, a half-present system prompt, and truncation that removes the end-of-turn token.","hero":{"kind":"figure","figure":{"t":"figure","label":"Figure","figure":{"type":"flow","steps":[{"label":"Alpaca: instruction, input, output","detail":"Two fields that have to become one user message. The separator you choose is part of the prompt format."},{"label":"ShareGPT: conversations, from, value","detail":"A conversation list with its own vocabulary for the roles."},{"label":"Convert on the way in, once","detail":"One function, used by training and by serving. Then count how many examples exceed your maximum length before the tokenizer decides for you."},{"label":"Messages: role and content","detail":"What TRL, Axolotl and Llama-Factory accept natively, what represents tool calls without extension, and what every chat template maps onto."}],"title":"Three formats in, one format used","caption":"The rule that prevents a whole class of bug: the code that formats training examples and the code that formats inference requests must be the same function, imported from one module."},"alt":"Figure: Three formats in, one format used. Steps: 1. Alpaca: instruction, input, output (Two fields that have to become one user message. The separator you choose is part of the prompt format.); 2. ShareGPT: conversations, from, value (A conversation list with its own vocabulary for the roles.); 3. Convert on the way in, once (One function, used by training and by serving. Then count how many examples exceed your maximum length before the tokenizer decides for you.); 4. Messages: role and content (What TRL, Axolotl and Llama-Factory accept natively, what represents tool calls without extension, and what every chat template maps onto.). The rule that prevents a whole class of bug: the code that formats training examples and the code that formats inference requests must be the same function, imported from one module."}},"blocks":[{"t":"h2","id":"three-shapes-you-will-meet","runs":[{"text":"Three shapes you will meet"}]},{"t":"p","runs":[{"text":"Instruction data comes in three formats. You will have to convert between them, and the conversion is where errors enter."}]},{"t":"p","runs":[{"text":"Alpaca style.","b":true},{"text":" Three fields, one of which is optional:"}]},{"t":"code","lang":"json","label":"JSON","text":"{\"instruction\": \"Summarise the complaint in one sentence.\",\n \"input\": \"I ordered on the 3rd and it still hasn't...\",\n \"output\": \"Customer reports a delayed order placed on the 3rd.\"}"},{"t":"p","runs":[{"text":"ShareGPT style.","b":true},{"text":" A conversation list with its own vocabulary:"}]},{"t":"code","lang":"json","label":"JSON","text":"{\"conversations\": [{\"from\": \"human\", \"value\": \"...\"},\n                    {\"from\": \"gpt\",   \"value\": \"...\"}]}"},{"t":"p","runs":[{"text":"Messages style.","b":true},{"text":" What the OpenAI API popularised and what has become the default:"}]},{"t":"code","lang":"json","label":"JSON","text":"{\"messages\": [{\"role\": \"system\", \"content\": \"You are a triage assistant.\"},\n              {\"role\": \"user\", \"content\": \"...\"},\n              {\"role\": \"assistant\", \"content\": \"...\"}]}"},{"t":"p","runs":[{"text":"Use messages style. TRL, Axolotl and Llama-Factory all accept it natively, it represents multi-turn and tool calls without extension, and it maps cleanly onto every model’s chat template. Convert everything to it on the way in, once, and never think about the others again."}]},{"t":"h2","id":"the-trap-in-alpaca-s-input-field","runs":[{"text":"The trap in Alpaca’s "},{"text":"input","code":true},{"text":" field"}]},{"t":"p","runs":[{"text":"Alpaca’s two-part prompt has to become one user message. Almost everybody concatenates them, and the question is "},{"text":"how","i":true},{"text":"."}]},{"t":"p","runs":[{"text":"The original Alpaca template inserted specific headers: an instruction block, then an "},{"text":"### Input:","code":true},{"text":" block, then "},{"text":"### Response:","code":true},{"text":". If you concatenate with a newline instead, you have changed the prompt format. That is fine, as long as you do it identically at inference time. It is a disaster if training used one separator and serving uses another, because the model learned to respond to a pattern that never arrives."}]},{"t":"code","lang":"python","label":"Python","text":"def to_messages(row):\n    user = row[\"instruction\"]\n    if row.get(\"input\"):\n        user += \"\\n\\n\" + row[\"input\"]        # decide once, apply everywhere\n    return {\"messages\": [{\"role\": \"user\", \"content\": user},\n                          {\"role\": \"assistant\", \"content\": row[\"output\"]}]}"},{"t":"p","runs":[{"text":"The rule that prevents the whole class of bug: "},{"text":"the code that formats training examples and the code that formats inference requests must be the same function.","b":true},{"text":" Not similar. The same function, imported from one module, used by both."}]},{"t":"h2","id":"the-system-prompt-decision","runs":[{"text":"The system prompt decision"}]},{"t":"p","runs":[{"text":"Three options, and they have different consequences:"}]},{"t":"ol","start":1,"items":[[{"text":"The same system prompt on every example, and at inference.","b":true},{"text":" The model learns to rely on it. Fine, and it is what most production systems do. Do not then change the system prompt after training without re-evaluating."}],[{"text":"No system prompt anywhere.","b":true},{"text":" Also fine, and the simplest. The model’s behaviour is entirely in its weights."}],[{"text":"Varied system prompts.","b":true},{"text":" Genuinely useful when you want the model to remain steerable: different tones, different output formats, selected at request time. Costs more data, because each variant needs examples."}]]},{"t":"p","runs":[{"text":"The failure is mixing them by accident: half your examples carrying a system prompt because they came from a log that included one, half not. The model then learns that the system prompt is optional and roughly ignorable, which is the opposite of what you wanted."}]},{"t":"h2","id":"validate-before-you-train","runs":[{"text":"Validate before you train"}]},{"t":"p","runs":[{"text":"Four checks, worth the ten minutes:"}]},{"t":"code","lang":"python","label":"Python","text":"for i, row in enumerate(data):\n    m = row[\"messages\"]\n    assert m[-1][\"role\"] == \"assistant\", f\"{i}: must end with assistant\"\n    assert all(x[\"content\"].strip() for x in m), f\"{i}: empty content\"\n    roles = [x[\"role\"] for x in m if x[\"role\"] != \"system\"]\n    assert roles == [\"user\", \"assistant\"] * (len(roles)//2), f\"{i}: bad alternation\"\n    assert len(tok.apply_chat_template(m)) <= MAX_LEN, f\"{i}: too long\""},{"t":"p","runs":[{"text":"The last one matters more than it looks. Examples over your maximum sequence length are silently truncated, and truncation removes the "},{"text":"end","i":true},{"text":" of the assistant’s answer, including the end-of-turn token. Train on a few hundred of those and you have taught the model not to stop. Count how many examples exceed the limit before you train, and if it is more than a couple of percent, either raise the limit or drop those examples deliberately rather than letting the tokenizer decide."}]},{"t":"h2","id":"the-one-line-test-that-catches-most-of-it","runs":[{"text":"The one-line test that catches most of it"}]},{"t":"p","runs":[{"text":"Before any run, take one example, apply the chat template, and print the string:"}]},{"t":"code","lang":"python","label":"Python","text":"print(tok.apply_chat_template(data[0][\"messages\"], tokenize=False))"},{"t":"p","runs":[{"text":"Read it with your eyes. Every special token should be where you expect, the roles should be marked correctly, and there should be exactly one end-of-turn marker at the end. Five seconds, and it catches format bugs that would otherwise cost you a run and a day of confusion."}]}],"check":{"question":[{"text":"A team trains on Alpaca-format data converted with a newline separator, then serves with the original Alpaca template’s ‘### Input:’ headers. Quality is far worse in production than in evaluation. Why?"}],"options":[{"text":[{"text":"The ‘### Input:’ header tokens consume context, leaving less room for the actual input at inference time."}],"correct":false,"tempting":[{"text":"Identifies a genuine but trivial cost; a few header tokens cannot account for a large quality gap."}]},{"text":[{"text":"Evaluation used the training format and production uses another, so the prompt pattern was never learned."}],"correct":true,"tempting":null},{"text":[{"text":"The headers contain ‘#’ characters, which some tokenizers treat as markdown and split differently from plain text."}],"correct":false,"tempting":[{"text":"Focuses on a tokenisation detail of the separator rather than the fact that the model never saw this prompt structure in training."}]},{"text":[{"text":"Alpaca’s template was designed for a different base model, so its headers conflict with the chat template’s special tokens."}],"correct":false,"tempting":[{"text":"Blames an incompatibility between templates; the same mismatch causes the failure even when the headers contain no special tokens at all."}]}],"order":[3,0,1,2],"why":null},"prev":{"slug":"where-the-data-comes-from","title":"Where the data legitimately comes from","minutes":9},"next":{"slug":"cleaning-at-scale","title":"Cleaning at scale","minutes":9}}}