LLM von Grund auf bauen: Was du dabei lernst

Ashley Innocent

Ashley Innocent

7 April 2026

LLM von Grund auf bauen: Was du dabei lernst

Apidog für Unternehmen

On-Premises-Bereitstellung

SSO & RBAC

SOC 2 konform

Apidog Enterprise entdecken

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.

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.
button

Was macht ein Sprachmodell "winzig"?

Ein produktives LLM wie GPT-4 hat Hunderte Milliarden Parameter. Ein "winziges" LLM liegt im Bereich von 1 Million bis 25 Millionen Parametern. Projekte wie GuppyLM (8,7 Millionen), Karpathys nanoGPT (124 Millionen) und MicroLM (1-2 Millionen) fallen alle in diese Kategorie.

Winzige LLMs können:

Sie können nicht:

Der Wert liegt nicht im Ergebnis. Er liegt im Verständnis, das Sie durch den Bau eines solchen Modells gewinnen.

Kernkomponenten: Wie ein LLM tatsächlich funktioniert

Bevor Sie Code schreiben, müssen Sie wissen, was die vier Hauptteile tun.

Tokenizer

Der Tokenizer wandelt Rohtext in Ganzzahl-IDs um. "Hallo, Welt!" wird zu so etwas wie [15496, 11, 995, 0]. Jede Ganzzahl entspricht einer Unterwort-Einheit aus einem festen Vokabular.

Warum das für die API-Arbeit wichtig ist: Token-Anzahlen wirken sich direkt auf Latenz und Kosten aus. Das Verständnis, wie Tokenizer Text aufteilen, hilft Ihnen, Prompts zu schreiben, die in Kontextfenster passen und unerwartetes Abschneiden vermeiden.

GuppyLM verwendet einen einfachen Zeichen-basierten Tokenizer. Produktionsmodelle wie GPT-4 verwenden BPE (Byte-Pair Encoding) mit Vokabularen von 50.000 bis 100.000 Tokens.

Einbettungsschicht

Die Einbettungsschicht wandelt Token-IDs in dichte Vektoren um. Jedes Token erhält einen gelernten Vektor (z.B. 384 Dimensionen in GuppyLM). Diese Vektoren tragen semantische Bedeutung: Ähnliche Tokens landen im Vektorraum nah beieinander.

Positions-Einbettungen werden hinzugefügt, damit das Modell die Token-Reihenfolge kennt.

Transformer-Blöcke

Dies ist die Kernberechnung. Jeder Block besteht aus zwei Teilen:

Self-Attention: Ermöglicht es jedem Token, alle anderen Tokens in der Sequenz zu betrachten und zu entscheiden, welche für die Vorhersage des nächsten Tokens wichtig sind. GuppyLM verwendet 6 Attention-Köpfe über 6 Schichten hinweg.

Feed-Forward-Netzwerk: Ein zweischichtiges MLP, das nach der Attention auf die Darstellung jedes Tokens angewendet wird. GuppyLM verwendet ReLU-Aktivierung, die einfacher ist als die in neueren Architekturen verwendete SwiGLU.

Ausgabekopf

Nach dem letzten Transformer-Block projiziert eine lineare Schicht die Darstellung jedes Tokens auf einen Vektor, dessen Größe dem Vokabular entspricht. Wenden Sie Softmax an, um Wahrscheinlichkeiten zu erhalten, wählen Sie das wahrscheinlichste nächste Token (oder sampeln Sie) und wiederholen Sie den Vorgang.

Ein minimales LLM in Python bauen

Hier ist ein funktionierendes, minimales LLM, das auf dem GuppyLM-Ansatz basiert. Es läuft in Standard-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

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}")

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()

Was dies über das Verhalten von KI-APIs lehrt

Der Bau dieses Modells offenbart mehrere Dinge, die Sie zu einem besseren API-Konsumenten machen.

Temperatur und Sampling sind mechanisch, nicht magisch

Die Temperatur teilt die Logits vor dem Softmax. Höhere Temperatur = flachere Verteilung = zufälligere Ausgabe. Niedrigere Temperatur = schärfere Verteilung = deterministischere Ausgabe. Wenn Ihre Produktions-API bei temperature=0.0 inkonsistente Ergebnisse liefert, ist das kein Fehler. Eine echte Temperatur von Null ist ein gieriges Argmax, und viele APIs setzen sie leicht über Null, um degenerierte Ausgaben zu vermeiden.

Kontextfenster sind harte Grenzen, keine sanften Vorschläge

Die Zeile idx_cond = ids[:, -SEQ_LEN:] in der Inferenzschleife zeigt genau, was an der Kontextgrenze passiert. Das Modell verwirft ältere Tokens stillschweigend. Wenn Ihre API-Integration davon ausgeht, dass sich das Modell den gesamten Gesprächsverlauf merkt, tut es dies nach einem bestimmten Punkt nicht mehr. Siehe [intern: how-ai-agent-memory-works] wie Agenten dieses Problem lösen.

Streaming-Tokens sind nur sichtbar gemachte Inferenzschritte

Streaming-APIs tun architektonisch nichts anderes. Sie führen die Inferenzschleife aus und senden jedes Token an den Antwortstrom, sobald es generiert wird. Das Verständnis dessen hilft Ihnen beim Schreiben von Wiederholungslogik: Ein unterbrochener Stream mitten in der Generierung kann nicht fortgesetzt werden, er muss neu starten.

