DEV Community

Cover image for LLM von Grund auf bauen: Was du dabei lernst
Emre Demir
Emre Demir

Posted on • Originally published at apidog.com

LLM von Grund auf bauen: Was du dabei lernst

TL;DR

Der Bau eines minimalen Sprachmodells von Grund auf erfordert weniger als 300 Zeilen Python. Dieser Prozess enthüllt genau, wie Tokenisierung, Attention und Inferenz funktionieren, was Sie zu einem wesentlich besseren API-Konsumenten macht, wenn Sie produktive LLMs in Ihre Anwendungen integrieren.

Teste Apidog noch heute

Einleitung

Die meisten Entwickler behandeln Sprachmodelle als Black Boxes. Sie senden Text ein, Tokens kommen heraus, und irgendwo dazwischen geschieht Magie. Dieses mentale Modell funktioniert gut, bis Sie eine fehlerhafte API-Integration debuggen, Sampling-Parameter optimieren oder herausfinden müssen, warum Ihr Modell ständig strukturierte Daten halluziniert.

GuppyLM, ein Projekt, das kürzlich mit 842 Punkten die Titelseite von HackerNews erreichte, macht die Interna sichtbar. Es ist ein 8,7 Millionen Parameter starker Transformer, der von Grund auf in Python geschrieben wurde. Er trainiert in weniger als einer Stunde auf einer Consumer-GPU. Der Code passt in eine einzige Datei. Das Ziel ist nicht, mit GPT-4 zu konkurrieren, sondern zu entmystifizieren, was LLMs tatsächlich tun.

Dieser Artikel erklärt, wie man ein winziges LLM baut, was jede Komponente tut und welche Erkenntnisse Sie aus dem Verständnis der Interna gewinnen, wenn Sie beruflich mit KI-APIs arbeiten.

💡 Wenn Sie KI-API-Integrationen testen, können Sie mit den Test-Szenarien von Apidog Streaming-Antworten überprüfen, die Token-Struktur bestätigen und Edge-Case-Vervollständigungen simulieren, ohne Produktionsguthaben zu verbrauchen. Mehr dazu später.

Was macht ein Sprachmodell "winzig"?

Produktive LLMs wie GPT-4 haben Hunderte Milliarden Parameter. Winzige LLMs bewegen sich im Bereich von 1 bis 25 Millionen Parametern, z.B. GuppyLM (8,7M), nanoGPT (124M), MicroLM (1-2M).

Vorteile winziger LLMs:

  • Training auf Laptop/Colab möglich
  • Passen komplett in den CPU-Speicher
  • Gewichte können einfach inspiziert und modifiziert werden

Limitationen:

  • Keine komplexen Schlussfolgerungen
  • Kein langer, kohärenter Text
  • Geringere faktische Tiefe

Der eigentliche Wert liegt im Verständnis des Modells, nicht im Output.

Kernkomponenten: Wie ein LLM tatsächlich funktioniert

Bevor Sie eigenen Code schreiben, kennen Sie die vier Hauptbestandteile:

Tokenizer

Der Tokenizer wandelt Rohtext in Ganzzahl-IDs um. Beispiel: "Hallo, Welt!" → [15496, 11, 995, 0]. Jeder Wert steht für ein Subwort aus dem Vokabular.

Praxis: Die Anzahl der Tokens beeinflusst Kosten und Latenz Ihrer API-Nutzung. Verstehen Sie, wie Tokenizer Text aufteilen, um Prompts effizient zu schreiben.

GuppyLM nutzt einen simplen Zeichen-basierten Tokenizer. Produktionsmodelle wie GPT-4 setzen auf BPE mit 50.000+ Tokens.

Einbettungsschicht

Token-IDs werden in Vektoren transformiert (z.B. 384 Dimensionen). Ähnliche Tokens liegen im Vektorraum nah beieinander. Positions-Einbettungen codieren die Reihenfolge.

