Skip to content
This page has been auto-translated and may contain errors.View in English

Chamando modelos a partir do código

docs.scrimba.com

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).

Você chama uma função de biblioteca, alguns segundos passam e texto volta. A biblioteca faz parecer local, mas nada do modelo está na sua máquina.

O movimento a entender é o que essa função está embrulhando. Seu código empacota a requisição como JSON, abre uma conexão HTTPS para o provedor e espera enquanto o hardware deles roda o loop de previsão e transmite tokens de volta. O SDK (a biblioteca de software do provedor, como openai) é uma fina camada de conveniência sobre uma requisição POST.

Saber que o formato de fio embaixo é JSON simples compensa na prática. Quando uma chamada se comporta de forma estranha, você pode inspecionar a requisição exata e a resposta. Quando você quer uma funcionalidade que o SDK não expôs ainda, você pode enviar o campo à mão.

Tudo sendo uma chamada de rede é o que direciona o custo, latência e tratamento de erros para o resto deste capítulo. Os exemplos usam OpenAI, mas a forma da requisição, uma lista de mensagens entrando, uma mensagem saindo, é quase idêntica entre provedores.

Uma chamada de modelo parece uma chamada de função e se comporta como um procedimento remoto sobre infraestrutura instável. Essa falta de correspondência é de onde vêm os problemas de produção, então vale a pena manter a imagem real desde o início.

Embaixo do SDK, toda chamada é um POST HTTPS: um corpo JSON sobe, o provedor roda inferência no hardware dele e tokens voltam, opcionalmente transmitidos sobre a conexão aberta. O SDK (a biblioteca cliente do provedor) lhe compra autenticação, tentativas novamente, tipagem e análise de streaming, e custa você uma camada de abstração sobre churn que você não controla. Trate como um wrapper que você pode ver através, não uma caixa preta.

Duas consequências configuram o resto do capítulo. Primeiro, essa é uma chamada de rede cobrada por token, então custo, latência, idempotência e tratamento de falha são preocupações de design, não afterthoughts que você adiciona depois. Segundo, o SDK e seus nomes de campo são a superfície mais volátil que você depende, então o movimento durável é isolar as partes específicas do provedor atrás de sua própria fronteira. Os exemplos usam OpenAI; a forma da requisição é quase universal, mas o wrapper ao redor é exatamente o que você não quer espalhando através do seu codebase.

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.

python
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.

JunoA requisição Uma chamada precisa de apenas duas coisas: 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.

Dois campos são obrigatórios, model e messages, e tudo o mais tem um padrão. O nome do modelo em uma constante não é um ponto de estilo: é o detalhe único volátil que você quer em um lugar, porque um modelo mais barato ou melhor chega frequentemente o bastante que codificar à mão através dos arquivos transforma uma troca em uma caçada.

Agora olhe um nível para baixo. Essa chamada create serializa para um corpo JSON e faz POST. Soletrado, o formato de fio é a requisição literal que o provedor recebe:

python
import httpx, os

# a mesma requisição que o SDK envia, à mão
resp = httpx.post(
    "https://api.openai.com/v1/chat/completions",
    headers={"Authorization": f"Bearer {os.environ['OPENAI_API_KEY']}"},
    json={
        "model": "gpt-4o-mini",
        "messages": [
            {"role": "system", "content": "Você é um assistente conciso."},
            {"role": "user", "content": "Por que o céu é azul?"},
        ],
    },
    timeout=20,
)
data = resp.json()  # mesma forma que o SDK analisa em objetos

Você chegará para o SDK em código real: ele lida com autenticação, tentativas novamente e tipagem para você. Mas ver que o corpo é um dict simples torna o resto concreto. A URL e nomes de campo aqui são da OpenAI, e o endpoint exato e chaves variam por provedor, mas o movimento, JSON dentro, JSON fora, é o mesmo em todo lugar.

