Python e APIs: Ferramentas do Ofício

Continuação: Python e APIs

Na semana passada (Python Mínimo), montamos a base: variáveis, listas, dicionários e funções. E vimos a estrutura mais importante desta aula:

Mensagens de LLM = lista de dicionários com role e content.

Hoje o Python conversa de verdade com o modelo — e tudo roda na sua própria máquina.

O conceito universal: HTTP request/response

Toda API de LLM funciona com o mesmo padrão, chame ela do nome que chamar:

Você manda um JSON via HTTP e recebe um JSON de volta. Muda a URL, muda um detalhe do formato, mas o conceito é universal. Entender HTTP = entender todas as APIs de LLM.

Pense num garçom: você entrega o pedido (request) e recebe o prato (response). O garçom não cozinha — ele transporta.

O que você precisa ter

Item Ollama (local) Ollama no Colab
API key ❌ Não precisa ❌ Não precisa
Conta em serviço ❌ Nenhuma Só a conta Google pro Colab
Internet ⚠️ Só pra baixar o modelo Sempre (Colab é cloud)
GPU Opcional (roda em CPU) T4 do Colab free
Python 3.10+ ✅ Sim ✅ Sim (vem pronto)
Biblioteca requests ✅ Sim ✅ Sim

Zero API key, zero conta de provedor, zero custo. O modelo é baixado uma vez e roda na máquina — o oposto do cloud, onde os dados saem da sua rede.

Setup: Ollama

O Ollama é um servidor que roda modelos de linguagem na sua máquina. Em quatro passos:

# 1. Instalar (Linux/macOS)
curl -fsSL https://ollama.com/install.sh | sh

# 2. Baixar o modelo do curso (~2-3 GB, roda até em notebook simples)
ollama pull gemma4:e2b-it-qat

# 3. Conferir o que está instalado
ollama list

# 4. Conversar via terminal (teste rápido, sem código!)
ollama run gemma4:e2b-it-qat

Depois disso, o Ollama fica servindo na porta 11434. Qualquer programa pode mandar requisições HTTP pra http://localhost:11434 e conversar com o modelo.

No Colab, o notebook desta aula (📓 abrir o Colab) faz o setup por você: T4 GPU, instalação e download já estão nas células. A primeira inferência demora ~2-3 min (warm up — o modelo carrega na GPU) e depois as respostas ficam rápidas.

A API nativa do Ollama: /api/chat

Aqui está o coração da aula: requests puro, sem SDK nenhum.

import requests

OLLAMA_URL = "http://localhost:11434/api/chat"
MODEL = "gemma4:e2b-it-qat"

def chat(messages, stream=False):
    """Envia mensagens pro Ollama e retorna a resposta."""
    payload = {
        "model": MODEL,
        "messages": messages,
        "stream": stream,
        "keep_alive": -1,   # mantém o modelo carregado
    }
    response = requests.post(OLLAMA_URL, json=payload, timeout=300)
    if response.status_code != 200:
        raise Exception(f"Erro {response.status_code}: {response.text}")
    return response

# Teste
r = chat([
    {"role": "system", "content": "Você é um assistente útil. Responda em português."},
    {"role": "user", "content": "Qual é a capital de Minas Gerais?"}
])
resultado = r.json()
print(resultado["message"]["content"])

A resposta chega em JSON — o campo message.content traz o texto do modelo:

{
  "model": "gemma4:e2b-it-qat",
  "message": {
    "role": "assistant",
    "content": "A capital de Minas Gerais é Belo Horizonte."
  },
  "eval_count": 14
}

Repare como a semana passada já preparou tudo: o payload é uma lista de dicts, e a resposta também é um dict. Python puro contra um modelo rodando na sua máquina.

Parâmetros de configuração

O modelo obedece a instruções, mas o estilo da resposta você controla com parâmetros:

payload["options"] = {
    "temperature": 0.7,     # 0.0 determinístico → 1.0 criativo
    "num_ctx": 8192,        # janela de contexto (tokens)
}
Faixa Comportamento Uso
0.0-0.3 Muito determinístico Código, fatos, análises
0.4-0.7 Equilibrado Conversa geral
0.8-1.0 Criativo Escrita criativa, brainstorming

Chat com memória (multi-turno)

Cada chamada envia o histórico completo. O modelo não "lembra" — ele recebe toda a conversa de novo:

historico = [
    {"role": "system", "content": "Você é um assistente útil. Responda em português. Seja conciso."}
]

historico.append({"role": "user", "content": "Meu nome é Lucas e eu amo IA."})
r1 = chat(historico)
historico.append({"role": "assistant", "content": r1.json()["message"]["content"]})
print(f"🤖: {r1.json()['message']['content']}")

historico.append({"role": "user", "content": "Qual é o meu nome?"})
r2 = chat(historico)
historico.append({"role": "assistant", "content": r2.json()["message"]["content"]})
print(f"🤖: {r2.json()['message']['content']}")   # Deve lembrar do nome!

É como repetir toda a conversa toda vez que você fala. O "contexto" do modelo é essa lista — e é ela que vamos manipular nas próximas aulas pra construir agentes.

