Llamar a LLM API: temperature, top_p y max tokens
1. Descripción general
Cuando llamas a una LLM API, además del prompt, envías algunos parámetros de generación de texto. Los tres parámetros más comúnmente utilizados son temperature, top_p y max_tokens. Determinan si la respuesta es estable o creativa, y cuánto tiempo es.
Este artículo explica el mecanismo de cada parámetro mediante una pequeña simulación en Java. Luego configurarás con Spring AI y descubrirás cuándo las respuestas se cortan. Después de leer, sabrás qué valores elegir para cada tipo de tarea.
2. Cómo el modelo selecciona un token
En cada paso, el LLM calcula una puntuación (logit) para cada token posible que podría aparecer a continuación. La función softmax convierte estos logits en probabilidades. Luego el modelo muestrea un token de acuerdo con esa distribución.
temperature y top_p no cambian el “conocimiento” del modelo. Solo intervienen en este paso de muestreo.
2.1. Temperature: hacer la distribución más aguda o más plana
La fórmula softmax con temperature T:
p_i = exp(z_i / T) / Σ exp(z_j / T)
El siguiente código simula un modelo eligiendo la siguiente palabra para la oración “El sombrero de palma es de color …”:
import java.util.*;
public class SamplingDemo {
static final List<String> TOKENS = List.of("verde", "rojo", "amarillo", "morado");
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 función softmax divide cada logit por temperature antes de tomar exp. La función main imprime la distribución con tres niveles de temperature:
T=0.5: verde=0.83 rojo=0.11 amarillo=0.04 morado=0.02
T=1.0: verde=0.57 rojo=0.21 amarillo=0.13 morado=0.09
T=1.5: verde=0.46 rojo=0.24 amarillo=0.17 morado=0.13
Los resultados muestran claramente el efecto de temperature:
- T bajo (0.5): el token “verde” ocupa el 83%. El modelo casi siempre elige la respuesta más probable. Las respuestas son estables y predecibles.
- T alto (1.5): las probabilidades se distribuyen más uniformemente. “morado” aumenta del 2% al 13%. Las respuestas son más diversas, pero también más propensas a desviarse del tema.
- T = 0: en teoría siempre elige el token con la probabilidad más alta (greedy).
Nota:
temperature = 0no garantiza resultados idénticos entre llamadas. La aritmética de números reales en GPU y cómo el proveedor agrupa datos por lotes aún pueden crear pequeñas diferencias. No escribas pruebas que comparen cadenas exactas con la salida de un LLM real.
2.2. Top_p: cortar la cola de probabilidad baja
top_p (nucleus sampling) funciona de manera diferente. El modelo ordena los tokens por probabilidad descendente. Retiene el grupo más pequeño con probabilidad acumulada que alcanza el umbral p, luego solo muestrea dentro de ese grupo.
Añades el siguiente método a 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;
}
Llamar a nucleus(softmax(LOGITS, 1.0), 0.8) devolverá:
[verde, rojo, amarillo]
La suma de “verde” y “rojo” es 0.785, aún no alcanza 0.8. Por lo tanto, “amarillo” se añade y la suma se convierte en 0.914. El token “morado” se descarta, sin importar cuán alto sea temperature.
La diferencia clave entre los dos parámetros:
temperaturecambia la forma de la distribución.top_pcambia el conjunto de candidatos permitido elegir.
Nota: La mayoría de proveedores recomiendan ajustar solo uno de los dos parámetros y dejar el otro en su valor predeterminado (
top_p = 1.0). Ajustar ambos a la vez hace difícil saber cuál cambio causó el resultado.
3. Max tokens: limitar la longitud de salida
max_tokens es el número máximo de tokens que el modelo puede generar en una respuesta. No es una longitud objetivo. El modelo no “intenta escribir lo suficiente” para alcanzar este número. Cuando alcanza el límite, se detiene inmediatamente, incluso si está a mitad de una frase.
Hay tres puntos que necesitas entender:
- Los tokens no son palabras. En inglés, un token equivale aproximadamente a 3/4 de una palabra. El vietnamita con acentos generalmente requiere más tokens por la misma cantidad de contenido.
- Los costos se calculan por tokens.
max_tokenses un punto de corte para costo y latencia. - La ventana de contexto incluye tanto entrada como salida. Si el prompt ya ha usado casi toda la ventana de contexto, la salida tendrá muy poco espacio.
Cuando se corta, la API devuelve finish_reason como length en lugar de stop. Una respuesta abreviada de una API de estilo OpenAI se vería así:
{
"choices": [{
"message": { "role": "assistant", "content": "El sombrero de palma de Hue está hecho de hojas de palma jóvenes, secadas" },
"finish_reason": "length"
}],
"usage": { "prompt_tokens": 42, "completion_tokens": 20, "total_tokens": 62 }
}
El contenido se detiene a mitad de camino porque max_tokens = 20. Si tu código solo lee content, mostrarás al usuario una frase incompleta sin ni siquiera saberlo.
4. Configuración con Spring AI
Esta sección utiliza Spring AI para llamar a OpenAI desde Spring Boot 3.x.
4.1. Dependencia y configuración predeterminada
Añades el BOM y starter a 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>
Los valores predeterminados para toda la aplicación están en 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
Cada solicitud que no se sobrescriba usará temperature = 0.3 y un máximo de 300 tokens de salida. La clave de API se toma de una variable de entorno, no está codificada en el archivo.
4.2. Un perfil de parámetros para cada tipo de tarea
Cada tarea necesita un conjunto diferente de parámetros. En lugar de dispersar números “mágicos” en el código, los agrupas en una enumeración:
import org.springframework.ai.openai.OpenAiChatOptions;
public enum GenerationProfile {
EXTRACTION(0.0, 1.0, 200), // extracción de datos, clasificación
SUPPORT_REPLY(0.4, 1.0, 400), // respuesta al cliente
MARKETING_COPY(0.9, 1.0, 600); // escribir descripciones de productos, eslóganes
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();
}
}
Cada constante describe un tipo de tarea. EXTRACTION necesita resultados consistentes, así que usa temperature 0. MARKETING_COPY necesita diversidad, así que usa 0.9. Los tres mantienen top_p = 1.0, lo que significa que solo se ajusta temperature como se recomendó en la sección 2.2.
4.3. Sobrescribir parámetros por solicitud y detectar truncamiento
Spring Boot configura automáticamente el bean ChatClient.Builder. Lo inyectas en el servicio:
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("Eres un redactor de la tienda Nón Lá Shop. Escribe máximo 3 oraciones.")
.build();
}
public String describe(String productName) {
ChatResponse response = chatClient.prompt()
.user(u -> u.text("Escribe una descripción para el producto: {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(
"La descripción se truncó después de " + response.getMetadata().getUsage().getCompletionTokens() + " tokens");
}
return result.getOutput().getText();
}
}
Analicemos cada parte:
.options(...)sobrescribe los valores enapplication.properties, solo para esta solicitud..chatResponse()devuelve la respuesta completa con metadatos, en lugar de solo la cadena como.content().getFinishReason()te dice por qué se detuvo. Spring AI puede devolverLENGTHen mayúsculas, así que el código usaequalsIgnoreCase.getUsage().getCompletionTokens()te dice cuántos tokens de salida se usaron realmente. Este número es útil para logging y seguimiento de costos.
La excepción se define simplemente así:
public class TruncatedResponseException extends RuntimeException {
public TruncatedResponseException(String message) {
super(message);
}
}
Cuando encuentres esta excepción, tienes dos opciones. La primera es aumentar max_tokens e intentar de nuevo. La segunda es reducir el prompt para solicitar una respuesta más corta. La segunda opción suele ser más barata y produce resultados más concisos.
5. Pruebas
No debes probar la salida del LLM real comparando cadenas. Lo que debes probar es tu propia lógica: los perfiles de parámetros y el algoritmo de simulación.
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("verde", "rojo", "amarillo");
}
}
Las dos primeras pruebas aseguran que nadie cambie accidentalmente los parámetros de un perfil importante. Las dos pruebas siguientes confirman los comportamientos descritos en la sección 2: temperature bajo concentra probabilidad en el token superior, mientras que top_p = 0.8 excluye el token “morado”.
6. Errores comunes y cuándo no usar
Usar temperature alto para tareas que requieren precisión. La extracción de JSON, clasificación de tickets o generación de SQL necesitan resultados consistentes. Temperature 0.8 o superior aumenta el riesgo de formato incorrecto e información inventada.
Pensar que temperature bajo previene “alucinaciones”. Temperature bajo solo hace que el modelo elija lo que cree que es la respuesta más probable. Si el modelo no sabe la respuesta, puede estar seguro y decir algo incorrecto. Para reducir alucinaciones, necesitas proporcionar contexto correcto (RAG) y verificar la salida.
Establecer max_tokens muy bajo para ahorrar. No ahorras nada si la respuesta se trunca y tienes que llamar de nuevo. Limita la longitud mediante el prompt (“máximo 3 oraciones”), mientras que max_tokens solo actúa como red de seguridad.
No verificar finish_reason. Este es el error más común. El usuario ve una oración incompleta, pero tu log no registra nada.
Llevar los parámetros tal cual a un modelo diferente. Cada proveedor tiene sus propias convenciones:
- Los rangos de temperature varían. OpenAI usa 0–2, Anthropic usa 0–1, así que el mismo valor 1.0 significa algo diferente.
- Los reasoning models (como la línea o-series de OpenAI) pueden rechazar
temperatureytop_p. - Algunos modelos requieren
max_completion_tokensen lugar demax_tokens. Spring AI lo soporta a través deOpenAiChatOptions.builder().maxCompletionTokens(...).
Nota: Con reasoning models, los tokens de “pensamiento” internos también cuentan hacia el límite de salida. Si estableces el límite muy bajo, puedes recibir contenido vacío con
finish_reason = length.
7. Conclusión
Este artículo ha cubierto los tres parámetros principales al llamar a una LLM API:
temperaturecambia lo aguda que es la distribución de probabilidades. Valores bajos producen resultados estables, valores altos producen resultados diversos.top_plimita el conjunto de tokens candidatos. Debes ajustar uno u otro parámetro, no ambos a la vez.max_tokenses el techo de longitud y costo. Siempre verificafinish_reasonpara detectar si la respuesta se truncó.
Con Spring AI, estableces valores predeterminados en application.properties e anulas por solicitud usando .options(...). Agrupar los conjuntos de parámetros en una enumeración como GenerationProfile hace que el código sea más legible, más fácil de probar y más fácil de ajustar cuando cambias de modelo.