Modelos de LLM e APIs
O que é um modelo de linguagem grande (LLM)?
Como modelos de linguagem grandes transformam texto em tokens, preveem o próximo e por que soam confiantes quando erram.
Como é uma requisição de API de chat: modelo, mensagens, tokens, streaming, chaves de API, limites e erros HTTP, com exemplos anotados.
Uma API de LLM é um endereço web que recebe texto e devolve a resposta de um modelo. Seu programa envia uma requisição HTTP com o nome do modelo, uma lista de mensagens e um limite de tokens. O serviço devolve o texto gerado, o motivo de ter parado e a contagem de tokens que leu e escreveu. Um app de chat faz o mesmo trabalho por trás de uma caixa de texto. A API entrega as peças cruas, então é o seu código que decide o que entra e o que acontece com a resposta.
Num app de chat, o modelo parece um interlocutor. Por baixo, cada mensagem enviada é uma requisição HTTP completa. O app junta as mensagens anteriores, acrescenta a nova, envia tudo a um servidor e mostra o texto que volta. O modelo não guarda memória da sua última mensagem, a menos que o app envie o histórico de novo.
A API pula a interface. Seu código monta a lista de mensagens, define o limite de tokens e trata a resposta. O serviço não se importa se quem chama é um navegador, um script ou uma tarefa agendada. Ele verifica o formato da requisição e a chave.
Três termos importam desde o início:
Uma janela de chat serve para uma pessoa fazer perguntas. Código precisa de entradas repetíveis, de saídas que o programa consiga interpretar e de erros que ele consiga capturar. Quando um script chama um modelo, a chave, a cobrança e os modos de falha passam a ser sua responsabilidade. A maior parte dos problemas de uma primeira integração vem de um corpo malformado, de uma chave ausente ou errada, de um limite que ninguém verificou ou de uma resposta tratada como se sempre tivesse dado certo.
Quase toda API de LLM que você vai encontrar é uma API web sobre HTTP. A requisição tem método, URL, cabeçalhos e, geralmente, um corpo JSON. A chamada ao modelo quase sempre é um POST. A RFC 9110, a norma atual de semântica do HTTP, define o que essas partes significam e como o servidor deve responder. Os códigos de status e os cabeçalhos que você vai ver depois vêm dela.
O corpo é um objeto JSON. No mínimo, ele nomeia o modelo e envia a conversa até ali. Este é o formato de mensagens, um dos dois estilos de requisição comparados mais abaixo. Os valores são marcadores, e nada aqui é executado.
{
"model": "<MODEL_ID_FROM_CURRENT_CATALOG>",
"max_tokens": 300,
"system": "You explain security concepts in plain English. Keep answers under 120 words.",
"messages": [
{
"role": "user",
"content": "What is the difference between hashing and encryption?"
}
],
"stream": false
}Anotações:
model é o ID do modelo que gera a resposta. Copie-o da lista atual do provedor, não de um tutorial antigo.max_tokens limita quantos tokens o modelo pode escrever. Não limita o seu prompt.system guarda as instruções permanentes. Nesse formato, ele fica fora da lista de mensagens.messages guarda a conversa. Aqui há um único turno do usuário.stream está como false, então o servidor espera a resposta completa antes de responder.Cada mensagem tem um papel que diz quem está falando:
user: texto do usuário final ou do seu programa.assistant: uma resposta que o modelo escreveu antes. Você a envia de volta para continuar a conversa.system ou developer: instruções permanentes. O nome depende do formato.tool: resultado de uma chamada de ferramenta, tema de um artigo futuro.Para continuar uma conversa, acrescente a última resposta do modelo como mensagem assistant e depois a nova mensagem user. Se você esquecer os turnos anteriores, o modelo responde à pergunta nova sem contexto, e nada avisa que houve erro. Esse é um dos bugs mais comuns.
max_tokens (ou o campo equivalente) limita quanto o modelo escreve. Se a resposta chegar a esse limite, o texto para no meio de uma frase e a resposta informa isso. Verifique o motivo de parada, descrito no passo 6, em vez de supor que toda resposta está completa. O limite não reduz o que você paga pelo prompt, porque os tokens de entrada continuam contando para o uso.
Com "stream": true, o servidor envia a resposta em pedaços conforme a gera, usando eventos enviados pelo servidor (server-sent events). O usuário vê as palavras aparecerem uma após a outra. Seu código lê os eventos e junta os pedaços, e os números de uso e o motivo de parada chegam nos eventos finais. Use streaming quando uma pessoa estiver acompanhando a saída. Para um job em lote que guarda a resposta, uma requisição normal é mais simples de escrever e de testar.
Uma resposta típica contém:
choices.Leia o motivo de parada antes de confiar no texto. Uma resposta que parou no limite de tokens está incompleta. Uma resposta que termina numa chamada de ferramenta não é a resposta final, e seu código precisa executar a ferramenta e enviar o resultado de volta.
O objeto de uso define a sua cobrança. Registre as contagens, o ID do modelo, o horário, o motivo de parada e o código de status. Não registre prompts nem respostas por padrão, porque podem conter dados de clientes, código privado ou segredos. Metadados bastam para achar picos de custo e falhas.
Dois estilos de requisição são muito usados na documentação pública. Muitos serviços copiam um deles para que o código cliente existente continue funcionando com outra URL base. Conhecer os dois ajuda a ler exemplos de qualquer fonte.
O primeiro é o formato de mensagens, documentado na primeira referência de Fontes. Ele recebe system como campo de primeiro nível, exige max_tokens, devolve blocos de conteúdo com stop_reason e informa input_tokens e output_tokens.
O segundo é o formato de chat completions, documentado na segunda referência de Fontes. Ele coloca as instruções de sistema ou de developer dentro de messages, aceita max_completion_tokens e devolve um array choices, em que cada item traz um finish_reason e uma message. O objeto de uso informa prompt_tokens, completion_tokens e total_tokens.
| Tema | Formato de mensagens | Formato de chat completions |
|---|---|---|
| Instruções permanentes | Campo system no topo |
Mensagem system ou developer dentro de messages |
| Campos obrigatórios | model, messages, max_tokens |
model, messages |
| Limite de saída | max_tokens (obrigatório) |
max_completion_tokens (opcional) |
| Onde fica a resposta | Lista content de blocos |
choices[0].message |
| Por que parou | stop_reason |
choices[0].finish_reason |
| Contagem de tokens | usage.input_tokens, usage.output_tokens |
usage.prompt_tokens, usage.completion_tokens, usage.total_tokens |
| Streaming | stream: true, eventos enviados pelo servidor |
stream: true, pedaços em eventos enviados pelo servidor |
As diferenças são de forma, não de sentido. Os dois enviam modelo, conversa e limite, e devolvem texto, motivo e contagens. Quando um exemplo usa o outro formato, relacione os campos pelo papel que cada um cumpre nesta tabela, em vez de copiar o JSON.
A mesma pergunta no formato de chat completions:
{
"model": "<MODEL_ID_FROM_CURRENT_CATALOG>",
"max_completion_tokens": 300,
"messages": [
{
"role": "developer",
"content": "You explain security concepts in plain English. Keep answers under 120 words."
},
{
"role": "user",
"content": "What is the difference between hashing and encryption?"
}
],
"stream": false
}Uma resposta nesse formato tem a seguinte forma. Os valores são marcadores, não uma resposta observada:
{
"id": "<RESPONSE_ID>",
"object": "chat.completion",
"model": "<MODEL_ID_FROM_CURRENT_CATALOG>",
"choices": [
{
"index": 0,
"message": { "role": "assistant", "content": "<GENERATED_TEXT>" },
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 0,
"completion_tokens": 0,
"total_tokens": 0
}
}Seu código lê choices[0].message.content, choices[0].finish_reason e depois o objeto de uso. O formato de mensagens guarda as mesmas informações em content, stop_reason e usage, com outros nomes.
Uma chave de API identifica a conta que paga a chamada e define quais modelos e limites valem. Os dois formatos a enviam de jeitos diferentes. O formato de chat completions usa um cabeçalho Authorization com o esquema Bearer. O formato de mensagens usa um cabeçalho próprio para a chave e também exige um cabeçalho de versão da API, para que o provedor mantenha estável o comportamento das versões antigas.
Os nomes dos cabeçalhos mudam conforme o provedor e a versão, então copie-os da documentação atual. As formas ficam assim:
# Placeholder shape only. Do not run this with a real key in a shared shell history.
curl https://<API_HOST>/<MESSAGES_PATH> \
-H "x-api-key: <YOUR_API_KEY>" \
-H "anthropic-version: <VERSION_FROM_CURRENT_DOCS>" \
-H "content-type: application/json" \
-d '{"model":"<MODEL_ID>","max_tokens":300,"messages":[{"role":"user","content":"Hello"}]}'# Placeholder shape only. Same caution as above.
curl https://<API_HOST>/<CHAT_COMPLETIONS_PATH> \
-H "Authorization: Bearer <YOUR_API_KEY>" \
-H "Content-Type: application/json" \
-d '{"model":"<MODEL_ID>","messages":[{"role":"user","content":"Hello"}]}'Anotações:
Authorization: Bearer é o esquema padrão do HTTP para tokens. A RFC 9110 define o cabeçalho Authorization na seção de autenticação.É aqui que começa a maior parte dos custos e vazamentos evitáveis. As regras abaixo seguem o Secrets Management Cheat Sheet da OWASP.
Mantenha as chaves fora do código-fonte. Uma chave escrita num arquivo Python existe em toda cópia desse arquivo: no seu notebook, nas máquinas da equipe, no sistema de CI e em cada commit que o tocou. A OWASP aponta segredos fixos no código e em arquivos de configuração como prática arriscada.
Nunca faça commit de uma chave. O Git guarda o histórico. Apagar a chave num commit posterior não a remove da versão antiga. A OWASP observa que tirar um segredo do histórico pode exigir reescrevê-lo, o que tem efeitos colaterais. Trate uma chave que chegou a um repositório compartilhado como exposta. Revogue, crie outra e verifique na conta se há uso que você não fez.
Mantenha as chaves fora de logs, capturas de tela e código de navegador. Qualquer coisa numa linha de log, numa mensagem de erro ou num script de página pode ser lida por outras pessoas. Chamadas que precisam de chave devem rodar num servidor que você controla.
Não embuta chaves em imagens de container. A OWASP alerta contra segredos em instruções ENV ou ARG do Docker, porque o valor acaba nos metadados da imagem e no histórico do build.
Prefira um cofre de segredos. A OWASP diz que variáveis de ambiente costumam ser acessíveis a todos os processos e podem aparecer em logs ou dumps, então recomenda usá-las só quando não há opção melhor. Num notebook, funciona um gerenciador de senhas ou o cofre de credenciais do sistema operacional. Em servidores, use um serviço gerenciado de segredos ou arquivos montados com permissões restritas.
Limite e troque. Dê a cada chave só o acesso de que ela precisa e substitua as chaves periodicamente. A OWASP diz que segredos devem ter o mínimo de privilégios necessário e recomenda rotação automatizada quando possível.
Um padrão local seguro mantém o valor no ambiente e o lê pelo nome:
# Read the key from the environment. Fail early if it is missing.
import os
api_key = os.environ.get("LLM_API_KEY")
if not api_key:
raise SystemExit("LLM_API_KEY is not set. Set it in your shell or secret store.")Adicione os arquivos de configuração local ao .gitignore antes de criá-los. Faça commit de um modelo, como .env.example, que liste os nomes das variáveis com valores vazios, e nunca do arquivo real.
Os provedores limitam quantas requisições e quantos tokens uma conta pode usar numa janela de tempo. Esses limites costumam ser chamados de limites de taxa (rate limits), e algumas contas também têm teto de gasto. Os números variam por conta, por modelo e ao longo do tempo, então consulte o console do provedor e a documentação atual. Este artigo não informa nenhum número.
Ao bater num limite, o servidor normalmente responde com 429 Too Many Requests. A RFC 6585 define esse código, que estende o conjunto básico do HTTP. A resposta pode trazer Retry-After, que a RFC 9110 define na seção de campos de resposta e que indica quanto esperar.
Seu código deve ler o status antes do corpo, esperar antes de tentar de novo (usando Retry-After quando existir, ou um atraso crescente com alguma variação aleatória) e parar após algumas tentativas, em vez de tentar para sempre. Conte os tokens antes de enviar prompts muito grandes, porque uma requisição excessiva falha mesmo com saldo.
O primeiro dígito de um código de status indica a classe: problema do lado do cliente, falha do servidor ou outra coisa. A RFC 9110 define as classes e os códigos individuais. Estes são os erros que integrações novas encontram com mais frequência.
| Status | Nome | O que costuma significar | O que verificar |
|---|---|---|---|
| 400 | Bad Request | O corpo está malformado ou um campo é inválido | Sintaxe do JSON, nomes de campos, campos obrigatórios, faixas de valores |
| 401 | Unauthorized | A chave está ausente, malformada ou revogada | Nome do cabeçalho, prefixo Bearer, valor da chave no ambiente |
| 403 | Forbidden | A chave é válida, mas a conta pode não usar esse recurso | Acesso ao modelo, permissões da conta, situação do faturamento |
| 404 | Not Found | O caminho está errado, ou o ID do modelo não existe para a sua conta | URL base, caminho, ID exato do modelo no catálogo atual |
| 429 | Too Many Requests | Limite de taxa ou cota atingidos | Retry-After, página de uso, volume de requisições |
| 500 | Internal Server Error | O servidor falhou do lado dele | Tentar de novo com espera crescente e depois consultar a página de status do provedor |
| 503 | Service Unavailable | O serviço está sobrecarregado ou em manutenção | Tentar de novo com espera crescente e um número máximo de tentativas |
Um erro 4xx significa que a requisição precisa mudar, então reenviar a mesma não ajuda. Um 5xx ou 429 pode funcionar depois, por isso a espera crescente faz sentido ali. Registre o código de status, o ID do modelo e o ID da requisição. Nunca registre a chave nem o prompt.
Confiar na primeira resposta. Uma resposta pode ser cortada pelo limite de tokens ou parar por outro motivo. Verifique o motivo de parada sempre.
Esquecer os turnos anteriores. Uma API sem estado não sabe nada da requisição anterior, a não ser que você a envie de novo. Monte a lista de mensagens a partir do seu próprio armazenamento.
Fixar o ID do modelo no código. IDs de modelo são descontinuados e renomeados. Mantenha o ID na configuração, para que a troca seja uma linha editada.
Misturar os dois formatos. Enviar um corpo de chat completions a um endpoint de mensagens, ou o contrário, gera erros 400. Confirme qual formato o endpoint documenta antes de copiar um exemplo.
Leia a referência de um provedor do início ao fim. As seções de requisição, resposta e erros são curtas e respondem à maioria das dúvidas da primeira semana. Depois, leia sobre streaming e janelas de contexto, que decidem quanta conversa você consegue enviar antes de uma requisição falhar.
O gateway da NotHoly documenta os próprios campos e códigos de erro. Compare-os com as duas formas acima, e use a documentação dele como referência para o comportamento.
Se você quer que o modelo chame o seu código, o próximo passo é o uso de ferramentas: um turno do assistente cujo motivo de parada pede ao seu programa que execute algo e devolva o resultado. O próximo artigo cobre esse ciclo.
Retry-After.429 Too Many Requests.Modelos de LLM e APIs
Como modelos de linguagem grandes transformam texto em tokens, preveem o próximo e por que soam confiantes quando erram.