JunoA requisição Campos obrigatórios são model e messages, nada mais. O SDK chama create é um POST JSON sob o capô, então quando algo parece errado você pode descer ao fio e inspecionar o corpo literal. Mantenha o nome do modelo em uma constante: é a parte que muda, e você quer em um lugar.

Campos obrigatórios são model e messages. A pergunta interessante não é o que eles são mas onde vivem no seu código, porque model é o único valor mais volátil na requisição e o SDK é a dependência mais volátil ao redor disso.

A chamada create serializa para um POST JSON. Isso importa porque significa que a requisição é dados que você possui, não magia que o SDK realiza. Você pode construir o corpo, logar, diff através das runs, ou enviar à mão quando o SDK fica atrás de um novo recurso do provedor. Conhecer o formato de fio é o que permite você debugar uma chamada que a abstração está escondendo de você.

O hábito de produção é rotear toda chamada através de uma função estreita de sua propriedade:

python
def complete(messages, *, model=DEFAULT_MODEL, **params):
    # um ponto de estrangulamento: troque provedores, adicione logging, mude tentativas novamente aqui
    return client.chat.completions.create(model=model, messages=messages, **params)

Esse ponto de estrangulamento é onde uma troca de modelo vira uma mudança de uma linha e onde o churn específico do provedor fica contido em vez de vazar em cada chamador. O caminho do endpoint e nomes de campo mostrados são da OpenAI; outro provedor renomeia a rota e algumas chaves, então o valor da fronteira é que a renomeação toca um arquivo, não todo o codebase. Pin também a versão do modelo: "latest" se movendo silenciosamente embaixo de você é o mesmo risco de regressão coberto em Como os LLMs funcionam, agora no seu corpo da requisição.

JunoA requisição Apenas model e messages são obrigatórios, e ambos merecem viver atrás de uma fronteira que você controla. Roteie toda chamada através de uma função: é onde uma troca de modelo é uma linha, onde logging vai, e onde churn do provedor para antes de chegar em seus chamadores. Pin a versão do modelo enquanto estiver nisso, porque "latest" se movendo embaixo de você é uma regressão que você não deployou e não pode ver.

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.

python
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.

JunoLendo a resposta O texto da resposta está em 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.

Três campos carregam o sinal. choices[0].message.content é o texto. finish_reason é por que o modelo parou: "stop" é um término limpo, "length" significa que atingiu seu cap de token e foi truncado, então checá-lo é como você pega uma resposta cortada no código em vez de shippar meia resposta. E usage é a contagem de tokens, prompt e conclusão separados.

O movimento que compensa aqui é transformar usage em dinheiro real. O objeto usage é dados de cobrança por chamada, então você pode computar custo no local:

python
# preços do provedor são por token; check taxas atuais, elas mudam
PROMPT_RATE = 0.15 / 1_000_000      # dólares por token de prompt
COMPLETION_RATE = 0.60 / 1_000_000  # dólares por token de conclusão

u = response.usage
cost = u.prompt_tokens * PROMPT_RATE + u.completion_tokens * COMPLETION_RATE
print(f"{u.total_tokens} tokens, ${cost:.6f}")

Prompt e conclusão são usualmente preçados diferentemente, e tokens de conclusão frequentemente custam mais, então o split importa quando você otimiza. Log o custo por chamada e você pode ver qual funcionalidade é cara antes da fatura mensal faz o relato. Os nomes de campo aqui são da OpenAI; outros provedores expõem os mesmos counts sob chaves um pouco diferentes, então leia a forma, não o atributo exato.

JunoLendo a resposta Leia três campos: content para o texto, finish_reason para pegar uma truncação "length" no código, e usage para tokens. Transforme usage em dólares no local, prompt e conclusão preçados separadamente, e faça log por chamada. Você verá a funcionalidade cara muito antes da fatura faz.