Transformer-Blöcke

Jeder Block enthält:

  • Self-Attention: Tokens berücksichtigen frühere Tokens für die nächste Vorhersage. GuppyLM nutzt 6 Attention-Köpfe, 6 Schichten.
  • Feed-Forward-Netzwerk: Zweischichtiges MLP pro Token. GuppyLM verwendet ReLU, neuere Modelle oft SwiGLU.

Ausgabekopf

Nach dem letzten Block projiziert eine lineare Schicht auf die Vokabulargröße. Softmax liefert Wahrscheinlichkeiten, dann wird das nächste Token ausgewählt.

Ein minimales LLM in Python bauen

Hier ein minimal lauffähiges LLM nach GuppyLM-Vorbild. Läuft mit PyTorch.

import torch
import torch.nn as nn
import torch.nn.functional as F

# Hyperparameter
VOCAB_SIZE = 256     # Zeichen-basiert: ein Platz pro ASCII-Zeichen
D_MODEL = 128        # Einbettungsdimension
N_HEADS = 4          # Attention-Köpfe
N_LAYERS = 3         # Transformer-Blöcke
SEQ_LEN = 64         # Kontextfenster
DROPOUT = 0.1

class SelfAttention(nn.Module):
    def __init__(self, d_model, n_heads):
        super().__init__()
        self.n_heads = n_heads
        self.head_dim = d_model // n_heads
        self.qkv = nn.Linear(d_model, 3 * d_model, bias=False)
        self.proj = nn.Linear(d_model, d_model, bias=False)
        self.dropout = nn.Dropout(DROPOUT)

    def forward(self, x):
        B, T, C = x.shape
        qkv = self.qkv(x).reshape(B, T, 3, self.n_heads, self.head_dim)
        q, k, v = qkv.unbind(dim=2)
        q = q.transpose(1, 2)
        k = k.transpose(1, 2)
        v = v.transpose(1, 2)
        # Kausale Maske: Jedes Token kann nur frühere Tokens berücksichtigen
        scale = self.head_dim ** -0.5
        attn = (q @ k.transpose(-2, -1)) * scale
        mask = torch.triu(torch.ones(T, T, device=x.device), diagonal=1).bool()
        attn = attn.masked_fill(mask, float('-inf'))
        attn = F.softmax(attn, dim=-1)
        attn = self.dropout(attn)
        out = (attn @ v).transpose(1, 2).reshape(B, T, C)
        return self.proj(out)

class TransformerBlock(nn.Module):
    def __init__(self, d_model, n_heads):
        super().__init__()
        self.attn = SelfAttention(d_model, n_heads)
        self.ff = nn.Sequential(
            nn.Linear(d_model, 4 * d_model),
            nn.ReLU(),
            nn.Linear(4 * d_model, d_model),
            nn.Dropout(DROPOUT),
        )
        self.ln1 = nn.LayerNorm(d_model)
        self.ln2 = nn.LayerNorm(d_model)

    def forward(self, x):
        x = x + self.attn(self.ln1(x))
        x = x + self.ff(self.ln2(x))
        return x

class TinyLLM(nn.Module):
    def __init__(self):
        super().__init__()
        self.embed = nn.Embedding(VOCAB_SIZE, D_MODEL)
        self.pos_embed = nn.Embedding(SEQ_LEN, D_MODEL)
        self.blocks = nn.ModuleList([
            TransformerBlock(D_MODEL, N_HEADS) for _ in range(N_LAYERS)
        ])
        self.ln_f = nn.LayerNorm(D_MODEL)
        self.head = nn.Linear(D_MODEL, VOCAB_SIZE, bias=False)

    def forward(self, idx):
        B, T = idx.shape
        tok_emb = self.embed(idx)
        pos = torch.arange(T, device=idx.device)
        pos_emb = self.pos_embed(pos)
        x = tok_emb + pos_emb
        for block in self.blocks:
            x = block(x)
        x = self.ln_f(x)
        logits = self.head(x)
        return logits

