LLM-API aufrufen: temperature, top_p und max_tokens

1. Übersicht

Wenn Sie eine LLM-API aufrufen, senden Sie neben dem Prompt auch einige Parameter zur Texterzeugung mit. Die drei am häufigsten verwendeten Parameter sind temperature, top_p und max_tokens. Sie bestimmen, ob die Antwort stabil oder kreativ ist und wie lang sie sein darf.

Dieser Artikel erklärt die Funktionsweise jedes Parameters anhand einer kleinen Java-Simulation. Dann werden Sie sie mit Spring AI konfigurieren und erkennen, wenn die Antwort abgeschnitten wird. Danach wissen Sie, welche Werte Sie für verschiedene Aufgabentypen wählen.

2. Wie das Modell ein Token auswählt

Bei jedem Schritt berechnet das LLM eine Punktzahl (Logit) für alle möglichen nächsten Token. Die Softmax-Funktion wandelt diese Logits in Wahrscheinlichkeiten um. Dann tastet das Modell ein Token gemäß dieser Verteilung aus.

temperature und top_p ändern nicht das „Wissen” des Modells. Sie greifen nur in diesen Sampling-Schritt ein.

2.1. Temperature: die Verteilung spitzer oder flacher machen

Die Softmax-Formel mit Temperature T:

p_i = exp(z_i / T) / Σ exp(z_j / T)

Der folgende Code simuliert, wie das Modell das nächste Wort für den Satz „Der Nón-Lá-Hut ist … “ auswählt:

import java.util.*;

public class SamplingDemo {

    static final List<String> TOKENS = List.of("grün", "rot", "gelb", "violett");
    static final double[] LOGITS = {2.0, 1.0, 0.5, 0.1};

    static double[] softmax(double[] logits, double temperature) {
        double[] exp = new double[logits.length];
        double sum = 0;
        for (int i = 0; i < logits.length; i++) {
            exp[i] = Math.exp(logits[i] / temperature);
            sum += exp[i];
        }
        for (int i = 0; i < exp.length; i++) {
            exp[i] /= sum;
        }
        return exp;
    }

    public static void main(String[] args) {
        for (double t : new double[]{0.5, 1.0, 1.5}) {
            double[] p = softmax(LOGITS, t);
            StringBuilder line = new StringBuilder("T=" + t + ": ");
            for (int i = 0; i < p.length; i++) {
                line.append(String.format(Locale.ROOT, "%s=%.2f ", TOKENS.get(i), p[i]));
            }
            System.out.println(line);
        }
    }
}

Die softmax-Funktion teilt jeden Logit durch temperature, bevor sie exp anwendet. Die main-Funktion gibt die Verteilung bei drei Temperaturstufen aus:

T=0.5: grün=0.83 rot=0.11 gelb=0.04 violett=0.02
T=1.0: grün=0.57 rot=0.21 gelb=0.13 violett=0.09
T=1.5: grün=0.46 rot=0.24 gelb=0.17 violett=0.13

Die Ergebnisse zeigen deutlich die Auswirkung von temperature:

  • T niedrig (0,5): Das Token „grün” macht 83 % aus. Das Modell wählt fast immer die wahrscheinlichste Antwort. Die Antwort ist stabil und vorhersehbar.
  • T hoch (1,5): Die Wahrscheinlichkeiten sind gleichmäßiger verteilt. „Violett” steigt von 2 % auf 13 %. Die Antwort ist vielfältiger, aber leichter vom Thema abzuweichen.
  • T = 0: Dies wählt theoretisch immer das Token mit der höchsten Wahrscheinlichkeit (Greedy-Auswahl).

Hinweis: temperature = 0 garantiert nicht, dass die Ergebnisse bei aufeinanderfolgenden Aufrufen identisch sind. Gleitkommaberechnungen auf der GPU und wie der Anbieter Batches zusammenfasst, können immer noch kleine Unterschiede erzeugen. Schreiben Sie keine Tests, die Strings mit der echten LLM-Ausgabe vergleichen.

2.2. Top_p: die Tail mit niedriger Wahrscheinlichkeit abschneiden

top_p (Nucleus Sampling) funktioniert anders. Das Modell sortiert Token nach abnehmender Wahrscheinlichkeit. Es behält die kleinste Gruppe, deren Wahrscheinlichkeitssumme den Schwellenwert p erreicht, und tastet nur aus dieser Gruppe ab.