Streaming (resposta em tempo real)

Com stream=True, o servidor manda pedaços da resposta conforme gera — a mesma experiência do chat "digitando":

response = chat([
    {"role": "user", "content": "Explique o que é uma API em 3 frases."}
], stream=True)

for line in response.iter_lines():
    if line:
        chunk = json.loads(line)
        if "message" in chunk and "content" in chunk["message"]:
            print(chunk["message"]["content"], end="", flush=True)
print()

O padrão universal: /v1/chat/completions

O Ollama também expõe um endpoint OpenAI-compatible: /v1/chat/completions. O formato nasceu na API da OpenAI e virou padrão de mercado — praticamente todo servidor de LLM fala esse dialeto.

OPENAI_URL = "http://localhost:11434/v1/chat/completions"

payload = {
    "model": "gemma4:e2b-it-qat",
    "messages": [
        {"role": "system", "content": "Você é um assistente útil. Responda em português."},
        {"role": "user", "content": "O que é Python em uma frase?"}
    ],
    "temperature": 0.7,
    "max_tokens": 64
}

response = requests.post(OPENAI_URL, json=payload, timeout=300)
data = response.json()
print(data["choices"][0]["message"]["content"])

Ponto-chave: o formato é idêntico ao da OpenAI. A única diferença aqui é a URL (localhost em vez de api.openai.com). Isso é poderoso: o mesmo código funciona com qualquer servidor que fale esse dialeto.

Outros servidores: a família llama.cpp

Todo modelo de linguagem (mesmo os do Ollama) roda, lá no fundo, no llama.cpp — o motor de inferência open source que virou a infraestrutura do ecossistema local. O ollama é uma camada amigável em cima dele.

Quando você quer controle fino, pode subir o llama-server direto — apontando pra um arquivo GGUF no disco. Este exemplo roda o LFM2.5-2.6B (um modelo agêntico da Liquid AI, treinado pra usar ferramentas), com contexto de 8K tokens:

llama-server \
  --port 8081 \
  --model ~/.llama-models/lfm2.5-2.6b/LFM2.5-2.6B-QAD-Q4_0.gguf \
  --fit on --fit-target 512 \
  --ctx-size 8192 \
  --jinja \
  --temp 0.1 --top-p 0.9 --top-k 50 --min-p 0.0 \
  --repeat-penalty 1.1 \
  --cache-type-k q4_0 --cache-type-v q4_0 \
  --flash-attn auto

O que cada flag faz:

Flag Papel
--model O GGUF do disco (LFM2.5 quantizado Q40)
--fit on / --fit-target Encaixa o modelo na VRAM disponível (deixa 512 MB de folga)
--ctx-size Janela de contexto: 8192 tokens
--jinja Usa o chat template embutido no GGUF
--temp, --top-p, --top-k, --min-p, --repeat-penalty Amostragem (recomendação da Liquid AI: temp bem baixa, 0.1)
--cache-type-k/v KV cache quantizado — economiza VRAM
--flash-attn Flash Attention — contexto grande com menos memória

O servidor sobe em http://localhost:8081 — e aí é só apontar o mesmo código de antes:

# O MESMO payload, trocando URL e modelo. Só isso.
response = requests.post(
    "http://localhost:8081/v1/chat/completions",
    json=payload,   # model: "lfm2.5-2.6b" (o nome é livre!)
    timeout=120
)
print(response.json()["choices"][0]["message"]["content"])

⚠️ maxtokens folgado: esse modelo raciocina antes de responder — o rascunho de pensamento entra no campo reasoning_content e consome o orçamento de tokens. Com max_tokens curto, content chega vazio (~200+ tokens dá folga pra uma resposta tranquila). Comportamento do modelo, não do servidor — o Ollama não separa os campos, o llama-server sim.

Um código, dois servidores na mesma máquina. Ollama e llama-server falam o mesmo dialeto — e ferramentas como o llama-swap (na porta 12434) colocam um gerenciador na frente de vários servidores pra trocar de modelo a quente.

Tratamento de Erros

Erros são iguais com qualquer API — o padrão é try/except com retry e backoff:

import time

def chamar_api(url, payload, timeout=30, tentativas=3):
    """Faz request com retry automático."""
    for tentativa in range(tentativas):
        try:
            response = requests.post(url, json=payload, timeout=timeout)
            response.raise_for_status()  # levanta exceção para erros HTTP
            return response.json()
        except requests.exceptions.ConnectionError:
            print(f"⚠️ Servidor fora do ar. Tentativa {tentativa + 1}/{tentativas}...")
            time.sleep(2)
        except requests.exceptions.Timeout:
            print(f"⚠️ Timeout. Tentativa {tentativa + 1}/{tentativas}...")
            time.sleep(2)
        except requests.exceptions.HTTPError as e:
            if e.response.status_code == 429:
                print("⚠️ Rate limit. Aguardando...")
                time.sleep(5)
            else:
                raise Exception(f"❌ HTTP {e.response.status_code}: {e.response.text}")
    raise Exception("❌ Número máximo de tentativas excedido")

Códigos de erro comuns:

