Das nächste Wort — wie Sprachmodelle funktionieren

Das nächste Wort — wie Sprachmodelle funktionieren

Artikel 1 von 8 · Serie: Wie LLMs wirklich funktionieren

Über diese Serie

ChatGPT, Claude, Gemini — Sprachmodelle sind aus dem Alltag nicht mehr wegzudenken. Nur was sind eigentlich Sprachmodelle und was passiert unter der Haube? In dieser Serie erkläre ich Schritt für Schritt wie Sprachmodelle funktionieren — von den Grundkonzepten bis zur fertigen Architektur. Acht Artikel, aufeinander aufbauend, ein Fundament.


Was passiert eigentlich in der Millisekunde zwischen einer Eingabe und dem ersten Buchstaben der Antwort?

Die meisten Menschen haben eine ungefähre Vorstellung — irgendwas mit KI, irgendwas mit neuronalen Netzen, irgendwas mit sehr viel Rechenleistung. Diese ungefähre Vorstellung reicht aus, um ein Sprachmodell zu benutzen. Sie reicht nicht aus, um zu verstehen warum es sich so verhält wie es sich verhält — warum es manchmal Dinge erfindet, warum minimale Änderungen am Prompt die Antwort komplett kippen, warum es bei manchen Aufgaben brilliert und bei anderen auf eine Weise scheitert, die fast komisch wäre, wenn es nicht so teuer wäre.

Dieser Artikel legt das Fundament. Wir schauen uns an was ein Sprachmodell mechanisch tut — Schritt für Schritt, mit echtem Code, ohne Abkürzungen.


Nicht Wörter, sondern Tokens

Das erste was die meisten Menschen falsch verstehen: Ein Sprachmodell liest keine Wörter. Es liest Tokens.

Ein Token ist ein Textstück — manchmal ein ganzes Wort, manchmal ein Wortteil, manchmal ein einzelnes Zeichen oder ein Satzzeichen. Die genaue Aufteilung hängt vom jeweiligen Tokenizer ab, aber das Prinzip ist überall dasselbe: Text wird in handhabbare Einheiten zerlegt, bevor das Modell ihn zu Gesicht bekommt.

Warum nicht einfach Wörter? Drei Gründe.

Erstens ist der Begriff „Wort" sprachabhängig und unscharf — in deutschen Komposita wie „Donaudampfschifffahrtsgesellschaft" stecken konzeptuell mehrere Wörter. Zweitens wäre ein rein wortbasiertes Vokabular riesig — Millionen von Einträgen für alle Sprachen, Konjugationen, Fachbegriffe. Tokens ermöglichen ein kompaktes Vokabular von typischerweise 32.000 bis 100.000 Einträgen, das trotzdem jeden Text darstellen kann. Drittens lassen sich mit Sub-Word-Tokens unbekannte Wörter aus bekannten Teilen zusammensetzen — das Modell sieht „Quantencomputer" vielleicht zum ersten Mal, kennt aber „Quanten" und „computer" bereits.

Schauen wir uns an wie das in der Praxis aussieht:

import tiktoken

# pip install tiktoken
enc = tiktoken.get_encoding("cl100k_base")  # Vokabular von GPT-4

text = "Sprachmodelle sind faszinierend."
tokens = enc.encode(text)

print(f"Text:   {text}")
print(f"Tokens: {tokens}")
print(f"Anzahl: {len(tokens)}\n")

for token_id in tokens:
    print(f"  {token_id:6d} → '{enc.decode([token_id])}'")
Text:   Sprachmodelle sind faszinierend.
Tokens: [50, 3981, 19122, 1543, 7953, 13]
Anzahl: 6

    50 → 'Spr'
  3981 → 'ach'
 19122 → 'modelle'
  1543 → ' sind'
  7953 → ' faszin'
    13 → 'ierend.'

„Sprachmodelle" wird in drei Teile zerlegt. „sind" bleibt als ganzes Wort — aber mit einem Leerzeichen davor als Teil des Tokens. Das ist kein Fehler, sondern eine Designentscheidung: Leerzeichen gehören zum folgenden Token, nicht zum vorherigen.