A resposta carrega três campos que você age sobre: content, finish_reason e usage. O que construtores pulam e lamentam é finish_reason. Um valor "length" significa que o modelo atingiu seu max_tokens e a saída é truncada, frequentemente como JSON malformado ou uma sentença meio-terminada que seu parser downstream então engasga. Trate qualquer valor que não seja "stop" como uma chamada falhada e trate, não assuma que content é completo.

O objeto usage é sua telemetria de unit-economics, e merece estar conectada a observabilidade, não impresso e esquecido. Tokens de prompt e conclusão são preçados separadamente, conclusão usualmente mais caro, então emita ambos por chamada taggeados por funcionalidade:

python
u = response.usage
log.info("llm_call", feature="invoice_extract", model=MODEL,
         prompt_tokens=u.prompt_tokens, completion_tokens=u.completion_tokens,
         finish_reason=response.choices[0].finish_reason)

Com isso em lugar, custo por funcionalidade e por usuário vira uma query, não uma adivinhação, e um prompt em fuga que silenciosamente triplicou em tamanho aparece como uma métrica em vez de uma cobrança surpresa. Observe a assimetria: tokens de entrada são baratos e paralelos para processar, tokens de saída são a parte cara, serial coberta em Como os LLMs funcionam, então uma funcionalidade que emite respostas longas custa e fica lenta mais do que seu tamanho de prompt sugere. Os nomes de atributo são da OpenAI e shift por provedor, então log os counts atrás de sua própria fronteira em vez de espalhar response.usage.prompt_tokens através da app.

JunoLendo a resposta Trate qualquer finish_reason que não seja "stop" como uma chamada falhada: uma truncação "length" passa JSON quebrado para seu parser e você perseguirá o bug downstream. Wire usage em seus logs taggeados por funcionalidade, prompt e conclusão split, então custo por funcionalidade é uma query e um prompt inchado é uma métrica, não uma surpresa de cobrança. Tokens de saída são a parte cara serial, então uma funcionalidade faladora custa mais do que seu prompt sugere.

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.

python
response = client.chat.completions.create(
    model=MODEL,
    messages=messages,
    temperature=0.2,  # focado e consistente
    max_tokens=300,   # cap o comprimento da resposta
)
JunoOs parâmetros que valem a pena conhecertemperature 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.

Dois parâmetros carregam a maioria do peso: temperature e max_tokens. A mecânica está em Como os LLMs funcionam; o movimento aqui é corresponder o valor à tarefa em vez de confiar em qualquer padrão que shippa.

Pense em temperature como um setting por tarefa, não global. Pick dele do que a tarefa precisa:

python
TEMPERATURE_BY_TASK = {
    "extraction": 0.0,      # uma resposta certa, quer toda vez
    "classification": 0.0,  # pick um label, nenhuma criatividade quer
    "summary": 0.3,         # principalmente fiel, um pouco de liberdade de fraseado
    "brainstorm": 0.9,      # variedade é o ponto inteiro
}

Para qualquer coisa que você analisa depois, dados estruturados, uma categoria, um número puxado de um documento, vá para 0 e pare de raciocinar sobre aleatoriedade. Para drafting e ideação onde variedade é o valor, vá alto.

max_tokens é um ceiling, não um target: caps gasto e respostas em fuga, mas set sob o que a tarefa precisa e você pega a truncação "length" da seção anterior, que é pior do que uma conta um pouco maior. Size ao resposta legítima mais longa mais headroom. Esses dois nomes de campo são estáveis entre provedores; alguns renomeiam max_tokens, então confirme a chave quando você troca.

JunoOs parâmetros que valem a pena conhecer Set temperature por tarefa, não uma vez globalmente: 0 para qualquer coisa que você analisa depois, alta para drafting onde variedade é o ponto. max_tokens é um ceiling de segurança, então size ao resposta real mais longa mais headroom; set muito apertado e você troca uma pequena economia de conta para uma resposta truncada. Pare de confiar o padrão que o SDK shippa.

temperature e max_tokens parecem dials e se comportam como decisões de política com custo e confiabilidade anexados. A profundidade aqui é em como eles interagem com tudo mais que você shippa.