# Modell initialisieren und Parameter zählen
model = TinyLLM()
total_params = sum(p.numel() for p in model.parameters())
print(f"Modellgröße: {total_params:,} Parameter")  # ~1.2M
Enter fullscreen mode Exit fullscreen mode

Trainingsschleife

import torch.optim as optim

def train(model, data, epochs=100, lr=3e-4):
    optimizer = optim.AdamW(model.parameters(), lr=lr)
    model.train()
    for epoch in range(epochs):
        # data: Tensor von Token-IDs, Shape [Batch, Seq_len+1]
        x = data[:, :-1]   # Eingabe: alle Tokens außer dem letzten
        y = data[:, 1:]    # Ziel: alle Tokens um 1 verschoben
        logits = model(x)
        loss = F.cross_entropy(logits.reshape(-1, VOCAB_SIZE), y.reshape(-1))
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        if epoch % 10 == 0:
            print(f"Epoche {epoch}, Verlust: {loss.item():.4f}")
Enter fullscreen mode Exit fullscreen mode

Inferenz (Texterzeugung)

@torch.no_grad()
def generate(model, prompt_ids, max_new_tokens=50, temperature=1.0, top_k=10):
    model.eval()
    ids = torch.tensor([prompt_ids])
    for _ in range(max_new_tokens):
        idx_cond = ids[:, -SEQ_LEN:]  # Auf Kontextfenster kürzen
        logits = model(idx_cond)
        logits = logits[:, -1, :] / temperature  # Nur letztes Token
        # Top-k Sampling
        v, _ = torch.topk(logits, min(top_k, logits.size(-1)))
        logits[logits < v[:, [-1]]] = float('-inf')
        probs = F.softmax(logits, dim=-1)
        next_id = torch.multinomial(probs, num_samples=1)
        ids = torch.cat([ids, next_id], dim=1)
    return ids[0].tolist()
Enter fullscreen mode Exit fullscreen mode

Was dies über das Verhalten von KI-APIs lehrt

Das Nachbauen eines LLMs bringt praktische Erkenntnisse für API-Konsumenten:

Temperatur und Sampling sind mechanisch, nicht magisch

Die Temperatur teilt die Logits vor Softmax. Höhere Temperatur = flachere Verteilung = mehr Zufall. Geringe Temperatur = deterministisch. temperature=0.0 ist ein gieriges Argmax – viele APIs setzen sie bewusst leicht über Null.

Kontextfenster sind harte Grenzen

Die Zeile idx_cond = ids[:, -SEQ_LEN:] zeigt: Das Modell verwirft ältere Tokens. Ihre API merkt sich den Gesprächsverlauf nicht unendlich. Siehe [intern: how-ai-agent-memory-works] für Lösungen.

Streaming-Tokens sind sichtbare Inferenzschritte

Streaming-APIs senden jedes Token direkt nach der Generierung. Ein unterbrochener Stream kann nicht fortgesetzt werden, sondern muss neu starten.

Logits erklären strukturierte Ausgabe

Das Modell vergibt pro Schritt Wahrscheinlichkeiten für jedes Token. Gültiges JSON zu generieren bedeutet, dass an jedem Schritt das richtige Token "gewinnt". Tools wie Outlines/Guidance erzwingen Grammatik durch Einschränkung der Logits. "Strukturierte Ausgabe"-Modi in APIs machen intern genau das.

Wie man KI-API-Integrationen mit Apidog testet

Mit dem Verständnis der LLM-Inferenz können Sie gezielt KI-API-Tests gestalten. Apidog (apidog.com) erlaubt, Test-Szenarien zu erstellen, API-Aufrufe zu verketten und KI-Antworten zu validieren.