Jeder Token hat eine numerische ID. Das ist alles was das Modell sieht — eine Sequenz von Zahlen. Kein Text, keine Bedeutung, keine Grammatik. Nur Zahlen.


Eine Verteilung, keine Antwort

Hier ist der Kern des Ganzen, und er ist einfacher als er klingt.

Ein Sprachmodell nimmt eine Sequenz von Token-IDs als Eingabe und gibt — für jeden möglichen nächsten Token — eine Wahrscheinlichkeit aus. Nicht die Antwort. Eine Wahrscheinlichkeitsverteilung über alle möglichen Antworten.

Bei einem Vokabular von 50.000 Tokens bekommt man also 50.000 Wahrscheinlichkeiten. Die Summe aller Wahrscheinlichkeiten ist immer 1.0. Das Modell sagt im Grunde: „Gegeben alles was bisher stand — mit 34% Wahrscheinlichkeit kommt als nächstes ‚ist’, mit 12% ‚war’, mit 8% ‚wird’…" und so weiter durch das gesamte Vokabular.

Intern passiert das über einen softmax-Schritt. Softmax nimmt beliebige Zahlen — die sogenannten Logits, die rohen Modell-Ausgaben — und verwandelt sie in eine Wahrscheinlichkeitsverteilung:

import numpy as np

def softmax(logits):
    # Numerisch stabil: Maximum subtrahieren verhindert Overflow
    logits = logits - np.max(logits)
    exp_logits = np.exp(logits)
    return exp_logits / exp_logits.sum()

# Vereinfachtes Beispiel: Das Modell hat 5 mögliche nächste Tokens bewertet
# In der Praxis sind es 50.000+
token_texts = [" ist", " war", " wird", " sei", " hat"]
logits      = [  3.2,    1.8,    2.1,    0.5,   1.2 ]

probabilities = softmax(np.array(logits))

print(f"{'Token':12s}  {'Logit':6s}  {'Wahrsch.':10s}")
print("-" * 34)
for text, logit, prob in zip(token_texts, logits, probabilities):
    bar = "█" * int(prob * 40)
    print(f"{text:12s}  {logit:5.1f}   {prob:.4f}  {bar}")
Token          Logit   Wahrsch.
----------------------------------
 ist            3.2   0.4821  ████████████████████
 wird           2.1   0.1623  ██████
 war            1.8   0.1200  ████
 hat            1.2   0.0662  ██
 sei            0.5   0.0328  █

Zwei Dinge fallen auf. Erstens: Der Logit-Unterschied zwischen „ist" (3.2) und „war" (1.8) ist 1.4 — aber der Wahrscheinlichkeitsunterschied ist massiv: 48% zu 12%. Softmax verstärkt Unterschiede exponentiell. Kleine Änderungen in den Logits führen zu großen Verschiebungen in der Verteilung.

Zweitens: Das Modell ist sich nie zu 100% sicher. Selbst der wahrscheinlichste Token liegt hier unter 50%. Das bedeutet: In mehr als der Hälfte der Fälle wird etwas anderes als das häufigste Token gewählt — sofern man nicht deterministisch vorgeht. Dazu kommen wir gleich.


Wie aus der Verteilung ein Token wird

Das Modell hat eine Verteilung berechnet. Jetzt muss es einen Token auswählen. Wie das passiert, hat enormen Einfluss auf das Verhalten des Modells — und ist der Grund warum dieselbe Frage unterschiedliche Antworten produzieren kann.

Greedy Decoding

Die naheliegendste Strategie: Nimm immer den Token mit der höchsten Wahrscheinlichkeit.

def greedy_decode(probabilities, token_texts):
    best_idx = np.argmax(probabilities)
    return token_texts[best_idx]

next_token = greedy_decode(probabilities, token_texts)
print(f"Greedy: '{next_token}'")  # → ' ist'

Deterministisch, reproduzierbar — und in der Praxis für längere Texte problematisch. Greedy Decoding neigt zu Wiederholungen und flachen, vorhersehbaren Ausgaben. Das Modell wählt lokal immer die beste Option, was global zu schlechten Ergebnissen führen kann — ein klassisches Greedy-Problem das jeder aus dem Algorithmik-Studium kennt.