Trate temperature como parte do seu contrato de confiabilidade. Para qualquer saída que você analisa ou armazena, rode em 0 então a variância de amostragem de Como os LLMs funcionam é tão pequena quanto o provedor permite. Não é perfeitamente determinístico até em 0, então não construa caching de match exato ou testes byte-idênticos em cima disso, mas é o piso que você quer para trabalho estruturado. Reserve temperaturas mais altas para superfícies onde variedade é o produto, e trate essa variância como algo que seus evals medem em runs, não um passe único que você eyeballs.

max_tokens é uma alavanca de latência e custo, não apenas uma rede de segurança. Tokens de saída são a parte serial, cara de geração, então capping comprimento bounds tanto sua conta no pior caso quanto sua espera no pior caso.

A armadilha é sizá-lo a olho: muito apertado trunca respostas válidas na falha "length" que passa seu parser saída quebrada, muito solto deixa um loop degenerativo correr conta e latência acima antes de qualquer coisa pará-lo. Size ao resposta legítima mais longa mais margem, por tarefa, e log quando você atingir o cap então uma funcionalidade que mantém truncando vira visível. Ambos nomes de parâmetro são da OpenAI e um casal de provedores os renomeiam, então set-os atrás da função de fronteira em vez de em todo call site.

JunoOs parâmetros que valem a pena conhecertemperature é um contrato de confiabilidade: 0 para qualquer coisa que você analisa, mais alto apenas onde variância é o produto, e nunca cache de match exato em cima de 0 porque não é byte-determinístico. max_tokens bounds seu custo e latência no pior caso, então size ao resposta real mais longa mais margem e log quando você atinge, porque uma funcionalidade que mantém truncando é um bug se escondendo como parâmetro. Set ambos atrás de sua fronteira, não em todo site de chamada.

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.

python
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.

JunoStreaming O modelo gera tokens um de cada vez, então por padrão uma resposta longa é uma espera longa silenciosa. Streaming avança cada token no momento que chega, então a resposta enche ao vivo, que é por que apps de chat sentem responsivos. Mesmo tempo total, você apenas vê os tokens sooner iterando sobre chunks e imprimindo cada delta.

Streaming avança cada token conforme o modelo o produz, então o usuário vê texto imediatamente em vez de esperar pela resposta inteira. O tempo de geração total é inalterado; o que cai é time to first token, que é a maioria do que "sente rápido" significa.

A versão limpa trata duas coisas que o loop toy pula: chunks sem texto, e acumular a resposta inteira enquanto você a exibe.

python
stream = client.chat.completions.create(model=MODEL, messages=messages, stream=True)

full = []
for chunk in stream:
    delta = chunk.choices[0].delta
    piece = delta.content
    if piece is None:          # role-only e chunks finais carregam sem texto
        continue
    full.append(piece)
    print(piece, end="", flush=True)

reply = "".join(full)          # a mensagem completa, para história ou armazenamento

O check delta vazio importa: o primeiro chunk frequentemente carrega o role e sem texto, e o chunk final pode carregar um finish_reason com conteúdo vazio. Pule o None e você evita imprimir "None" em sua saída.

Acumular em full lhe dá a mensagem inteira para anexar a história, porque streaming exibe texto mas não salva para você. Um trade-off a conhecer: uma chamada streamada não retorna um bloco usage por padrão, então se você precisa de contagens de tokens você as requisita explicitamente ou as conta você mesmo. Streaming existe em provedores, mas a forma do chunk, delta, content, onde finish_reason vive, é específica do provedor.

JunoStreaming Streaming corta time to first token, não tempo total. Guard para o `delta` vazio: o primeiro chunk frequentemente é role-only e o final carrega finish_reason sem texto, então pule None ou você imprime "None" na resposta. Acumule as peças você mesmo para história, e lembre que uma chamada streamada pula usage a menos que você peça.