Praxis-Beispiel: Streaming-Chat-API testen

  1. Legen Sie ein Test-Szenario in Apidog für Ihren /v1/chat/completions-Endpunkt an.
  2. Definieren Sie Assertions wie:
    • response.choices[0].finish_reason == "stop"
    • response.usage.total_tokens < 4096
  3. Senden Sie die Antwort als Kontext in einen Folgeschritt, um Multi-Runden-Konversation zu simulieren.
  4. Nutzen Sie Apidogs Smart Mock, um Fehlerfälle zu testen:
    • Simulieren Sie finish_reason: "length",
    • finish_reason: "content_filter",
    • Netzwerk-Timeout im Stream.

So testen Sie KI-Integrationen, ohne bei jedem CI-Lauf API-Guthaben zu verbrauchen. Siehe [intern: api-testing-tutorial] für umfassendere Testansätze.

Testen von Token-Zählungs-Assertions

{
  "assertions": [
    {
      "field": "response.usage.completion_tokens",
      "operator": "less_than",
      "value": 512
    },
    {
      "field": "response.choices[0].finish_reason",
      "operator": "equals",
      "value": "stop"
    },
    {
      "field": "response.choices[0].message.content",
      "operator": "not_empty"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Führen Sie dieses Szenario über mehrere Modelle (GPT-4o, Claude 3.5 Sonnet, Gemini 1.5 Pro) aus, um API-Schema-Unterschiede frühzeitig zu erkennen.

Fortgeschritten: Quantisierung und Inferenzoptimierung

Nach dem Bau eines kleinen LLMs sind folgende Techniken direkt praxisrelevant:

Quantisierung

Die Modellgewichte liegen standardmäßig als 32-Bit-Floats vor. Quantisierung reduziert auf z.B. INT8 oder INT4. So lässt sich der Speicherbedarf um das 4- bis 8-fache senken – mit minimalem Genauigkeitsverlust.

# Beispiel: dynamische INT8-Quantisierung in PyTorch
import torch.quantization
quantized_model = torch.quantization.quantize_dynamic(
    model, {nn.Linear}, dtype=torch.qint8
)
Enter fullscreen mode Exit fullscreen mode

Viele Produktions-APIs nutzen quantisierte Modelle. Unterschiedliche Ausgabequalitäten verschiedener Modell-"Versionen" können oft auf Quantisierung zurückgeführt werden.

KV-Cache

In der Inferenzschleife wird die Attention bei jedem Schritt neu berechnet. Produktionssysteme cachen die Key-Value-Paare (KV-Cache), sodass jedes neue Token effizienter generiert werden kann. Das erste Token in einer Streaming-Antwort dauert daher länger als die folgenden.

Winziges LLM vs. Produktions-API: Wann man welches verwendet

Anwendungsfall Winziges LLM Produktions-API
Modell-Interna lernen Am besten geeignet Überdimensioniert
Prototyping einer neuen App Unzureichende Qualität Am besten geeignet
Private/sensible Daten Gute Option Abhängig vom Anbieter
Offline/Edge-Bereitstellung Machbar Nicht möglich
Kostenempfindlich, hohes Volumen Möglich mit Kompromissen Teuer im großen Maßstab
Aufwändige Denkaufgaben Nicht machbar Erforderlich

Die Empfehlung: Nutzen Sie die Produktions-API für echte Anwendungen, bauen Sie ein winziges Modell, um die Funktionsweise zu verstehen. Beide Ansätze ergänzen sich. [intern: open-source-coding-assistants-2026] zeigt, wie BYOM-Setups die Grenze aufweichen.

Fazit

Ein winziges LLM ist an einem Wochenende gebaut. Es ist kein Produktionssystem, aber vermittelt ein präzises mentales Modell – von GuppyLM bis GPT-4o. Dieses Wissen hilft bei jedem Debugging, Sampling-Tuning oder API-Test.

Das GuppyLM-Projekt ist ein idealer Startpunkt. Klonen, mit beliebigen Texten trainieren, Inferenzschleife nachvollziehen – und mit geschärftem Verständnis zurück zu Ihren API-Integrationen gehen.

Nutzen Sie die Test-Szenarien von Apidog (apidog.com), um KI-APIs so gründlich zu testen wie jedes andere Backend-System.

FAQ

Wie viele Parameter benötigt ein "winziges" LLM, um kohärenten Text zu generieren?

Etwa 10 bis 50 Millionen Parameter mit einem anständigen Trainingsdatensatz können lokal kohärente Sätze produzieren. Unter 1 Million erhält man bei den meisten Aufgaben Kauderwelsch. GuppyLM mit 8,7 Millionen funktioniert für kurze Unterhaltungen in seinem Trainingsbereich (60 Themen).

Kann ich ein winziges LLM ohne GPU ausführen?

Ja. Modelle unter 100 Millionen Parametern laufen gut auf der CPU, obwohl die Inferenz langsamer ist. Das obige Modell (1,2 Millionen Parameter) generiert Tokens in Millisekunden auf einer Laptop-CPU.

Welchen Datensatz soll ich zum Training verwenden?

Zeichen-basierte Modelle funktionieren gut mit Texten von Project Gutenberg, Wikipedia-Teilmengen oder jedem einfachen Textkorpus. GuppyLM verwendet einen Konversationsdatensatz mit 60.000 Einträgen auf HuggingFace (arman-bd/guppylm-60k-generic). Für die Codegenerierung verwenden Sie The Stack oder CodeParrot.

Was ist der Unterschied zwischen Temperatur und Top-K-Sampling?

Die Temperatur skaliert die Logit-Verteilung (kontrolliert die Gesamtzufälligkeit). Top-K beschränkt den Sampling-Pool auf die k wahrscheinlichsten Tokens, bevor die Temperatur angewendet wird. Sie werden zusammen angewendet: Zuerst filtert Top-K die Kandidaten, dann formt die Temperatur die Wahrscheinlichkeiten innerhalb dieser Menge.

Warum wiederholt sich mein LLM manchmal?

Wiederholung ist ein Fehlermodus, bei dem das Modell Tokens, die es gerade generiert hat, eine hohe Wahrscheinlichkeit zuweist, weil sie im Kontext erschienen sind. Produktions-APIs verwenden Wiederholungsstrafen (eine Logit-Anpassung, die kürzlich generierte Tokens abwertet). Fügen Sie repetition_penalty=1.1 in Ihren API-Aufruf ein, um dies zu reduzieren.

Wie lange dauert es, ein winziges LLM zu trainieren?

Das obige Modell trainiert in weniger als 2 Stunden auf einer einzelnen GPU (RTX 3060 oder gleichwertig) zu kohärenter Ausgabe. GuppyLM trainiert in Colab in etwa der gleichen Zeit. Größere Modelle (100 Millionen+) benötigen Multi-GPU-Setups und Tage des Trainings.

Was ist der schnellste Weg, um von einem winzigen LLM zu einem echten API-Endpunkt zu gelangen?

Exportieren Sie in das GGUF-Format mit dem Konvertierungsskript von llama.cpp und stellen Sie es dann mit llama-server bereit. Dies gibt Ihnen einen OpenAI-kompatiblen API-Endpunkt, der lokal läuft. Sie können dann Apidog darauf zeigen, um ihn zu testen, siehe [intern: rest-api-best-practices].

Wie gehen Produktions-LLMs mit Kontext um, der länger ist als ihr Trainingsfenster?

Techniken wie RoPE (Rotary Position Embedding) mit erweiterter Skalierung, Sliding-Window-Attention und Retrieval-Augmented Generation erweitern alle den effektiven Kontext. Die Kern-Transformer-Architektur ändert sich nicht; dies sind Modifikationen der Art und Weise, wie Positionsinformationen kodiert und das Aufmerksamkeitsfenster angewendet wird.

Top comments (0)