Voltar ao início

AI Internals - Como LLMs Funcionam

16 min de leitura
Cover Image for AI Internals - Como LLMs Funcionam
Lucas LemosLucas Lemos

Introdução

Em AI Internals - Essenciais separamos o mundo em stack do modelo e stack do app, e deixamos o lado do modelo como caixa-preta: messages entram, completion sai.

Um large language model (LLM) é um preditor de próximo token com vocabulário fixo, orçamento de contexto fixo e um passo de sampling que transforma uma distribuição de probabilidade na string que você vê na resposta. Quando essas peças ficam claras, parâmetros de API deixam de ser folclore: temperature, max_tokens e erros de "context too long" apontam para estágios concretos do pipeline.

Este artigo fica na stack do modelo: tokens, treino versus inferência, o loop de decode do transformer, janelas de contexto e o key-value (KV) cache, e sampling. Prompts como superfície de controle vêm em Prompts & Context.

Tokens são o alfabeto de verdade

Você escreve strings. O modelo nunca vê caracteres ou palavras como unidades de primeira classe. Um tokenizer mapeia texto para uma sequência de token IDs inteiros de um vocabulário fixo (muitas vezes dezenas ou centenas de milhares de entradas). O mapa inverso transforma IDs de volta em texto na completion.

Modelos de chat modernos costumam usar um esquema de subword — Byte Pair Encoding (BPE) ou um parente próximo (SentencePiece, BPE no estilo tiktoken). Strings frequentes viram um único token (the, ing, https). Strings raras ou estranhas se partem em pedaços menores, às vezes até bytes.

Isso tem consequências práticas que aparecem em produção:

  • Contagem de tokens não é contagem de caracteres e não é contagem de palavras. Prosa em inglês muitas vezes cai em ~0.7–1.3 tokens por palavra; código, JSON e texto em outros idiomas podem ficar bem diferentes.
  • Edições mínimas no texto mudam bastante a tokenização. "hello" e " hello" (espaço na frente) costumam ser sequências diferentes. O mesmo vale para "JSON" vs "json".
  • Preço, rate limits e janelas de contexto quase sempre são em tokens, não em caracteres. Chutar com len(prompt) eventualmente cobra o preço.

Você não precisa reimplementar BPE para construir apps. Precisa medir tokens em tudo que toca custo ou contexto. Respostas de provedores muitas vezes devolvem usage.prompt_tokens e usage.completion_tokens. Ferramentas locais de contagem (tiktoken para vocabulários compatíveis com OpenAI, tokenizers Hugging Face para modelos abertos) importam quando você empacota chunks de retrieval-augmented generation (RAG) ou corta histórico antes do request falhar.

A mesma chamada de completion dos Essenciais, mas lendo o usage que o provedor já calculou:

usage é a posteriori. Se você precisa saber se um pacote RAG de 40k tokens cabe antes de pagar a chamada, conte localmente com a mesma família de tokenizer que o modelo usa. Tokenizers trocados dão orçamento errado.

Special tokens também importam nas bordas: chat templates envolvem suas messages com marcadores de role com os quais o modelo foi treinado (<|im_start|>, etc.). APIs de Chat Completions costumam aplicar esse template por você. Endpoints raw de completion e servers locais muitas vezes deixam isso na sua mão — template errado, respostas piores, mesmos pesos.

Next-token prediction

Por baixo, um LLM decoder-only é treinado para responder uma pergunta repetidamente: dados os tokens t1..tn, qual é a distribuição sobre o próximo token t(n+1)?

O treino minimiza o quanto o modelo se surpreende com o próximo token real em corpora grandes (e depois, com respostas preferidas no fine-tuning). Na inferência os pesos ficam congelados. O modelo ainda emite uma distribuição sobre o vocabulário; o software amostra (ou faz argmax) um ID, anexa, e repete até uma condição de parada.

