Skip to the solution
MediumAI Engineering14 min read

What is the difference between a base model, an instruction-tuned model and a chat model?

1.1k views

The problem

Question presented to candidate: "Model hubs list the same model as a base version and an instruct or chat version. What is actually different between a base model, an instruction-tuned model and a chat model, and which one would you pick for a product feature?"

What a strong answer should cover:

  • A base model is the output of pretraining: a next-token predictor that continues documents; it does not reliably answer, follow instructions or stop
  • An instruction-tuned model is the same network after supervised fine-tuning on (instruction, response) examples, usually followed by preference tuning (RLHF or DPO); it learns the habit of answering, a format and when to stop
  • A chat model is instruction-tuned on multi-turn conversations with roles (system, user, assistant) encoded by a chat template with special tokens; the serving stack or apply_chat_template must reproduce that exact format
  • Post-training changes behaviour more than knowledge: InstructGPT showed human raters preferred a 1.3B instruction-tuned model over the 175B GPT-3 base model
  • When to use which: chat or instruct models for almost all product work; base models for further fine-tuning, pure completion tasks, few-shot pattern continuation and research
  • In practice the names blur: most open-weight "Instruct" releases are chat models with a template, and hosted APIs serve only post-trained chat models

Code / implementation expected: Optional โ€” showing how a chat template turns messages into one token string, or how a base model continues a prompt instead of answering it.

  • base-model
  • instruction-tuning
  • rlhf
  • post-training

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

14 min read ยท 10 sections

Read the code

Base vs chat in miniature: the same n-gram model before and after training on templated conversations
// Base model vs chat model, in miniature. The "model" is a back-off n-gram
// (it predicts the next token from the longest context it has seen), so the
// same code shows how pretraining data and fine-tuning data change behaviour.
const MAX_CTX = 6;

function train(texts) {
  const table = {};
  for (const text of texts) {
    const t = text.split(" ");
    for (let i = 1; i < t.length; i++)
      for (let k = 1; k <= Math.min(MAX_CTX, i); k++) {
        const ctx = t.slice(i - k, i).join(" ");
        table[ctx] = table[ctx] || {};
        table[ctx][t[i]] = (table[ctx][t[i]] || 0) + 1;
      }
  }
  return table;
}

function generate(table, prompt, maxNew, stop) {
  const out = prompt.split(" ");
  for (let n = 0; n < maxNew; n++) {
    let counts = null;
    for (let k = Math.min(MAX_CTX, out.length); k >= 1 && !counts; k--) counts = table[out.slice(-k).join(" ")] || null;
    if (!counts) return { text: out.slice(prompt.split(" ").length).join(" ") || "(nothing)", why: "no known context" };
    const next = Object.keys(counts).sort().reduce((a, b) => (counts[b] > counts[a] ? b : a));
    if (next === stop) return { text: out.slice(prompt.split(" ").length).join(" "), why: "stop token " + stop };
    out.push(next);
  }
  return { text: out.slice(prompt.split(" ").length).join(" "), why: "hit max " + maxNew + " tokens" };
}

// Stage 1, pretraining: raw web-style text. Nobody is "answering" anything.
const webText = [
  "what is the capital of france ? what is the capital of spain ? what is the capital of italy ? " +
  "quiz books list questions like these . paris is the capital of france . " +
  "madrid is the capital of spain . rome is the capital of italy .",
];

// Stage 2, instruction tuning: conversations rendered with a chat template.
// Special tokens mark who is speaking and where a turn ends.
const render = (messages, addGenerationPrompt) =>
  messages.map((m) => "<|im_start|> " + m.role + " " + m.content + " <|im_end|>").join(" ") +
  (addGenerationPrompt ? " <|im_start|> assistant" : "");
const sftPairs = [
  ["what is the capital of france ?", "paris is the capital of france ."],
  ["what is the capital of spain ?", "madrid is the capital of spain ."],
];
const sftTexts = sftPairs.map(([q, a]) => render([{ role: "user", content: q }, { role: "assistant", content: a }], false));
const EPOCHS = 3; // fine-tuning repeats the small dataset, shifting the counts

const base = train(webText);
const chat = train([...webText, ...Array(EPOCHS).fill(sftTexts).flat()]);
console.log("one training conversation as the model sees it:");
console.log("  " + sftTexts[1]);

const question = "what is the capital of spain ?";
let r = generate(base, question, 14, "<|im_end|>");
console.log("BASE, raw prompt   -> " + r.text + "   [" + r.why + "]");

const prompt = render([{ role: "user", content: question }], true);
console.log("chat prompt string -> " + prompt);
r = generate(base, prompt, 14, "<|im_end|>");
console.log("BASE, chat prompt  -> " + r.text + "   [" + r.why + "]");
r = generate(chat, prompt, 14, "<|im_end|>");
console.log("CHAT, chat prompt  -> " + r.text + "   [" + r.why + "]");

const ctx = "is the capital of spain .";
const show = (t) => Object.keys(t[ctx]).sort().map((w) => w + ":" + t[ctx][w]).join(" ");
console.log("after '" + ctx + "'  base counts: " + show(base) + " | chat counts: " + show(chat));

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 18 of 83 in AI engineering

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

Up nextMedium

How do you get reliable structured JSON output from an LLM?

Continue