Appeler une API LLM : temperature, top_p et max tokens
1. Aperçu
Lorsque vous appelez une API LLM, au-delà du prompt, vous envoyez également quelques paramètres de génération de texte. Les trois paramètres les plus courants sont temperature, top_p et max_tokens. Ils déterminent si la réponse est stable ou créative, et sa longueur.
Cet article explique le mécanisme de chaque paramètre via une petite simulation Java. Vous allez ensuite les configurer avec Spring AI et détecter les réponses tronquées. Après la lecture, vous saurez quelle valeur choisir pour chaque type de tâche.
2. Comment le modèle choisit les tokens
À chaque étape, le LLM calcule un score (logit) pour chaque token pouvant apparaître ensuite. La fonction softmax convertit ces logits en probabilités. Ensuite, le modèle échantillonne un token selon cette distribution.
temperature et top_p ne modifient pas la « connaissance » du modèle. Ils n’interviennent que dans cette étape d’échantillonnage.
2.1. Temperature : rendre la distribution plus pointue ou plus plate
La formule softmax avec temperature T :
p_i = exp(z_i / T) / Σ exp(z_j / T)
Le code suivant simule le modèle choisissant le token suivant pour la phrase « chapeau de couleur … » :
import java.util.*;
public class SamplingDemo {
static final List<String> TOKENS = List.of("vert", "rouge", "jaune", "violet");
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);
}
}
}
La fonction softmax divise chaque logit par temperature avant de prendre exp. La fonction main affiche la distribution avec trois niveaux de temperature :
T=0.5: vert=0.83 rouge=0.11 jaune=0.04 violet=0.02
T=1.0: vert=0.57 rouge=0.21 jaune=0.13 violet=0.09
T=1.5: vert=0.46 rouge=0.24 jaune=0.17 violet=0.13
Le résultat montre clairement l’effet de la temperature :
- T faible (0.5) : le token « vert » représente 83 %. Le modèle choisit presque toujours la réponse la plus probable. Les réponses sont stables et prévisibles.
- T élevé (1.5) : la probabilité est mieux distribuée. « violet » augmente de 2 % à 13 %. Les réponses sont plus variées, mais risquent de dévier davantage.
- T = 0 : en théorie, choisit toujours le token avec la probabilité la plus élevée (greedy).
Note :
temperature = 0ne garantit pas des résultats identiques d’un appel à l’autre. Les calculs en virgule flottante sur GPU et la manière dont le fournisseur agrège les batches peuvent créer de petites différences. N’écrivez pas de tests qui comparent des chaînes exactes avec la sortie d’un vrai LLM.
2.2. Top_p : supprimer la queue de probabilité basse
top_p (nucleus sampling) fonctionne différemment. Le modèle trie les tokens par probabilité décroissante. Il conserve le plus petit groupe dont la probabilité cumulée atteint le seuil p, puis n’échantillonne que dans ce groupe.
Vous ajoutez la méthode suivante à SamplingDemo :
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;
}
L’appel de nucleus(softmax(LOGITS, 1.0), 0.8) retournera :
[vert, rouge, jaune]
La somme de « vert » et « rouge » est 0.785, ce qui n’atteint pas 0.8. Donc « jaune » est ajouté et la somme devient 0.914. Le token « violet » est complètement exclu, peu importe la temperature.
Les différences principales entre les deux paramètres :
temperaturechange la forme de la distribution.top_pchange l’ensemble des candidats autorisés à être choisis.
Note : La plupart des fournisseurs recommandent de n’ajuster qu’un des deux paramètres et de laisser l’autre à sa valeur par défaut (
top_p = 1.0). Ajuster les deux à la fois rend difficile de savoir quel changement produit quel résultat.
3. Max tokens : limiter la longueur de la sortie
max_tokens est le nombre maximum de tokens que le modèle peut générer dans une réponse. Ce n’est pas une longueur cible. Le modèle ne « s’efforce pas de remplir » ce nombre. Quand il atteint la limite, il s’arrête immédiatement, même au milieu d’une phrase.
Trois points à retenir :
- Un token n’est pas un mot. En anglais, un token représente environ 3/4 d’un mot. Le français avec ses accents consomme généralement plus de tokens pour le même contenu.
- Les coûts sont calculés par token.
max_tokensest le verrou qui contrôle le coût et la latence. - La fenêtre de contexte comprend à la fois l’entrée et la sortie. Si le prompt a presque épuisé la fenêtre de contexte, il ne reste très peu d’espace pour la sortie.
Lorsqu’elle est tronquée, l’API retourne finish_reason comme length au lieu de stop. Une réponse condensée d’une API de type OpenAI ressemble à ceci :
{
"choices": [{
"message": { "role": "assistant", "content": "Le nón lá de Huế est confectionné à partir de jeunes feuilles de rotin, p" },
"finish_reason": "length"
}],
"usage": { "prompt_tokens": 42, "completion_tokens": 20, "total_tokens": 62 }
}
Le contenu s’arrête à mi-phrase parce que max_tokens = 20. Si votre code ne lit que content, vous afficherez à l’utilisateur une phrase tronquée sans même le savoir.
4. Configuration avec Spring AI
Cette section utilise Spring AI pour appeler OpenAI à partir de Spring Boot 3.x.
4.1. Dépendances et configuration par défaut
Vous ajoutez le BOM et le starter à pom.xml :
<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>
Les valeurs par défaut pour toute l’application se trouvent dans 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
Toute requête sans override utilisera temperature = 0.3 et au maximum 300 tokens de sortie. La clé API est tirée d’une variable d’environnement, pas en dur dans le fichier.
4.2. Un profil de paramètres pour chaque type de tâche
Chaque tâche nécessite un ensemble de paramètres différent. Au lieu de disperser des nombres « magiques » partout dans le code, vous les regroupez dans une énumération :
import org.springframework.ai.openai.OpenAiChatOptions;
public enum GenerationProfile {
EXTRACTION(0.0, 1.0, 200), // extraction de données, classification
SUPPORT_REPLY(0.4, 1.0, 400), // réponses aux clients
MARKETING_COPY(0.9, 1.0, 600); // rédaction de descriptions produit, 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();
}
}
Chaque constante décrit un type de tâche. EXTRACTION a besoin de résultats cohérents, donc utilise temperature 0. MARKETING_COPY a besoin de diversité, donc utilise 0.9. Tous trois conservent top_p = 1.0, ce qui signifie que seule la température est ajustée comme recommandé à la section 2.2.
4.3. Remplacer les paramètres par requête et détecter les troncatures
Spring Boot configure automatiquement le bean ChatClient.Builder. Vous l’injectez dans le 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("Vous êtes copywriter de la boutique Nón Lá Shop. Écrivez au maximum 3 phrases.")
.build();
}
public String describe(String productName) {
ChatResponse response = chatClient.prompt()
.user(u -> u.text("Écrivez une description pour le produit : {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(
"Description tronquée après " + response.getMetadata().getUsage().getCompletionTokens() + " tokens");
}
return result.getOutput().getText();
}
}
Analyse de chaque partie :
.options(...)remplace les valeurs dansapplication.properties, uniquement pour cette requête..chatResponse()retourne la réponse complète avec les métadonnées, au lieu de simplement la chaîne comme.content().getFinishReason()indique la raison de l’arrêt. Spring AI peut retournerLENGTHen majuscules, d’où l’utilisation deequalsIgnoreCase.getUsage().getCompletionTokens()indique le nombre réel de tokens de sortie. Ce chiffre est utile pour la journalisation et le suivi des coûts.
L’exception est définie simplement comme suit :
public class TruncatedResponseException extends RuntimeException {
public TruncatedResponseException(String message) {
super(message);
}
}
Quand vous rencontrez cette exception, vous avez deux choix. Le premier est d’augmenter max_tokens et de réessayer. Le second est de resserrer le prompt pour demander une réponse plus courte. Le second est généralement moins coûteux et donne des résultats plus concis.
5. Tests
Vous ne devez pas tester la sortie du vrai LLM par comparaison de chaînes. Ce que vous devez tester est votre propre logique : les profils de paramètres et l’algorithme de simulation.
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("vert", "rouge", "jaune");
}
}
Les deux premiers tests garantissent que personne ne modifie accidentellement les paramètres d’un profil important. Les deux derniers tests affirment les comportements décrits à la section 2 : une temperature basse concentre la probabilité sur le token principal, tandis que top_p = 0.8 exclut le token « violet ».
6. Erreurs courantes et quand l’éviter
Utiliser une temperature élevée pour des tâches exigeant de la précision. L’extraction JSON, la classification des tickets ou la génération SQL nécessitent des résultats cohérents. Une temperature de 0.8 ou plus augmente le risque de format incorrect et d’hallucinations.
Croire qu’une temperature basse prévient les « hallucinations ». Une temperature basse ne fait que pousser le modèle à choisir la réponse qu’il considère comme la plus probable. Si le modèle ne connaît pas la réponse, il peut quand même répondre avec confiance de manière incorrecte. Pour réduire les hallucinations, vous devez fournir le bon contexte (RAG) et valider la sortie.
Définir max_tokens trop bas pour économiser. Vous n’économisez rien si la réponse est tronquée et que vous devez la redemander. Limitez la longueur avec le prompt (« maximum 3 phrases »), max_tokens n’est qu’un filet de sécurité.
Ne pas vérifier finish_reason. C’est l’erreur la plus courante. L’utilisateur voit une phrase tronquée tandis que vos journaux ne détectent rien.
Transporter les paramètres en l’état vers un autre modèle. Chaque fournisseur a ses propres conventions :
- Échelles de temperature différentes. OpenAI utilise 0–2, Anthropic utilise 0–1, donc la même valeur 1.0 a des significations différentes.
- Les modèles de raisonnement (comme la série o d’OpenAI) peuvent refuser
temperatureettop_p. - Certains modèles nécessitent
max_completion_tokensà la place demax_tokens. Spring AI supporte ce champ viaOpenAiChatOptions.builder().maxCompletionTokens(...).
Note : Avec un modèle de raisonnement, les tokens de « pensée » interne comptent aussi dans la limite de sortie. Si vous définissez la limite trop basse, vous pouvez recevoir un contenu vide avec
finish_reason = length.
7. Conclusion
Cet article a parcouru les trois paramètres essentiels lors de l’appel d’une API LLM :
temperaturechange l’acuité de la distribution de probabilité. Les valeurs basses donnent des résultats stables, les valeurs élevées donnent des résultats variés.top_plimite l’ensemble des tokens candidats. Vous devez ajuster l’un ou l’autre, pas les deux à la fois.max_tokensest un plafond de longueur et de coût. Vérifiez toujoursfinish_reasonpour détecter les réponses tronquées.
Avec Spring AI, vous définissez les valeurs par défaut dans application.properties et remplacez par requête avec .options(...). Regrouper les ensembles de paramètres dans une énumération comme GenerationProfile rend le code plus lisible, plus facile à tester et plus facile à modifier lors du passage à un autre modèle.