Esse único loop explica bastante comportamento de produto:

  • O modelo não "busca uma resposta" e depois digita. Ele cresce a resposta um token por vez, condicionando em tudo que já está no contexto — inclusive nos tokens anteriores dele mesmo.
  • Erros cedo condicionam os seguintes. Um nome de tool errado no token 3 pode envenenar o resto da chamada.
  • Latência muitas vezes é dominada por quantos tokens você gera (decode), não só pelo tamanho do prompt (prefill) — mais sobre isso abaixo.

As pessoas dizem "o modelo sabe X." Operacionalmente isso significa: depois do treino, os pesos implementam uma distribuição condicional que coloca alta probabilidade em certas continuações quando o contexto tem um certo formato. Não há uma base de conhecimento separada dentro da chamada de API a menos que você coloque documentos ou resultados de tools nas messages (artigos seguintes).

Treino versus inferência

São trabalhos diferentes que compartilham arquitetura e vocabulário.

Pretraining roda em corpora enormes de texto (e código). O objetivo é language modeling: prever o próximo token. É daí que vem a maior parte da capacidade bruta e o formato do "conhecimento de mundo". Também é onde o modelo aprende sintaxe, fatos comuns que apareceram com frequência suficiente, e muito lixo da internet.

Post-training adapta esse modelo base na direção de assistentes úteis:

  • Supervised fine-tuning (SFT) — mostrar pares input → output desejado (formato de chat, traces de tool-use, estilos de recusa).
  • Estágios de preferência / estilo reinforcement learning (RL) — Reinforcement Learning from Human Feedback (RLHF), Direct Preference Optimization (DPO), Reinforcement Learning from AI Feedback (RLAIF) e parentes — empurrar a distribuição para respostas que humanos ou juízes ranqueiam melhor, e longe das proibidas ou de baixa qualidade.

Você quase nunca roda esses estágios num app. Você escolhe um checkpoint que um provedor ou projeto open-weight já publicou. Seu trabalho na inferência é dirigir essa distribuição fixa com contexto e parâmetros de decoding.

TreinoInferência (API / serve local)
PesosAtualizados pelo otimizadorCongelados
BatchBatches grandes sobre datasetsUma (ou poucas) sequences; latência interativa importa
DireçãoCalcular loss sobre próximos tokens conhecidosPrever próximos tokens desconhecidos, amostrar, anexar
Objetivo de hardwareThroughput / tokens por dólar de treinoLatência, concorrência, tokens por segundo para usuários
Sua alavancaEscolher modelo; raramente fine-tunarMessages, tools, retrieval, params de sampling, evals

Fine-tuning ainda existe como feature de produto, mas é opcional e caro perto de trabalho de prompt e retrieval. A maior parte dos sistemas desta série assume um modelo hospedado congelado.

Mais uma distinção que aparece em stacks de serving: prefill versus decode.

  • Prefill — processar os tokens do prompt, em geral em paralelo entre posições, e montar o KV cache inicial (tensores key e value em cache que a attention reutiliza).
  • Decode — gerar tokens novos um a um (ou em chunks especulativos pequenos). Cada passo reutiliza o cache e anexa uma posição.

System prompt longo mais um dump grande de RAG deixa o prefill pesado. Resposta longa do assistant deixa o decode pesado. UIs com streaming parecem "vivas" durante o decode porque os tokens chegam aos poucos; a pausa silenciosa no começo costuma ser prefill.

Dentro de um forward pass (o bastante para ser perigoso)

Você não precisa derivar attention do zero para entregar features. Precisa de um quadro do que queima compute e do que a janela de contexto está pagando.

Um decoder block típico empilha:

  1. Causal self-attention — cada posição monta uma query e atende a keys/values de posições anteriores (e dela mesma), não das futuras. Essa máscara causal é o motivo de o modelo poder ser treinado com teacher forcing e ainda rodar da esquerda para a direita na inferência.
  2. Feed-forward / multilayer perceptron (MLP) — transformada não linear por posição; uma fração grande da contagem de parâmetros mora aqui.
  3. Residuals e normalization — mantêm stacks profundas treináveis.

