{
  "source_file": "0930",
  "source_platform": "Hashnode",
  "author": "Aman Jain",
  "title_listed": "Building a GPT Model from Scratch (Part 6): Fine-Tuning It to Follow Instructions",
  "keywords": "",
  "domain": "大模型算法",
  "content_type": "深度技术解析",
  "is_paid_flag": "否",
  "url": "https://curious-pm.hashnode.dev/fine-tuning-gpt-to-follow-instructions",
  "publish_date": "2026-07-16",
  "corpus_stage": "",
  "notes": "",
  "id": "bde9b4e3d0d5",
  "collected_at": "2026-10-02T14:11:45",
  "title": "Fine-Tuning GPT-2 to Follow Instructions",
  "body_markdown": "Building a GPT Model from Scratch (Part 6): Fine-Tuning It to Follow Instructions\nHow I prepared instruction data, fine-tuned GPT-2 Medium, and used a locally running LLM to evaluate open-ended responses.\nUpdated\nJuly 16, 2026\n•\n13\nmin read\n•\nView as Markdown\nA\nAman Jain\nProduct manager learning to build AI from the inside out — from LLM architecture to product applications.\nPart of series\nBuilding a GPT Model From Scratch\nIn the previous article, I fine-tuned a pretrained GPT model for classification. The model received a text message and predicted one of two labels: spam or not spam.\nInstruction fine-tuning is much more open-ended.\nInstead of selecting from predefined labels, the model receives a natural-language instruction and must generate an appropriate response. The instruction could ask it to rewrite a sentence, answer a question, explain a concept, classify an item, or perform a transformation.\nThis starts to look much closer to the assistant-style applications we use today, where a user can provide an open-ended request and expect a relevant response.\nThat shift changes several parts of the pipeline:\nThe dataset has a different structure.\nEach example must be converted into a consistent prompt.\nThe model needs enough capacity to generate useful responses.\nEvaluation cannot rely on accuracy or loss alone.\nIn this article, I will walk through these changes and explain how I fine-tuned GPT-2 Medium to follow instructions. I will also cover the most interesting part of the process: using another LLM, running locally through Ollama, to evaluate the generated responses.\nWhat Are We Teaching the Model?\nA pretrained GPT model can already predict the next token and generate text. That does not automatically mean it will follow a user’s request.\nFor example, we might provide:\nConvert 45 kilometers to meters.\nA pretrained model may continue the sentence, discuss kilometers generally, repeat the prompt, or produce an unrelated continuation. It has learned language patterns, but not necessarily the interaction pattern we expect from an assistant:\nInstruction → relevant response\nInstruction fine-tuning gives the model many examples of this pattern. Each example shows a task and an appropriate response.\nWe are not replacing GPT’s original training objective. The model still learns through next-token prediction. Instead, we restructure the training text so that next-token prediction teaches instruction-following behavior.\nA Different Dataset Structure\nInstruction data has a richer structure than the classification dataset:\nField\nPurpose\ninstruction\nDescribes what the model should do\ninput\nProvides optional context or information\noutput\nContains the expected response\nA sample record may look like this:\n{\n    \"instruction\": \"Edit the following sentence for grammar.\",\n    \"input\": \"He go to the park every day.\",\n    \"output\": \"He goes to the park every day.\"\n}\nGPT does not consume these as three separate fields. It receives a sequence of token IDs, so the fields first need to be combined into a consistent text format.\nConverting Each Record into a Prompt\nI used a small formatting function to transform every structured record into the same prompt format.\nThe function itself is not large, but the design decision is important. The template tells the model where the instruction begins, where optional context appears, and where the response should start.\nIf the data consistently follows the same structure, the model can learn to associate\n### Response:\nwith the answer it is expected to generate.\n# Implement the prompt-formatting function\n\ndef format_input(entry):\n    instruction_text = (\n        f\"below is an instruction that describes the task. \"\n        f\"Write a response that appropriately completes the request.\"\n        f\"\\n\\n### Instruction:\\n{entry['instruction']}\"\n    )\n\n    input_text = (\n        f\"\\n\\n### Input:\\n{entry['input']}\"\n        if entry[\"input\"]\n        else \"\"\n    )\n\n    return instruction_text + input_text\n\n\nmodel_input = format_input(data[50])\ndesired_output = f\"\\n\\n### Response: \\n{data[50]['output']}\"\nprint(model_input + desired_output)\nThe formatted example looks like this:\nbelow is an instruction that describes the task. Write a response that appropriately completes the request.\n\n### Instruction:\nIdentify the correct spelling of the following word.\n\n### Input:\nOcassion\n\n### Response:\nThe correct spelling is 'Occasion.'\nTo prepare the training dataset, the custom dataset class performs four steps for each example:\nFormats the instruction and optional input.\nAppends the expected response.\nTokenizes the complete text.\nStores the resulting token IDs.\nclass InstructionDataset(Dataset):\n    def __init__(self, data, tokenizer):\n        self.data = data\n        self.encoded_texts = []\n\n        for entry in data:\n            instruction_plus_input = format_input(entry)\n            response_text = f\"\\n\\n### Response: \\n{entry['output']}\"\n            full_text = instruction_plus_input + response_text\n            self.encoded_texts.append(tokenizer.encode(full_text))\n\n    def __getitem__(self, index):\n        return self.encoded_texts[index]\n\n    def __len__(self):\n        return len(self.data)\nThere is no separate numeric label such as\n0\nor\n1\n. The model predicts the next token throughout the formatted instruction-response sequence, using the same causal language-modeling objective that GPT was originally built for.\nWhy I Moved to GPT-2 Medium\nFor classification fine-tuning, GPT-2 Small with approximately 124 million parameters was sufficient for my experiment. The output was narrow: the model only needed to distinguish between two labels.\nInstruction following is more demanding. The model must interpret different tasks, connect instructions with optional input, and generate coherent multi-token responses.\nWhen I experimented with the 124-million-parameter model, its responses were not strong enough. I therefore moved to GPT-2 Medium, with approximately 355 million parameters. I loaded its pretrained weights into the same GPT architecture I had built earlier and fine-tuned it on the instruction dataset.\n# Use GPT-2 Medium instead of GPT-2 Small for instruction fine-tuning\n\nfrom gpt_download import download_and_load_gpt2\nfrom chap05 import GPTModel, load_weights_into_gpt\n\n\nBASE_CONFIG = {\n    \"vocab_size\": 50257,\n    \"context_length\": 1024,\n    \"drop_rate\": 0.1,\n    \"qkv_bias\": True\n}\n\nmodel_configs = {\n    \"gpt2-medium (355M)\": {\n        \"emb_dim\": 1024,\n        \"n_layers\": 24,\n        \"n_heads\": 16\n    }\n}\n\nCHOOSEN_MODEL = \"gpt2-medium (355M)\"\nBASE_CONFIG.update(model_configs[CHOOSEN_MODEL])\n\nmodel_size = CHOOSEN_MODEL.split(\"(\")[-1].lstrip(\")\").rstrip(\")\")\nsettings, params = download_and_load_gpt2(\n    model_size=model_size,\n    models_dir=\"gpt2\"\n)\n\nmodel = GPTModel(BASE_CONFIG)\nload_weights_into_gpt(model, params)\nmodel.eval()\nThe lesson is not simply that a larger model is always better. Fine-tuning shapes capabilities already present in the pretrained model. A stronger base model gives fine-tuning more useful language capability to redirect toward the new task.\nThe larger model also increased the computational cost on my local machine, but the improvement in response quality made the change worthwhile.\nFine-Tuning Still Uses Next-Token Prediction\nThe training loop did not require a new learning algorithm. For each batch, the model produced next-token logits, compared them with the shifted targets, calculated cross-entropy loss, backpropagated the gradients, and updated its weights.\nThe model was not explicitly given a rule called “follow instructions.” It repeatedly saw patterns like this:\n### Instruction:\nSummarize the following text.\n\n### Input:\n...\n\n### Response:\n...\nAcross many examples, it learned that the text following\n### Response:\nshould satisfy the instruction that appeared earlier.\nThis is also why an instruction-tuned model should not automatically be treated as a calculator. If the dataset contains kilometer-to-meter conversions, the model may learn the pattern, but it is still generating tokens from learned relationships rather than executing a guaranteed formula.\nFor reliable arithmetic or other deterministic operations, production systems often combine an LLM with external tools.\nstart_time = time.time()\ntorch.manual_seed(123)\n\noptimizer = torch.optim.AdamW(\n    model.parameters(),\n    lr=5e-5,\n    weight_decay=0.1\n)\n\nnum_epochs = 2\n\ntrain_loss, val_loss, tokens_seen = train_model_simple(\n    model,train_loader, val_loader, optimizer, device,\n    num_epochs=num_epochs,eval_freq=5,eval_iter=5,\n    start_context=format_input(val_data[0]),\n    tokenizer=tokenizer\n)\n\nend_time = time.time()\nexecution_time_minutes = (end_time - start_time) / 60\nprint(f\"Fine-tuning completed in {execution_time_minutes:.2f} minutes.\")\nGenerating Responses After Fine-Tuning\nAt inference time, I formatted only the instruction and optional input. I did not include the expected answer. The fine-tuned model then generated the continuation, and I extracted the content after the\n### Response:\nmarker when it appeared.\nIt was also important to call\nmodel.eval()\nbefore generation. Evaluation mode disables training-time behavior such as dropout. Forgetting this step can introduce unnecessary randomness into the generated responses.\n# Extract and save generated responses\n\ntorch.manual_seed(123)\nmodel.eval()\n\nfor entry in test_data[:3]:\n    input_text = format_input(entry)\n\n    token_ids = generate(\n        model=model,\n        idx=text_to_token_ids(input_text, tokenizer),\n        max_new_tokens=256,\n        context_size=BASE_CONFIG[\"context_length\"],\n        eos_id=50256\n    )\n\n    generated_text = token_ids_to_text(token_ids, tokenizer)\n    response_text = generated_text[len(input_text):].replace(\n        \"### Response:\", \"\"\n    ).strip()\n\n    print(input_text)\n    print(f\"Reference response: {entry['output']}\")\n    print(f\"\\nModel response: {response_text}\")\nA few generated examples looked like this:\nInstruction:\nRewrite the sentence using a simile.\n\nInput:\nThe car is very fast.\n\nReference response:\nThe car is as fast as lightning.\n\nModel response:\nThe car is as fast as a bullet.\nInstruction:\nWhat type of cloud is typically associated with thunderstorms?\n\nReference response:\nThe type of cloud typically associated with thunderstorms is cumulonimbus.\n\nModel response:\nA thunderstorm is a type of storm that typically produces a dense, fast-moving, and intense thunderstorm.\nInstruction:\nName the author of 'Pride and Prejudice'.\n\nReference response:\nJane Austen.\n\nModel response:\nThe author of 'Pride and Prejudice' is Jane Austen.\nThe first and third responses are good, even though they do not exactly match the reference wording. The second response is fluent but incorrect.\nThis is where evaluation becomes more complicated.\nWhy Accuracy Is No Longer Enough\nFor spam classification, evaluation was straightforward:\nExpected label: spam\nPredicted label: spam\nResult: correct\nInstruction following does not have a small, fixed set of labels. It produces open-ended text, and multiple answers may be equally valid:\nThe capital of France is Paris.\nParis is the capital of France.\nFrance's capital city is Paris.\nA direct string comparison would treat these as different even though they communicate the same answer.\nI still had a finite test dataset. What changed was the output space: instead of two labels, the model could generate a huge number of possible responses, and there was not always one exact string that should be considered correct.\nTraining and validation loss were still useful, but they did not fully capture whether a generated response was correct, relevant, and useful. I needed a different evaluation approach.\nUsing an LLM as a Judge\nI used another LLM to evaluate the generated responses.\nFor each evaluation example, the judge received:\nThe original instruction.\nThe optional input.\nThe reference response.\nThe response generated by my fine-tuned model.\nIt was then asked to score how well the generated answer satisfied the instruction.\nA simplified evaluation prompt looked like this:\nInstruction:\nConvert 45 kilometers to meters.\n\nReference response:\n45 kilometers is equal to 45,000 meters.\n\nModel response:\n45 km equals 45,000 meters.\n\nEvaluate the model response for correctness and relevance.\nReturn a score between 0 and 100.\nThis evaluates meaning rather than exact wording. The judge can often recognize that a generated answer is correct even when it does not exactly match the reference response.\nThe evaluation pipeline became:\nInstruction dataset\n        ↓\nFine-tuned GPT-2 Medium\n        ↓\nGenerated responses\n        ↓\nJudge LLM\n        ↓\nQuality scores\nThis was the first point in the project where model evaluation became a separate AI workflow of its own.\nRunning the Judge Locally with Ollama\nI used Ollama to run the judge model locally. Ollama provides a local API through which a Python function can send an evaluation prompt and receive the judge model’s response.\nThis allowed me to automate scoring without sending every example to a hosted API or manually reviewing the entire test set.\nFirst, I checked whether the local Ollama endpoint was available:\ndef check_if_running(process_name):\n    if process_name.lower() == \"ollama\":\n        try:\n            response = requests.get(\n                \"http://127.0.0.1:11434/api/tags\",\n                timeout=5\n            )\n            return response.status_code == 200\n        except requests.exceptions.RequestException:\n            return False\n\n    return False\n\n\nollama_running = check_if_running(\"ollama\")\n\nif not ollama_running:\n    print(\"Ollama is not running.\")\n\nprint(\"ollama_running:\", ollama_running)\nI then passed the reference and model responses to the judge:\nfor entry in test_data[:3]:\n    prompt = (\n        f\"Given the input: {format_input(entry)}\"\n        f\"and correct response: {entry['output']}\"\n        f\"scores the model response: {entry['model_response']}\"\n        f\"on a scale of 0 to 100, where 100 is best score\"\n    )\n\n    print(\"\\nDataset response:\")\n    print(\">>\", entry[\"output\"])\n    print(\"\\nModel response:\")\n    print(\">>\", entry[\"model_response\"])\n    print(\"\\nScore\")\n    print(\">>\", query_model(prompt, model=model))\n    print(\"-\" * 50)\nFor the car example, the reference response and model response were:\nDataset response:\nThe car is as fast as lightning.\n\nModel response:\nThe car is as fast as a bullet.\nThe judge returned:\nHere's my attempt:\n\nModel Response: The car is as fast as a bullet.\n\nScore: 90\n\nI think this simile effectively conveys the idea that the car is extremely quick, and \"a bullet\" is a common reference point for speed. It's not too obvious or cliché, but still gets the point across.\nThis is exactly where an LLM judge becomes useful. A direct comparison would mark the model response as different from the reference answer. The judge instead recognized that\n“as fast as a bullet”\nis also a valid simile and gave it a high score.\nFinally, I ran the evaluation across all 110 test examples:\nscores = generate_model_scores(test_data, \"model_response\")\n\nprint(f\"Number of scores generated: {len(scores)} of {len(test_data)}\")\nprint(f\"Average score: {sum(scores) / len(scores):.2f}\")\nThe output was:\nScoring entries: 100%|██████████| 110/110 [07:35<00:00, 4.14s/it]\n\nNumber of scores generated: 106 of 110\nAverage score: 74.65\nFour responses could not be converted into integers because the judge returned additional text or formatting alongside the score. For example:\ncould not convert score to integer:\n\nThe car is as fast as a bullet.\n\nScore: 95\nAnother response was formatted as:\ncould not convert score to integer:\n\n**Score: 95**\nThis exposed another practical challenge with LLM-based workflows: even when a model is asked to return a number, it may not follow the requested output format consistently.\nAs a result, the evaluator prompt and output parser are also part of the evaluation system. A stricter prompt, structured output format, or more flexible parser could improve the number of successfully processed scores.\nThe average score of\n74.65\nwas calculated across the 106 responses that were successfully parsed.\nConclusion\nInstruction fine-tuning moved the model closer to assistant-like behavior. The underlying GPT architecture and next-token prediction objective remained the same, but the training examples taught the model a new interaction pattern.\nAn LLM judge was useful, but its scores depended on the judge model, prompt, scoring scale, and its own limitations. A balanced evaluation should therefore combine:\nTraining and validation loss.\nManual review of generated responses.\nLLM-based scoring.\nLoss measures token prediction, manual review reveals visible failure modes, and an LLM judge provides a scalable estimate of semantic response quality.\nWith this step, the model had progressed from generating text, to performing classification, and finally to following natural-language instructions.\nReferences and Further Reading\nSebastian Raschka,\nBuild a Large Language Model (From Scratch)\n— the primary source for this project and the instruction-fine-tuning workflow discussed in this article\nAlec Radford et al.,\nLanguage Models are Unsupervised Multitask Learners\n— OpenAI’s original technical report introducing GPT-2.\nOpenAI,\nGPT-2: Six-Month Follow-Up\n— information about the staged releases of the 124M, 355M, and 774M GPT-2 models.\nOllama,\nLlama 3 Model Library\nand\nAPI Documentation\n— instructions for running Llama 3 locally and interacting with it through Ollama’s local API.\nLianmin Zheng et al.,\nJudging LLM-as-a-Judge with MT-Bench and Chatbot Arena\n— research on using strong language models to evaluate open-ended model responses, including the benefits and limitations of this approach.\n25\nviews",
  "metadata": {
    "title": "Fine-Tuning GPT-2 to Follow Instructions",
    "description": "Learn how I formatted instruction data, fine-tuned GPT-2 Medium, and used a local LLM through Ollama to evaluate open-ended responses.",
    "keywords": null,
    "author": null,
    "og:title": "Fine-Tuning GPT-2 to Follow Instructions",
    "og:description": "Learn how I formatted instruction data, fine-tuned GPT-2 Medium, and used a local LLM through Ollama to evaluate open-ended responses.",
    "og:url": "https://curious-pm.hashnode.dev/fine-tuning-gpt-to-follow-instructions",
    "og:site_name": "Curious PM - Product to Parameters",
    "og:image": "https://cdn.hashnode.com/uploads/covers/6a4f47d631bb27227f782535/839e1472-8523-4035-9837-6347ac7db873.png",
    "og:image:width": "1200",
    "og:image:height": "630",
    "og:type": "article",
    "twitter:card": "summary_large_image",
    "twitter:title": "Fine-Tuning GPT-2 to Follow Instructions",
    "twitter:description": "Learn how I formatted instruction data, fine-tuned GPT-2 Medium, and used a local LLM through Ollama to evaluate open-ended responses.",
    "twitter:image": "https://cdn.hashnode.com/uploads/covers/6a4f47d631bb27227f782535/839e1472-8523-4035-9837-6347ac7db873.png",
    "article:published_time": "2026-07-16T05:41:47.075Z",
    "article:modified_time": "2026-07-16T05:41:47.075Z",
    "article:author": "https://curious-pm.hashnode.dev/@aman454"
  },
  "paywall": {
    "listed_paid": false,
    "detected": false,
    "marker": "",
    "public_only": false,
    "policy": "付费/会员内容只保留页面公开可见部分，不绕过付费墙"
  },
  "crawl": {
    "success": true,
    "status_code": 200,
    "error": ""
  },
  "code_blocks": [
    {
      "file": "attachments/bde9b4e3d0d5/code/body-000.txt",
      "chars": 33,
      "language": "text"
    },
    {
      "file": "attachments/bde9b4e3d0d5/code/body-001.txt",
      "chars": 32,
      "language": "text"
    },
    {
      "file": "attachments/bde9b4e3d0d5/code/body-002.txt",
      "chars": 159,
      "language": "text"
    },
    {
      "file": "attachments/bde9b4e3d0d5/code/body-003.txt",
      "chars": 577,
      "language": "text"
    },
    {
      "file": "attachments/bde9b4e3d0d5/code/body-004.txt",
      "chars": 251,
      "language": "text"
    },
    {
      "file": "attachments/bde9b4e3d0d5/code/body-005.txt",
      "chars": 547,
      "language": "text"
    },
    {
      "file": "attachments/bde9b4e3d0d5/code/body-006.txt",
      "chars": 741,
      "language": "text"
    },
    {
      "file": "attachments/bde9b4e3d0d5/code/body-007.txt",
      "chars": 82,
      "language": "text"
    },
    {
      "file": "attachments/bde9b4e3d0d5/code/body-008.txt",
      "chars": 541,
      "language": "text"
    },
    {
      "file": "attachments/bde9b4e3d0d5/code/body-009.txt",
      "chars": 640,
      "language": "text"
    },
    {
      "file": "attachments/bde9b4e3d0d5/code/body-010.txt",
      "chars": 183,
      "language": "text"
    },
    {
      "file": "attachments/bde9b4e3d0d5/code/body-011.txt",
      "chars": 295,
      "language": "text"
    },
    {
      "file": "attachments/bde9b4e3d0d5/code/body-012.txt",
      "chars": 158,
      "language": "text"
    },
    {
      "file": "attachments/bde9b4e3d0d5/code/body-013.txt",
      "chars": 59,
      "language": "text"
    },
    {
      "file": "attachments/bde9b4e3d0d5/code/body-014.txt",
      "chars": 96,
      "language": "text"
    },
    {
      "file": "attachments/bde9b4e3d0d5/code/body-015.txt",
      "chars": 247,
      "language": "text"
    },
    {
      "file": "attachments/bde9b4e3d0d5/code/body-016.txt",
      "chars": 129,
      "language": "text"
    },
    {
      "file": "attachments/bde9b4e3d0d5/code/body-017.txt",
      "chars": 509,
      "language": "text"
    },
    {
      "file": "attachments/bde9b4e3d0d5/code/body-018.txt",
      "chars": 503,
      "language": "text"
    },
    {
      "file": "attachments/bde9b4e3d0d5/code/body-019.txt",
      "chars": 100,
      "language": "text"
    },
    {
      "file": "attachments/bde9b4e3d0d5/code/body-020.txt",
      "chars": 281,
      "language": "text"
    },
    {
      "file": "attachments/bde9b4e3d0d5/code/body-021.txt",
      "chars": 190,
      "language": "text"
    },
    {
      "file": "attachments/bde9b4e3d0d5/code/body-022.txt",
      "chars": 127,
      "language": "text"
    },
    {
      "file": "attachments/bde9b4e3d0d5/code/body-023.txt",
      "chars": 80,
      "language": "text"
    },
    {
      "file": "attachments/bde9b4e3d0d5/code/body-024.txt",
      "chars": 51,
      "language": "text"
    }
  ],
  "images": [
    {
      "status": "ok",
      "url": "https://cdn.hashnode.com/uploads/covers/6a4f47d631bb27227f782535/839e1472-8523-4035-9837-6347ac7db873.png",
      "bytes": 1152316,
      "reused": true,
      "local_path": "attachments/bde9b4e3d0d5/images/000.png",
      "src": "https://cdn.hashnode.com/uploads/covers/6a4f47d631bb27227f782535/839e1472-8523-4035-9837-6347ac7db873.png",
      "alt": "Building a GPT Model from Scratch (Part 6): Fine-Tuning It to Follow Instructions",
      "downloaded": true
    },
    {
      "status": "ok",
      "url": "https://cdn.hashnode.com/uploads/covers/6a4f47d631bb27227f782535/0681d470-71e1-420e-98c3-d1efe2f5faab.png",
      "bytes": 23140,
      "reused": true,
      "local_path": "attachments/bde9b4e3d0d5/images/001.png",
      "src": "https://cdn.hashnode.com/uploads/covers/6a4f47d631bb27227f782535/0681d470-71e1-420e-98c3-d1efe2f5faab.png",
      "alt": "",
      "downloaded": true
    },
    {
      "status": "ok",
      "url": "https://cdn.hashnode.com/uploads/covers/6a4f47d631bb27227f782535/0106ad50-2b11-43e3-a24a-20f33524f48b.png",
      "bytes": 76313,
      "reused": true,
      "local_path": "attachments/bde9b4e3d0d5/images/002.png",
      "src": "https://cdn.hashnode.com/uploads/covers/6a4f47d631bb27227f782535/0106ad50-2b11-43e3-a24a-20f33524f48b.png",
      "alt": "",
      "downloaded": true
    }
  ],
  "audio": [],
  "videos": [],
  "links": [
    {
      "href": "https://curious-pm.hashnode.dev/fine-tuning-gpt-to-follow-instructions.md",
      "label": "View as Markdown",
      "type": "a"
    },
    {
      "href": "https://hashnode.com/@aman454",
      "label": "A",
      "type": "a"
    },
    {
      "href": "https://www.linkedin.com/in/aman-jain-1049188/",
      "label": "",
      "type": "a"
    },
    {
      "href": "https://curious-pm.hashnode.dev/series/building-gpt-from-scratch",
      "label": "Part of series Building a GPT Model From Scratch",
      "type": "a"
    },
    {
      "href": "https://sebastianraschka.com/llms-from-scratch/",
      "label": "Build a Large Language Model (From Scratch)",
      "type": "a"
    },
    {
      "href": "https://cdn.openai.com/better-language-models/language_models_are_unsupervised_multitask_learners.pdf",
      "label": "Language Models are Unsupervised Multitask Learners",
      "type": "a"
    },
    {
      "href": "https://openai.com/index/gpt-2-6-month-follow-up/",
      "label": "GPT-2: Six-Month Follow-Up",
      "type": "a"
    },
    {
      "href": "https://ollama.com/library/llama3",
      "label": "Llama 3 Model Library",
      "type": "a"
    },
    {
      "href": "https://docs.ollama.com/api/introduction",
      "label": "API Documentation",
      "type": "a"
    },
    {
      "href": "https://arxiv.org/abs/2306.05685",
      "label": "Judging LLM-as-a-Judge with MT-Bench and Chatbot Arena",
      "type": "a"
    }
  ],
  "retry_at": "2026-10-03T08:09:15",
  "repair_version": 2,
  "repaired_at": "2026-10-14T10:50:55Z",
  "source_url": "https://curious-pm.hashnode.dev/fine-tuning-gpt-to-follow-instructions",
  "papers": [
    {
      "status": "ok",
      "url": "https://cdn.openai.com/better-language-models/language_models_are_unsupervised_multitask_learners.pdf",
      "bytes": 582775,
      "reused": true,
      "local_path": "papers/repair/3c62359064baa28a7743.pdf"
    },
    {
      "status": "ok",
      "url": "https://arxiv.org/pdf/2306.05685",
      "bytes": 1580141,
      "reused": false,
      "local_path": "papers/repair/d2e940e28ba60d6a1822.pdf"
    }
  ],
  "actual_video_files": [],
  "body_chars": 17787,
  "expected_body_images": 3,
  "discovery_status": "ok",
  "delivery_status": "ok",
  "missing_reason": "",
  "raw_html": "attachments/bde9b4e3d0d5/pages/source.html",
  "local_html": "attachments/bde9b4e3d0d5/pages/article.html",
  "body_base_path": "attachments/bde9b4e3d0d5/pages/",
  "body_markdown_path": "attachments/bde9b4e3d0d5/pages/article.md"
}