Streaming troca uma resposta mais simples para um perfil de latência melhor: você corta time to first token, mas você renuncia ao objeto de resposta simples e assume montar estado conforme chunks chegam. Vale para qualquer coisa que um humano assista, raramente vale para uma chamada backend que você analisa inteira.

Os detalhes que mordem em produção estão nas fronteiras do chunk. O primeiro chunk é tipicamente role-only, o chunk final carrega finish_reason com conteúdo vazio, e você deve acumular texto você mesmo porque nada lhe passa a mensagem montada:

python
full, finish = [], None
for chunk in stream:
    choice = chunk.choices[0]
    if choice.delta.content:
        full.append(choice.delta.content)
    if choice.finish_reason:        # chega no chunk último
        finish = choice.finish_reason
reply = "".join(full)
if finish == "length":              # truncação ainda acontece mid-stream
    handle_truncated(reply)

Três realidades de produção:

  • finish_reason ainda chega no fim de um stream, então a truncação "length" não vai embora quando você streameia, você apenas a nota depois, e você checa antes de confiar o texto montado.
  • Um stream pode quebrar no meio do voo: a conexão cai depois de dez tokens de quarenta, deixando você uma resposta parcial, então chamadas streamadas precisam seu próprio tratamento de falha parcial e são incômodas para retentar limpamente.
  • Respostas streamadas usualmente omitem usage, então você passa uma flag para incluir ou tokeniza o texto montado você mesmo para rastreamento de custo.

O framing SSE e schema do chunk são específicos do provedor, então mantenha análise de stream atrás da mesma fronteira que o resto, e deixe chamadores ver seu resultado montado, não chunks brutos.

JunoStreaming Streaming compra time to first token e custa você o objeto de resposta limpo, então use onde um humano está assistindo e pule para análise backend. O chunk final ainda carrega finish_reason, então uma truncação "length" é real mid-stream, checa antes de confiar o texto. E um stream pode morrer após dez tokens de quarenta, então trate a resposta parcial e mantenha análise do chunk específica do provedor atrás de sua fronteira.

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.

python
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 um

A 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.

JunoToda chamada é independente A API é stateless: cada requisição roda exatamente nas mensagens que você envia, e o servidor esquece a troca no momento que responde. Uma conversa vive no seu código, não no modelo, então você mantém a lista de mensagens crescente e reenvia tudo a cada turn. É por isso que chats longos custam mais ao longo do tempo, cada chamada reenvia cada turn prévio como tokens de entrada.

A API é stateless: o servidor roda exatamente nas mensagens que você envia e mantém nada depois. A conversa vive no seu código, e você reenvia todo history todo turn para fingir continuidade, que é por que um chat longo custa mais por mensagem até quando a pergunta nova é curta.

Esse crescimento é um problema que você gerencia, não um que você pode ignorar, porque o history eventualmente ultrapassa a context window. O primeiro movimento é uma janela deslizante: mantenha a mensagem do sistema mais os turns mais recentes.

python
def trim(history, keep_recent=10):
    system = history[:1]               # sempre mantenha a mensagem do sistema
    recent = history[1:][-keep_recent:]  # apenas turnos mais novos
    return system + recent

Isso é barato e bounds seu custo, mas silenciosamente derruba turns mais velhos, então o modelo esquece fatos do início da conversa que o usuário ainda espera que ele saiba. Quando isso importa, o próximo step up é sumarizando os turns derrubados em uma mensagem curta em vez de deletá-los direto. Qual você pick depende da funcionalidade: um bot de suporte que referencia o problema original precisa o sumário, um Q e A rápido é fino com a janela. A falta de estado em si é universal entre provedores; apenas o formato de mensagem ao redor varia.

JunoToda chamada é independente Stateless significa o servidor esquece após cada resposta, então você reenvia todo history e paga conforme cresce. Trim com uma janela deslizante de turns recentes mais a mensagem do sistema para bounds custo, mas saiba que silenciosamente derruba fatos antigos que o usuário ainda espera que você lembre. Quando isso morde, sumarize os turns derrubados em vez de deletá-los.