Sie fügen die folgende Methode zu SamplingDemo hinzu:

static List<String> nucleus(double[] probs, double topP) {
    Integer[] idx = {0, 1, 2, 3};
    Arrays.sort(idx, (a, b) -> Double.compare(probs[b], probs[a]));

    List<String> kept = new ArrayList<>();
    double cumulative = 0;
    for (int i : idx) {
        kept.add(TOKENS.get(i));
        cumulative += probs[i];
        if (cumulative >= topP) {
            break;
        }
    }
    return kept;
}

Der Aufruf nucleus(softmax(LOGITS, 1.0), 0.8) gibt Folgendes zurück:

[grün, rot, gelb]

Die Summe von „grün” und „rot” ist 0,785, nicht genug für 0,8. Daher wird „gelb” hinzugefügt und die Summe beträgt 0,914. Das Token „violett” wird komplett ausgeschlossen, egal wie hoch die Temperatur ist.

Der Hauptunterschied zwischen den beiden Parametern:

  • temperature ändert die Form der Verteilung.
  • top_p ändert die Menge der Kandidaten, die ausgewählt werden dürfen.

Hinweis: Die meisten Anbieter empfehlen, nur einen der beiden Parameter anzupassen und den anderen auf dem Standardwert zu belassen (top_p = 1,0). Wenn Sie beide gleichzeitig anpassen, wird es schwierig herauszufinden, welche Änderung das Ergebnis verursacht hat.

3. Max_tokens: die Ausgabelänge begrenzen

max_tokens ist die maximale Anzahl von Token, die das Modell in der Antwort generieren darf. Es ist keine Zieldauer. Das Modell versucht nicht, diesen Wert zu erreichen. Wenn das Limit erreicht ist, stoppt es sofort, auch wenn es mitten in einem Satz ist.

Es gibt drei Dinge, die Sie verstehen müssen:

  1. Token sind keine Wörter. Im Englischen ist etwa ein Token 3/4 eines Wortes. Vietnamesisch mit Diakritika verbraucht oft mehr Token für den gleichen Inhalt.
  2. Kosten werden nach Token berechnet. max_tokens begrenzt Kosten und Latenz.
  3. Das Kontextfenster umfasst Input und Output. Wenn der Prompt das Kontextfenster fast vollständig verbraucht hat, bleibt wenig Platz für die Ausgabe.

Wenn die Antwort abgeschnitten wird, gibt die API finish_reason als length statt stop zurück. Eine gekürzte Antwort von einer OpenAI-ähnlichen API sieht folgendermaßen aus:

{
  "choices": [{
    "message": { "role": "assistant", "content": "Der Nón-Lá-Hut wird aus jungen Palmblättern hergestellt, getr" },
    "finish_reason": "length"
  }],
  "usage": { "prompt_tokens": 42, "completion_tokens": 20, "total_tokens": 62 }
}

Der Inhalt stoppt in der Mitte, weil max_tokens = 20. Wenn Ihr Code nur content liest, zeigen Sie dem Benutzer einen abgeschnittenen Satz an, ohne es zu bemerken.

4. Konfiguration mit Spring AI

Dieser Abschnitt verwendet Spring AI, um OpenAI von Spring Boot 3.x aus aufzurufen.

4.1. Abhängigkeiten und Standardkonfiguration

Sie fügen die BOM und den Starter zu pom.xml hinzu:

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-bom</artifactId>
            <version>1.0.0</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

<dependencies>
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-starter-model-openai</artifactId>
    </dependency>
</dependencies>

Die Standardwerte für die gesamte Anwendung befinden sich in application.properties:

spring.ai.openai.api-key=${OPENAI_API_KEY}
spring.ai.openai.chat.options.model=gpt-4o-mini
spring.ai.openai.chat.options.temperature=0.3
spring.ai.openai.chat.options.max-tokens=300

Jede Anfrage ohne Überschreibung verwendet temperature = 0,3 und maximal 300 Output-Token. Der API-Schlüssel wird aus einer Umgebungsvariable bezogen, nicht hartcodiert in die Datei.

4.2. Ein Parameterprofil für jeden Aufgabentyp

Jede Aufgabe benötigt einen anderen Satz von Parametern. Statt magische Zahlen überall im Code zu verteilen, fassen Sie sie in einer Enum zusammen:

import org.springframework.ai.openai.OpenAiChatOptions;

public enum GenerationProfile {