Attention é onde o contexto vira real. Informação do token 2 pode influenciar a distribuição no token 500 se os dois ainda cabem na janela. Também é onde o custo cresce com o comprimento da sequência: attention ingênua é O(n²) no número de tokens para os scores par a par (stacks de serving usam kernels, paging e outros truques; a pressão de escala permanece).

Embeddings na entrada são só uma tabela: token ID → vetor. Informação posicional entra na mistura (Rotary Position Embedding (RoPE) e amigos em modelos modernos) para a ordem não se perder. A camada final mapeia o último hidden state para logits — um score bruto por entrada do vocabulário — e esse vetor é o que o sampling consome.

Modelos multimodais adicionam encoders que transformam imagem ou áudio em embeddings parecidos com tokens no mesmo residual stream. Do ponto de vista do app eles ainda consomem orçamento de contexto e ainda produzem distribuições de próximo token; a história do tokenizer só ganha um irmão visual ou de áudio.

Janelas de contexto e o KV cache

A context window é o número máximo de tokens que a stack de serving segura numa geração — prompt mais completion, com contabilidade específica do provedor para special tokens e às vezes para payloads de tools.

Quando você passa do limite, a API erra ou o server trunca. Não existe memória infinita macia dentro de uma única chamada. Chat multi-turno parece memória só porque o seu app (ou a camada de sessão do provedor) reenvia as messages anteriores a cada turno. Cada reenvio paga tokens de novo.

Por que existe um limite duro:

  • Attention e armazenamento de cache crescem com o comprimento da sequência.
  • A qualidade muitas vezes cai quando você entope a janela com texto pouco relevante (efeitos de "lost in the middle" aparecem em evals).
  • Ofertas de produto são precificadas e planejadas em capacidade em torno do contexto máximo.

O KV cache é a estrutura em tempo de inferência que torna o decode viável. Para cada layer e cada posição passada de token, o server guarda keys e values para não recomputar o prompt inteiro a cada token novo. O tamanho do cache escala com layers × heads × dimensions × sequence length × precisão. Contextos longos são um problema de memória tanto quanto de floating-point operations (FLOPs) — por isso o batch size por GPU cai quando o contexto cresce, e por isso modelos "128k context" ainda cobram pelo uso.

Regras práticas que saem disso:

  • Coloque instruções estáveis uma vez; não as duplique em cada chunk recuperado.
  • Resuma ou descarte turnos antigos em vez de anexar para sempre.
  • Retrieval existe para você não colar corpora inteiros na janela — essa é a ponte para Embeddings & Retrieval e RAG.
  • max_tokens (ou max_completion_tokens) limita o comprimento da saída; não aumenta a janela. Tokens do prompt mais saída máxima ainda precisam caber.

Finish reasons na resposta dizem como o loop parou: stop (natural / stop sequence), length (bateu no teto de saída), content_filter, tool calls, e por aí. length em geral significa truncamento no meio do raciocínio — muitas vezes pior do que pedir uma resposta mais curta de antemão.

Dos logits ao texto: sampling

Depois do forward pass você tem logits z sobre o vocabulário. Decoding greedy pega o logit mais alto. A maioria das UIs de chat não faz isso por padrão — elas amostram, e por isso o mesmo prompt pode render respostas diferentes.

Controles comuns (os nomes variam por API; a matemática é compartilhada):

Temperature escala os logits antes do softmax. Softmax transforma logits escalados em probabilidades p_i ∝ exp(z_i / T). Temperature mais baixa agudiza a distribuição; mais alta a achata.

  • T → 0 se aproxima de greedy (numericamente você só faz argmax).
  • T = 1 deixa os logits como no treino.
  • T > 1 gasta mais probabilidade na cauda longa — criativo, instável, às vezes nonsense.

Top-k mantém só os k logits mais altos, renormaliza, depois amostra.

