Chamando modelos a partir do código


No capítulo anterior, você montou uma lista de mensagens. Neste capítulo, você vai enviar essa lista para um modelo real a partir do Python e receber uma resposta de volta. Esse ciclo completo é o menor loop possível ao construir com esses modelos, e praticamente toda funcionalidade que você vai escrever se apoia nele.
Você digita um prompt, seu código o envia para algum lugar, e o texto volta. Parece que o modelo mora dentro do seu programa. Mas não mora, e entender onde ele de fato roda explica o custo, a velocidade e as peculiaridades que você vai encontrar pelo caminho.
Então imagine o que está acontecendo fisicamente. O modelo é um enorme conjunto de parâmetros hospedado nos servidores do provedor, em hardware que você nunca vai ver. Quando você o chama, seu código envia uma requisição web comum pela internet, carregando suas mensagens.
O provedor executa o loop de previsão na máquina dele e envia os tokens gerados de volta para você. Chamar um modelo é alugar alguns instantes do computador de outra pessoa. Essa forma de pensar explica bastante coisa: a latência é o modelo gerando tokens um a um do lado deles, o custo é a cobrança por esse processamento, e tudo isso, no fim das contas, é uma chamada de rede, com tudo que isso implica.
Os exemplos usam a biblioteca oficial da OpenAI. Outros provedores diferem em detalhes, mas o formato é quase o mesmo em todos os lugares: você envia uma lista de mensagens e recebe uma mensagem de volta. Essa uniformidade é o que te protege de ficar preso a um único provedor, já que o mesmo código pode apontar para um modelo aberto que você hospeda ou aluga em outro lugar, muitas vezes só trocando a URL base (o capítulo Modelos abertos e fechados trata dessa escolha).
A requisição
Instale a biblioteca com pip install openai, crie um cliente e faça a chamada. O cliente lê sua chave de API a partir de uma variável de ambiente, então a chave nunca aparece no seu código.
from openai import OpenAI
client = OpenAI() # lê OPENAI_API_KEY do ambiente
MODEL = "gpt-4o-mini" # a única linha que você muda para trocar de modelo
response = client.chat.completions.create(
model=MODEL,
messages=[
{"role": "system", "content": "You are a concise assistant. Answer in one sentence."},
{"role": "user", "content": "Why is the sky blue?"},
],
)Dois itens são obrigatórios: model, o nome do modelo que você quer executar, e messages, a lista de mensagens com papéis marcados do capítulo anterior. Todo o resto tem um valor padrão.
Note que o nome do modelo fica em uma única constante. Em uma área em que um modelo melhor e mais barato surge a cada poucos meses, isso é intencional. Mantenha o nome do modelo em um único lugar, para que trocar de modelo seja uma mudança de uma linha só. Essa é a regra da mudança constante aplicada na prática: mantenha os detalhes instáveis em um lugar fácil de encontrar, não espalhados pelo seu código.
model e messages. O nome do modelo deve ficar em uma única constante, porque um modelo mais barato ou melhor aparece a cada poucos meses e você não quer ter que caçá-lo depois. Aprendi isso do jeito difícil, depois que um modelo foi renomeado e eu descobri que ele estava colado em nove arquivos diferentes. Lendo a resposta
A resposta volta envolvida em alguma estrutura. O texto que você quer é um caminho dentro dela, mas vale a pena conhecer o resto da estrutura, porque ela conta o que aconteceu.
choice = response.choices[0]
print(choice.message.content) # o texto da resposta
print(choice.finish_reason) # "stop" -> o modelo terminou por conta própria
print(response.usage) # Usage(prompt_tokens=24, completion_tokens=18, total_tokens=42)Três coisas ali são importantes. message.content é o texto, e ele fica dentro de choices[0] porque a API pode retornar mais de uma opção, embora quase sempre você peça só uma e leia a primeira.
finish_reason diz por que o modelo parou. "stop" significa que ele terminou o raciocínio, enquanto "length" significa que ele bateu no seu limite de tokens e foi cortado no meio da resposta. Verificar esse campo é como você detecta uma resposta truncada pelo código, em vez de perceber apenas olhando.
E usage informa os tokens que a chamada consumiu, divididos entre o prompt que você enviou e a resposta que o modelo escreveu. Esse objeto usage é a sua conta, detalhada item a item. Toda chamada te diz exatamente quanto custou em tokens, e esse é o número que você deve acompanhar enquanto constrói suas funcionalidades.
response.choices[0].message.content, mas dois campos vizinhos também importam. finish_reason diz por que o modelo parou: "stop" significa que terminou, "length" significa que bateu no seu limite de tokens e foi cortado. E response.usage é a sua conta em tokens, com prompt e resposta separados, para você nunca precisar chutar quanto uma chamada custou. Os parâmetros que vale a pena conhecer
Além de model e messages, duas configurações opcionais aparecem o tempo todo.
temperature é o controle de aleatoriedade de Como os LLMs funcionam, agora de forma concreta. Valores baixos (próximos de 0) mantêm o modelo perto de suas escolhas mais prováveis: focado e repetível, o que você quer para extração e classificação. Valores mais altos (entre 0,7 e 1) permitem que ele busque tokens menos prováveis: mais variado e criativo, e mais propenso a se dispersar. Você está ajustando o quão ousadamente o modelo amostra, nada mais.
max_tokens limita até quantos tokens a resposta pode chegar. Isso te protege de uma resposta descontrolada e de uma conta descontrolada. Defina max_tokens muito baixo e o modelo será cortado, exatamente o motivo de finalização "length" da seção anterior, então deixe uma margem real.
response = client.chat.completions.create(
model=MODEL,
messages=messages,
temperature=0.2, # focado e consistente
max_tokens=300, # limita o tamanho da resposta
)temperature ajusta o quão ousadamente o modelo amostra: baixo para respostas focadas e repetíveis, alto para respostas variadas e criativas. max_tokens limita o tamanho da resposta para proteger sua conta, mas se definido muito baixo dispara o corte por "length", então deixe margem. Para extração e classificação, use sempre uma temperature baixa. Streaming
Por padrão, você espera a resposta inteira terminar de ser gerada, e então ela chega toda de uma vez. Como o modelo produz tokens um a um, uma resposta longa significa uma longa espera olhando para o nada.
Streaming envia cada token para você no instante em que ele é gerado, então o texto aparece imediatamente e vai se completando em tempo real, do jeito que os aplicativos de chat mostram uma resposta sendo "digitada" na tela.
stream = client.chat.completions.create(
model=MODEL,
messages=messages,
stream=True,
)
for chunk in stream:
piece = chunk.choices[0].delta.content
if piece:
print(piece, end="", flush=True)O tempo total de geração é o mesmo nos dois casos; o modelo não fica mais rápido. O streaming só muda quando você vê os tokens, exibindo-os conforme são produzidos, em vez de guardá-los até o final.
Você lida com isso iterando sobre os pedaços (chunks) e imprimindo cada delta de texto. O custo é um pouco mais de código em troca de uma espera muito melhor. O streaming muda quando você vê os tokens, não a velocidade com que eles chegam.
delta. Toda chamada é independente
Este é o comportamento mais importante do capítulo, e ele decorre diretamente da lição sobre a janela de contexto. A API é sem estado: cada requisição se sustenta completamente por si só. O servidor do provedor não lembra da sua chamada anterior. Ele executa a previsão exatamente sobre as mensagens que você enviou, retorna o resultado, e não guarda nada sobre essa troca para a próxima vez.
Então a conversa não é armazenada em lugar nenhum pelo modelo ou pela API. Ela vive no seu código. Se você quer que o segundo turno saiba o que aconteceu no primeiro, você mesmo mantém a lista de mensagens acumulada e a envia toda de novo. Um vaivém de chat é uma ilusão que você cria reenviando um histórico cada vez maior a cada chamada.
É também por isso que uma conversa longa custa mais por mensagem, ao longo do tempo: cada chamada reenvia todos os turnos anteriores como tokens de entrada, então o prompt continua crescendo mesmo quando a nova pergunta do usuário é curta.
history = [
{"role": "system", "content": "You are a friendly travel assistant. Keep answers short."},
]
def chat(user_text):
history.append({"role": "user", "content": user_text})
# o histórico INTEIRO é enviado a cada vez; o servidor não lembra de nada
response = client.chat.completions.create(model=MODEL, messages=history)
reply = response.choices[0].message.content
history.append({"role": "assistant", "content": reply}) # guarda a resposta para o próximo turno
return reply
chat("Tenho um fim de semana no Rio de Janeiro. O que devo ver?")
chat("Qual desses lugares é melhor para crianças?") # só funciona porque o history carrega o primeiro turnoA segunda pergunta só faz sentido porque a primeira pergunta e sua resposta ainda estão em history e são enviadas junto. Você é a memória. Essa ideia central, que você monta e reenvia o contexto completo a cada vez, sustenta a conversa, e mais adiante vai sustentar também o uso de ferramentas, a busca de informações e os agentes. Todos eles são variações da mesma decisão: o que entra na lista de mensagens antes de cada chamada independente.
Quando as coisas dão errado
Chamar um modelo é fazer uma chamada de rede para os servidores de outra pessoa, então ela falha de todas as formas que chamadas de rede falham. Projete pensando nisso desde o início.
- Erros e limites de taxa. Os provedores limitam quantas requisições você pode enviar dentro de uma janela de tempo. Se você atingir esse limite, a chamada falha com um erro de limite de taxa. A resposta padrão é esperar e tentar de novo, aumentando a espera após cada falha (isso se chama backoff) para você não sobrecarregar um serviço já ocupado.
- Tempos limite (timeouts). Uma chamada pode ficar travada. Defina um tempo limite para que uma requisição lenta não congele seu aplicativo.
- As chaves ficam no servidor. Sua chave de API é uma senha que gasta o seu dinheiro. Nunca coloque a chave de API em código que roda no navegador. Qualquer pessoa pode abrir as ferramentas de desenvolvedor e lê-la. O navegador conversa com o seu backend, e o seu backend, que guarda a chave, conversa com o modelo.
- Assuma que qualquer resposta pode estar errada. Mesmo uma chamada bem-sucedida pode retornar uma alucinação ou uma saída malformada, então os próximos capítulos tratam de como transformar o resultado em algo que o seu código possa confiar.
def ask_model(messages):
try:
response = client.chat.completions.create(
model=MODEL,
messages=messages,
timeout=20, # desiste depois de 20 segundos
)
return response.choices[0].message.content
except Exception as err:
print("Model call failed:", err)
return "Sorry, something went wrong. Please try again."