    EXTRACTION(0.0, 1.0, 200),      // Datenbeschaffung, Klassifizierung
    SUPPORT_REPLY(0.4, 1.0, 400),   // Kundenreply
    MARKETING_COPY(0.9, 1.0, 600);  // Produktbeschreibungen, Slogans

    private final double temperature;
    private final double topP;
    private final int maxTokens;

    GenerationProfile(double temperature, double topP, int maxTokens) {
        this.temperature = temperature;
        this.topP = topP;
        this.maxTokens = maxTokens;
    }

    public OpenAiChatOptions toOptions() {
        return OpenAiChatOptions.builder()
                .temperature(temperature)
                .topP(topP)
                .maxTokens(maxTokens)
                .build();
    }
}

Jede Konstante beschreibt einen Aufgabentyp. EXTRACTION benötigt konsistente Ergebnisse, daher temperature 0. MARKETING_COPY benötigt Vielfalt, daher 0,9. Alle drei behalten top_p = 1,0, d. h. sie passen nur die Temperatur an, wie in Abschnitt 2.2 empfohlen.

4.3. Parameter pro Request überschreiben und Abschneiden erkennen

Spring Boot konfiguriert automatisch das ChatClient.Builder-Bean. Sie injizieren es in den Service:

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.model.Generation;
import org.springframework.stereotype.Service;

@Service
public class ProductDescriptionService {

    private final ChatClient chatClient;

    public ProductDescriptionService(ChatClient.Builder builder) {
        this.chatClient = builder
                .defaultSystem("Sie sind ein Copywriter im Nón-Lá-Shop. Schreiben Sie maximal 3 Sätze.")
                .build();
    }

    public String describe(String productName) {
        ChatResponse response = chatClient.prompt()
                .user(u -> u.text("Schreiben Sie eine Beschreibung für das Produkt: {name}")
                            .param("name", productName))
                .options(GenerationProfile.MARKETING_COPY.toOptions())
                .call()
                .chatResponse();

        Generation result = response.getResult();
        String finishReason = result.getMetadata().getFinishReason();

        if ("length".equalsIgnoreCase(finishReason)) {
            throw new TruncatedResponseException(
                "Die Beschreibung wurde nach " + response.getMetadata().getUsage().getCompletionTokens() + " Token abgeschnitten");
        }
        return result.getOutput().getText();
    }
}

Analyse der einzelnen Teile:

  • .options(...) überschreibt die Werte in application.properties nur für diese Anfrage.
  • .chatResponse() gibt die gesamte Antwort mit Metadaten zurück, nicht nur einen String wie .content().
  • getFinishReason() zeigt den Grund für den Stopp an. Spring AI kann LENGTH in Großbuchstaben zurückgeben, daher verwendet der Code equalsIgnoreCase.
  • getUsage().getCompletionTokens() zeigt die tatsächliche Anzahl der Output-Token an. Diese Zahl ist hilfreich zum Protokollieren und Überwachen der Kosten.

Die Ausnahme ist einfach definiert wie folgt:

public class TruncatedResponseException extends RuntimeException {
    public TruncatedResponseException(String message) {
        super(message);
    }
}

Wenn Sie auf diese Ausnahme stoßen, haben Sie zwei Möglichkeiten. Der erste Weg ist, max_tokens zu erhöhen und erneut zu versuchen. Der zweite Weg ist, den Prompt zu verschärfen, um eine kürzere Antwort zu fordern. Der zweite Weg ist in der Regel billiger und liefert prägnantere Ergebnisse.

5. Testen

Sie sollten die echte LLM-Ausgabe nicht durch Stringvergleiche testen. Sie sollten Ihre eigene Logik testen: die Parameterprofile und Simulationsalgorithmen.

import org.junit.jupiter.api.Test;
import org.springframework.ai.openai.OpenAiChatOptions;

import static org.assertj.core.api.Assertions.assertThat;

class GenerationProfileTest {

    @Test
    void extractionProfile_shouldBeDeterministicAndShort() {
        OpenAiChatOptions options = GenerationProfile.EXTRACTION.toOptions();

        assertThat(options.getTemperature()).isZero();
        assertThat(options.getTopP()).isEqualTo(1.0);
        assertThat(options.getMaxTokens()).isEqualTo(200);
    }

    @Test
    void marketingProfile_shouldBeMoreCreativeThanSupport() {
        assertThat(GenerationProfile.MARKETING_COPY.toOptions().getTemperature())
                .isGreaterThan(GenerationProfile.SUPPORT_REPLY.toOptions().getTemperature());
    }

