mozak.tech Engenharia de IA Corporativa 12%

Parte I — Alicerces da Inteligência Artificial Moderna

1.2 — Como os Modelos de Linguagem Processam Texto: Tokens, Vetores e Atenção (LLMs)

Objetivo da Aula

Explicar o pipeline completo de processamento de um LLM: texto → tokens → embeddings → atenção → distribuição de próximo token

Implementar similaridade cosseno do zero, entendendo cada componente da fórmula geometricamente

Interpretar métricas de uso da API (input_tokens, output_tokens) para estimar custo de chamadas em produção

Distinguir embedding didático (vetores de características manuais) de embedding de produção (vetores de alta dimensão aprendidos)

Debugar comportamentos inesperados de LLMs usando conhecimento de como tokens funcionam

Por que isso importa

Todo bug estranho de LLM tem uma explicação em como o modelo processa tokens. Veja estes exemplos reais:

“Por que o LLM errou na contagem de letras?” Pergunta clássica: “Quantas letras ‘r’ tem em ‘strawberry’?” A maioria dos LLMs erra (responde 2 em vez de 3). Motivo: a palavra “strawberry” não é processada como caracteres individuais — é tokenizada como ["str", "aw", "berry"] ou similar. O modelo “vê” subpalavras, não letras. Quem entende tokenização prevê esse comportamento.

“Por que a API ficou cara de repente?” Um sistema de suporte enviava o histórico completo de conversas para cada mensagem. Com usuários fidelizados acumulando 50+ mensagens, os input_tokens por chamada explodiram. O custo não é fixo por chamada — é proporcional ao contexto. Quem não entende tokenização não prevê esse comportamento.

“Por que o modelo esqueceu o que foi dito no começo?” Context window tem limite. Quando a conversa excede o limite, o início é truncado. Saber isso permite projetar sistemas com summarização incremental em vez de truncação silenciosa.

Esses três problemas — contagem de caracteres, custo variável, esquecimento — têm a mesma raiz: tokenização e context window. Entenda isso e você prevê uma classe inteira de bugs antes de escrever uma linha de código.

Conceitos Fundamentais

Tokenização: a fronteira entre texto e números

LLMs não processam texto. Processam sequências de inteiros. O processo de converter texto em inteiros é a tokenização.

Texto: "Attention is all you need"

Tokens: [13362, 374, 682, 499, 1205]  (exemplo com tokenização GPT)

Cada número é um token — uma unidade de “vocabulário” que o modelo aprendeu durante o treinamento. O vocabulário de modelos modernos tem ~50.000-130.000 tokens.

Como os tokens são definidos?

Fundamento: Tokenização e Custo

O vocabulário de um modelo é fixo, definido no treino: tipicamente 50k a 130k subpalavras (não palavras inteiras). Palavras frequentes viram um token; palavras raras são quebradas em fragmentos. Consequência direta: português custa mais tokens que inglês porque o corpus de treino era predominantemente inglês — palavras portuguesas são menos frequentes no vocabulário e fragmentam mais. JSON e código também tokenizam mal. Na prática, isso significa que o mesmo conteúdo em português pode custar 20-40% mais tokens — e mais tokens = mais custo e mais context window consumido.

Algoritmos como BPE (Byte Pair Encoding) ou SentencePiece analisam corpus de texto e identificam as sequências mais frequentes. Palavras comuns viram um único token; palavras raras são divididas em subpalavras.

Tokens comuns (1 token cada):

  " the"    → 262

  " of"     → 286

  " and"    → 290

  " a"      → 257



Palavras raras (múltiplos tokens):

  "Transformer" → ["Transform", "er"]  (2 tokens)

  "LangChain"   → ["Lang", "Chain"]    (2 tokens)

  "tokenização" → ["token", "iza", "ção"] (3 tokens, no GPT)

Implicações práticas:

Situação

Tokens

Custo relativo

Texto em inglês

~1 token por 4 caracteres

Baseline

Texto em português

