Skip to main content

Command Palette

Search for a command to run...

O jeito certo de integrar o FastAPI com a OpenAI

Updated
5 min read
O jeito certo de integrar o FastAPI com a OpenAI

Introdução

O FastAPI é uma ferramenta moderna e super eficiente, com uma comunidade que não para de crescer, conquistou seu espaço, principalmente em projetos que envolvem IA. A performance e a facilidade de uso são incríveis!

Mas, como nem tudo são flores, esse sucesso todo traz alguns desafios. Com muitas empresas começando a usar, algumas implementações podem acabar tropeçando em armadilhas comuns, simplesmente pela falta de experiência dos desenvolvedores com a ferramenta ou por seguirmos o caminho que parece mais fácil de início.

Esse post nasceu justamente de uma situação real que enfrentei num projeto e de uma thread bem interessante que acompanhei lá no GitHub do FastAPI. Vamos falar sobre como gerenciar o cliente da OpenAI (ou qualquer outro recurso similar) do jeito certo dentro do FastAPI usando o lifespan.

O cenário

Imagine que estamos construindo o backend para um chat, tipo um ChatGPT mais simples. Poderia ser para uma interface customizada, para consumo sob demanda, ou até para usar modelos open-source compatíveis com a biblioteca openai do Python.

E para essa missão, claro, vamos usar o FastAPI, já que até as SPAs eu estou substituindo pelo FastAPI.

Pra você que está lendo, não ficar perdido. Eu vou mostrar primeiro a forma como muita gente começa: o jeito simples, que funciona na hora, mas que esconde alguns riscos e pode trazer muita dor de cabeça lá na frente.

Depois, vamos mergulhar na solução usando o lifespan do FastAPI. O foco desse artigo é mostrar como usar o lifespan especificamente para integrar o cliente da biblioteca openai de forma correta. O lifespan tem mais utilidades, mas não vamos nos aprofundar em todas elas hoje, talvez em um post futuro!

O jeito ruim de fazer: Instânciando o cliente no topo do arquivo

Quando estamos começando um projeto, a intuição (e muitos exemplos por aí) nos leva a fazer algo assim: instanciar o cliente assíncrono da openai diretamente no escopo global do nosso arquivo principal. É simples, né?

# main.py
from fastapi import FastAPI
from openai import AsyncOpenAI
import os

client = AsyncOpenAI(api_key=os.getenv("OPENAI_API_KEY"))

app = FastAPI()

@app.post("/chat")
async def chat(prompt: str):
    # Usando o cliente global diretamente
    response = await client.chat.completions.create(
        model="..."
        messages=[{"role": "user", "content": prompt}]
    )
    return {"reply": response.choices[0].message.content}

Bom, como eu disse, isso funciona. Você roda o Uvicorn e consegue fazer chamadas. Mas por baixo dos panos, vários problemas estão se acumulando:

  1. Instanciação prematura: O cliente é criado antes mesmo do servidor FastAPI estar totalmente pronto para receber requisições. Se você precisasse carregar alguma configuração assíncrona antes de criar o cliente, já não daria certo.

  2. Gerenciamento de recursos falhos: Essa instância cliente fica "viva" durante todo o tempo que o processo do servidor rodar. Mais importante: ela não é fechada corretamente quando o servidor termina. O AsyncOpenAI usa o httpx.AsyncClient por baixo, que mantém um pool de conexões HTTP. Sem chamar await client.aclose(), essas conexões ficam abertas, vazando recursos (sockets, memória).

  3. Dificuldade em testes: Testar endpoints que dependem de estado global é um pé no saco. Mockar ou substituir essa instância cliente para testes unitários ou de integração fica bem mais complicado.

  4. Replicação por worker: Se você rodar sua aplicação com múltiplos workers (como o Gunicorn faz em produção), cada worker vai criar sua própria instância global do cliente, multiplicando o desperdício de recursos e os problemas de conexão.

“O jeito certo”: Usando o lifespan do FastAPI

Eu coloquei entre aspas pois existem outras formas de fazer, como o before ou on_event e outras formas que também pode funcionar e ao mesmo tempo seguir as boas práticas. Cagar regra na nossa área definitivamente não é uma boa prática.

Voltando ao assunto. Felizmente, o FastAPI oferece uma solução elegante para gerenciar o ciclo de vida de recursos como nosso cliente openai: o parâmetro lifespan.

Ele utiliza um async context manager (gerenciador de contexto assíncrono) para garantir que seu recurso seja inicializado depois que a aplicação começa e finalizado antes que ela termine completamente.

Veja como fica bem mais interessante:

# main.py
import os
from openai import AsyncOpenAI
from fastapi import FastAPI, Request
from contextlib import asynccontextmanager

# O gerenciador de contexto para o ciclo de vida
@asynccontextmanager
async def lifespan(app: FastAPI):
    # Setup: Roda antes da aplicação iniciar
    print("Iniciando cliente OpenAI...")
    app.state.openai_client = AsyncOpenAI(api_key=os.getenv("OPENAI_API_KEY"))
    print("Cliente OpenAI pronto!")

    yield # Essa função congela nesse momento e aplicação entra em ação

    # Teardown: A mágica acontece aqui, depois que voc6e para a aplicação esse código é executado
    print("Fechando cliente OpenAI...")
    await app.state.openai_client.close()
    print("Cliente OpenAI fechado.")

app = FastAPI(lifespan=lifespan)

@app.post("/chat")
async def chat(request: Request, prompt: str): # Simplificado para receber só o prompt
    # Acessa o cliente via app.state
    client: AsyncOpenAI = request.app.state.openai_client

    response = await client.chat.completions.create(c
        model="...",
        messages=[{"role": "user", "content": prompt}]
    )
    return {"reply": response.choices[0].message.content}

Entendendo a mágica: asynccontextmanager e yield

O decorador @asynccontextmanager (da biblioteca contextlib do Python) é a chave aqui. Ele transforma nossa função lifespan em um gerenciador de contexto assíncrono especial.

  1. Antes do yield: Todo o código antes da linha yield é a fase de setup. O FastAPI executa isso durante a inicialização (aqui eu imagino que você já entendeu o lance). É aqui que criamos nosso cliente AsyncOpenAI e o guardamos em app.state.openai_client. O app.state é um objeto tipo dicionário feito exatamente para guardar recursos que precisam viver junto com a aplicação.

  2. O yield: A palavra yield é o ponto de "pausa". A função lifespan fica congelada aqui, e o controle volta para o FastAPI, que finalmente começa a aceitar e processar requisições. Enquanto a aplicação está rodando, nossos endpoints (como /chat) podem acessar o cliente via request.app.state.openai_client.

  3. Depois do yield: Quando o FastAPI recebe um sinal para desligar (por exemplo, quando você pressiona Ctrl+C no terminal), ele "descongela" a função lifespan depois do yield. Essa é a fase de teardown. Aqui, executamos a limpeza: await client.close(), garantindo que as conexões HTTP sejam fechadas corretamente.

Conclusão

Sim, instanciar o cliente globalmente vai funcionar no começo. O FastAPI é robusto e vai saber lidar com isso. Mas essa abordagem não escala bem e esconde problemas que vão te assombrar em produção ou em aplicações maiores.

E pra ser honesto, eu usei a openai para chamar a sua atenção mas a minha verdadeira intenção era te mostrar o uso do lifespan.

O jeito certo de integrar o FastAPI com a OpenAI