    @Test
    void lowerTemperature_shouldConcentrateProbabilityOnTopToken() {
        double[] cold = SamplingDemo.softmax(SamplingDemo.LOGITS, 0.5);
        double[] hot = SamplingDemo.softmax(SamplingDemo.LOGITS, 1.5);

        assertThat(cold[0]).isGreaterThan(hot[0]);
        assertThat(cold[3]).isLessThan(hot[3]);
    }

    @Test
    void nucleus_shouldDropLowProbabilityTail() {
        double[] probs = SamplingDemo.softmax(SamplingDemo.LOGITS, 1.0);

        assertThat(SamplingDemo.nucleus(probs, 0.8))
                .containsExactly("grün", "rot", "gelb");
    }
}

Die ersten beiden Tests stellen sicher, dass niemand versehentlich Parameter eines wichtigen Profils ändert. Die letzten beiden Tests bestätigen die in Abschnitt 2 beschriebenen Verhaltensweisen: niedrige Temperatur konzentriert die Wahrscheinlichkeit auf das führende Token, während top_p = 0,8 das Token „violett” ausschließt.

6. Häufige Fehler und wann man es nicht verwenden sollte

Verwenden Sie hohe Temperatur nicht für Aufgaben, die Genauigkeit benötigen. JSON-Extraktion, Ticket-Klassifizierung oder SQL-Generierung benötigen konsistente Ergebnisse. Eine Temperatur von 0,8 oder höher erhöht die Chance für Formatfehler und Halluzinationen.

Denken Sie nicht, dass niedrige Temperatur gegen „Halluzinationen” schützt. Niedrige Temperatur lässt das Modell nur die Antwort auswählen, die es für wahrscheinlich hält. Wenn das Modell die Antwort nicht kennt, kann es trotzdem zuversichtlich das Falsche sagen. Um Halluzinationen zu reduzieren, müssen Sie den richtigen Kontext (RAG) bereitstellen und die Ausgabe validieren.

Setzen Sie max_tokens nicht zu niedrig, um zu sparen. Sie sparen nichts, wenn die Antwort abgeschnitten wird und Sie erneut aufrufen müssen. Begrenzen Sie die Länge durch den Prompt („maximal 3 Sätze”), und max_tokens dient nur als Sicherheitsnetz.

Überprüfen Sie finish_reason nicht. Dies ist der häufigste Fehler. Der Benutzer sieht einen abgeschnittenen Satz, aber Ihr Log verzeichnet nichts.

Übernehmen Sie nicht einfach Parameter auf ein anderes Modell. Jeder Anbieter hat seine eigenen Konventionen:

  • Unterschiedliche Temperaturbereiche. OpenAI verwendet 0–2, Anthropic 0–1, sodass die gleiche Temperatur 1,0 unterschiedliche Bedeutungen hat.
  • Reasoning-Modelle (wie OpenAIs o-Reihe) können temperature und top_p möglicherweise ablehnen.
  • Einige Modelle erfordern max_completion_tokens statt max_tokens. Spring AI unterstützt dieses Feld über OpenAiChatOptions.builder().maxCompletionTokens(...).

Hinweis: Bei Reasoning-Modellen werden interne „Gedanken”-Token auch auf das Ausgabelimit angerechnet. Wenn Sie das Limit zu niedrig setzen, erhalten Sie möglicherweise leere Inhalte mit finish_reason = length.

7. Fazit

Dieser Artikel behandelte die drei wesentlichen Parameter beim Aufrufen einer LLM-API:

  • temperature ändert die Schärfe der Wahrscheinlichkeitsverteilung. Niedrige Werte ergeben stabile Ergebnisse, hohe Werte diverse Ergebnisse.
  • top_p begrenzt die Menge der Kandidaten-Token. Sie sollten nur einen dieser Parameter anpassen, nicht beide gleichzeitig.
  • max_tokens ist die Obergrenze für Länge und Kosten. Überprüfen Sie immer finish_reason, um abgeschnittene Antworten zu erkennen.

Mit Spring AI legen Sie Standardwerte in application.properties fest und überschreiben sie pro Anfrage mit .options(...). Das Gruppieren von Parametersätzen in einer Enum wie GenerationProfile macht den Code lesbarer, testbarer und leichter zu ändern, wenn Sie zu einem anderen Modell wechseln.

llmspring-aispring-bootjavaopenai