Top-p (nucleus) mantém o menor conjunto de tokens cuja probabilidade cumulativa é pelo menos p, renormaliza, depois amostra. Adapta melhor a distribuições pontiagudas versus flat do que um k fixo.

Stop sequences encerram o decode quando o modelo emite uma string escolhida (ou sequência de tokens). APIs de chat também param em fronteiras de role que os templates definem.

Presence / frequency penalties (quando expostas) ajustam logits para desencorajar repetição. São instrumentos grosseiros; prompts melhores e temperature mais baixa muitas vezes resolvem repetição com menos dano colateral.

Compare duas temperatures num prompt criativo curto e logue o texto. Mantenha o resto fixo (top_p, modelo, seed se a API suportar):

Para extração, classificação e a maior parte dos loops de controle com tool-calling, temperature baixa (muitas vezes 0 ou perto) é o default que desperdiça menos tempo. Guarde temperature alta para ideação onde diversidade é o ponto e você mesmo vai filtrar ou ranquear saídas.

Determinismo é mais fraco do que as pessoas esperam. temperature: 0 costuma ser quase greedy, mas stacks de provedor ainda podem variar entre réplicas, batching e roteamento em Mixture of Experts (MoE) esparso. Se você precisa de evals reproduzíveis, fixe a versão do modelo, registre os parâmetros e trate igualdade bit a bit como bônus — não como garantia.

Mapeando knobs da API para o pipeline

Um request no estilo Chat Completions é um wrapper fino sobre os estágios acima:

Campo do requestEstágio que atinge
messages / texto do promptTokenize → prefill → KV cache
tools / functionsSchema extra no contexto; decode restrito depois
temperature, top_p, top_kSampling a partir dos logits
max_tokens / max_completion_tokensCondição de parada do decode
stopCondição de parada do decode
seed (quando suportado)Random number generator (RNG) do sampler
logit_bias / grammar / JSON modeMáscara ou bias nos logits antes do sampling

Structured output e JSON mode não são um segundo cérebro. Eles restringem ou enviesam quais tokens são legais enquanto se amostra, para a continuação ter mais chance de parsear. O modelo ainda pode emitir nonsense confiante dentro de um JSON válido — por isso grounding e evals importam depois.

Streaming (stream: true) não muda a matemática. Muda a entrega: o server descarrega tokens conforme o decode os produz. Seu client ainda concatena na mesma string final; você só ganha TTFT (time to first token) e uma UI progressiva.

Modos de falha que são de fato da stack do modelo

Antes de reescrever o prompt pela quinta vez, cheque se a falha é mecânica:

  • Context overflow — prompt + saída reservada passam da janela. Encolha histórico, chunks ou max_tokens.
  • JSON truncado / corte no meio da frasefinish_reason é length. Suba o orçamento de saída ou peça estrutura mais curta.
  • Variância selvagem entre runs — temperature / top-p altos demais para uma tarefa de controle.
  • Loops de repetição — patologia de decoding; baixe temperature, penalties ou stop sequences; às vezes contexto mais curto ajuda.
  • Surpresas de tokenizer — conta de custo ou truncamento errada porque você contou caracteres ou usou o vocab errado.
  • Template mismatch — modelo local com chat template que não bate com o treino.

Falhas de app (retrieval ruim, tools faltando, evals fracos) dominam quando a chamada de fato roda. Elas ganham artigos próprios. O ponto aqui não é culpar o modelo por tudo — é reconhecer quando o gargalo é tokens, janela ou sampling.

Conclusão

Uma chamada de LLM é um loop tokenize → prefill → sample-and-append repetido → detokenize sobre uma distribuição congelada de próximo token. O tamanho do contexto é um orçamento duro sustentado por attention e memória de KV cache. Temperature e companhia só remodelam a distribuição da qual você amostra; não adicionam conhecimento.

O próximo é Prompts & Context — messages, system prompts e structured output como superfície de controle em cima dessa maquinaria.