Logits erklären, warum strukturierte Ausgabe schwierig ist

Das Modell weist jedem Token im Vokabular bei jedem Schritt eine Wahrscheinlichkeit zu. Das Generieren von gültigem JSON erfordert, dass das richtige Token an jeder Position gewinnt. Bibliotheken wie Outlines und Guidance schränken die Logit-Verteilung ein, um die Grammatik zur Inferenzzeit zu erzwingen. Wenn Sie sehen, dass KI-APIs "strukturierte Ausgabe"-Modi anbieten, tun sie dies intern.

Wie man KI-API-Integrationen mit Apidog testet

Sobald Sie verstehen, wie LLM-Inferenz funktioniert, können Sie viel bessere API-Tests schreiben. Die Test-Szenarien von Apidog ermöglichen es Ihnen, API-Aufrufe zu verketten und die Struktur von KI-Antworten zu überprüfen.

Zum Beispiel beim Testen einer Streaming-Chat-API:

  1. Erstellen Sie ein Test-Szenario in Apidog mit Ihrem /v1/chat/completions-Endpunkt.
  2. Setzen Sie Assertions, um die Antwortstruktur zu überprüfen: response.choices[0].finish_reason == "stop", response.usage.total_tokens < 4096
  3. Fügen Sie einen Folgeschritt hinzu, der die Antwort als Kontext für die nächste Runde sendet, um eine mehrstufige Konversation zu simulieren.
  4. Verwenden Sie Apidogs Smart Mock, um den KI-Endpunkt zu stubben und die Fehlerbehandlung Ihrer App zu testen: simulieren Sie finish_reason: "length" (abgeschnittene Ausgabe), finish_reason: "content_filter" und einen Netzwerk-Timeout mitten im Stream.

So testen Sie KI-Integrationen, ohne bei jedem CI-Durchlauf API-Guthaben zu verbrennen. Siehe [intern: api-testing-tutorial] für einen breiteren Überblick über API-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"
    }
  ]
}

Führen Sie dies über mehrere Modelle (GPT-4o, Claude 3.5 Sonnet, Gemini 1.5 Pro) in einem einzigen Test-Szenario aus, um API-Schema-Unterschiede zu erkennen, bevor sie in Produktion gehen.

Fortgeschritten: Quantisierung und Inferenzoptimierung

Sobald Sie ein funktionierendes winziges LLM haben, sind zwei Techniken erwähnenswert, da sie direkt auf die Bereitstellung von Produktionsmodellen zutreffen.

Quantisierung

Die Gewichte in unserem Modell sind standardmäßig 32-Bit-Floats. Quantisierung reduziert sie auf 8-Bit-Ganzzahlen (INT8) oder sogar 4-Bit (INT4). Dies reduziert den Speicherverbrauch um das 4- bis 8-fache mit einem geringen Genauigkeitsverlust.

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

Produktions-APIs verwenden quantisierte Modelle. Wenn Sie unterschiedliche Ausgabequalitäten bei verschiedenen "Versionen" desselben Modells sehen, ist oft Quantisierung im Spiel.

KV-Cache

In unserer Inferenzschleife berechnen wir die Attention bei jedem Schritt über die gesamte Sequenz neu. Produktionssysteme cachen die Key-Value-Paare von früheren Tokens (den KV-Cache), sodass jedes neue Token nur eine neue Attention-Berechnung benötigt. Aus diesem Grund dauert das erste Token in einer Streaming-Antwort länger als die nachfolgenden.

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 wahre Antwort für die meisten Entwickler: Verwenden Sie die Produktions-API für Ihre Anwendung, aber betreiben Sie ein winziges Modell, um zu verstehen, was im Hintergrund passiert. Die beiden konkurrieren nicht miteinander. Der Artikel [intern: open-source-coding-assistants-2026] behandelt Tools, die diese Grenze mit Bring-your-own-model-Setups verwischen.

Fazit

Der Bau eines winzigen LLM von Grund auf dauert ein Wochenende. Was Sie erhalten, ist kein Produktionssystem, sondern ein funktionierendes mentales Modell, wie jedes Sprachmodell, von GuppyLM bis GPT-4o, tatsächlich funktioniert. Dieses Verständnis zahlt sich jedes Mal aus, wenn Sie eine Streaming-Integration debuggen, Sampling-Parameter optimieren oder Assertions für Ihre KI-API-Tests entwerfen.

Das GuppyLM-Projekt ist ein guter Ausgangspunkt. Klonen Sie es, trainieren Sie es mit einem beliebigen Textdatensatz und verbringen Sie einen Nachmittag damit, die Inferenzschleife zu lesen. Dann kehren Sie zu Ihren Produktions-API-Integrationen zurück und Sie werden sie anders sehen.

Probieren Sie die Test-Szenarien von Apidog aus, um die gleiche Sorgfalt beim Testen Ihrer KI-API anzuwenden, die Sie jedem anderen Backend-System entgegenbringen würden.

button

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.

Praktizieren Sie API Design-First in Apidog

Entdecken Sie eine einfachere Möglichkeit, APIs zu erstellen und zu nutzen