~1 token por 3-4 caracteres

Levemente mais caro

Código com identação

Espaços viram tokens

Significativamente mais caro

JSON com muita estrutura

{, ", :, } cada viram tokens

Caro

Markdown

Símbolos #, **, - viram tokens

Levemente mais caro

Isso explica por que “escreva em JSON” é mais caro do que “escreva em texto simples” quando o formato não é necessário.

Embeddings: texto como ponto no espaço

Fundamento: Embedding

Um embedding é um vetor — uma lista de números reais — que representa um token num espaço de centenas ou milhares de dimensões. A posição desse ponto no espaço codifica significado: tokens semanticamente próximos ficam próximos no espaço. Não se mede distância euclidiana, mas cosseno — o ângulo entre os vetores — porque o que importa é a direção, não o comprimento. Na prática, isso significa que "banco financeiro" e "banco de dados" ficam em regiões diferentes do espaço, mesmo tendo a mesma palavra. Essa representação é a base física do RAG: buscar contexto relevante é encontrar vetores próximos ao vetor da sua pergunta.

Após tokenizar, cada token é mapeado para um embedding: um vetor de números reais de alta dimensão. Modelos modernos usam dimensões entre 768 (modelos menores) e 12.288 (GPT-4).

"gato" → [0.23, -0.87, 0.14, 0.56, ..., -0.33]  (12.288 dimensões)

"cachorro" → [0.21, -0.82, 0.16, 0.59, ..., -0.28]  (similar ao gato)

"avião" → [-0.43, 0.12, -0.78, 0.03, ..., 0.91]   (diferente)

A chave: tokens com significado similar têm embeddings próximos no espaço. Isso não foi programado explicitamente — emergiu do treinamento em bilhões de exemplos de texto.

Visualização geométrica (em 3D para simplificar):

Espaço semântico (simplificado):

          animais

             │

     gato ───┼─── cachorro

             │

             │              objetos tecnológicos

             │         computador ─── smartphone

─────────────┼─────────────────────────────────── eixo abstrato

             │

       veículos

     avião ──┤

    ônibus ──┘

Similaridade cosseno mede o ângulo entre dois vetores — não a distância euclidiana. Isso é importante porque normaliza pela magnitude dos vetores.

cos(θ) = (A · B) / (|A| × |B|)



Onde:

  A · B = produto escalar = Σ(Ai × Bi)

  |A| = norma de A = √(Σ Ai²)

  

Resultado:

  1.0 = vetores apontam na mesma direção (identicos semanticamente)

  0.0 = vetores perpendiculares (sem relação semântica)

 -1.0 = vetores opostos (semanticamente opostos)

Self-Attention: como tokens se relacionam

Fundamento: Softmax e Temperatura

Softmax pega um conjunto de scores e os converte em probabilidades que somam 1: aplica exponencial em cada score e divide pelo total. A temperatura reescala esses scores antes do softmax: temperatura baixa (0.1) deixa a distribuição pontuda — o token mais provável domina. Temperatura alta (1.5) nivela as probabilidades — tokens menos prováveis ganham chance. Em produção, temperatura baixa = saída determinística e previsível (boa para extração estruturada); temperatura alta = mais variação criativa (boa para brainstorming). A temperatura 0 é o equivalente a "sempre o token mais provável".

Self-attention é o mecanismo que permite que cada token “olhe” para todos os outros tokens no contexto ao mesmo tempo. É o coração da arquitetura Transformer.

Sequência: "O gato sentou no tapete"



Para gerar o próximo token, o modelo calcula:

  Quanto "O" importa para o contexto atual?     → 0.05

  Quanto "gato" importa para o contexto atual?  → 0.45

  Quanto "sentou" importa para o contexto atual?→ 0.30

  Quanto "no" importa para o contexto atual?    → 0.10

  Quanto "tapete" importa para o contexto atual?→ 0.10



(pesos somam 1.0 — isso é um softmax sobre os "scores de atenção")

Os pesos de atenção são calculados a partir de três matrizes aprendidas: Query (Q), Key (K), Value (V). A intuição: - Query: “O que estou procurando?” - Key: “O que eu tenho?” - Value: “O que contribuo para o output?”

Fundamento: Self-Attention Q/K/V

A atenção funciona como uma busca: cada token procura (Query) nos outros tokens o que é relevante para ele, os outros tokens anunciam o que têm (Key), e contribuem com seu conteúdo (Value). O custo é O(N²): cada posição compara com todas as outras. Com N=8.192 tokens, são 67 milhões de comparações por camada. Isso explica por que context window longa aumenta latência e custo não linearmente — é quadrático, não linear. Em produção, esse é o principal motivo para limitar o tamanho do contexto quando custo e latência importam.

Atenção(Q, K, V) = softmax(Q × Kᵀ / √dk) × V

Por que dividir por √dk? Para estabilizar o gradiente durante o treinamento. Sem isso, produtos escalares grandes causariam softmax saturado (gradientes próximos de zero).

Geração autoregressiva: um token por vez

Fundamento: Geração Autoregressiva

O modelo gera um token de cada vez, e cada token novo é adicionado ao contexto antes de gerar o próximo. Isso explica o streaming: você recebe tokens conforme são gerados porque o modelo não sabe o final antes de chegar lá. Também explica por que max_tokens controla custo de output — o modelo para de gerar quando atinge esse limite. O fenômeno "lost in the middle" vem daí: como cada token presta atenção no contexto inteiro, informações no meio de um contexto longo competem com mais ruído. Posicione dados críticos no início ou no fim do prompt, não no meio.

LLMs geram texto token por token. Para gerar “O gato sentou no tapete”:

Passo 1: ["<início>"] → distribuição → escolhe "O"

Passo 2: ["<início>", "O"] → distribuição → escolhe " gato"

Passo 3: ["<início>", "O", " gato"] → distribuição → escolhe " sentou"

...

Cada passo processa o contexto completo (todos os tokens gerados até agora). Isso explica por que: - Streaming existe: você não precisa esperar todos os tokens para exibir resultados - Geração é lenta para textos longos: mais tokens anteriores = mais processamento por passo - max_tokens é um limite de segurança: sem ele, modelos podem gerar indefinidamente

Aprofundamento Técnico

Temperatura: controlando a “criatividade”

Quando o LLM calcula distribuição sobre o próximo token, aplica um parâmetro chamado temperatura antes do softmax:

# Logits brutos (scores antes do softmax):

logits = [3.2, 1.8, 0.9, 0.3, ...]  # cada posição = um token do vocabulário



# Com temperatura T:

logits_ajustados = [l / T for l in logits]

probs = softmax(logits_ajustados)

Temperatura

Efeito

Quando usar

T = 0.0

Sempre escolhe o token mais provável (greedy)

Extração de dados, respostas determinísticas

T = 0.3

Conservador, alta consistência

Sumarização, QA factual

T = 1.0

Distribuição “natural” do modelo

Geração de texto geral

T = 1.5+

Mais aleatório, mais “criativo”

Brainstorming, ficção

Atenção: temperatura não é “criatividade” de forma geral. Alta temperatura aumenta variância do output — pode gerar resultados mais interessantes OU mais incorretos. Para tarefas de código ou extração de informações, use temperatura baixa (0.0-0.3).

Como estimar custo antes de chamar a API

Anthropic cobra separadamente por input e output tokens. Preços de referência (2025):

Claude Haiku:

  Input:  $0.25 por 1M tokens = $0.00000025 por token

  Output: $1.25 por 1M tokens = $0.00000125 por token



Claude Sonnet:

  Input:  $3.00 por 1M tokens

  Output: $15.00 por 1M tokens



Claude Opus:

  Input:  $15.00 por 1M tokens

  Output: $75.00 por 1M tokens

Estimativa antes de chamar:

def estimar_custo(texto_input: str, max_tokens_output: int, modelo: str = "haiku") -> float:

    # Regra de ouro: ~4 caracteres por token (inglês), ~3.5 (português)

    tokens_input_estimados = len(texto_input) / 3.5

    

    precos = {

        "haiku":  {"input": 0.00000025, "output": 0.00000125},

        "sonnet": {"input": 0.000003,   "output": 0.000015},

        "opus":   {"input": 0.000015,   "output": 0.000075},

    }

    

    p = precos[modelo]

    custo_input = tokens_input_estimados * p["input"]

    custo_output_max = max_tokens_output * p["output"]

    

    return custo_input + custo_output_max



# Exemplo: sumarização de documento de 10 páginas com Haiku

custo = estimar_custo("documento de 10 páginas...", max_tokens_output=500, modelo="haiku")

print(f"Custo estimado: ${custo:.6f}")  # ~$0.000600

Context window na prática

Context window é o número máximo de tokens que o modelo processa em uma única chamada (input + output juntos).

Modelo

Context Window

Claude Haiku

200.000 tokens

Claude Sonnet

200.000 tokens

Claude Opus

200.000 tokens

GPT-4o

128.000 tokens

200.000 tokens ≈ 150.000 palavras ≈ 600 páginas de texto. Parece imenso — mas atenção:

Custo: cada token no contexto é cobrado, mesmo que seja histórico de conversa antiga

Atenção quadrática: processar 200k tokens é muito mais caro computacionalmente do que 2k tokens

Performance degrada: modelos tendem a “prestar menos atenção” a informações no meio de contextos muito longos (fenômeno “lost in the middle”)

Exemplos Anotados

Exemplo 1: Implementação de similaridade cosseno com geometria explicada

import math



def similaridade_cosseno(vec_a: list[float], vec_b: list[float]) -> float:

    """

    Calcula a similaridade entre dois vetores usando o ângulo entre eles.

    

    Por que cosseno em vez de distância euclidiana?

    Distância euclidiana é afetada pela magnitude dos vetores.

    Cosseno normaliza pela magnitude — dois vetores que apontam na mesma

    direção têm similaridade 1.0 independente do seu tamanho.

    

    Isso importa em embeddings porque a magnitude do vetor não representa

    a "força" do significado — apenas a direção representa semântica.

    """

    # Produto escalar: mede quanto os vetores "se alinham"

    # Se todos os componentes têm mesmo sinal → produto escalar alto (similar)

    # Se componentes têm sinais opostos → produto escalar negativo (oposto)

    produto_escalar = sum(a * b for a, b in zip(vec_a, vec_b))

    

    # Norma: tamanho do vetor no espaço N-dimensional

    # math.sqrt(Σ x²) é o teorema de Pitágoras generalizado para N dimensões

    norma_a = math.sqrt(sum(a ** 2 for a in vec_a))

    norma_b = math.sqrt(sum(b ** 2 for b in vec_b))

    

    # Proteção contra divisão por zero (vetor nulo)

    # Na prática, embeddings bem treinados não são nulos, mas é boa prática

    if norma_a == 0 or norma_b == 0:

        return 0.0

    

    # Dividir pelo produto das normas normaliza para [-1, 1]

    return produto_escalar / (norma_a * norma_b)





# Teste com embeddings simplificados: [tecnologia, animal, comida, transporte, emoção]

embeddings = {

    "gato":        [0.1, 0.9, 0.2, 0.0, 0.3],

    "cachorro":    [0.1, 0.8, 0.2, 0.0, 0.4],

    "computador":  [0.9, 0.0, 0.0, 0.1, 0.1],

    "smartphone":  [0.9, 0.0, 0.0, 0.0, 0.2],

    "pizza":       [0.0, 0.0, 0.9, 0.0, 0.3],

    "avião":       [0.2, 0.0, 0.0, 0.9, 0.1],

}



pares = [

    ("gato", "cachorro"),         # esperado: ~0.98 (muito similar)

    ("computador", "smartphone"), # esperado: ~0.98 (muito similar)

    ("gato", "pizza"),            # esperado: ~0.18 (pouco similar)

    ("avião", "computador"),      # esperado: ~0.20 (pouco similar)

]



for palavra_a, palavra_b in pares:

    sim = similaridade_cosseno(embeddings[palavra_a], embeddings[palavra_b])

    # Formatamos com 4 casas decimais para ver diferenças pequenas

    print(f"'{palavra_a}' ↔ '{palavra_b}': {sim:.4f}")
Como o arquiteto lê este código

O código calcula produto escalar entre dois vetores. zip(vec_a, vec_b) pareia os vetores elemento a elemento — como fazer JOIN entre duas listas pela posição. O sum(a * b for a, b in ...) multiplica cada par e soma os resultados — é a operação de "similaridade por sobreposição". As linhas seguintes calculam o comprimento de cada vetor e dividem para normalizar: o resultado final (entre -1 e 1) é o cosseno do ângulo entre eles. Quanto mais próximo de 1, mais similares. Você não precisa escrever isso — as APIs de vector store fazem essa busca para você; mas saber o que ela calcula ajuda a entender por que a ordem dos documentos retornados faz sentido.

Output esperado:

'gato' ↔ 'cachorro': 0.9810

'computador' ↔ 'smartphone': 0.9965

'gato' ↔ 'pizza': 0.1793

'avião' ↔ 'computador': 0.1890

Exemplo 2: Analisando custo real de chamada de API

import anthropic

import os



client = anthropic.Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY"))



def chamar_api_com_analise(prompt: str, max_tokens: int = 200) -> dict:

    """

    Chama a API e analisa os custos reais — não estimados.

    

    Por que usar response.usage em vez de apenas ler a resposta?

    Porque a contagem real de tokens difere da estimativa por caracteres.

    JSON estruturado, código, emojis e caracteres especiais tokenizam de 

    forma imprevisível. A API sempre retorna a contagem exata.

    """

    response = client.messages.create(

        model="claude-haiku-4-5-20251001",

        max_tokens=max_tokens,

        messages=[{"role": "user", "content": prompt}]

    )

    

    # Extraímos métricas reais de uso — esses dados são o que Anthropic fatura

    tokens_input = response.usage.input_tokens

    tokens_output = response.usage.output_tokens

    

    # Calculamos custo com preços de Haiku (mais barato para experimentação)

    custo_input = tokens_input * 0.00000025   # $0.25 por 1M tokens

    custo_output = tokens_output * 0.00000125  # $1.25 por 1M tokens

    custo_total = custo_input + custo_output

    

    return {

        "texto": response.content[0].text,

        "tokens_input": tokens_input,

        "tokens_output": tokens_output,

        "custo_usd": custo_total,

        # Razão output/input indica "eficiência" da geração

        # Razões altas = modelo expandiu muito o conteúdo

        "razao_output_input": tokens_output / tokens_input,

    }



# Testamos com três tipos de prompt para ver como tokenização afeta custo

prompts = [

    "O que é um transformer?",              # Pergunta curta

    "Explique transformers em 3 parágrafos", # Pedido de expansão

    '{"tipo": "consulta", "tema": "IA"}',   # JSON como input

]



for p in prompts:

    resultado = chamar_api_com_analise(p)

    print(f"\nPrompt: {p[:40]}...")

    print(f"  Input: {resultado['tokens_input']} tokens")

    print(f"  Output: {resultado['tokens_output']} tokens")

    print(f"  Custo: ${resultado['custo_usd']:.6f}")

Output esperado (valores aproximados):

Prompt: O que é um transformer?...

  Input: 8 tokens

  Output: 87 tokens

  Custo: $0.000111



Prompt: Explique transformers em 3 parágrafos...

  Input: 9 tokens

  Output: 178 tokens

  Custo: $0.000225



Prompt: {"tipo": "consulta", "tema": "IA"}...

  Input: 15 tokens

  Output: 64 tokens

  Custo: $0.000082

Note: o JSON tokenizou em mais tokens que o texto equivalente (“tipo consulta tema IA” = ~6 tokens vs 15 para o JSON).

Padrões e Armadilhas

Padrões recomendados

Padrão 1: Sempre leia response.usage em produção Não confie em estimativas de custo baseadas em contagem de caracteres. O custo real vem da contagem de tokens da API. Logue input_tokens e output_tokens em cada chamada — isso é o que você vai precisar para alertas de custo e capacity planning.

Padrão 2: Escolha temperatura de acordo com a tarefa Temperatura 0.0 para tarefas determinísticas (extração, classificação, código), temperatura 0.3–0.7 para geração com consistência, temperatura 1.0+ apenas quando variabilidade é desejada (brainstorming). Documente a temperatura escolhida junto com a justificativa no código.

Padrão 3: Use embedding de produção (não Word2Vec simplificado) para similaridade real O exemplo desta unidade usa vetores de 5 dimensões com características manuais. Em produção, use APIs de embedding dedicadas que retornam vetores de centenas ou milhares de dimensões aprendidos de corpus massivos. A diferença de qualidade é enorme.

Armadilhas comuns

⚠️ Armadilha 1: Assumir que LLMs processam caracteres O que acontece: você pede ao LLM para contar letras, verificar palíndromos, ou processar texto posição por posição. O modelo falha ou alucina. Versão correta: LLMs processam tokens, não caracteres. Para tarefas que exigem processamento por caractere, use código Python/JS convencional — não LLM.

⚠️ Armadilha 2: Colocar dados dinâmicos no system prompt de cada chamada O que acontece: você inclui dados do usuário (nome, preferências, histórico) no system prompt. Com muitos usuários, cada chamada tem um system prompt diferente — você não aproveita o cache de prompt da Anthropic. Versão correta: mantenha system prompt estático e mova dados dinâmicos para o user message. O Anthropic Cache é muito mais eficiente para tokens repetidos.

⚠️ Armadilha 3: Ignorar truncação de contexto O que acontece: seu chatbot “esquece” o que foi dito mais cedo na conversa sem avisar o usuário. O comportamento parece um bug randômico. Versão correta: implemente lógica de summarização incremental antes de atingir o limite de context window. Monitore input_tokens em cada resposta e acione summarização quando passar de 80% do limite do modelo.

No próximo capítulo, você vai colocar esses conceitos em prática diretamente no navegador — tokens e embeddings deixam de ser abstrações e viram tensores que você pode manipular e visualizar.

⚗ Laboratório prático — mozak.tech
Se não for realizar o laboratório, pule para o próximo capítulo.

Ponte para o Lab

O starter desta unidade tem dois TODOs principais:

TODO — similaridade_cosseno() Seção de referência: “Conceitos Fundamentais → Embeddings: texto como ponto no espaço”. A fórmula é (A · B) / (|A| × |B|). Implemente em três passos: 1. produto_escalar = sum(a * b for a, b in zip(vec_a, vec_b)) 2. norma_a = math.sqrt(sum(a ** 2 for a in vec_a)) 3. return produto_escalar / (norma_a * norma_b) O TODO mais difícil é entender por que usar zip() — zip une os dois vetores posição por posição para calcular o produto escalar. Veja a seção “Exemplos Anotados → Exemplo 1”.

TODO — comparar_similaridade() (chamada da função) Seção de referência: “Exemplos Anotados → Exemplo 1”. Após implementar similaridade_cosseno(), substitua None por similaridade_cosseno(embeddings[palavra_a], embeddings[palavra_b]) no loop. Verifique os resultados: gato-cachorro deve ser próximo de 0.98, gato-pizza próximo de 0.18.

Dica para o TODO mais difícil: se similaridade_cosseno retornar nan (Not a Number), verifique se algum vetor tem norma zero — o caso de borda que a proteção if norma_a == 0 cobre.

Agora você está pronto para o lab.