Chamando modelos a partir do código

O capítulo anterior construiu uma lista de mensagens. Este capítulo envia essa lista para um modelo real a partir de Python e recebe uma resposta. Esse vai e vem é o menor loop completo para construir com esses modelos, e quase toda funcionalidade que você escreverá fica em cima dele.
Você digita um prompt, seu código o envia para algum lugar e texto volta. Parece que o modelo vive no seu programa. Não é assim, e ver onde ele realmente roda explica o custo, a velocidade e as características que você encontrará.
Então visualize o que está acontecendo fisicamente. O modelo é um conjunto imenso de parâmetros sitting nos servidores do provedor, em hardware que você nunca verá. Quando você o chama, seu código envia uma requisição web ordinária pela internet com suas mensagens.
O provedor roda o loop de previsão na máquina dele e envia os tokens gerados de volta para você. Uma chamada de modelo é alugar alguns momentos no computador de outra pessoa. Esse enquadramento faz sentido de muita coisa: latência é o modelo gerando tokens um de cada vez no lado dele, custo é eles cobrando por esse compute, e tudo é uma chamada de rede, com tudo que isso implica.
Os exemplos usam a biblioteca oficial da OpenAI. Outros provedores diferem em detalhes, mas a forma é quase a mesma em todo lugar: você envia uma lista de mensagens, recebe uma mensagem de volta. Essa similaridade é seu seguro contra lock-in, já que o mesmo código pode apontar para um modelo aberto que você hospeda ou aluga em outro lugar, frequentemente com apenas uma mudança de URL base (Modelos abertos e fechados cobre essa escolha).
A requisição
Instale a biblioteca com pip install openai, crie um cliente e chame-o. O cliente lê sua chave API 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 modelos
response = client.chat.completions.create(
model=MODEL,
messages=[
{"role": "system", "content": "Você é um assistente conciso. Responda em uma frase."},
{"role": "user", "content": "Por que o céu é azul?"},
],
)Duas peças são necessárias: model, o nome do modelo que você quer rodar, e messages, a lista de mensagens com role tag do capítulo anterior. Tudo o mais tem um padrão.
Note que o nome do modelo vive em uma constante única. Em um campo onde um modelo melhor, mais barato chega a cada poucos meses, isso é intencional. Mantenha o nome do modelo em um lugar para que trocar modelos seja uma mudança de uma linha. Essa é a regra de churn na prática: mantenha as especificidades voláteis onde você pode encontrá-las, não espalhadas pelo seu código.
model e messages. O nome do modelo pertence em uma única constante, porque um modelo mais barato ou melhor aparece frequentemente o bastante que codificar à mão através dos arquivos transforma uma troca em uma caçada. Lendo a resposta
A resposta volta embrulhada em alguma estrutura. O texto que você quer está em um caminho, mas o resto da estrutura vale a pena conhecer, porque diz 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 lá importam. message.content é o texto, sentado dentro de choices[0] porque a API pode retornar mais de uma opção, apesar de você quase sempre pedir uma e ler a primeira.
finish_reason diz por que o modelo parou. "stop" significa que terminou seu pensamento, enquanto "length" significa que rodou em seu cap de token e foi cortado no meio da resposta. Checar esse campo é como detectar uma resposta truncada no código em vez de a olho.
E usage relata os tokens que a chamada gastou, split entre o prompt que você enviou e a conclusão que escreveu. Esse objeto usage é sua conta, itemizado. Toda chamada diz exatamente o que custou em tokens, que é o número a observar enquanto você constrói.
response.choices[0].message.content, mas dois vizinhos importam também. finish_reason diz por que parou: "stop" significa pronto, "length" significa que atingiu seu cap de token e foi cortado. E response.usage é sua conta em tokens, prompt e conclusão split, então você nunca tem que adivinhar o que uma chamada custou. Os parâmetros que valem a pena conhecer
Além de model e messages, duas configurações opcionais aparecem constantemente.
temperature é o dial de aleatoriedade de Como os LLMs funcionam, feito concreto. Valores baixos (perto de 0) mantêm o modelo perto de seus top picks: focado e repetível, o que você quer para extração e classificação. Valores mais altos (em torno de 0.7 a 1) deixam alcançar tokens menos prováveis: mais variado e criativo, e mais propenso a se desviar. Você está ajustando quão boldamente o modelo amostra, nada mais.
max_tokens caps quantos tokens a resposta pode rodar. Ele protege você de uma resposta em fuga e uma conta em fuga. Set max_tokens muito baixo e o modelo fica cortado, que é exatamente a razão "length" da seção anterior, então deixe headroom real.
response = client.chat.completions.create(
model=MODEL,
messages=messages,
temperature=0.2, # focado e consistente
max_tokens=300, # cap o comprimento da resposta
)temperature ajusta quão boldamente o modelo amostra: baixo para respostas focadas, repetíveis, mais alto para criativas, variadas. max_tokens caps o comprimento da resposta para proteger sua conta, mas set muito baixo dispara o cutoff "length", então deixe headroom. Para extração e classificação, chegue para uma temperatura baixa toda vez. Streaming
Por padrão você espera por toda resposta terminar gerando, então ela chega de uma vez. Como o modelo produz tokens um de cada vez, uma resposta longa significa uma espera longa olhando para nada.
Streaming envia cada token para você no momento que é gerado, então texto aparece imediatamente e enche ao vivo, do jeito que apps de chat mostram uma resposta digitando a si mesma.
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 de qualquer forma; o modelo não é mais rápido. Streaming apenas muda quando você vê os tokens, superficializando-os conforme são produzidos em vez de retê-los até o fim.
Você trata iterando sobre chunks e imprimindo cada delta de texto. O trade-off é um pouco mais de código para uma espera muito melhor. Streaming muda quando você vê tokens, não o quão rápido chegam.
delta. Toda chamada é independente
Esse é o comportamento mais importante do capítulo, e segue direto da lição context window. A API é stateless: cada requisição fica completamente por conta própria. O servidor do provedor não lembra sua chamada anterior. Ele roda a previsão exatamente nas mensagens que você enviou, retorna o resultado, e mantém nada sobre a troca para próxima vez.
Então a conversa não é armazenada em lugar algum pelo modelo ou pela API. Ela vive no seu código. Se você quer que o turn dois saiba o que aconteceu no turn um, você mantém a lista de mensagens corrente você mesmo e envia tudo novamente. Um back-and-forth chat é uma ilusão que você cria reenviando uma história sempre crescente a cada chamada.
Isso é também por que uma conversa longa custa mais por mensagem ao longo do tempo: cada chamada reenvia todos os turns prévios como tokens de entrada, então o prompt mantém crescendo até quando a pergunta nova do usuário é curta.
history = [
{"role": "system", "content": "Você é um assistente de viagem amigável. Mantenha respostas curtas."},
]
def chat(user_text):
history.append({"role": "user", "content": user_text})
# O INTEIRO history sobe toda vez; o servidor não lembra nada
response = client.chat.completions.create(model=MODEL, messages=history)
reply = response.choices[0].message.content
history.append({"role": "assistant", "content": reply}) # mantenha a resposta para o próximo turn
return reply
chat("Tenho um fim de semana em São Paulo. O que devo ver?")
chat("Qual desses é melhor para crianças?") # funciona apenas porque history carrega turn umA segunda pergunta faz sentido apenas porque a primeira pergunta e sua resposta ainda estão em history e são enviadas junto. Você é a memória. Essa ideia única, que você monta e reenvia o contexto inteiro toda vez, fundamenta conversa, e depois fundamenta uso de ferramenta, recuperação e agentes também. Eles são todos variações em decidir o que vai na lista de mensagens antes de cada chamada independente.
Quando as coisas dão errado
Uma chamada de modelo é uma chamada de rede para servidores de outra pessoa, então falha de todas as formas que chamadas de rede falham. Design para isso desde o início.
- Erros e rate limits. Provedores capeam quantas requisições você pode enviar em uma janela. Atinja o cap e a chamada falha com um erro de rate-limit. A resposta padrão é esperar e retentar, alongando a espera após cada falha (isso é chamado backoff) então você não martela um serviço ocupado.
- Timeouts. Uma chamada pode pendurar. Set um timeout então um request lento não congela sua app.
- Chaves ficam no servidor. Sua chave API é uma senha que gasta seu dinheiro. Nunca coloque a chave API em código do browser. Qualquer um pode abrir dev tools e lê-la. O browser fala com seu backend, e seu backend, mantendo a chave, fala com o modelo.
- Assuma qualquer resposta pode estar errada. Até uma chamada bem-sucedida pode retornar uma alucinação ou saída malformada, então os próximos capítulos tratam de fazer o resultado algo que seu código pode confiar.
def ask_model(messages):
try:
response = client.chat.completions.create(
model=MODEL,
messages=messages,
timeout=20, # desista após 20 segundos
)
return response.choices[0].message.content
except Exception as err:
print("Model call failed:", err)
return "Desculpe, algo deu errado. Por favor tente novamente."
