Chamando a API LLM: temperature, top_p e max_tokens
1. Visão geral
Ao chamar uma API LLM, além do prompt, você também envia alguns parâmetros de geração de texto. Os três parâmetros mais comumente usados são temperature, top_p e max_tokens. Eles determinam se as respostas são estáveis ou criativas, e qual será sua duração.
Este artigo explica o mecanismo de cada parâmetro através de uma pequena simulação em Java. Depois você aprenderá a configurá-los com Spring AI e detectará respostas truncadas. Ao final, você saberá qual valor escolher para cada tipo de tarefa.
2. Como o modelo seleciona tokens
Em cada passo, a LLM calcula uma pontuação (logit) para cada token que pode aparecer a seguir. A função softmax converte esses logits em probabilidades. Depois o modelo amostra um token de acordo com essa distribuição.
temperature e top_p não alteram o “conhecimento” do modelo. Eles apenas interferem nesta etapa de amostragem.
2.1. Temperature: tornando a distribuição mais acentuada ou mais suave
A fórmula softmax com temperatura T:
p_i = exp(z_i / T) / Σ exp(z_j / T)
O código a seguir simula o modelo escolhendo a próxima palavra para a frase “Chiếc nón lá màu …”:
import java.util.*;
public class SamplingDemo {
static final List<String> TOKENS = List.of("xanh", "đỏ", "vàng", "tím");
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);
}
}
}
A função softmax divide cada logit por temperature antes de tirar exp. A função main imprime a distribuição com três níveis de temperatura:
T=0.5: xanh=0.83 đỏ=0.11 vàng=0.04 tím=0.02
T=1.0: xanh=0.57 đỏ=0.21 vàng=0.13 tím=0.09
T=1.5: xanh=0.46 đỏ=0.24 vàng=0.17 tím=0.13
Os resultados mostram claramente o efeito da temperature:
- T baixo (0,5): o token “xanh” ocupa 83%. O modelo quase sempre escolhe a resposta mais popular. Respostas estáveis e previsíveis.
- T alto (1,5): a probabilidade fica mais uniforme. “tím” aumenta de 2% para 13%. Respostas mais diversas, mas mais propensas a sair do assunto.
- T = 0: teoricamente é sempre escolher o token com maior probabilidade (greedy).
Nota:
temperature = 0não garante resultados idênticos entre as chamadas. Cálculos de ponto flutuante em GPU e como o provedor agrupa o batch ainda podem criar pequenas diferenças. Não escreva testes comparando strings com precisão com a saída do LLM real.
2.2. Top_p: descartando a cauda de baixa probabilidade
top_p (amostragem por núcleo) funciona de forma diferente. O modelo ordena os tokens em ordem de probabilidade decrescente. Ele retém o menor grupo cuja probabilidade cumulativa atinge o limite p, e então amostra apenas dentro desse grupo.
Você adiciona o método a seguir em 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;
}
Chamar nucleus(softmax(LOGITS, 1.0), 0.8) retornará:
[xanh, đỏ, vàng]
A soma de “xanh” e “đỏ” é 0,785, ainda não atinge 0,8. Portanto, “vàng” é adicionado e o total se torna 0,914. O token “tím” é completamente descartado, não importa quanto a temperatura seja elevada.
A principal diferença entre os dois parâmetros:
temperaturemuda a forma da distribuição.top_pmuda o conjunto de candidatos permitidos para seleção.
Nota: A maioria dos provedores recomenda ajustar apenas um dos dois parâmetros e deixar o outro em seu valor padrão (
top_p = 1.0). Ajustar ambos ao mesmo tempo torna difícil saber qual mudança causa o resultado.
3. Max tokens: limitando o comprimento da saída
max_tokens é o número máximo de tokens que o modelo pode gerar na resposta. Não é um comprimento de destino. O modelo não tenta escrever especificamente essa quantidade. Quando atinge o limite, ele para imediatamente, mesmo que esteja no meio de uma frase.
Há três pontos que você precisa entender:
- Tokens não são palavras. Para o inglês, um token é aproximadamente 3/4 de uma palavra. Vietnamita com marcas diacríticas geralmente custa mais tokens para a mesma quantidade de conteúdo.
- Custo é calculado por token.
max_tokensé um limite para custo e latência. - A janela de contexto inclui entrada e saída. Se o prompt já usou quase toda a janela de contexto, o restante para saída será muito pequeno.
Quando truncado, a API retorna finish_reason como length em vez de stop. Uma resposta abreviada de uma API do tipo OpenAI se parece com isto:
{
"choices": [{
"message": { "role": "assistant", "content": "Nón lá Huế được làm từ lá cọ non, phơi" },
"finish_reason": "length"
}],
"usage": { "prompt_tokens": 42, "completion_tokens": 20, "total_tokens": 62 }
}
O conteúdo para no meio porque max_tokens = 20. Se o seu código apenas ler content, você exibirá ao usuário uma frase truncada sem nem saber.
4. Configuração com Spring AI
Esta seção usa Spring AI para chamar OpenAI de Spring Boot 3.x.
4.1. Dependência e configuração padrão
Você adiciona BOM e starter ao 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>
Os valores padrão para toda a aplicação estão em 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 requisição que não sobrescrever usará temperature = 0,3 e no máximo 300 tokens de saída. A chave de API é extraída da variável de ambiente, não é codificada no arquivo.
4.2. Um perfil de parâmetros para cada tipo de tarefa
Cada tarefa precisa de um conjunto diferente de parâmetros. Em vez de espalhar números “mágicos” por todo o código, você os agrupa em uma enum:
import org.springframework.ai.openai.OpenAiChatOptions;
public enum GenerationProfile {
EXTRACTION(0.0, 1.0, 200), // extração de dados, classificação
SUPPORT_REPLY(0.4, 1.0, 400), // resposta ao cliente
MARKETING_COPY(0.9, 1.0, 600); // descrição de produto, slogan
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 descreve um tipo de tarefa. EXTRACTION precisa de resultados consistentes então usa temperature 0. MARKETING_COPY precisa de diversidade então usa 0,9. Todos os três mantêm top_p = 1.0, isto é, apenas ajusta temperature conforme recomendado na seção 2.2.
4.3. Sobrescrevendo parâmetros por requisição e detectando truncamento
Spring Boot configura automaticamente o bean ChatClient.Builder. Você o injeta no serviço:
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("Você é copywriter da Nón Lá Shop. Escreva no máximo 3 frases.")
.build();
}
public String describe(String productName) {
ChatResponse response = chatClient.prompt()
.user(u -> u.text("Escreva uma descrição para o produto: {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(
"Descrição foi truncada após " + response.getMetadata().getUsage().getCompletionTokens() + " tokens");
}
return result.getOutput().getText();
}
}
Análise de cada parte:
.options(...)sobrescreve os valores emapplication.properties, apenas para esta requisição..chatResponse()retorna a resposta completa com metadados, em vez de apenas a string como.content().getFinishReason()informa o motivo da parada. Spring AI pode retornarLENGTHem maiúsculas, então o código usaequalsIgnoreCase.getUsage().getCompletionTokens()informa o número real de tokens de saída. Esse número é útil para log e rastreamento de custos.
A exceção é definida simplesmente como:
public class TruncatedResponseException extends RuntimeException {
public TruncatedResponseException(String message) {
super(message);
}
}
Quando você encontra essa exceção, você tem duas opções. A primeira maneira é aumentar max_tokens e tentar novamente. A segunda maneira é restringir o prompt para solicitar respostas mais curtas. A segunda maneira é geralmente mais barata e produz resultados mais concisos.
5. Testes
Você não deve fazer testes da saída real do LLM comparando strings. O que você deve testar é a sua própria lógica: os perfis de parâmetros e algoritmos de simulação.
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("xanh", "đỏ", "vàng");
}
}
Os dois primeiros testes garantem que ninguém mude acidentalmente os parâmetros de um perfil importante. Os dois testes seguintes afirmam os comportamentos descritos na seção 2: temperatura baixa concentra probabilidade no token principal, enquanto top_p = 0,8 remove o token “tím”.
6. Erros comuns e quando não usar
Usar temperatura alta para tarefas que exigem precisão. Extração JSON, classificação de tickets ou geração de SQL precisam de resultados consistentes. Temperatura 0,8 ou superior aumenta a chance de formatação incorreta e alucinação.
Pensar que temperatura baixa previne “alucinações”. Temperatura baixa apenas faz com que o modelo escolha a resposta que considera mais provável. Se o modelo não sabe a resposta, ele ainda pode dizer algo errado com confiança. Para reduzir alucinações, você precisa fornecer contexto correto (RAG) e validar a saída.
Definir max_tokens muito baixo para economizar. Você não economiza nada se a resposta for truncada e tiver que chamar novamente. Limite o comprimento através do prompt (“máximo 3 frases”), enquanto max_tokens apenas atua como uma rede de segurança.
Não verificar finish_reason. Este é o erro mais comum. Os usuários veem respostas truncadas, enquanto seu log não registra nada.
Levar parâmetros diretos para outro modelo. Cada provedor tem suas próprias convenções:
- Escalas de temperatura diferentes. OpenAI usa 0–2, Anthropic usa 0–1, então o mesmo valor 1.0 tem significados diferentes.
- Modelos de raciocínio (como a linha o-series do OpenAI) podem rejeitar
temperatureetop_p. - Alguns modelos exigem
max_completion_tokensem vez demax_tokens. Spring AI suporta este campo viaOpenAiChatOptions.builder().maxCompletionTokens(...).
Nota: Com modelos de raciocínio, tokens de “pensamento” interno também são contados no limite de saída. Se você definir o limite muito baixo, você pode receber conteúdo vazio com
finish_reason = length.
7. Conclusão
O artigo passou por três parâmetros essenciais ao chamar a LLM API:
temperaturealtera a nitidez da distribuição de probabilidade. Valores baixos produzem resultados estáveis, valores altos produzem resultados diversos.top_plimita o conjunto de tokens candidatos. Você deve ajustar um dos dois parâmetros, não ajuste ambos ao mesmo tempo.max_tokensé o teto de comprimento e custo. Sempre verifiquefinish_reasonpara detectar respostas truncadas.
Com Spring AI, você define valores padrão em application.properties e sobrescreve por requisição usando .options(...). Agrupar conjuntos de parâmetros em uma enum como GenerationProfile torna o código legível, fácil de testar e fácil de ajustar ao mudar para outro modelo.