Temperature Sampling

Die elegantere Lösung: Die Wahrscheinlichkeitsverteilung vor dem Sampling verformen — durch einen Parameter namens Temperature.

def temperature_sample(logits, temperature, token_texts, n_samples=10000):
    # Temperature skaliert die Logits vor dem Softmax
    scaled_logits = np.array(logits) / temperature
    probs = softmax(scaled_logits)

    # Zufälliges Sampling aus der Verteilung
    chosen_indices = np.random.choice(len(token_texts), size=n_samples, p=probs)

    from collections import Counter
    counts = Counter(chosen_indices)

    print(f"\nTemperature = {temperature}")
    print(f"{'Token':12s}  {'Wahrsch.':10s}  {'Gezogen':8s}")
    print("-" * 38)
    for idx, text in enumerate(token_texts):
        prob = probs[idx]
        drawn = counts.get(idx, 0) / n_samples
        bar = "█" * int(drawn * 30)
        print(f"{text:12s}  {prob:.4f}      {drawn:.4f}  {bar}")

logits = [3.2, 1.8, 2.1, 0.5, 1.2]
temperature_sample(logits, temperature=0.2, token_texts=token_texts)
temperature_sample(logits, temperature=1.0, token_texts=token_texts)
temperature_sample(logits, temperature=2.0, token_texts=token_texts)
Temperature = 0.2
Token          Wahrsch.    Gezogen
--------------------------------------
 ist           0.9801      0.9795  ██████████████████████████████
 wird          0.0145      0.0143
 war           0.0050      0.0051
 hat           0.0003      0.0003
 sei           0.0001      0.0000

Temperature = 1.0
Token          Wahrsch.    Gezogen
--------------------------------------
 ist           0.4821      0.4834  ██████████████
 wird          0.1623      0.1618  ████
 war           0.1200      0.1204  ███
 hat           0.0662      0.0655  █
 sei           0.0328      0.0331  █

Temperature = 2.0
Token          Wahrsch.    Gezogen
--------------------------------------
 ist           0.2879      0.2887  ████████
 wird          0.2016      0.2014  ██████
 war           0.1789      0.1802  █████
 hat           0.1427      0.1415  ████
 sei           0.1116      0.1110  ███

Bei Temperature 0.2 dominiert ein Token fast vollständig — das Verhalten nähert sich Greedy an. Bei Temperature 2.0 werden alle Tokens fast gleich wahrscheinlich — die Ausgabe wird zufälliger, kreativer, aber auch fehleranfälliger. Temperature 1.0 lässt die ursprüngliche Verteilung des Modells unangetastet.

Sampling — interaktiv

Temperature 1.0
Modus

Top-p Sampling

Eine Erweiterung die in der Praxis häufig mit Temperature kombiniert wird: Top-p — auch Nucleus Sampling genannt. Statt alle Tokens in den Pool zu nehmen, wählt Top-p nur die wahrscheinlichsten Tokens — so viele, bis ihre kumulierte Wahrscheinlichkeit p erreicht.

def top_p_sample(probabilities, token_texts, p=0.9):
    # Tokens nach Wahrscheinlichkeit absteigend sortieren
    sorted_indices = np.argsort(probabilities)[::-1]
    sorted_probs = probabilities[sorted_indices]

    # Kumulative Summe — bis p erreicht ist
    cumulative_probs = np.cumsum(sorted_probs)
    cutoff_idx = np.searchsorted(cumulative_probs, p) + 1

    nucleus_indices = sorted_indices[:cutoff_idx]
    nucleus_probs = sorted_probs[:cutoff_idx]
    nucleus_probs = nucleus_probs / nucleus_probs.sum()  # Renormalisieren

    print(f"\nTop-p = {p}{cutoff_idx} Tokens im Nucleus:")
    for idx, prob in zip(nucleus_indices, nucleus_probs):
        print(f"  {token_texts[idx]:12s}  {prob:.4f}")

    return token_texts[np.random.choice(nucleus_indices, p=nucleus_probs)]

probs = softmax(np.array(logits))
top_p_sample(probs, token_texts, p=0.9)
Top-p = 0.9 → 3 Tokens im Nucleus:
   ist          0.5891
   wird         0.1984
   war          0.1467
  (hat und sei werden ausgeschlossen)