A API é stateless: você possui a conversa e reenvia a cada chamada. A versão ingênua, anexe para sempre e envie o lote, falha duas formas em scale: eventualmente transborda a context window, e torna cada turn mais caro e mais lento que o último, porque entrada cresce sem bound.

Então gerenciar history é um problema de budgeting com três estratégias padrão, cada uma falhando de seu próprio jeito:

  • Uma janela deslizante mantém os N turns mais recentes: barato e bounded, mas silenciosamente derruba fatos antigos que o usuário ainda referencia.
  • Sumarização comprime turns antigos em um sumário corrente: mantém mais history por token, mas perde detalhe e pode assar seus próprios erros em cada turn posterior.
  • Recuperação armazena turns externamente e puxa de volta apenas os relevantes: scales para histórias longas, mas depende de seu índice retornando as peças certas e adiciona uma lookup.

Pick por caso de uso e instrumente, porque cada um eventualmente superficializará o contexto errado.

python
def build_messages(system, history, user_msg, budget_tokens=6000):
    msgs = [system]
    running = count_tokens(system) + count_tokens(user_msg)
    for turn in reversed(history):           # mais novos primeiro
        t = count_tokens(turn)
        if running + t > budget_tokens:
            break                             # turns mais velhos caem do budget
        msgs.insert(1, turn)
        running += t
    msgs.append(user_msg)
    return msgs

Budget por contagem de tokens, não contagem de turns, porque turns variam selvagemente em tamanho e uma janela de contagem de turns estoura seu budget de contexto no momento alguém cola um documento. A estratégia é provider-independente; o tokenizador que você conta com não é, então conte com o próprio do modelo. Esse é o mesmo trabalho de construção de contexto que prompting e RAG constroem, agora na conversa em si.

JunoToda chamada é independente Stateless significa gerenciar history é seu problema de budgeting, e "anexe para sempre" transborda a janela e infla o custo de cada turn. Pick uma estratégia por caso de uso: uma janela deslizante é barata mas derruba fatos antigos, sumarização mantém mais mas assa seus próprios erros, recuperação scales mas depende de seu índice. Budget por contagem de tokens não contagem de turns, porque um documento colado estoura uma janela de contagem de turns instantaneamente.

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.
python
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."
JunoQuando as coisas dão errado Uma chamada de modelo é uma chamada de rede para servidores de outra pessoa, então espere rate limits, timeouts e falhas simples, e embrulhe toda chamada em tratamento de erro com retry-and-backoff. Mantenha sua chave API no servidor, nunca no browser, porque gasta dinheiro real e dev tools a lerão. E trate até uma resposta bem-sucedida como possivelmente errada, que os próximos capítulos ajudam você a trata.

Uma chamada de modelo falha do jeito que chamadas de rede falham: rate limits, timeouts, erros de servidor transientes. O movimento é pegar as falhas específicas e retentar as que valem a pena retentar, em vez de embrulhar tudo em um except nú que esconde bugs reais.

Pegue exceções tipadas e aplique backoff, esperando mais tempo após cada falha então você para de martelar um serviço ocupado:

python
import time
from openai import RateLimitError, APITimeoutError, APIError

def ask_model(messages, retries=3):
    for attempt in range(retries):
        try:
            r = client.chat.completions.create(model=MODEL, messages=messages, timeout=20)
            return r.choices[0].message.content
        except (RateLimitError, APITimeoutError, APIError) as err:
            if attempt == retries - 1:
                raise
            wait = 2 ** attempt          # 1s, 2s, 4s: exponential backoff
            print(f"retry {attempt + 1} após {err}")
            time.sleep(wait)