Código Significado O que fazer
ConnectionError Servidor não existe Conferir se ollama serve~/~llama-server está rodando
404 Modelo não encontrado Conferir o nome listado em /api/tags
401 Não autorizado Servidor exigindo auth (raríssimo em local)
429 Rate limit Aguardar e tentar novamente
500 Erro no servidor Conferir payload; tentar de novo

No mundo local, o erro mais comum não é 401 nem 429 — é ConnectionError: você esqueceu de subir o servidor antes de fazer a requisição.

Exercícios Práticos

Exercício 1: Uma função, qualquer servidor

Crie uma função perguntar() que conversa com qualquer servidor OpenAI-compatible — só mudando a URL:

def perguntar(mensagem, url, model, system="Você é um assistente útil."):
    """Faz uma pergunta a qualquer API de LLM, via requests.post()."""
    # Implemente aqui
    pass

📝 Gabarito

def perguntar(mensagem, url, model,
              system="Você é um assistente útil.",
              temperature=0.7):
    payload = {
        "model": model,
        "messages": [
            {"role": "system", "content": system},
            {"role": "user", "content": mensagem}
        ],
        "temperature": temperature
    }
    response = requests.post(url, json=payload, timeout=120)
    response.raise_for_status()
    data = response.json()
    return data["choices"][0]["message"]["content"]

# Ollama local
print(perguntar(
    "Qual é a capital de Minas Gerais?",
    url="http://localhost:11434/v1/chat/completions",
    model="gemma4:e2b-it-qat"
))

Exercício 2: Chat com memória

Implemente um chat com histórico usando dicionários (sem classes):

historico = {"mensagens": [
    {"role": "system", "content": "Você é um assistente útil. Responda em português."}
]}

def conversar(mensagem):
    """Adiciona a mensagem ao histórico, chama a API, retorna a resposta."""
    # Implemente
    pass

def ver_historico():
    """Retorna o histórico sem o system prompt."""
    # Implemente
    pass

def limpar_historico():
    """Limpa o histórico mantendo o system prompt."""
    # Implemente
    pass

📝 Gabarito

def conversar(mensagem):
    historico["mensagens"].append({"role": "user", "content": mensagem})
    resposta = perguntar(
        mensagem,
        url="http://localhost:11434/v1/chat/completions",
        model="gemma4:e2b-it-qat",
        system=historico["mensagens"][0]["content"]
    )
    historico["mensagens"].append({"role": "assistant", "content": resposta})
    return resposta

def ver_historico():
    return historico["mensagens"][1:]

def limpar_historico():
    sistema = historico["mensagens"][0]
    historico["mensagens"] = [sistema]

# Uso
print(conversar("Meu nome é Lucas e eu amo IA."))
print(conversar("Qual é o meu nome?"))  # Deve lembrar!
print(ver_historico())

Exercício 3: Dois servidores, mesma requisição

Rode a mesma perguntar() do Exercício 1 contra dois servidores: o Ollama (localhost:11434) e um llama-server subido com o comando da apostila (localhost:8081, com o LFM2.5). O que muda de uma chamada pra outra?

📝 Gabarito

# Só a URL e o nome do modelo mudam. O payload é IDÊNTICO:
for url, model in [
    ("http://localhost:11434/v1/chat/completions", "gemma4:e2b-it-qat"),
    ("http://localhost:8081/v1/chat/completions",  "lfm2.5-2.6b"),
]:
    resposta = perguntar(
        "O que é uma API em uma frase?",
        url=url,
        model=model
    )
    print(f"{model}: {resposta}\n")

Compare as duas respostas: modelos diferentes, padrão HTTP idêntico.

Exercício 4: Tratamento de erros

Envolva a conversar() do Exercício 2 com a chamar_api() da seção anterior, para tratar:

  • Servidor offline (ConnectionError)
  • Timeout (Timeout)
  • Erro HTTP genérico (raise_for_status)

Para ir mais longe

  • LangChain: framework que abstrai as diferenças entre servidores — veremos na Semana 06!
  • llama.cpp (github.com/ggml-org/llama.cpp): o motor que roda tudo — a documentação das flags é riquíssima.
  • llama.app (https://llama.app): o instalador oficial do llama.cpp desktop, pela própria equipe ggml — Mac e Windows, sem configuração; roda modelos e fala o mesmo dialeto /v1.
  • Liquid AI LFM2.5: o modelo agêntico de 2.6B da demo — família completa em huggingface.co/LiquidAI.
  • Biblioteca do Ollama: ollama.com/library — centenas de modelos, um pull de distância.

Dica: comece com o Ollama (setup em um comando). Explore outros servidores e modelos quando o básico estiver confortável — o dialeto HTTP é sempre o mesmo.

Para lembrar

"Python é a ferramenta. O LLM é o cérebro. A API é a ponte entre eles."

  • O padrão universal: JSON via HTTP — request e response
  • Ollama → localhost:11434 (nativo e /v1 OpenAI-compatible)
  • llama-server → localhost:8081 — mesmo payload, só muda a URL
  • Chat com memória → a lista de dicts É o contexto
  • Próxima aula (16/10) → a estrutura de um agente de verdade!

← Voltar para Guilda de IA