Der Vorteil gegenüber reinem Temperature Sampling: Die Nucleus-Größe passt sich dynamisch an. Wenn das Modell sehr sicher ist und ein Token 95% hat, ist der Nucleus minimal. Wenn das Modell unsicher ist, wächst der Nucleus — aber nicht unbegrenzt. Unwahrscheinliche Ausreißer werden immer ausgeschlossen.


Die Strategien im Vergleich

StrategieDeterministischKreativitätTypischer Einsatz
Greedy (temp = 0)JaKeineFaktische Abfragen, Code-Completion
Temperature < 1NeinGeringStrukturierte Outputs, JSON
Temperature = 1NeinMittelAllgemeine Chatbots
Temperature > 1NeinHochKreativtext, Brainstorming
Top-p = 0.9NeinKontextabhängigStandard in den meisten APIs

In der Praxis setzen fast alle produktiven Systeme eine Kombination ein: Temperature zwischen 0.7 und 1.0, kombiniert mit Top-p zwischen 0.9 und 0.95. Die meisten API-Defaults liegen genau in diesem Bereich.


Was das alles erklärt

Jetzt lassen sich eine Reihe von Verhaltensweisen erklären, die vorher wie schwarze Magie wirkten.

Halluzinationen sind kein Bug — sie sind eine direkte Konsequenz des Sampling-Prozesses. Das Modell wählt in jedem Schritt aus einer Wahrscheinlichkeitsverteilung. Wenn der richtige Token bei 60% liegt und ein falscher aber plausibler Token bei 15%, wird der falsche in einem von sechs Fällen gezogen — und dann als nächste Eingabe verwendet, was die nachfolgende Verteilung verschiebt. Fehler akkumulieren sich über die Sequenz.

Prompt-Sensitivität erklärt sich dadurch, dass minimale Änderungen im Eingabetext die Logits verschieben — und Softmax macht aus kleinen Logit-Unterschieden große Wahrscheinlichkeitsunterschiede. Ein anderes Wort im Prompt kann eine komplett andere Verteilung erzeugen.

Nicht-Determinismus ist bei Temperature > 0 fundamental. Dasselbe Modell gibt auf dieselbe Frage unterschiedliche Antworten — das ist kein Fehler, das ist das Design. Reproduzierbare Outputs erfordern Temperature 0 und einen festen Random Seed.

Lange Ausgaben driften, weil jeder generierte Token zur nächsten Eingabe wird. Fehler früh in der Ausgabe beeinflussen alle folgenden Tokens. Das ist der Grund warum Structured Output und JSON-Mode in vielen APIs existieren — sie schränken den Sampling-Raum ein, damit das Modell nicht vom gewünschten Format abweicht.


Was noch fehlt

An diesem Punkt haben wir beschrieben was ein Sprachmodell tut: Es nimmt eine Sequenz von Token-IDs, berechnet eine Wahrscheinlichkeitsverteilung über das gesamte Vokabular, und wählt den nächsten Token.

Was wir noch nicht beschrieben haben: Wie kommt das Modell überhaupt zu diesen Wahrscheinlichkeiten? Was passiert zwischen der Token-ID-Sequenz und den Logits?

Das ist die Frage nach der internen Repräsentation — und die Antwort beginnt mit einem Problem. Das Modell sieht nur Zahlen: Token 19122 ist für das Modell genauso abstrakt wie Token 3981. Um sinnvolle Wahrscheinlichkeiten berechnen zu können, braucht das Modell eine Möglichkeit, Bedeutung in diese Zahlen zu kodieren. Es braucht eine Art zu wissen, dass „Katze" und „Hund" mehr gemeinsam haben als „Katze" und „Autobahn".

Das ist das Thema von Artikel 2: Embeddings — wie Tokens in einen hochdimensionalen Raum projiziert werden, in dem Bedeutung messbar wird.


Nächster Artikel: Wörter als Punkte im Raum — was Embeddings wirklich sind und warum ähnliche Konzepte nah beieinander liegen.

Serie: Wie LLMs wirklich funktionieren · rotecodefraktion.de