Os tipos de exceção dizem o que fazer: RateLimitError e APITimeoutError são transientes e valem a pena retentar, enquanto uma requisição ruim (mensagens malformadas, modelo desconhecido) é seu bug e deveria falhar alto, não retentar. Dobrar a espera cada tentativa mantém você de pilhar em um serviço que já está lutando. O SDK retenta algumas falhas para você, então check os defaults antes de adicionar sua própria camada.

Mantenha a chave server-side, e trate uma resposta bem-sucedida como possivelmente errada, que os próximos capítulos tratam. Os nomes de classe de exceção aqui são da OpenAI; outros SDKs levantam as suas, então mapeie-as em sua fronteira.

JunoQuando as coisas dão errado Pegue exceções tipadas, não um `except` nú: `RateLimitError` e `APITimeoutError` são transientes e valem a pena uma retry, uma requisição ruim é seu bug e deveria falhar alto. Faça backoff exponencialmente, dobrando a espera, então você para de martelar um serviço lutando. Check se o SDK já retenta antes de adicionar sua própria camada, e mantenha a chave server-side.

O tratamento de falha é onde isso para de ser uma chamada de função e vira trabalho de sistemas distribuídos. A chamada falha de formas conhecidas, rate limits, timeouts, erros 5xx transientes, e a parte insegura não é a retry, é retentar sem pensar sobre o que uma retry custa.

A armadilha é idempotência (se rodar uma operação duas vezes tem o mesmo efeito que rodar uma vez). Uma retry ingênua após um timeout pode cobrar-lhe duplamente: a primeira requisição pode ter completado e gerado tokens no lado do provedor, a resposta nunca lhe chegou, então sua retry paga por uma segunda geração inteira. Para uma chamada estilo-leitura que é dinheiro gasto, para qualquer coisa com um side effect é uma ação duplicada. Onde o provedor suporta uma chave de idempotência, envie uma então uma retry é reconhecida como a mesma requisição; onde não, deixe a operação circundante segura para repetir.

python
import random, time
from openai import RateLimitError, APITimeoutError, APIError

RETRYABLE = (RateLimitError, APITimeoutError, APIError)

def complete(messages, *, retries=3, **params):
    for attempt in range(retries):
        try:
            return client.chat.completions.create(messages=messages, model=MODEL, **params)
        except RETRYABLE as err:
            if attempt == retries - 1:
                raise
            time.sleep((2 ** attempt) + random.random())  # backoff + jitter

Três mais realidades de produção:

  • Jitter, a fração aleatória adicionada a cada espera, para uma frota de clientes de retentarem em lockstep e re-espicar o rate limit (o problema de thundering-herd).
  • Reuso de conexão importa sob load: crie um cliente e o compartilhe, então conexões são pooled em vez de pagar um handshake TLS fresco por chamada, e rode chamadas independentes concorrentemente em vez de em um loop serial lento.
  • Classifique antes de você retentar: um RateLimitError é transiente, um erro de requisição-malformada é seu bug e retentar desperdiça tempo e dinheiro em uma chamada que nunca terá sucesso.

Os tipos de exceção e o mecanismo de idempotência são específicos do provedor, então essa política inteira pertence atrás da função de fronteira, onde você mapeia os erros de cada provedor para sua própria decisão de retry uma vez. A chave fica server-side, e uma resposta bem-sucedida ainda é confiada até validada, que saída estruturada e o capítulo de segurança tratam.

JunoQuando as coisas dão errado A retry é a parte perigosa: uma retry ingênua após um timeout pode cobrar-lhe duplamente, porque a primeira chamada pode ter gerado tokens que você nunca recebeu, então use uma chave de idempotência ou deixe a operação segura para repetir. Adicione jitter ao seu backoff então uma frota de clientes não retenta em lockstep e re-espicar o limit, reutilize um cliente pooled, e rode chamadas independentes concorrentemente. Classifique primeiro, retente erros transientes e falhe alto em suas próprias requisições ruins, e mantenha a política inteira atrás de uma fronteira então as especificidades de cada provedor mapeiam para sua decisão de retry em um lugar.