๐Ÿค–

How I Built a Custom LLM
for CS Education with Blix.ai

A deep dive into training, fine-tuning, and deploying a domain-specific language model for computer science subjects โ€” from dataset curation to self-hosted inference on a budget.

Sayan Dutta
Sayan Dutta
Full-Stack Developer & AI Enthusiast ยท IIT Patna

Creator of Blix.ai โ€” a privacy-first, self-hosted AI tutor for CS students, currently in closed beta with IIT Patna classmates. This is the complete technical story of how it was built, what failed, and what actually works.


Every CS student has been there: it's 1am, you're staring at a question about semaphore implementation, your professor is unreachable, Stack Overflow gives you five contradictory answers, and the textbook explanation assumes you already understand the thing it's supposed to be explaining.

That gap โ€” between needing a clear, contextual answer and getting one โ€” is what Blix.ai exists to close. Not by wrapping GPT-4 with a system prompt (though that's a valid approach), but by training a model specifically on the subjects CS students actually struggle with: Operating Systems, DBMS, Data Structures & Algorithms, Computer Networks, AI/ML fundamentals, and more.

This post is the complete technical account of how that happened โ€” what I built, what broke, what I'd do differently, and why the hardest problem wasn't the model at all.

6CS subjects covered
50k+Training samples
FreeGoal for public launch

Why Not Just Use GPT-4?

This is always the first question. And it's a fair one โ€” frontier models are excellent and getting better. But there are three reasons a fine-tuned domain-specific model made sense for this project:

โ„น๏ธ
Honest caveat: For truly open-ended reasoning or code generation across arbitrary languages, frontier models still outperform fine-tuned smaller models. Blix.ai is designed specifically for the Q&A and conceptual explanation tasks where domain depth matters most โ€” not as a general replacement for GPT-4.

The Build: Four Phases

01
Dataset Curation
Scrape, clean, structure, and quality-filter Q&A pairs across six CS subjects. The most time-consuming phase and the most important one.
~3 weeks
02
Base Model Selection & Fine-tuning
Choose a quantised base model, configure LoRA adapters, run supervised fine-tuning on a combination of Colab TPUs and a rented A100 instance.
~2 weeks
03
Alignment & Preference Training
Collect preference pairs โ€” correct vs plausible-but-wrong answers. Apply DPO to reduce confident hallucination, the hardest problem in the project.
~3 weeks
04
Inference Server & Deployment
Build a FastAPI streaming server, containerise with Docker, deploy on a VPS. Wire up the React frontend and a Redis response cache.
~2 weeks

Phase 1: Dataset Curation

The quality of a fine-tuned model is almost entirely determined by the quality of its training data. I spent more time on this phase than any other, and I'd spend even more if I were doing it again.

Sources

The dataset was assembled from five categories of sources:

Source typeContentVolumeQuality filter
TextbooksTanenbaum OS, Silberschatz DBMS, CLRS, Forouzan Networks~8k pairsManual review
Lecture notesIIT NPTEL transcripts, MIT OCW, Stanford CS229~12k pairsPerplexity filter
Stack OverflowCS-tagged, score โ‰ฅ 10, accepted answers only~18k pairsScore + length
Past exam Q&AGATE CS, university end-sems, IIT internal exams~7k pairsAnswer verified
SyntheticGPT-4 generated then human-verified edge cases~5k pairsHuman review

Total after deduplication and quality filtering: ~50,000 instruction-response pairs across six subjects, formatted as {"instruction": "...", "response": "..."} with a subject tag on each sample.

The Cleaning Pipeline

python โ€” quality filter pipeline
def is_quality_sample(sample: dict) -> bool:
    instr  = sample["instruction"]
    resp   = sample["response"]

    # length gates
    if len(instr.split()) < 8:   return False  # too vague
    if len(resp.split())  < 30:  return False  # too short to be useful
    if len(resp.split())  > 800: return False  # likely padded / off-topic

    # reject samples with uncertainty markers
    uncertainty = ["i'm not sure", "i think", "maybe",
                   "i believe", "not certain"]
    if any(p in resp.lower() for p in uncertainty):
        return False

    # reject if response doesn't address the instruction topic
    instr_keywords = extract_keywords(instr)
    overlap = keyword_overlap(instr_keywords, resp)
    if overlap < 0.15: return False

    return True
โš ๏ธ
The deduplication problem: Many CS Q&A sources repeat the same questions with slightly rephrased answers. Exact dedup using hashing isn't enough โ€” I used MinHash LSH to catch near-duplicates within an 80% similarity threshold. Without this, the model memorises paraphrases instead of learning concepts.

Phase 2: Base Model & Fine-Tuning

Choosing the Base Model

The constraints were clear: the model needed to run on a single consumer-grade VPS (4 vCPU, 16GB RAM, no GPU) at inference time. That ruled out anything above 7B parameters without aggressive quantisation.

I evaluated three candidates โ€” a 7B Llama-2 variant, Mistral 7B, and Phi-2 (2.7B). Mistral 7B with 4-bit GPTQ quantisation hit the best balance of response quality, instruction following, and inference speed (~2.1 tokens/sec on the VPS, which streams acceptably).

LoRA Fine-Tuning Setup

Full fine-tuning a 7B model was out of the question on available compute. LoRA (Low-Rank Adaptation) lets you fine-tune a fraction of the parameters โ€” typically 1โ€“3% โ€” while keeping the rest frozen. The trainable adapter weights are tiny (~50MB) and can be merged back into the base model after training.

python โ€” LoRA configuration
from peft import LoraConfig, get_peft_model

lora_config = LoraConfig(
    r=16,                    # rank โ€” higher = more capacity
    lora_alpha=32,           # scaling factor
    target_modules=[         # which layers to adapt
        "q_proj", "v_proj",
        "k_proj", "o_proj"
    ],
    lora_dropout=0.05,
    bias="none",
    task_type="CAUSAL_LM"
)

model = get_peft_model(base_model, lora_config)
model.print_trainable_parameters()
# trainable params: 41,943,040 / 7,241,748,480 โ€” 0.58%

Training ran on Google Colab's free TPU tier for initial experiments, then on a rented A100 instance (via Vast.ai) for the full run. Total compute cost for the full fine-tuning: approximately $18.

Phase 3: The Hardest Problem โ€” Alignment

Fine-tuning on Q&A pairs improved the model's CS knowledge substantially. But it introduced a new problem that was worse than ignorance: confident wrongness.

A fine-tuned model that has seen many OS questions will, when faced with an unfamiliar edge case, generate a fluent, confident, textbook-sounding answer โ€” that is completely wrong. A student trusting that answer is worse off than a student who got no answer at all.

"The goal wasn't a model that always answered. It was a model that knew when not to."

โ€” the core alignment challenge in Blix.ai

DPO: Teaching the Model to Prefer Honesty

I used Direct Preference Optimisation (DPO) โ€” a simpler alternative to RLHF โ€” to teach the model to prefer correct, hedged answers over confident wrong ones. DPO requires preference pairs: for each prompt, a "chosen" response and a "rejected" response.

json โ€” DPO preference pair example
{
  "prompt": "Can a deadlock occur with only one process?",

  "chosen": "No, a deadlock requires at least two processes
    competing for resources held by each other.
    A single process can block waiting for a resource
    it holds itself (self-deadlock in some contexts),
    but the classical four Coffman conditions require
    multiple processes.",

  "rejected": "Yes, a deadlock can occur with a single process
    if it requests a resource it already holds and
    the system does not support recursive locking.
    This is the most common form of deadlock in
    single-threaded applications."
}

I collected ~4,000 preference pairs โ€” roughly half from cases where the SFT model gave confident wrong answers (I verified these manually against textbooks), and half synthetically generated with GPT-4 and verified.

๐Ÿ’ก
Key insight from DPO training: The rejected responses didn't need to be dramatically wrong โ€” subtle errors were more valuable. Teaching the model to prefer precise answers over plausible-but-imprecise ones had a bigger impact on real-world quality than filtering out completely fabricated answers.

Phase 4: Inference Server & Architecture

The model doesn't help anyone sitting in a notebook. The beta testing architecture currently serving Blix.ai to a closed group of IIT Patna students looks like this:

client
React Frontend
Mobile Browser
โ†“ HTTPS + SSE streaming
FastAPI Inference Server
server-side layers
Redis Cache
RAG Retriever
Vector Store
โ†“
Mistral 7B โ€” GPTQ 4-bit

Streaming with Server-Sent Events

Waiting 8โ€“12 seconds for a full response is unusable. Streaming tokens as they're generated makes the same latency feel fast. The FastAPI endpoint uses Python generators and SSE to push tokens to the browser in real time:

python โ€” streaming inference endpoint
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
import asyncio

app = FastAPI()

async def token_stream(prompt: str):
    # Check Redis cache first
    cached = await redis.get(cache_key(prompt))
    if cached:
        yield f"data: {cached}\n\n"
        return

    full_response = []
    # Stream tokens from the quantised model
    for token in model.generate_stream(prompt):
        full_response.append(token)
        yield f"data: {token}\n\n"
        await asyncio.sleep(0)   # yield to event loop

    # Cache the full response async
    await redis.setex(cache_key(prompt),
                     3600,
                     "".join(full_response))

@app.post("/ask")
async def ask(req: AskRequest):
    return StreamingResponse(
        token_stream(req.prompt),
        media_type="text/event-stream"
    )

RAG for Grounding

For complex multi-step questions โ€” like tracing through a specific scheduling algorithm with a given process table โ€” the fine-tuned model alone sometimes drifted. I added a lightweight RAG layer: a FAISS vector store containing chunked sections from the core textbooks. For queries above a complexity threshold, relevant chunks are retrieved and prepended to the prompt as context.

This combination โ€” fine-tuned domain knowledge plus retrieval-augmented grounding for edge cases โ€” reduced the confident hallucination rate from approximately 12% in the SFT-only model to under 3% in beta testing.

The Challenges I Didn't Expect

๐ŸŒก๏ธ
Temperature calibration
Too high and the model invents plausible-sounding nonsense. Too low and it repeats the question back in different words.
0.35 for factual Q&A, 0.6 for "explain intuitively" prompts. Separate configs per question type.
๐Ÿ”„
Repetition loops
The model occasionally got stuck repeating a phrase or sentence fragment 20+ times before the context window ran out.
Repetition penalty of 1.15 in the generation config. Sliding window check to kill generation if the last 50 tokens repeat.
๐Ÿ“
Response length variance
Some questions got 4-word answers. Others got 800-word essays. Inconsistency made the UI feel broken.
Min/max token constraints per question category, plus a post-generation length classifier that flags suspiciously short responses for re-generation.
๐Ÿ’พ
Memory under concurrent load
The 4-bit quantised model loads into ~4.5GB VRAM equivalent in CPU RAM. Two concurrent requests caused OOM crashes on the 16GB VPS.
Request queue with max concurrency of 1. Subsequent requests wait โ€” median wait time is under 3 seconds given the cache hit rate.

What the Numbers Look Like Now

After six months of running Blix.ai in closed beta with a group of IIT Patna classmates, here's where things stand:

๐Ÿ’ก
The biggest lesson: The difference between a usable educational LLM and an unusable one isn't model size or training compute. It's alignment โ€” specifically, teaching the model that "I'm not certain about this" is a valid and valuable response. That one change, implemented through DPO, had more real-world impact than doubling the training data.

What's Next: Blix.ai v2

Version 2 is currently in development, with a public launch planned for February 2027. The main improvements planned for v2:

The code for Blix.ai โ€” the data pipeline, fine-tuning scripts, inference server, and frontend โ€” is open-source on GitHub. If you're building something similar for another domain, it's a reasonable starting point. Contributions and issue reports are welcome.

"Quality Education for All" โ€” UN SDG4. That's the north star. A CS student in a tier-3 city with no TA access should have the same quality of conceptual help as someone at a well-resourced institution. Blix.ai is one small step toward that.
Sayan Dutta
Sayan Dutta
Full-Stack Developer & AI Enthusiast ยท IIT Patna
GitHub Get in touch