Adeus APIs Pagas: Como Dominar LLMs Locais com Python e Ollama (Do Chat Básico a Agentes e APIs em Produção)
Adeus APIs Pagas: Como Dominar LLMs Locais com Python e Ollama (Do Chat Básico a Agentes e APIs em Produção)
A popularização dos Modelos de Linguagem de Grande Porte (LLMs) transformou o desenvolvimento de software, mas o uso de soluções em nuvem frequentemente levanta preocupações com a privacidade de dados sensíveis, custos recorrentes por token e a dependência de conexão constante com a internet. Para superar esses desafios, a execução de LLMs em hardware próprio tornou-se uma alternativa viável e eficiente. Ferramentas de código aberto como o Ollama funcionam como um "Docker para modelos de linguagem", simplificando o processo de baixar e rodar modelos abertos — como Llama 3, Mistral e Qwen — diretamente no seu computador. Integre essa infraestrutura ao Python utilizando a biblioteca oficial do Ollama ou requisições REST HTTP para construir assistentes privados, automações e agentes inteligentes capazes de executar funções com segurança total e custo zero de API.
1. Arquitetura & Fundamentos de LLMs Locais
- 1.1. Como Funciona a Execução Local: Alocação em VRAM vs. RAM, offloading parcial para CPU e o papel do motor
llama.cpp. - 1.2. Quantização e Formato GGUF: Como a compressão em 4-bit e 8-bit reduz a pegada de memória mantendo o desempenho do modelo.
- 1.3. Comparativo do Ecossistema: Quando utilizar Ollama, Hugging Face Transformers Pipeline ou vLLM para cenários de produção.
2. Configuração e Gerenciamento do Ollama
- 2.1. Comandos Essenciais do CLI:
ollama serve,ollama pull,ollama rune gerenciamento de imagens. - 2.2. Customização de Modelos com
Modelfile: Ajustando System Prompts e hiperparâmetros de amostragem (temperature,top_p,top_k).
3. Integração Básica em Python (SDK vs. REST API)
- 3.1. Cliente Oficial (
ollama-python):- Código: Conversas com contexto de histórico via
ollama.chat(). - Código: Prompting direto sem contexto usando
ollama.generate().
- Código: Conversas com contexto de histórico via
- 3.2. Requisições REST HTTP puras (
requests):- Código: Consumindo o endpoint
/api/chatcom JSON payload. - Código: Consumindo o endpoint
/api/generatede passagem única.
- Código: Consumindo o endpoint
4. Recursos Avançados de Programação com LLMs
- 4.1. Streaming de Respostas em Tempo Real:
- Código: Iterando sobre geradores com
stream=Trueeflush=Truepara exibir tokens no terminal à medida que são gerados.
- Código: Iterando sobre geradores com
- 4.2. Concorrência com Cliente Assíncrono (
AsyncClient):- Código: Executando múltiplas requisições paralelas sem bloquear a I/O da aplicação via
asyncio.gather().
- Código: Executando múltiplas requisições paralelas sem bloquear a I/O da aplicação via
- 4.3. Saídas Estruturadas (Structured Outputs / JSON Schema):
- Código: Forçando a resposta da LLM em formato JSON estrito compatível com Pydantic (
format="json").
- Código: Forçando a resposta da LLM em formato JSON estrito compatível com Pydantic (
- 4.4. Manipulação de Modelos de Raciocínio ("Thinking Models"):
- Código: Interceptando e separando os blocos de raciocínio intermediário (
<think>...</think>) da resposta final (ex: Qwen3).
- Código: Interceptando e separando os blocos de raciocínio intermediário (
5. Agentes Inteligentes e Chamada de Funções (Tool Calling)
- 5.1. Definição de Ferramentas em Python:
- Código: Criando assinaturas de funções e schemas de ferramentas com docstrings detalhadas.
- 5.2. O Ciclo Completo do Agente (Tool Loop):
- Código: Detectando a intenção do modelo (
tool_calls), executando a função local em Python e devolvendo o resultado com a roletool.
- Código: Detectando a intenção do modelo (
6. Projetos Práticos Prontos para o Blog
- 6.1. Ferramenta de CLI: Gerador Automático de Git Commits (
AI Commit):- Código: Capturando o
git diffviasubprocess, formatando as alterações com um prompt de sistema e gerando mensagens de commit convenções offline.
- Código: Capturando o
- 6.2. Microsserviço de Produção com FastAPI:
- Código: Criando uma API REST em FastAPI que encapsula o modelo local Ollama com validação Pydantic e documentação Swagger.
- 6.3. Compatibilidade Nativa com a SDK da OpenAI:
- Código: Redirecionando a biblioteca oficial da OpenAI para o endpoint
http://localhost:11434/v1/do Ollama.
- Código: Redirecionando a biblioteca oficial da OpenAI para o endpoint
7. Tratamento de Erros, Resiliência e Boas Práticas
- 7.1. Tratamento de Exceções e Timeouts: Lidadando com
ResponseErrore falhas de conexão de servidores remotos. - 7.2. Estratégias de Retry com Backoff Exponencial: Implementando lógica de retentativa para evitar indisponibilidades durante carregamento do modelo.
- 7.3. Otimização de Memória e Concorrência: Gerenciando a fila de inferência e evitando estouros de VRAM.
Capítulo 1: Arquitetura & Fundamentos de LLMs Locais
A transição da inferência de Modelos de Linguagem de Grande Porte (LLMs) de servidores em nuvem para hardware de nível de consumidor representa uma mudança fundamental na forma como desenvolvemos e implantamos sistemas de inteligência artificial. Enquanto o modelo tradicional baseia-se no envio de requisições para APIs de terceiros, a execução local permite rodar arquiteturas abertas diretamente no seu próprio computador, garantindo total privacidade de dados, eliminando custos recorrentes por token e permitindo o funcionamento completamente offline com baixa latência de rede.
No entanto, processar modelos compostos por bilhões de parâmetros em dispositivos pessoais impõe desafios computacionais e de memória significativos. Para superar os limites do hardware, a arquitetura de execução local depende do gerenciamento dinâmico de recursos entre a memória de vídeo (VRAM) da GPU e a memória do sistema (RAM), utilizando estratégias de divisão de camadas (CPU offloading). Paralelamente, técnicas de otimização como a quantização reduzem a precisão numérica dos pesos do modelo (como a conversão de FP16 para números inteiros de 4-bit ou 8-bit), reduzindo drasticamente a pegada de memória e acelerando o processamento sem comprometer substancialmente a acurácia do modelo.
Motores de inferência e formatos binários como o GGUF e o llama.cpp viabilizam o carregamento rápido de tensores e metadados estruturados diretamente na memória. Essa base tecnológica, somada a soluções que variam da simplicidade do Ollama ao alto rendimento de ferramentas como o vLLM e o ecossistema Hugging Face, fornece o alicerce necessário para transformar computadores pessoais e estações de trabalho em infraestruturas privadas e eficientes de IA.
1.1. Como Funciona a Execução Local: Alocação em VRAM vs. RAM, Offloading Parcial para CPU e o Papel do Motor llama.cpp
A execução local de Modelos de Linguagem de Grande Porte (LLMs) depende do gerenciamento eficiente dos recursos de memória e processamento do seu computador. Entender como os pesos do modelo são carregados e processados entre a GPU e a CPU é fundamental para otimizar o desempenho da inferência.
1. Alocação de Memória: VRAM vs. RAM do Sistema
Para que uma LLM gere texto, seus pesos matemáticos (parâmetros) e o cache de contexto precisam estar acessíveis na memória.
- VRAM (Memória de Vídeo da GPU): As placas de vídeo possuem milhares de núcleos especializados em multiplicação de matrizes paralelas. Quando o modelo cabe inteiramente na VRAM (por exemplo, um modelo de 7B ou 8B parâmetros quantizado em 4 bits ocupando cerca de 4 GB a 5 GB de VRAM), a inferência atinge sua velocidade máxima (frequentemente acima de 20 a 30 tokens por segundo).
- RAM do Sistema (CPU): Quando a GPU não possui VRAM suficiente (ou em sistemas sem placa dedicada), o modelo é carregado na RAM principal do computador. Embora processadores modernos executem LLMs utilizando instruções vetoriais (como AVX2/AVX-512), a menor largura de banda da RAM e o menor número de núcleos da CPU resultam em uma taxa de geração mais lenta (normalmente de 2 a 5 tokens por segundo).
2. Offloading Parcial para CPU (GPU Layer Offloading)
Quando um modelo excede a capacidade da sua placa de vídeo, não é necessário rodá-lo 100% na CPU. Tecnologias de inferência local permitem o offloading parcial de camadas:
- A rede neural do modelo é dividida camada por camada.
- O máximo de camadas possível é enviado para a VRAM da GPU para acelerar os cálculos.
- As camadas restantes que não couberem na VRAM "transbordam" e são alocadas na RAM do sistema, sendo processadas pela CPU.
Essa abordagem híbrida viabiliza a execução de modelos maiores (como variantes de 13B, 30B ou até 70B) em placas de vídeo intermediárias.
3. O Papel do Motor llama.cpp e o Formato GGUF
O motor de inferência por trás da maioria das ferramentas de IA local (incluindo o Ollama) é o llama.cpp, uma biblioteca de alto desempenho desenvolvida em C/C++ por Georgi Gerganov.
O llama.cpp introduziu o formato binário GGUF (GPT-Generated Unified Format), que traz avanços cruciais para a comunidade:
- Carregamento Unificado: Empacota em um único arquivo tanto os metadados do modelo quanto os tensores de pesos quantizados (como
Q4_K,Q6_KouQ8_0). - Mapeamento Direto de Memória (mmap): Permite carregar partes do modelo quase instantaneamente na memória.
- Controle Fino de GPU: Expõe o parâmetro
-ngl(number of GPU layers), permitindo definir exatamente quantas camadas do modelo devem ser enviadas à GPU.
Exemplo Prático em Python (llama-cpp-python)
Abaixo está um exemplo utilizando os bindings oficiais em Python para o llama.cpp (llama-cpp-python). O código demonstra como carregar um arquivo .gguf e configurar explicitamente o offloading de camadas para a GPU:
from llama_cpp import Llama
# Caminho para o modelo quantizado no formato GGUF
model_path = "models/llama-3-8b-instruct.Q4_K_M.gguf"
# Inicializa o modelo configurando o offloading de camadas
llm = Llama(
model_path=model_path,
n_gpu_layers=24, # Envia 24 camadas para a VRAM da GPU; o restante roda na CPU/RAM
n_ctx=2048, # Define a janela de contexto em tokens
verbose=True # Exibe os logs de alocação de memória (VRAM vs RAM)
)
# Prompt de teste
prompt = "<|user|>\nExplique em duas frases o que é offloading de GPU.<|end|>\n<|assistant|>"
# Executa a inferência local
output = llm(
prompt,
max_tokens=100,
temperature=0.7,
stop=["<|end|>"]
)
# Exibe o resultado
print("\n--- Resposta da LLM Local ---")
print(output["choices"]["text"].strip())
1.2. Quantização e Formato GGUF: Como a Compressão em 4-bit e 8-bit Reduz a Pegada de Memória Mantendo o Desempenho do Modelo
A quantização é a técnica de otimização mais impactante para viabilizar a execução de LLMs em hardware comum. Ela consiste em converter a precisão numérica dos pesos do modelo — originalmente representados em ponto flutuante de alta precisão de 32 bits (FP32) ou 16 bits (FP16/BF16) — para formatos de inteiros de menor precisão, como 8 bits (INT8) ou 4 bits (INT4/NF4).
1. O Impacto na Pegada de Memória (VRAM / RAM)
Sem quantização, cada parâmetro em FP16 consome cerca de 2 bytes de memória. Isso significa que um modelo de 7 bilhões de parâmetros (7B) exige aproximadamente 14 GB a 16 GB de RAM/VRAM apenas para carregar seus pesos na memória.
A compressão altera drasticamente essa equação:
- Quantização de 8-bit (INT8 /
Q8_0): Reduz o consumo de memória pela metade em relação ao FP16. Um modelo 7B/8B passa a ocupar cerca de 8,5 GB. - Quantização de 4-bit (INT4 /
Q4_K/ NF4): Proporciona uma compressão de cerca de 3,5x a 4x no tamanho do arquivo. O mesmo modelo de 7B/8B passa a ocupar apenas 4 GB a 5 GB de VRAM, permitindo sua execução em placas de vídeo intermediárias ou em notebooks com 16 GB de RAM. - Modelos Grandes (Ex: 70B): Um modelo de 70 bilhões de parâmetros em precisão FP16 precisaria de aproximadamente 140 GB de VRAM. Com a quantização em 4 bits, o tamanho cai para cerca de 40 GB, tornando viável a execução em estações de trabalho avançadas com GPUs de 24 GB via offload.
2. O Trade-off Entre Precisão, Perplexidade e Acurácia
O principal desafio da quantização é reduzir o consumo de memória sem comprometer a capacidade de raciocínio do modelo.
- O "Ponto Ideal" (Sweet Spot): Avaliações empíricas mostram que os níveis de quantização em 8 bits (
Q8_0) e 4 bits (Q4_K) mantêm um desempenho e uma acurácia extremamente próximos do modelo original sem quantização. A perda de qualidade na geração de texto e em tarefas de código costuma ser pequena para a maioria das aplicações práticas. - O Risco da Quantização Agressiva (2 bits): Reduzir a precisão para 2 bits (
Q2_K) pode levar a uma degradação severa do desempenho, provocando perda de coerência, aumento drástico da perplexidade (perplexity spike) e queda acentuada em testes de raciocínio complexo.
3. O Formato GGUF e os K-Quants
O formato binário GGUF (GPT-Generated Unified Format) foi criado no ecossistema do llama.cpp para unificar o armazenamento de metadados e tensores de pesos quantizados em um único arquivo de carregamento rápido.
Em vez de usar uma quantização uniforme simplista em toda a rede neural, o GGUF emprega técnicas conhecidas como k-quants (ex: Q4_K_M, Q5_K_S):
- O algoritmo aplica agrupamento por k-means para mapear os pesos em centroides não uniformes.
- Camadas mais sensíveis da rede neural mantêm maior precisão, enquanto camadas menos críticas sofrem compressão mais agressiva.
Exemplos de Código em Python
Você pode implementar e carregar modelos quantizados em Python utilizando o formato binário GGUF (via llama-cpp-python) ou aplicando quantização dinamicamente com Hugging Face transformers + bitsandbytes.
Opção A: Carregando Modelo GGUF de 4 bits com llama-cpp-python
from llama_cpp import Llama
# Carrega o arquivo binário GGUF quantizado em 4 bits (Q4_K_M)
llm = Llama(
model_path="models/llama-3-8b-instruct.Q4_K_M.gguf",
n_gpu_layers=-1, # -1 envia todas as camadas para a GPU se houver VRAM suficiente
n_ctx=2048,
verbose=False
)
# Executa a inferência
response = llm(
"Explique o que é quantização GGUF em um parágrafo.",
max_tokens=150,
temperature=0.7
)
print(response["choices"]["text"])
Opção B: Quantização On-the-Fly em 4 bits (NF4) com Hugging Face bitsandbytes
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
model_id = "meta-llama/Meta-Llama-3-8B-Instruct"
# Configura a quantização de 4 bits via BitsAndBytesConfig (QLoRA / NF4)
quant_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4", # Formato Normal Float 4
bnb_4bit_compute_dtype=torch.bfloat16, # Tipo numérico para os cálculos de inferência
bnb_4bit_use_double_quant=True # Quantização aninhada para economizar mais memória
)
# Carrega o modelo aplicando a quantização na VRAM
tokenizer = AutoTokenizer.from_pretrained(model_id)
model = AutoModelForCausalLM.from_pretrained(
model_id,
quantization_config=quant_config,
device_map="auto"
)
# Exibe a pegada de memória do modelo quantizado em GB
print(f"Memória ocupada pelo modelo: {model.get_memory_footprint() / 1e9:.2f} GB")
1.3. Comparativo do Ecossistema: Quando Utilizar Ollama, Hugging Face Transformers Pipeline ou vLLM em Produção
A escolha do framework correto para executar LLMs locais depende diretamente do seu cenário de uso: se você está construindo um protótipo rápido para desenvolvimento pessoal, experimentando com fine-tuning de modelos ou implantando um serviço em produção com alto tráfego de requisições simultâneas.
Tabela Comparativa do Ecossistema
| Critério | Ollama | Hugging Face (pipeline) | vLLM |
|---|---|---|---|
| Foco Principal | Facilidade de uso e automação local | Pesquisa, prototipagem e desenvolvimento nativo | Servimento em produção de alto throughput |
| Backend de Inferência | llama.cpp (C/C++) com quantização GGUF | PyTorch nativo / transformers | PagedAttention & gerenciamento avançado de KV Cache |
| Concorrência e Escala | Baixa a moderada (processa poucas requisições paralelas por modelo por padrão) | Moderada (requer controle manual de batching) | Extrema (múltiplos usuários simultâneos com continuous batching) |
| Compatibilidade API | REST API própria + Cliente Python + Compatível com OpenAI | Funções e classes Python nativas | Servidor REST 100% compatível com a API da OpenAI |
| Caso de Uso Ideal | Chatbots locais, ferramentas CLI, RAG pessoal e microsserviços leves | Experimentos de Ciência de Dados, fine-tuning e modificação de arquitetura | APIs de produção escaláveis, clusters Kubernetes e microsserviços corporativos |
1. Ollama: O Campeão da Produtividade e Desenvolvimento Local
O Ollama funciona como um "Docker para LLMs". Ele empacota pesos, configurações e prompts em um repositório simplificado, permitindo baixar e rodar modelos com apenas um comando (ollama pull / ollama run).
- Quando utilizar: Ideal para desenvolvimento de aplicações locais, protótipos rápidos, ferramentas de linha de comando (CLI) e automações onde a privacidade e o custo zero de API são prioridades.
- Vantagens: Instalação em poucos minutos, suporte multiplataforma (macOS, Linux, Windows), cliente Python leve e gerenciamento automático de hardware e quantização.
2. Hugging Face Transformers pipeline(): Flexibilidade Total e Controle Numérico
A biblioteca transformers da Hugging Face oferece a função de alto nível pipeline(), que abstrai o pré-processamento (tokenização), execução do modelo e pós-processamento em poucas linhas de código.
- Quando utilizar: Quando você precisa interagir diretamente com o ecossistema PyTorch, testar novos modelos do Hugging Face Hub, alterar camadas internas ou realizar fine-tuning.
- Vantagens: Acesso direto a mais de 800.000 modelos pré-treinados e total controle sobre tensores e pipelines de dados.
3. vLLM: O Padrão de Ouro para Produção de Alta Escala
Desenvolvido pela UC Berkeley, o vLLM é um motor de inferência projetado especificamente para servidores de produção de alto desempenho. Ele introduziu a técnica PagedAttention, que otimiza o uso da VRAM reduzindo o desperdício de memória no cache de chaves e valores (KV Cache).
- Quando utilizar: Quando sua aplicação precisa atender dezenas ou centenas de usuários simultaneamente com baixa latência e alto throughput de tokens por segundo.
- Vantagens: Suporta continuous batching, escala com eficiência em clusters e expõe uma API REST nativamente compatível com a SDK da OpenAI.
Exemplos Práticos de Código em Python
A. Ollama (Cliente Oficial em Python)
import ollama
# Executa uma chamada de chat de forma simples e direta
response = ollama.chat(
model="llama3.2:latest",
messages=[
{"role": "system", "content": "Você é um assistente técnico conciso."},
{"role": "user", "content": "Qual a principal vantagem de usar Ollama?"}
]
)
print("--- Ollama Python ---")
print(response['message']['content'])
B. Hugging Face Transformers pipeline()
from transformers import pipeline
# Carrega o pipeline de geração de texto usando PyTorch nativo
generator = pipeline(
"text-generation",
model="Qwen/Qwen2.5-1.5B-Instruct",
device_map="auto" # Aloca automaticamente entre GPU e CPU
)
outputs = generator(
"Explique o uso de pipelines no Hugging Face:",
max_new_tokens=100,
do_sample=True,
temperature=0.7
)
print("\n--- Hugging Face Pipeline ---")
print(outputs["generated_text"])
C. Consumindo um Servidor vLLM em Produção (SDK OpenAI)
from openai import OpenAI
# Conecta ao servidor vLLM rodando localmente ou em um cluster de produção
client = OpenAI(
base_url="http://localhost:8000/v1", # Endpoint do servidor vLLM
api_key="token-nao-necessario" # vLLM não exige chave por padrão
)
response = client.chat.completions.create(
model="meta-llama/Meta-Llama-3-8B-Instruct",
messages=[
{"role": "user", "content": "Como o vLLM otimiza o KV Cache?"}
],
temperature=0.7
)
print("\n--- vLLM Server via OpenAI SDK ---")
print(response.choices.message.content)
Capítulo 2: Configuração e Gerenciamento do Ollama
O Ollama consolidou-se como uma das ferramentas mais eficientes para a execução local de modelos de linguagem, atuando na prática como uma espécie de "Docker para LLMs". Ele abstrai a complexidade dos motores de inferência subjacentes e automatiza a gestão de dependências, empacotando pesos quantizados, metadados e configurações em artefatos unificados de fácil distribuição. Com suporte nativo para Linux, macOS e Windows, a plataforma permite transformar computadores pessoais e servidores em nós privados de inteligência artificial em poucos minutos.
Por trás de sua interface simplificada, o Ollama executa um serviço em segundo plano (ollama serve) que expõe uma API REST HTTP local no endereço http://localhost:11434. Através de uma interface de linha de comando (CLI) intuitiva, os desenvolvedores conseguem gerenciar todo o ciclo de vida dos modelos — desde o download (pull) e execução de sessões interativas (run) até a listagem e remoção de instâncias para otimização do armazenamento em disco. Além disso, o suporte a arquivos de receita (Modelfiles) oferece flexibilidade para adaptar instruções de sistema, ajustar parâmetros de amostragem e empacotar variantes personalizadas de modelos abertos.
2.1. Comandos Essenciais do CLI: ollama serve, ollama pull, ollama run e Gerenciamento de Imagens
A Interface de Linha de Comando (CLI) do Ollama foi projetada com uma experiência de uso fortemente inspirada no Docker. Ela abstrai a complexidade do gerenciamento de arquivos binários pesados, permitindo baixar, executar e organizar modelos locais com comandos curtos e padronizados.
1. O Daemon do Servidor: ollama serve
O Ollama funciona sobre uma arquitetura cliente-servidor. Para que qualquer comando ou script em Python consiga interagir com as LLMs, o serviço em segundo plano precisa estar ativo.
- O que faz: Inicia o servidor HTTP REST local escutando na porta
11434(http://localhost:11434). - No Linux/macOS: Em instalações padrão, o serviço costuma ser iniciado automaticamente pelo sistema. Caso precise iniciá-lo manualmente no terminal, utilize:
ollama serve
2. Download de Modelos: ollama pull
O comando ollama pull faz o download dos pesos quantizados e metadados de um modelo a partir do repositório oficial (ollama.com/library) diretamente para o seu disco rígido, sem abrir uma sessão interativa.
- Exemplos de uso:
(Os modelos são armazenados em diretórios internos como# Baixa a versão padrão do Llama 3.2 ollama pull llama3.2:latest # Baixa um modelo especializado em código ollama pull qwen2.5-coder:1.5b~/.ollama/modelsno Linux/macOS ou%USERPROFILE%\.ollama\modelsno Windows).
3. Execução Interativa e Execução Direta: ollama run
O ollama run é o comando principal de interação. Se o modelo solicitado ainda não estiver baixado localmente, ele realiza o pull automaticamente antes de iniciar a execução.
-
Modo Chat Interativo:
ollama run llama3.2:latestAbre um prompt interativo no terminal. Para encerrar a conversa e sair, digite
/byeou pressioneCtrl + D. -
Modo Passagem Única (One-shot): Você pode enviar um prompt direto entre aspas para receber a resposta imediatamente no stdout sem entrar no chat:
ollama run llama3.2:latest "Explique o conceito de DRY em programação em uma frase."
4. Gerenciamento de Imagens e Armazenamento Local
Assim como no Docker, manter o controle dos modelos baixados é essencial para gerenciar o espaço em disco (SSDs) e a alocação de memória.
| Comando | Descrição / Objetivo | Exemplo |
|---|---|---|
ollama list | Lista todos os modelos baixados no disco, com ID, tamanho e data de modificação. | ollama list |
ollama ps | Exibe quais modelos estão carregados no momento na memória (RAM/VRAM). | ollama ps |
ollama show | Exibe detalhes estruturais do modelo (Modelfile, template de chat e parâmetros). | ollama show llama3.2 |
ollama cp | Cria uma cópia/clone local do modelo com outro nome ou tag. | ollama cp llama3.2 meu-llama-custom |
ollama rm | Deleta um modelo do armazenamento para liberar espaço em disco. | ollama rm mistral |
Exemplo de Código em Python: Gerenciando Modelos Programaticamente
Você não precisa se limitar ao terminal. A biblioteca oficial ollama em Python permite listar, baixar e verificar modelos diretamente no seu código:
import ollama
def gerenciar_modelos_locais():
print("--- 1. Listando modelos instalados ---")
# Retorna os modelos disponíveis localmente (equivalente ao 'ollama list')
modelos = ollama.list()
modelos_instalados = [m['name'] for m in modelos.get('models', [])]
for nome in modelos_instalados:
print(f" - {nome}")
modelo_desejado = "qwen2.5-coder:1.5b"
# Verifica se o modelo já está disponível
if modelo_desejado not in modelos_instalados:
print(f"\n--- 2. Modelo '{modelo_desejado}' não encontrado. Baixando via SDK... ---")
# Baixa o modelo programaticamente (equivalente ao 'ollama pull')
ollama.pull(modelo_desejado)
print("Download concluído com sucesso!")
else:
print(f"\n--- Modelo '{modelo_desejado}' já está pronto para uso! ---")
# Executa uma geração rápida de teste
resposta = ollama.generate(
model=modelo_desejado,
prompt="Crie uma função Python para inverter uma lista."
)
print("\n--- Resultado do Teste ---")
print(resposta['response'])
if __name__ == "__main__":
gerenciar_modelos_locais()
2.2. Customização de Modelos com Modelfile: Ajustando System Prompts e Hiperparâmetros de Amostragem
O Modelfile é o arquivo de configuração do Ollama que permite criar variantes personalizadas de modelos abertos. Assim como um Dockerfile define a construção de uma imagem de contêiner, o Modelfile especifica o modelo base, as instruções permanentes do sistema (System Prompt) e os hiperparâmetros de amostragem que regem o comportamento e a criatividade da LLM.
1. Diretivas Principais do Modelfile
Um arquivo Modelfile é composto por instruções declarativas simples:
FROM: Define o modelo base baixado localmente (ex:FROM llama3.2).SYSTEM: Estabelece o papel (role), comportamento, regras de conduta ou contexto fixo que o modelo seguirá em todas as interações.PARAMETER: Configura os hiperparâmetros numéricos de amostragem e do motor de inferência:temperature: Controla a aleatoriedade da geração de tokens. Valores baixos (ex:0.0a0.2) tornam as respostas determinísticas e focadas (ideais para código e análise factual); valores mais altos (ex:0.7a1.0) aumentam a variedade e criatividade.top_p(Nucleus Sampling): Limita a amostragem aos tokens cuja probabilidade cumulativa atinge o valor definido (ex:0.9descarta os 10% de tokens menos prováveis).top_k: Restringe a seleção aos \(K\) tokens de maior probabilidade a cada passo de geração.repeat_penalty: Aplica uma penalidade para desestimular a repetição desnecessária de palavras ou frases.num_ctx: Define o tamanho máximo da janela de contexto em tokens (ex:2048ou4096).
2. Criando e Compilando um Modelfile
Crie um arquivo texto chamado Modelfile na sua pasta de projeto com a seguinte estrutura:
# Define o modelo base
FROM llama3.2
# Configura os hiperparâmetros de amostragem
PARAMETER temperature 0.2
PARAMETER top_p 0.9
PARAMETER top_k 40
PARAMETER repeat_penalty 1.1
PARAMETER num_ctx 4096
# Instrução de sistema permanente
SYSTEM """
Você é um arquiteto de software sênior especializado em Python.
Suas respostas devem ser estritamente técnicas, diretas e focadas em código limpo (Clean Code).
Sempre inclua type hints e docstrings explicativas nas funções geradas.
"""
Para compilar o novo modelo no servidor local do Ollama, execute o comando ollama create no terminal:
ollama create python-expert -f Modelfile
Após a criação, o modelo python-expert estará disponível na sua lista local (ollama list) e pronto para ser executado.
3. Exemplo Prático em Python: Criando e Chamando Modelos Customizados
Você pode criar o modelo programaticamente via SDK Python ou passar os hiperparâmetros de amostragem dinamicamente em tempo de execução:
Opção A: Criando o Modelo Programaticamente via SDK (ollama.create)
import ollama
# Conteúdo do Modelfile em string
modelfile_content = """
FROM llama3.2
PARAMETER temperature 0.1
SYSTEM "Você é um assistente especializado exclusivamente em refatoração e otimização de código Python."
"""
# Cria o modelo customizado no servidor Ollama
print("Criando o modelo 'refatorador-python'...")
ollama.create(model='refatorador-python', modelfile=modelfile_content)
print("Modelo criado com sucesso!")
# Executa uma geração com o novo modelo
resposta = ollama.generate(
model='refatorador-python',
prompt="Como otimizar a leitura de um arquivo texto de 5GB linha por linha?"
)
print("\n--- Resposta do Modelo Customizado ---")
print(resposta['response'])
Opção B: Passando Parâmetros Dinâmicos em Tempo de Execução (options)
Caso precise ajustar os hiperparâmetros apenas para uma consulta específica sem criar um novo modelo permanente, utilize o parâmetro options:
import ollama
# Executa o modelo base ajustando a temperatura e amostragem na requisição
resposta = ollama.generate(
model='llama3.2',
prompt='Escreva uma metáfora criativa para explicar o conceito de threads e assincronia.',
options={
'temperature': 0.85, # Maior criatividade
'top_p': 0.95,
'repeat_penalty': 1.2, # Evita repetição
'num_ctx': 2048
}
)
print("--- Geração Criativa Customizada ---")
print(resposta['response'])
Capítulo 3: Integração Básica em Python (SDK vs. REST API)
A conexão entre aplicações em Python e o servidor local do Ollama pode ser realizada por meio de duas abordagens principais, dependendo da arquitetura e das necessidades do seu projeto: a SDK oficial (ollama-python) ou a interação direta com a API REST HTTP. Ambas as estratégias comunicam-se com o mesmo serviço em segundo plano (http://localhost:11434), mas oferecem diferentes níveis de abstração e controle sobre a transmissão de dados.
O uso da SDK oficial em Python proporciona uma experiência de desenvolvimento nativa e idiomática. Ela abstrai a formatação manual de payloads JSON e o gerenciamento de conexões HTTP, expondo métodos diretos como chat() para conversas com histórico estruturado e generate() para prompts de passagem única. Essa abordagem reduz o código boilerplate e é a escolha recomendada para a maioria das aplicações e scripts em Python.
Por outro lado, a integração direta via API REST HTTP (utilizando bibliotecas como requests ou httpx) oferece independência total de pacotes adicionais e controle refinado sobre o protocolo de rede. Ao consumir diretamente os endpoints /api/chat e /api/generate, os desenvolvedores ganham flexibilidade para integrar o Ollama em ambientes com restrições de dependências, pipelines assíncronos customizados ou arquiteturas de microsserviços onde o controle preciso sobre headers, timeouts e serialização JSON é indispensável.
3.1. Cliente Oficial (ollama-python)
A biblioteca oficial ollama (pip install ollama) é a forma recomendada de integrar modelos locais em projetos Python. Ela expõe duas funções fundamentais para comunicação com o servidor Ollama: ollama.chat() e ollama.generate().
1. Conversas com Contexto de Histórico via ollama.chat()
A função ollama.chat() é projetada para diálogos estruturados (multi-turn), onde a preservação do contexto da conversa é essencial. O parâmetro messages recebe uma lista de dicionários Python, onde cada item possui uma chave 'role' ('system', 'user', 'assistant' ou 'tool') e uma chave 'content'.
Para manter o histórico entre interações, basta anexar a mensagem retornada pelo modelo (response['message']) de volta à lista de mensagens antes de realizar uma nova chamada.
- Exemplo de Código: Chat Conversacional com Histórico
import ollama
# 1. Define o histórico inicial de mensagens
messages = [
{"role": "system", "content": "Você é um tutor especializado em linguagem Python."},
{"role": "user", "content": "Explique o que é uma list comprehension em uma frase."}
]
# 2. Primeira chamada ao modelo
response = ollama.chat(model="llama3.2:latest", messages=messages)
print("Assistente:", response['message']['content'])
# 3. Anexa a resposta do assistente ao histórico para manter o contexto
messages.append(response['message'])
# 4. Adiciona a pergunta de acompanhamento do usuário
messages.append({
"role": "user",
"content": "Forneça um exemplo prático e curto disso."
})
# 5. Segunda chamada enviando todo o histórico acumulado
follow_up = ollama.chat(model="llama3.2:latest", messages=messages)
print("\nAssistente (com contexto):", follow_up['message']['content'])
2. Prompting Direto sem Contexto usando ollama.generate()
A função ollama.generate() é indicada para tarefas pontuais de passagem única (one-shot), tais como sumarização, extração de dados, tradução ou geração de código, nas quais não é necessário armazenar histórico de conversa. Ela aceita uma simples string no parâmetro prompt e retorna a resposta diretamente na chave 'response' do dicionário de saída.
- Exemplo de Código: Geração Única sem Contexto
import ollama
# 1. Geração de texto/sumarização simples
resposta_texto = ollama.generate(
model="llama3.2:latest",
prompt="Resuma o conceito de decoradores em Python em apenas uma frase."
)
print("--- Resumo ---")
print(resposta_texto['response'])
# 2. Geração de código com parâmetros customizados
prompt_codigo = "Crie uma função em Python para verificar se uma palavra é um palíndromo."
resposta_codigo = ollama.generate(
model="codellama:latest",
prompt=prompt_codigo,
options={
'temperature': 0.1 # Reduz aleatoriedade para garantir código mais preciso
}
)
print("\n--- Código Gerado ---")
print(resposta_codigo['response'])
Diferenças Fundamentais entre chat() e generate()
| Recurso | ollama.chat() | ollama.generate() |
|---|---|---|
| Estrutura de Entrada | Lista de dicionários com papéis (messages) | String de texto direta (prompt) |
| Gerenciamento de Estado | Mantém contexto e histórico conversacional | Sem estado (stateless), sem histórico |
| Acesso ao Resultado | response['message']['content'] | response['response'] |
| Casos de Uso Ideais | Chatbots, assistentes virtuais, agentes | Resumos, extração de dados, scripts |
3.2. Requisições REST HTTP Puras (requests)
A integração direta via requisições HTTP REST é uma alternativa prática para conectar suas aplicações em Python ao Ollama sem depender de SDKs adicionais ou pacotes pesados. Por padrão, o servidor do Ollama executa localmente no endereço http://localhost:11434 e disponibiliza endpoints estruturados que aceitam requisições no formato JSON.
1. Consumindo o Endpoint /api/chat com JSON Payload
O endpoint /api/chat permite enviar diálogos estruturados contendo papéis (system, user, assistant). Por padrão, a API REST do Ollama transmite a resposta de forma incremental (streaming linha a linha em formato NDJSON). Para processar essa transmissão em tempo real sem bloquear a execução, utiliza-se o método iter_lines() do pacote requests acompanhado de json.loads() para decodificar cada fragmento gerado.
- Exemplo de Código: Chat Conversacional com Streaming via HTTP REST
import requests
import json
# Endpoint REST de chat do Ollama
url = "http://localhost:11434/api/chat"
# Payload JSON contendo o modelo e a lista de mensagens
payload = {
"model": "llama3.2:latest",
"messages": [
{"role": "system", "content": "Você é um assistente especializado em Python."},
{"role": "user", "content": "Escreva uma função para inverter uma string."}
],
"stream": True # Habilita o envio de tokens em tempo real
}
# Envia a requisição POST
response = requests.post(url, json=payload, stream=True)
print("--- Resposta do /api/chat ---")
# Iteração linha a linha sobre o fluxo da resposta
for line in response.iter_lines():
if line:
data = json.loads(line)
# Extrai o fragmento de texto contido na mensagem do assistente
content = data.get("message", {}).get("content", "")
print(content, end="", flush=True)
print()
2. Consumindo o Endpoint /api/generate de Passagem Única
Para tarefas simples de geração de texto de passagem única (one-shot), nas quais não há necessidade de manter papéis ou histórico de conversa, utiliza-se o endpoint /api/generate. Ao definir "stream": False no payload JSON, o servidor processa o prompt completo e retorna um único objeto JSON consolidado contendo o texto final na chave "response".
- Exemplo de Código: Geração Única sem Streaming via HTTP REST
import requests
# Endpoint REST para geração de texto simples
url = "http://localhost:11434/api/generate"
# Payload JSON de passagem única desativando o streaming
payload = {
"model": "llama3.2:latest",
"prompt": "Explique o conceito de recursão em uma frase curta.",
"stream": False # Solicita uma resposta JSON única e completa
}
# Envia a requisição POST
response = requests.post(url, json=payload)
# Processa o retorno quando o status for HTTP 200 (OK)
if response.status_code == 200:
data = response.json()
print("--- Resposta do /api/generate ---")
print(data.get("response"))
else:
print(f"Erro na requisição: {response.status_code}")
Capítulo 4: Recursos Avançados de Programação com LLMs
Ir além de chamadas simples de texto e chats convencionais exige explorar recursos de programação projetados para tornar a interação com LLMs mais fluida, previsível e integrada a ecossistemas de software modernos. Quando migramos de scripts de teste para aplicações em nível de produção, desafios como a latência percebida pelo usuário, o processamento concorrente de múltiplas requisições, a validação estrita de respostas e a rastreabilidade do raciocínio do modelo tornam-se prioridades técnicas essenciais.
O ecossistema do Ollama e da sua SDK em Python oferece um conjunto robusto de primitivas avançadas para lidar com essas demandas. Através da transmissão de tokens em tempo real (streaming), é possível construir interfaces reativas que exibem a resposta palavra por palavra, eliminando o tempo de espera bloqueante. Paralelamente, o suporte a clientes assíncronos (AsyncClient) viabiliza a execução de requisições concorrentes de alta taxa de transferência em serviços web sem comprometer a I/O da aplicação. Além disso, a capacidade de impor saídas estruturadas estritas em formato JSON Schema (permitindo validação nativa com Pydantic) e a habilidade de interceptar e separar os blocos de raciocínio intermediário em "modelos de pensamento" (thinking models) capacitam os desenvolvedores a construir pipelines de IA confiáveis, auditáveis e prontos para integração em sistemas corporativos.
4.1. Streaming de Respostas em Tempo Real
Em aplicações interativas baseadas em LLMs, esperar que o modelo gere a resposta completa antes de exibi-la na tela pode criar uma percepção de alta latência e degradação na experiência do usuário (UX). O streaming de tokens resolve esse problema enviando cada fragmento de texto (chunk) imediatamente à medida que é calculado pelo modelo, gerando uma experiência fluida de digitação em tempo real.
Como Funciona o Streaming em Python
Ao definir o parâmetro stream=True em funções como ollama.chat() ou ollama.generate(), a SDK oficial do Ollama não retorna um objeto JSON estático consolidado, mas sim um gerador iterável de tokens.
Para exibir esses tokens instantaneamente no terminal ou interface, dois parâmetros da função nativa print() do Python desempenham um papel crucial:
end="": Substitui a quebra de linha padrão do Python (\n) por uma string vazia, permitindo que os tokens gerados sejam impressos lado a lado na mesma linha.flush=True: Força a limpeza e descarga imediata do buffer de saída do terminal (stdout), garantindo que cada token apareça na tela sem ser retingido na memória aguardando a próxima quebra de linha.
Exemplos de Código em Python
Opção A: Streaming via SDK Oficial (ollama.chat)
import ollama
def chat_com_streaming():
print("--- Assistente com Streaming em Tempo Real ---")
# Solicitando o streaming ativado
stream = ollama.chat(
model="llama3.2:latest",
messages=[
{"role": "system", "content": "Você é um desenvolvedor Python sênior."},
{"role": "user", "content": "Escreva um script em Python para verificar a temperatura da CPU."}
],
stream=True # Retorna um iterador de chunks em tempo real
)
# Iteração sobre o gerador para imprimir token por token
for chunk in stream:
token = chunk['message']['content']
print(token, end="", flush=True)
print() # Insere quebra de linha no final do fluxo
if __name__ == "__main__":
chat_com_streaming()
Opção B: Streaming via API REST HTTP (requests)
Se você estiver consumindo o endpoint /api/generate ou /api/chat usando apenas a biblioteca requests, o streaming pode ser processado iterando sobre as linhas da resposta HTTP com response.iter_lines():
import requests
import json
url = "http://localhost:11434/api/generate"
payload = {
"model": "llama3.2:latest",
"prompt": "Explique o conceito de geradores (generators) em Python em poucas frases.",
"stream": True
}
# Realiza a requisição mantendo a conexão aberta
response = requests.post(url, json=payload, stream=True)
print("--- Resposta REST via Streaming ---")
for line in response.iter_lines():
if line:
data = json.loads(line)
# Extrai o fragmento contido na chave 'response'
token = data.get("response", "")
print(token, end="", flush=True)
print()
4.2. Concorrência com Cliente Assíncrono (AsyncClient)
Quando desenvolvemos aplicações web (como microsserviços em FastAPI ou Starlette) ou pipelines de processamento em lote que lidam com múltiplos prompts simultâneos, realizar chamadas síncronas tradicionais pode bloquear a thread de E/S (I/O) do Python. Isso faz com que a aplicação fique ociosa aguardando a resposta do servidor local antes de aceitar ou processar novas requisições.
Para evitar esse gargalo de I/O, a SDK oficial do Ollama disponibiliza a classe AsyncClient, projetada para integração nativa com o ecossistema asyncio do Python.
Como Funciona a Execução Assíncrona com asyncio.gather()
Ao utilizar o AsyncClient, as chamadas a funções como chat() ou generate() tornam-se corrotinas assíncronas. A função asyncio.gather() permite disparar e aguardar a conclusão de múltiplas tarefas em paralelo dentro do mesmo event loop:
- I/O Não Bloqueante: A aplicação pode enviar várias perguntas de forma concorrente sem a necessidade de criar e gerenciar threads manualmente.
- Reaproveitamento de Conexões: Recomenda-se instanciar e reutilizar uma única instância do
AsyncClientpara evitar a sobretaxa (overhead) de criar novas conexões a cada requisição. - Desempenho x Throughput: A assincronia não reduz o tempo de inferência individual do modelo na GPU/CPU, mas melhora drasticamente a capacidade de concorrência da sua aplicação ao liberar o sistema para processar outras tarefas de rede e I/O enquanto aguarda o retorno das respostas.
Exemplo de Código em Python
O código abaixo demonstra como disparar 5 requisições de geração de texto de forma totalmente concorrente utilizando AsyncClient e asyncio.gather():
import asyncio
from ollama import AsyncClient
async def consultar_modelo(client: AsyncClient, prompt_id: int, pergunta: str):
"""Corrotina para enviar um prompt individual assincronamente."""
print(f"[Iniciando Task {prompt_id}] Pergunta: '{pergunta}'")
# Executa a geração assíncrona sem bloquear o event loop do Python
response = await client.generate(
model="llama3.2:latest",
prompt=pergunta
)
texto = response['response'].strip()
print(f"[Concluída Task {prompt_id}] Resposta recebida ({len(texto)} caracteres).")
return f"Task {prompt_id}: {texto}"
async def main():
# Instancia o cliente assíncrono (reutilizável)
client = AsyncClient(host="http://localhost:11434")
# Lista de perguntas a serem enviadas simultaneamente
prompts = [
"O que é uma função lambda em Python?",
"Qual a diferença entre lista e tupla em Python?",
"Como funciona o gerenciador de contexto (with)?",
"O que é um decorador em Python?",
"Explique o conceito de docstrings."
]
print("--- Disparando Requisições Assíncronas Concorrentes ---")
# Cria uma lista de tarefas corrotinas
tarefas = [
consultar_modelo(client, i + 1, p)
for i, p in enumerate(prompts)
]
# Executa todas as requisições em paralelo via asyncio.gather()
resultados = await asyncio.gather(*tarefas)
print("\n--- Todos os resultados foram recebidos ---")
for res in resultados:
print(res[:120] + "...\n")
if __name__ == "__main__":
# Executa o loop de eventos do asyncio
asyncio.run(main())
4.3. Saídas Estruturadas (Structured Outputs / JSON Schema)
Em aplicações de software profissionais, respostas em texto livre (Linguagem Natural) geradas por LLMs costumam ser imprevisíveis para integração direta com bancos de dados, APIs REST ou pipelines de automação. Para garantir determinismo e legibilidade computacional, o Ollama oferece suporte nativo a Saídas Estruturadas (Structured Outputs), permitindo forçar o modelo a responder estritamente dentro de um contrato no formato JSON Schema.
1. Como Funciona a Restrição de Formato no Ollama
O parâmetro format nas funções ollama.chat() e ollama.generate() ativa o modo de decodificação com restrição gramatical no motor de inferência:
format="json": Força o modelo a responder com um objeto JSON válido, embora sem impor nomes de chaves específicos.format=Schema(JSON Schema): Passa uma especificação formal de schema JSON. O motor de inferência garante que os tokens gerados sigam exatamente os nomes de campos e tipos de dados definidos.
A forma mais elegante e robusta de definir schemas em Python é utilizar a biblioteca Pydantic. O método .model_json_schema() de um modelo Pydantic gera automaticamente a especificação exigida pelo Ollama, e o método .model_validate_json() valida e converte a string retornada em um objeto Python fortemente tipado.
💡 Boas práticas: Ao trabalhar com saídas estruturadas, defina a
temperature: 0.0nas opções do modelo para eliminar variações criativas desnecessárias e aumentar a taxa de adesão ao schema.
Exemplos de Código em Python
Opção A: Integração Nativa com Pydantic (Recomendado)
import ollama
from pydantic import BaseModel, Field
from typing import List
# 1. Define o contrato de dados usando Pydantic
class ItemInfo(BaseModel):
nome: str = Field(description="Nome do produto ou entidade mencionada")
categoria: str = Field(description="Categoria principal do item")
preco_estimado: float = Field(description="Preço estimado em BRL")
class RelatorioAnalise(BaseModel):
resumo_executivo: str = Field(description="Resumo da análise em duas frases")
itens_identificados: List[ItemInfo]
classificacao_risco: str = Field(description="Nível de risco: Baixo, Médio ou Alto")
def extrair_dados_estruturados():
prompt_usuario = """
Analise o seguinte texto:
'Adquirimos 3 licenças do software de desenvolvimento DevPro por R$ 1200.00 cada e um servidor de teste RackServer por R$ 8500.00.
A compra foi aprovada com urgência para o projeto principal.'
"""
print("--- Solicitando resposta estruturada via Pydantic ---")
# 2. Chama a API passando o schema gerado pelo Pydantic no parâmetro 'format'
response = ollama.chat(
model="llama3.1:latest",
messages=[
{"role": "system", "content": "Você é um extrator de dados financeiro preciso. Responda apenas no formato JSON solicitado."},
{"role": "user", "content": prompt_usuario}
],
format=RelatorioAnalise.model_json_schema(), # Exporta o JSON Schema estrito
options={"temperature": 0.0} # Garante respostas determinísticas
)
json_string = response['message']['content']
print("\nJSON Bruto Retornado pela LLM:")
print(json_string)
# 3. Valida e desserializa para uma instância fortemente tipada de Pydantic
dados_validados = RelatorioAnalise.model_validate_json(json_string)
print("\n--- Objeto Python Validado ---")
print(f"Resumo: {dados_validados.resumo_executivo}")
print(f"Risco: {dados_validados.classificacao_risco}")
for item in dados_validados.itens_identificados:
print(f" - {item.nome} ({item.categoria}): R$ {item.preco_estimado:.2f}")
if __name__ == "__main__":
extrair_dados_estruturados()
Opção B: Saída JSON Simples usando format="json"
Caso você não utilize o Pydantic na sua aplicação, é possível utilizar a flag string format="json" e instruir o formato diretamente no prompt:
import ollama
import json
response = ollama.generate(
model="llama3.2:latest",
prompt="""
Gere um perfil de usuário fictício para testes contendo as chaves: 'id', 'nome', 'email' e 'habilidades' (lista de strings).
""",
format="json", # Garante sintaxe JSON válida
options={"temperature": 0.2}
)
# Faz o parse seguro do JSON retornado
dados = json.loads(response['response'])
print("--- JSON Parsado ---")
print(f"ID: {dados.get('id')}")
print(f"Nome: {dados.get('nome')}")
print(f"Habilidades: {', '.join(dados.get('habilidades', []))}")
4.4. Manipulação de Modelos de Raciocínio ("Thinking Models")
Uma das evoluções recentes no ecossistema de LLMs abertas é o surgimento dos Modelos de Raciocínio (como Qwen3 e variantes da família DeepSeek-R1). Diferente dos modelos tradicionais que geram a resposta final diretamente, esses modelos foram treinados para encadear pensamentos e exibir seus passos intermediários de lógica (Chain-of-Thought) antes de fornecer a solução final.
Por padrão, esses passos de reflexão são transmitidos na própria saída delimitados pelas tags <think>...</think>.
1. Por que Separar o Raciocínio da Resposta Final?
Separar o bloco de pensamento do resultado traz vantagens significativas no desenvolvimento de software:
- Auditoria e Depuração: Permite aos desenvolvedores inspecionar a lógica interna do modelo para entender como ele chegou a uma determinada conclusão ou identificar onde ocorreu uma alucinação.
- Interface de Usuário Limpa (UX): Em aplicações e chatbots, você pode ocultar os detalhes técnicos do usuário final ou exibi-los em um painel expansível (accordion), apresentando como texto principal apenas a resposta limpa.
- Pipelines de Automação: Evita que tags de controle e raciocínio contaminem saídas que serão enviadas para bancos de dados ou APIs externas.
2. Exemplo Prático em Python: Interceptando e Separando com Expressões Regulares (re)
Abaixo está o exemplo de código em Python que realiza a chamada a um modelo de raciocínio (como qwen3) via SDK do Ollama e utiliza expressões regulares com a flag re.DOTALL para isolar o bloco <think> da resposta final:
import ollama
import re
def processar_modelo_raciocinio(prompt_usuario: str, modelo: str = "qwen3"):
print(f"--- Solicitando ao modelo de raciocínio '{modelo}' ---")
# 1. Executa a chamada no Ollama
response = ollama.chat(
model=modelo,
messages=[
{"role": "user", "content": prompt_usuario}
]
)
# Conteúdo bruto retornado contendo as tags <think>...</think>
conteudo_bruto = response['message']['content']
# 2. Extrai o bloco de raciocínio dentro de <think>...</think>
match_raciocinio = re.findall(r"<think>(.*?)</think>", conteudo_bruto, flags=re.DOTALL)
raciocinio = match_raciocinio.strip() if match_raciocinio else "Nenhum bloco de raciocínio encontrado."
# 3. Remove as tags e o conteúdo de pensamento para obter a resposta limpa
resposta_limpa = re.sub(r"<think>.*?</think>", "", conteudo_bruto, flags=re.DOTALL).strip()
# 4. Exibe os resultados separados
print("\n🧠 --- Processo de Pensamento Intermediário (Raciocínio) ---")
print(raciocinio)
print("\n✅ --- Resposta Final ---")
print(resposta_limpa)
if __name__ == "__main__":
prompt = "Um fazendeiro precisa atravessar um rio com um lobo, uma cabra e um maço de couve. Como ele deve fazer?"
processar_modelo_raciocinio(prompt)
Capítulo 5: Agentes Inteligentes e Chamada de Funções (Tool Calling)
A evolução das LLMs de simples geradores de texto para Agentes Inteligentes representa um salto fundamental no desenvolvimento de software. Por definição, modelos de linguagem são sistemas probabilísticos limitados aos dados do seu treinamento ou ao contexto fornecido na mensagem, sendo incapazes de consultar APIs externas em tempo real, acessar bancos de dados ou executar cálculos matemáticos de forma determinística por conta própria.
O recurso de Chamada de Funções (Tool Calling) supera essa limitação ao permitir que o modelo atue como o cérebro decisório de uma aplicação. Ao receber uma instrução, a LLM analisa a intenção do usuário e determina se precisa acionar uma ferramenta cadastrada no sistema. No ecossistema do Ollama, a execução do código ocorre de forma segura no lado do cliente: o modelo não executa o código diretamente, mas retorna o nome da função e os argumentos numéricos ou textuais em formato JSON. Em seguida, a aplicação Python executa a função localmente e envia o resultado de volta com o papel (role) de "tool", permitindo que o modelo formule a resposta final devidamente fundamentada (grounded).
Essa abordagem em ciclo (Tool Loop) fornece a infraestrutura necessária para construir assistentes autônomos capazes de interagir com sistemas do mundo real, integrar pipelines de Retrieval-Augmented Generation (RAG) e automatizar fluxos de trabalho complexos, tudo mantendo a execução 100% local, privada e sem custos de API.
5.1. Definição de Ferramentas em Python
O recurso de Chamada de Funções (Tool Calling) permite que modelos locais estendam suas capacidades além da geração de texto, consultando dados em tempo real ou executando cálculos exatos através de código Python. No entanto, para que o modelo saiba exatamente quando e como acionar uma ferramenta, a definição correta das assinaturas de função, dos Type Hints e das Docstrings é fundamental.
1. Como o Ollama Interpreta Funções em Python
A SDK oficial do Ollama (ollama-python) aceita funções nativas do Python diretamente no parâmetro tools=[minha_funcao]. Sob o capô, a biblioteca inspeciona o código Python para extrair os metadados da função e convertê-los automaticamente em um schema no formato JSON Schema.
Para que essa conversão automática funcione de forma eficiente, a função precisa fornecer três elementos:
- Nome da Função: Deve ser claro e expressar a ação (ex:
consultar_clima_cidadeoucalcular_desconto). - Type Hints: Definem os tipos de dados aceitos por cada parâmetro (
str,int,float,bool) e o tipo do retorno. - Docstring Detalhada: Explica o propósito geral da ferramenta e descreve individualmente o que cada parâmetro representa. O modelo de linguagem lê essa descrição para decidir se a função é adequada para responder ao prompt do usuário.
⚠️ Atenção sobre compatibilidade: Nem todas as LLMs suportam chamada de funções. Certifique-se de utilizar modelos identificados com suporte a ferramentas na biblioteca do Ollama, como
llama3.1,qwen3.8ougemma4.
2. Código Exemplo 1: Criando Funções Nativas com Docstrings e Type Hints
Abaixo está a implementação de duas funções prontas para serem utilizadas como ferramentas pelo Ollama. Repare no uso de Type Hints explícitos e na documentação estruturada no formato docstring:
import math
from typing import Dict, Any
def consultar_clima_cidade(cidade: str, unidade: str = "celsius") -> str:
"""
Obtém a previsão do tempo atual para uma cidade específica.
Args:
cidade (str): O nome completo da cidade (ex: "São Paulo", "Lisboa", "Tóquio").
unidade (str): A unidade de medida da temperatura ("celsius" ou "fahrenheit"). Padrão é "celsius".
Returns:
str: Mensagem contendo o status do clima e a temperatura estimada.
"""
# Simulação de integração com uma API externa de meteorologia
cidade_formatada = cidade.strip().title()
temp_simulada = 24.5 if unidade.lower() == "celsius" else 76.1
return f"O clima em {cidade_formatada} está ensolarado com {temp_simulada}° {unidade.capitalize()}."
def calcular_desconto(preco_original: float, porcentagem: float) -> Dict[str, Any]:
"""
Calcula o valor do desconto e o preço final de um produto.
Args:
preco_original (float): O valor original do produto em reais.
porcentagem (float): A porcentagem de desconto a aplicar (ex: 15.0 para 15%).
Returns:
Dict[str, Any]: Dicionário com preco_original, valor_desconto e preco_final.
"""
# Boa prática: realizar cast manual dos argumentos para evitar falhas se a LLM passar números como string
preco = float(preco_original)
desc = float(porcentagem)
valor_desconto = preco * (desc / 100.0)
preco_final = preco - valor_desconto
return {
"preco_original": preco,
"desconto_aplicado": valor_desconto,
"preco_final": preco_final
}
3. Código Exemplo 2: Definindo Schemas JSON Manuais (Estilo OpenAI / REST API)
Se você preferir consumir a API REST HTTP diretamente via requests ou quiser controle absoluto sobre a especificação enviada ao modelo, pode definir o schema de ferramentas manualmente utilizando dicionários no formato JSON Schema:
# Definição manual de schema de ferramentas para requisições HTTP REST ou SDK da OpenAI
tools_schema_manual = [
{
"type": "function",
"function": {
"name": "consultar_clima_cidade",
"description": "Obtém a previsão do tempo atual para uma cidade específica.",
"parameters": {
"type": "object",
"properties": {
"cidade": {
"type": "string",
"description": "O nome da cidade (ex: São Paulo, Londres)"
},
"unidade": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "Unidade de medida da temperatura"
}
},
"required": ["cidade"]
}
}
}
]
4. Testando a Detecção de Ferramentas com ollama.chat()
Para verificar se o modelo identificou a necessidade de chamar a ferramenta, passamos a lista de funções no parâmetro tools da chamada. O modelo inspecionará a pergunta e retornará a intenção na chave tool_calls:
import ollama
# Lista de ferramentas disponíveis
minhas_ferramentas = [consultar_clima_cidade, calcular_desconto]
messages = [
{"role": "user", "content": "Qual é a temperatura atual em Tóquio?"}
]
# Chama o modelo fornecendo a lista de funções no parâmetro tools
response = ollama.chat(
model="llama3.1:latest",
messages=messages,
tools=minhas_ferramentas # Passa os objetos de função nativos
)
# Verifica se a LLM solicitou a execução de alguma ferramenta
if response.message.tool_calls:
for tool_call in response.message.tool_calls:
print(f"🔧 Ferramenta solicitada: {tool_call.function.name}")
print(f"📋 Argumentos extraídos: {tool_call.function.arguments}")
else:
print("A LLM respondeu diretamente sem acionar ferramentas:")
print(response.message.content)
5.2. O Ciclo Completo do Agente (Tool Loop)
A chamada de funções só se transforma em um Agente Inteligente completo quando implementamos o Ciclo de Ferramentas (Tool Loop). Modelos de linguagem não possuem acesso direto ao sistema operacional ou à internet por questões de segurança; por isso, a execução do código ocorre no ambiente Python local. A LLM atua como o orquestrador do raciocínio, identificando quando precisa de dados externos e delegando a execução para a sua aplicação.
As 5 Etapas do Tool Loop
O fluxo de execução segue um ciclo bem definido entre o seu código em Python e a LLM local:
- Envio da Pergunta + Ferramentas: A aplicação chama a função
ollama.chat()enviando a mensagem do usuário juntamente com a lista de funções disponíveis no parâmetrotools. - Detecção da Intenção (Tool Calling): O modelo analisa o prompt e, se identificar a necessidade de dados externos, retorna uma mensagem contendo uma estrutura em
tool_callscom o nome da função e os argumentos extraídos. - Execução Local do Código: O código Python intercepta a solicitação, extrai os argumentos (
call.function.arguments), localiza a função no ambiente local e executa a tarefa (ex: consultar um banco de dados, API REST ou realizar um cálculo). - Devolução do Resultado (
role="tool"): A resposta da função local é anexada ao histórico de mensagens (messages) utilizando arole="tool", informando o nome da ferramenta (tool_name) e o resultado convertido em string (content). - Geração da Resposta Final Fundamentada: A aplicação realiza uma nova chamada a
ollama.chat()enviando todo o histórico acumulado. O modelo lê o resultado fornecido pela ferramenta e gera a resposta final ao usuário em linguagem natural.
Exemplo Prático em Python: Agente de Cotações Financeiras
O código abaixo ilustra a implementação completa do ciclo do agente, permitindo que a LLM consulte preços de ativos em tempo real utilizando uma função Python local:
import ollama
# 1. Definição da ferramenta local com type hints e docstring
def consultar_preco_acoes(ticker: str) -> float:
"""
Obtém o preço atual de uma ação na bolsa de valores.
Args:
ticker (str): O código do ativo (ex: "PETR4", "VALE3", "AAPL").
Returns:
float: Preço atualizado do ativo em BRL.
"""
# Simulação de consulta a uma API financeira real
cotacoes_simuladas = {
"PETR4": 38.50,
"VALE3": 62.10,
"AAPL": 225.40
}
ativo = ticker.strip().upper()
return cotacoes_simuladas.get(ativo, 100.0)
# Mapeamento local para associar nomes de strings às funções Python
funcoes_registradas = {
"consultar_preco_acoes": consultar_preco_acoes
}
def executar_agente_financeiro(prompt_usuario: str):
print(f"👤 Usuário: {prompt_usuario}\n")
# Histórico inicial de mensagens
messages = [
{"role": "system", "content": "Você é um assistente financeiro útil. Use ferramentas sempre que precisar de dados de cotação."},
{"role": "user", "content": prompt_usuario}
]
# Passo 1: Primeira chamada ao Ollama informando as ferramentas disponíveis
response = ollama.chat(
model="llama3.1:latest",
messages=messages,
tools=[consultar_preco_acoes]
)
# Registra a resposta da LLM no histórico (necessário para manter o contexto do diálogo)
messages.append(response.message)
# Passo 2: Verifica se o modelo solicitou a execução de alguma ferramenta
if response.message.tool_calls:
print("🤖 A LLM solicitou a chamada de uma ferramenta local:")
for call in response.message.tool_calls:
nome_funcao = call.function.name
argumentos = call.function.arguments
print(f" -> Função: {nome_funcao}")
print(f" -> Argumentos extraídos: {argumentos}")
# Passo 3: Executa a função local correspondente em Python
funcao_local = funcoes_registradas.get(nome_funcao)
if funcao_local:
resultado = funcao_local(**argumentos)
print(f" -> Resultado da execução: {resultado}\n")
# Passo 4: Anexa o resultado de volta ao histórico com a role 'tool'
messages.append({
"role": "tool",
"content": str(resultado),
"tool_name": nome_funcao
})
# Passo 5: Segunda chamada ao Ollama com o histórico atualizado
resposta_final = ollama.chat(
model="llama3.1:latest",
messages=messages
)
print("✅ Resposta Final da LLM (Fundamentada):")
print(resposta_final.message.content)
else:
print("✅ Resposta Direta da LLM (sem ferramentas):")
print(response.message.content)
if __name__ == "__main__":
executar_agente_financeiro("Qual é o valor atual da ação PETR4 e quanto custariam 100 cotas?")
Capítulo 6: Projetos Práticos Prontos para o Blog
Consolidar os conceitos teóricos e as primitivas de código em aplicações do mundo real é o passo definitivo para demonstrar o valor prático das LLMs locais. Embora scripts isolados e testes no terminal sejam ideais para a aprendizagem inicial, o verdadeiro diferencial do Ollama e da execução privada de inteligência artificial se manifesta na criação de ferramentas utilitárias, microsserviços escaláveis e automações perfeitamente integradas ao dia a dia de desenvolvimento.
A implementação de projetos práticos permite explorar cenários críticos nos quais a privacidade e a ausência de custos por token são determinantes — como na análise de repositórios de código proprietário para automação de tarefas de desenvolvimento ou no empacotamento de modelos em APIs web prontas para produção. Além disso, ao adotar padrões consolidados da indústria, como a compatibilidade nativa com a SDK da OpenAI através do endpoint local (http://localhost:11434/v1), torna-se possível estruturar sistemas modulares em Python capazes de transitar entre infraestruturas locais e serviços na nuvem de forma transparente, sem a necessidade de reescrever a arquitetura da aplicação.
6.1. Ferramenta de CLI: Gerador Automático de Git Commits (AI Commit)
Escrever mensagens de commit claras e padronizadas é uma prática essencial para manter a rastreabilidade em projetos de software. No entanto, em repositórios corporativos ou projetos privados, enviar o histórico de alterações (git diff) para APIs em nuvem de terceiros traz sérios riscos de vazamento de código proprietário.
O uso de um modelo local rodando via Ollama resolve esse problema com três vantagens principais:
- Privacidade Absoluta: O código e as alterações tratadas no diff nunca saem do seu computador.
- Custo Zero: Execução ilimitada sem cobrança por token ou assinaturas.
- Funcionamento Offline: Funciona perfeitamente sem conexão com a internet.
Como Funciona o AI Commit
A ferramenta funciona seguindo quatro etapas principais:
- Captura do Diff Staged: Utiliza o módulo nativo
subprocessdo Python para executar o comandogit diff --staged, capturando apenas as modificações prontas para o commit. - Prompt de Sistema Estrito: Define regras para forçar o modelo a gerar mensagens no padrão Conventional Commits (ex:
feat:,fix:,docs:), limitando o título a 60 caracteres no modo imperativo e adicionando tópicos explicativos (bullets). - Streaming e Limpeza de Resposta: Transmite os tokens em tempo real e remove formatações desnecessárias (como blocos de código Markdown
```) geradas por modelos menores. - Execução Opcional do Commit: Pergunta ao usuário se ele aprova a mensagem gerada e, em caso afirmativo, executa automaticamente o
git commit -m.
Exemplo Prático de Código em Python (ai_commit.py)
Abaixo está a implementação completa da CLI em Python utilizando a SDK do Ollama:
import sys
import subprocess
import argparse
import re
import ollama
# 1. Prompt de sistema com regras estritas para Conventional Commits
SYSTEM_PROMPT = """
Você é um assistente especializado em controle de versão Git.
Sua tarefa é analisar o `git diff` fornecido e gerar uma mensagem de commit curta seguindo o padrão Conventional Commits.
REGRAS OBRIGATÓRIAS:
1. A primeira linha deve ser o resumo no modo imperativo (ex: "feat: adiciona validação de email").
2. Limite a primeira linha a no máximo 60 caracteres.
3. Use os prefixos corretos: feat, fix, docs, style, refactor, test, chore.
4. Após o título, insira uma linha em branco e até 3 tópicos curtos (bullet points) explicando o PORQUÊ da alteração.
5. Retorne APENAS o texto da mensagem de commit. Não inclua explicações ou saudações.
"""
def obter_git_diff() -> str:
"""Captura o git diff das alterações em staging (staged)."""
try:
resultado = subprocess.run(
["git", "diff", "--staged"],
capture_output=True,
text=True,
check=True
)
return resultado.stdout.strip()
except subprocess.CalledProcessError:
print("❌ Erro ao executar 'git diff'. Verifique se este é um repositório Git válido.")
sys.exit(1)
except FileNotFoundError:
print("❌ Git não foi encontrado no PATH do sistema.")
sys.exit(1)
def limpar_resposta_markdown(texto: str) -> str:
"""Remove blocos de código Markdown (```) que modelos menores costumam incluir."""
linhas = texto.strip().splitlines()
linhas_limpas = [linha for linha in linhas if not linha.strip().startswith("```")]
return "\n".join(linhas_limpas).strip()
def gerar_mensagem_commit(diff: str, modelo: str) -> str:
"""Envia o diff para a LLM local via Ollama em modo streaming."""
# Limita o diff para evitar estourar a janela de contexto de modelos menores
diff_truncado = diff[:6000]
print(f"🤖 Gerando mensagem de commit com o modelo local '{modelo}'...\n")
messages = [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": f"Analise o seguinte git diff:\n\n{diff_truncado}"}
]
# Chama a API do Ollama com streaming ativado
stream = ollama.chat(
model=modelo,
messages=messages,
stream=True,
options={"temperature": 0.2} # Baixa temperatura para menor aleatoriedade
)
resposta_completa = ""
for chunk in stream:
token = chunk['message']['content']
resposta_completa += token
print(token, end="", flush=True)
print("\n")
return limpar_resposta_markdown(resposta_completa)
def executar_commit_git(mensagem: str):
"""Executa o comando git commit com a mensagem gerada."""
try:
subprocess.run(["git", "commit", "-m", mensagem], check=True)
print("✅ Commit realizado com sucesso!")
except subprocess.CalledProcessError:
print("❌ Falha ao executar o git commit.")
def main():
parser = argparse.ArgumentParser(description="AI Commit: Gerador automático de commits Git usando LLMs locais.")
parser.add_argument("-m", "--model", default="qwen2.5-coder:1.5b", help="Modelo Ollama a ser utilizado (padrão: qwen2.5-coder:1.5b)")
parser.add_argument("-c", "--commit", action="store_true", help="Solicita confirmação e realiza o commit automaticamente")
args = parser.parse_args()
# 1. Obtém as alterações staged
diff = obter_git_diff()
if not diff:
print("⚠️ Nenhuma alteração adicionada ao staging. Use 'git add <arquivo>' antes de rodar o AI Commit.")
return
# 2. Gera a mensagem via LLM local
mensagem_commit = gerar_mensagem_commit(diff, args.model)
# 3. Se a flag --commit estiver ativa, solicita confirmação para commitar
if args.commit and mensagem_commit:
confirmacao = input("Deseja realizar o commit com esta mensagem? (s/N): ").strip().lower()
if confirmacao in ["s", "sim", "y", "yes"]:
executar_commit_git(mensagem_commit)
else:
print("Commit cancelado pelo usuário.")
if __name__ == "__main__":
main()
Como Usar no Terminal
- Adicione as alterações ao staging:
git add src/weather.py - Execute o script:
python ai_commit.py --commit - Para trocar de modelo rapidamente:
python ai_commit.py --model llama3.2:1b --commit
💡 Dica de Produtividade: Você pode criar um alias no seu terminal (
alias gcom="python3 /caminho/ai_commit.py -c") para gerar e realizar commits privados com um único comando!
6.2. Microsserviço de Produção com FastAPI
Embora o servidor do Ollama exponha nativamente endpoints REST no endereço http://localhost:11434, expor essa porta diretamente para aplicações clientes ou para a web em ambiente de produção não é recomendado. Construir um microsserviço intermediário com FastAPI permite encapsular a LLM local em uma arquitetura corporativa robusta.
Por que Encapsular o Ollama com FastAPI?
- Validação Estrita de Dados (Pydantic): Garante que todas as requisições recebidas contenham parâmetros válidos (como limites de caracteres no prompt ou intervalos seguros para a temperatura) antes de sobrecarregar o modelo.
- Segurança e Regras de Negócio: Permite adicionar camadas de autenticação (API Keys, OAuth2/JWT), políticas de CORS, limitação de taxa (rate limiting) e filtros de sanitização de conteúdo.
- Documentação Automática (Swagger UI / OpenAPI): O FastAPI gera interativamente uma interface visual em
/docsonde desenvolvedores e clientes podem testar os endpoints e inspecionar os contratos JSON. - Concorrência Assíncrona: A combinação do loop de eventos do FastAPI com o cliente assíncrono do Ollama (
AsyncClient) permite atender múltiplas requisições HTTP simultâneas sem bloquear a E/S (I/O) do servidor.
Exemplo de Código em Python (server.py)
O script abaixo cria um microsserviço REST completo utilizando FastAPI, Pydantic para validação dos dados e o AsyncClient do Ollama para chamadas não bloqueantes:
import time
from typing import Optional
from fastapi import FastAPI, HTTPException, status
from pydantic import BaseModel, Field
from ollama import AsyncClient, ResponseError
import uvicorn
# 1. Inicializa a aplicação FastAPI com metadados para a documentação Swagger
app = FastAPI(
title="API de Serviços LLM Privada",
description="Microsserviço de inferência local utilizando Ollama e Llama 3.",
version="1.0.0"
)
# Cliente assíncrono reutilizável do Ollama
ollama_client = AsyncClient(host="http://localhost:11434")
# 2. Definição dos Schemas Pydantic para Validação de Entrada e Saída
class ChatRequest(BaseModel):
prompt: str = Field(
...,
min_length=3,
max_length=4096,
description="O prompt ou pergunta enviada pelo usuário.",
examples=["Explique o conceito de microsserviços em duas frases."]
)
model: str = Field(
default="llama3.2:latest",
description="O nome do modelo Ollama baixado localmente."
)
system_prompt: Optional[str] = Field(
default="Você é um assistente técnico preciso e conciso.",
description="Instrução de sistema para guiar o comportamento da LLM."
)
temperature: float = Field(
default=0.7,
ge=0.0,
le=1.0,
description="Hiperparâmetro de amostragem (0.0 = determinístico, 1.0 = criativo)."
)
class ChatResponse(BaseModel):
response: str = Field(description="A resposta gerada pelo modelo de linguagem.")
model_used: str = Field(description="O nome do modelo utilizado na inferência.")
execution_time_seconds: float = Field(description="Tempo total gasto no processamento em segundos.")
# 3. Endpoint POST para geração de respostas
@app.post(
"/v1/chat",
response_model=ChatResponse,
status_code=status.HTTP_200_OK,
summary="Gera uma resposta de texto a partir de um modelo local."
)
async def generate_chat_response(request_data: ChatRequest):
start_time = time.time()
try:
# Prepara a estrutura de mensagens
messages = []
if request_data.system_prompt:
messages.append({"role": "system", "content": request_data.system_prompt})
messages.append({"role": "user", "content": request_data.prompt})
# Chamada assíncrona ao servidor Ollama
ollama_response = await ollama_client.chat(
model=request_data.model,
messages=messages,
options={
"temperature": request_data.temperature
}
)
elapsed_time = round(time.time() - start_time, 3)
return ChatResponse(
response=ollama_response['message']['content'].strip(),
model_used=request_data.model,
execution_time_seconds=elapsed_time
)
except ResponseError as e:
# Erros retornados diretamente pela API do Ollama (ex: modelo não encontrado)
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Erro no servidor Ollama: {e.error}"
)
except Exception as e:
# Outras falhas de conexão ou timeouts
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail=f"Falha na comunicação com a LLM local: {str(e)}"
)
# Endpoint de verificação de saúde do serviço (Healthcheck)
@app.get("/health", summary="Verifica se a API e o servidor Ollama estão online.")
async def health_check():
try:
# Tenta listar os modelos para confirmar conexão com o daemon
await ollama_client.list()
return {"status": "ok", "ollama_server": "connected"}
except Exception:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="Servidor Ollama indisponível no localhost:11434"
)
if __name__ == "__main__":
# Executa o servidor Uvicorn escutando na porta 8000
uvicorn.run("server:app", host="127.0.0.1", port=8000, reload=True)
Como Executar e Testar a Documentação Swagger UI
-
Instale as dependências do servidor:
pip install fastapi uvicorn pydantic ollama -
Inicie o microsserviço:
python server.py(O servidor exibirá no terminal que está rodando em
http://127.0.0.1:8000). -
Acesse a documentação interativa Swagger UI: Abra o seu navegador e acesse o endereço:
http://127.0.0.1:8000/docsNa página interativa do Swagger UI você poderá:
- Visualizar os schemas definidos via Pydantic (
ChatRequesteChatResponse). - Clicar no botão "Try it out" no endpoint
POST /v1/chat. - Preencher o payload JSON de teste e clicar em "Execute" para disparar chamadas para a sua LLM local e receber a resposta estruturada com o tempo exato de inferência em segundos.
- Visualizar os schemas definidos via Pydantic (
6.3. Compatibilidade Nativa com a SDK da OpenAI
A compatibilidade do Ollama com a especificação da API da OpenAI é uma das suas funcionalidades mais valiosas para a arquitetura de software. O servidor local do Ollama expõe um endpoint no endereço http://localhost:11434/v1/, permitindo que qualquer biblioteca, framework ou SDK desenvolvida para a OpenAI se comunique diretamente com modelos locais sem a necessidade de reescrever a lógica da aplicação.
Vantagens do Redirecionamento da SDK
- Portabilidade e Zero Lock-in: Permite alternar entre ambientes de desenvolvimento e teste locais (gratuitos e privados) e infraestruturas em nuvem alterando apenas a variável de configuração da URL base.
- Reaproveitamento de Ecossistema: Frameworks como LangChain, LlamaIndex e AutoGen funcionam nativamente apontando para o servidor do Ollama.
- Testes Offline: Permite executar suítes de testes automatizados e pipelines de CI/CD sem consumir cotas de APIs pagas ou expor chaves de acesso.
Como Funciona a Configuração
Para redirecionar o cliente oficial da OpenAI (pip install openai), ajustam-se dois parâmetros na instanciação do objeto OpenAI():
base_url="http://localhost:11434/v1": Altera o destino das requisições HTTP da nuvem para a porta local do Ollama.api_key="ollama": O Ollama não exige autenticação por chave para requisições locais, mas a SDK da OpenAI requer que uma string qualquer seja informada no construtor para evitar exceções de validação inicial.
Exemplos Práticos de Código em Python
Opção A: Chat e Streaming em Tempo Real com OpenAI
from openai import OpenAI
# 1. Instancia o cliente oficial da OpenAI apontando para o servidor Ollama local
client = OpenAI(
base_url="http://localhost:11434/v1",
api_key="ollama" # Chave fictícia necessária para inicializar o cliente
)
print("--- 1. Chamada Padrão de Chat ---")
# A sintaxe é 100% idêntica à API oficial da OpenAI
response = client.chat.completions.create(
model="llama3.2:latest", # Nome do modelo baixado localmente no Ollama
messages=[
{"role": "system", "content": "Você é um assistente técnico especializado em Python."},
{"role": "user", "content": "Explique o conceito de decorador em duas frases."}
],
temperature=0.3
)
print("Resposta:", response.choices.message.content)
print("\n--- 2. Streaming de Tokens em Tempo Real ---")
# Transmissão incremental idêntica ao comportamento do ChatGPT
stream = client.chat.completions.create(
model="llama3.2:latest",
messages=[
{"role": "user", "content": "Escreva um exemplo curto de regex para validar email em Python."}
],
stream=True
)
for chunk in stream:
# O conteúdo do token é lido da estrutura padronizada da OpenAI
token = chunk.choices.delta.content or ""
print(token, end="", flush=True)
print()
Opção B: Cliente Assíncrono (AsyncOpenAI) para Alta Concorrência
Caso sua aplicação já utilize o cliente assíncrono da OpenAI em ecossistemas como FastAPI ou asyncio, a transição é imediata:
import asyncio
from openai import AsyncOpenAI
async def main():
# Instancia o cliente assíncrono redirecionado
client = AsyncOpenAI(
base_url="http://localhost:11434/v1",
api_key="ollama"
)
prompts = [
"Qual a diferença entre tupla e lista em Python?",
"Explique como funciona o mecanismo de garbage collection em Python.",
"O que são geradores (generators) e qual sua vantagem de memória?"
]
# Dispara múltiplas requisições assíncronas concorrentes no mesmo event loop
tarefas = [
client.chat.completions.create(
model="llama3.2:latest",
messages=[{"role": "user", "content": p}]
)
for p in prompts
]
resultados = await asyncio.gather(*tarefas)
print("--- Respostas Processadas Assincronamente ---")
for i, res in enumerate(resultados):
print(f"\n[Consulta {i+1}]:")
print(res.choices.message.content[:150] + "...")
if __name__ == "__main__":
asyncio.run(main())
Capítulo 7: Tratamento de Erros, Resiliência e Boas Práticas
Ao evoluir uma aplicação baseada em LLMs locais para ambientes de uso real e produção, lidar de forma preventiva com falhas operacionais torna-se um requisito indispensável de engenharia. Diferente de APIs em nuvem gerenciadas, a execução privada depende inteiramente dos recursos de hardware locais (RAM, VRAM e CPU) e do estado do serviço em segundo plano. Cenários como a indisponibilidade do servidor local (ollama serve), modelos não encontrados ou pendentes de download, estouro de memória GPU (CUDA Out of Memory ou pressão de VRAM) e exceções de conexão ou timeout durante chamadas de longa duração exigem uma arquitetura defensiva e altamente resiliente.
Garantir a estabilidade contínua do sistema envolve adotar padrões consolidados de tolerância a falhas, como estratégias de reexecução automática com recuo exponencial (exponential backoff), validações preventivas da integridade do serviço e mecanismos de fallback para modelos menores ou execução em CPU durante momentos de escassez de memória. Além disso, a aplicação de boas práticas no gerenciamento de recursos, na sanitização de entradas e no tratamento de exceções específicas do cliente (como ResponseError) assegura que suas aplicações em Python operem de maneira previsível, robusta e capaz de se recuperar de forma graciosa perante instabilidades do ambiente local.
7.1. Tratamento de Exceções e Timeouts
Integrar LLMs locais em aplicações de produção exige resiliência contra falhas operacionais. Diferente de serviços na nuvem com SLA garantido, a inferência local ou em servidores remotos próprios depende da disponibilidade do daemon (ollama serve), da presença dos modelos em disco e da capacidade de memória física (RAM/VRAM) da máquina.
1. As Duas Categorias Principais de Erros
Ao interagir com o Ollama em Python (seja via SDK oficial ou via requisições HTTP REST), as falhas enquadram-se em duas categorias principais:
ResponseError(Erros do Servidor Ollama): Exceção nativa da SDKollamadisparada quando o servidor responde com um código HTTP de erro (4xx/5xx). Ocorre tipicamente quando o modelo solicitado não está baixado no servidor (HTTP 404), quando há parâmetros inválidos no payload ou quando o motor de inferência esgota a VRAM/RAM disponível.- Erros de Conexão e Timeouts (
ConnectError/Timeout): Lançados quando a biblioteca não consegue estabelecer conexão com o endpoint do Ollama (servidor desligado ou porta11434bloqueada por firewall) ou quando a inferência excede o tempo limite de espera em tarefas muito pesadas.
2. Código Exemplo 1: Tratamento com a SDK Oficial (ollama) e Exponential Backoff
O exemplo abaixo demonstra como capturar ResponseError e falhas de conexão utilizando a SDK do Ollama, combinadas com uma estratégia de reexecução (retry) com recuo exponencial (exponential backoff):
import time
import ollama
from ollama import ResponseError
def gerar_texto_com_resiliencia(prompt: str, modelo: str = "llama3.2:latest", max_tentativas: int = 3) -> str:
"""
Executa uma geração de texto no Ollama aplicando tratamento defensivo de erros
e retry com recuo exponencial em caso de falha de conexão.
"""
for tentativa in range(1, max_tentativas + 1):
try:
print(f"🔄 Tentativa {tentativa} de {max_tentativas}...")
# Chamada principal à SDK
resposta = ollama.generate(
model=modelo,
prompt=prompt,
options={"temperature": 0.3}
)
return resposta['response']
except ResponseError as e:
# Captura erros retornados pela API do Ollama (ex: Modelo não encontrado)
print(f"❌ Erro da API do Ollama (Status {e.status_code}): {e.error}")
if e.status_code == 404:
print(f"💡 Dica: Verifique se o modelo '{modelo}' foi baixado executando 'ollama pull {modelo}'.")
# Erros de API (4xx/5xx) normalmente não devem ser reexecutados em loop sem alteração
break
except Exception as e:
# Captura falhas de rede, conexão recusada ou servidor offline
print(f"⚠️ Falha de conexão/rede: {e}")
if tentativa < max_tentativas:
tempo_espera = 2 ** tentativa # Exponential backoff: 2s, 4s, 8s...
print(f"⏳ Aguardando {tempo_espera}s antes de tentar novamente...")
time.sleep(tempo_espera)
else:
print("❌ Número máximo de tentativas atingido. Verifique se 'ollama serve' está ativo.")
return ""
if __name__ == "__main__":
prompt = "Resuma as boas práticas de tratamento de erros em Python."
resultado = gerar_texto_com_resiliencia(prompt)
if resultado:
print("\n--- Resposta Obtida com Sucesso ---")
print(resultado)
3. Código Exemplo 2: Tratamento de Timeouts em Requisições HTTP REST (requests)
Se a sua aplicação utiliza requisições REST puras ou conecta-se a servidores Ollama remotos em rede, é indispensável definir o parâmetro timeout nas requisições HTTP para evitar que a thread da aplicação fique bloqueada indefinidamente:
import requests
from requests.exceptions import Timeout, ConnectionError, HTTPError
def consultar_ollama_rest_com_timeout(url_servidor: str, modelo: str, prompt: str, timeout_segundos: int = 30):
endpoint = f"{url_servidor}/api/generate"
payload = {
"model": modelo,
"prompt": prompt,
"stream": False
}
try:
print(f"📡 Enviando requisição para {endpoint} (Timeout: {timeout_segundos}s)...")
# O parâmetro timeout define o tempo máximo de espera pelo primeiro byte e pela resposta
response = requests.post(endpoint, json=payload, timeout=timeout_segundos)
response.raise_for_status() # Dispara exceção para códigos 4xx/5xx
dados = response.json()
return dados.get("response")
except Timeout:
print(f"🚨 Timeout: O servidor Ollama demorou mais de {timeout_segundos}s para responder.")
print("💡 Sugestão: Aumente o tempo limite ou utilize um modelo quantizado menor (ex: 1.5b / 3b).")
except ConnectionError:
print(f"🚨 Conexão Recusada: Não foi possível conectar a {url_servidor}.")
print("💡 Verifique se o serviço Ollama está rodando e se a porta 11434 está liberada.")
except HTTPError as err:
print(f"🚨 Erro HTTP do Servidor: {err.response.status_code} - {err.response.text}")
except Exception as err:
print(f"🚨 Erro inesperado: {err}")
return None
if __name__ == "__main__":
url_remota = "http://localhost:11434"
resposta = consultar_ollama_rest_com_timeout(url_remota, "llama3.2:latest", "Olá!", timeout_segundos=10)
if resposta:
print("Resposta:", resposta)
7.2. Estratégias de Retry com Backoff Exponencial
Quando um modelo de linguagem local não está previamente carregado na memória VRAM/RAM (fenômeno conhecido como cold start), a primeira requisição enviada ao Ollama pode falhar por timeout ou retornar erros temporários de indisponibilidade enquanto o sistema realiza o offloading e aloca os pesos na GPU.
Implementar uma lógica de reexecução com recuo exponencial (Exponential Backoff) evita que a sua aplicação falhe imediatamente perante esses gargalos momentâneos de inicialização.
1. O Conceito de Recuo Exponencial com Jitter
Em vez de repetir a requisição imediatamente em intervalos fixos (o que pode sobrecarregar ainda mais a CPU/GPU durante o carregamento do modelo), o Backoff Exponencial aumenta progressivamente o tempo de espera a cada tentativa malsucedida:
\[\text{Tempo de Espera} = \text{base_delay} \times 2^{\text{tentativa}} + \text{jitter}\]
- Crescimento Exponencial: Se a primeira tentativa falha, o sistema aguarda 2s; na segunda, 4s; na terceira, 8s, e assim por diante.
- Adição de Jitter (Ruído Aleatório): Adicionar uma pequena variação aleatória no tempo de espera previne o problema de Thundering Herd (onde múltiplas requisições concorrentes tentam reconectar ao servidor exatamente no mesmo milissegundo).
2. Código Exemplo 1: Decorador Reutilizável em Python Puro
A forma mais elegante de aplicar essa estratégia em Python é criando um decorador customizado que pode ser acoplado a qualquer função que interaja com a SDK do Ollama:
import time
import random
from functools import wraps
import ollama
from ollama import ResponseError
def retry_com_backoff(
max_tentativas: int = 4,
delay_inicial: float = 2.0,
fator_multiplicador: float = 2.0,
usar_jitter: bool = True
):
"""
Decorador para reexecutar funções que interagem com o Ollama em caso de falhas temporárias.
"""
def decorador(func):
@wraps(func)
def wrapper(*args, **kwargs):
delay_atual = delay_inicial
for tentativa in range(1, max_tentativas + 1):
try:
return func(*args, **kwargs)
except (ResponseError, ConnectionError, Exception) as e:
# Se for um erro do servidor Ollama indicando que o modelo ainda está carregando ou indisponível
print(f"⚠️ [Tentativa {tentativa}/{max_tentativas}] Falha ao executar '{func.__name__}': {e}")
if tentativa == max_tentativas:
print("❌ Limite máximo de tentativas atingido. Disparando exceção final.")
raise e
# Calcula o próximo delay com ou sem jitter
jitter = random.uniform(0.1, 0.5) if usar_jitter else 0.0
tempo_espera = delay_atual + jitter
print(f"⏳ Modelo pode estar em carregamento. Aguardando {tempo_espera:.2f}s antes da próxima tentativa...")
time.sleep(tempo_espera)
# Incrementa o delay exponencialmente
delay_atual *= fator_multiplicador
return wrapper
return decorador
# Exemplo de aplicação do decorador em uma função de geração
@retry_com_backoff(max_tentativas=3, delay_inicial=2.0)
def requisitar_modelo_local(prompt: str, modelo: str = "llama3.2:latest") -> str:
response = ollama.generate(
model=modelo,
prompt=prompt,
options={"temperature": 0.2}
)
return response['response']
if __name__ == "__main__":
print("--- Testando Chamada Resiliente ---")
try:
resposta = requisitar_modelo_local("Explique o que é backoff exponencial em duas frases.")
print("\n✅ Resposta obtida:")
print(resposta)
except Exception as err:
print(f"\n❌ A requisição falhou permanentemente: {err}")
3. Código Exemplo 2: Backoff Exponencial em Fluxos Assíncronos (asyncio)
Para microsserviços em FastAPI ou pipelines assíncronos que utilizam o AsyncClient, a lógica deve utilizar asyncio.sleep() para evitar o bloqueio do event loop:
import asyncio
import random
from ollama import AsyncClient, ResponseError
async def consultar_ollama_async_com_retry(
client: AsyncClient,
prompt: str,
modelo: str = "llama3.2:latest",
max_tentativas: int = 4
) -> str:
"""
Função corrotina com retentativa assíncrona não bloqueante.
"""
delay = 2.0 # Delay base inicial em segundos
for tentativa in range(1, max_tentativas + 1):
try:
response = await client.generate(
model=modelo,
prompt=prompt
)
return response['response']
except (ResponseError, Exception) as e:
print(f"🚨 [Async Retry {tentativa}/{max_tentativas}] Erro: {e}")
if tentativa == max_tentativas:
raise e
# Jitter aleatório para desacoplar requisições concorrentes
jitter = random.uniform(0.1, 0.4)
tempo_espera = delay + jitter
print(f"⏳ Suspendendo corrotina por {tempo_espera:.2f}s...")
await asyncio.sleep(tempo_espera) # Liberador não bloqueante do event loop
delay *= 2.0 # Dobra o tempo de espera para a próxima rodada
return ""
async def main():
client = AsyncClient(host="http://localhost:11434")
prompt = "Crie uma lista com 3 boas práticas de resiliência em sistemas distribuídos."
resultado = await consultar_ollama_async_com_retry(client, prompt)
print("\n--- Resposta Assíncrona Recebida ---")
print(resultado)
if __name__ == "__main__":
asyncio.run(main())
7.3. Otimização de Memória e Concorrência
Em ambientes de produção e execução local, a gestão eficiente da VRAM (RAM da GPU) e da memória do sistema é o fator crítico para prevenir falhas de estouro de memória (CUDA Out of Memory) e garantir baixas latências. O consumo de memória de uma LLM em execução é composto principalmente por duas partes: os pesos do modelo (que variam com a contagem de parâmetros e a precisão da quantização) e o KV Cache (a memória dinâmica reservada para manter o histórico de tokens da janela de contexto).
Quando o contexto das conversas se estende ou múltiplas requisições paralelas tentam alocar contexto simultaneamente, a demanda de memória pode ultrapassar a capacidade da placa de vídeo, resultando em quedas de desempenho devido ao swapping com a memória RAM do sistema.
1. Parâmetros-Chave para Controle de Memória no Ollama
O servidor do Ollama e a sua SDK em Python oferecem configurações diretas para controlar a retenção de modelos e a pegada de memória de cada chamada:
num_ctx: Ajusta a largura da janela de contexto em tokens (ex:2048,4096). Como o consumo de memória do KV Cache escala de forma linear em relação ao tamanho da janela de contexto, reduzir onum_ctxpara o valor estritamente necessário para a sua aplicação alivia drasticamente a pressão sobre a VRAM.keep_alive: Define por quanto tempo os pesos do modelo permanecem alocados na GPU após a última requisição."5m"(padrão): Mantém o modelo na GPU por 5 minutos aguardando novas chamadas."0": Descarrega o modelo da VRAM imediatamente após a resposta, liberando memória para outras aplicações ou modelos."-1": Preserva o modelo indefinidamente na GPU, evitando a latência de carregamento inicial (cold start).
OLLAMA_NUM_PARALLEL: Variável de ambiente do servidor Ollama que estabelece o número máximo de requisições simultâneas que podem ser processadas em paralelo dentro de uma mesma instância de modelo.OLLAMA_MAX_LOADED_MODELS: Define quantos modelos distintos podem permanecer carregados simultaneamente na GPU/RAM.
2. Controle de Fila e Concorrência em Python
Para evitar a sobrecarga do servidor local quando sua aplicação precisa lidar com picos de tráfego, a prática recomendada em Python é utilizar um Semáforo Assíncrono (asyncio.Semaphore). Essa abordagem cria uma fila de inferência que limita a quantidade de tarefas em execução simultânea na GPU, enfileirando as demais requisições de forma organizada.
3. Exemplo Prático de Código em Python: Gerenciador de Fila e Memória
O código a seguir demonstra como combinar o controle de parâmetros de memória (num_ctx e keep_alive) com um asyncio.Semaphore para gerenciar a concorrência em lote sem estourar a VRAM:
import asyncio
import time
from ollama import AsyncClient, ResponseError
# Define o limite máximo de inferências simultâneas na GPU
LIMITE_CONCORRENCIA_GPU = 2
semaforo_gpu = asyncio.Semaphore(LIMITE_CONCORRENCIA_GPU)
async def processar_requisicao_com_controle_vram(
client: AsyncClient,
id_requisicao: int,
prompt: str,
modelo: str = "llama3.2:latest"
):
"""
Processa a chamada no Ollama respeitando a fila do semáforo de GPU
e aplicando configurações otimizadas de memória.
"""
async with semaforo_gpu: # Aguarda a liberação de vaga na fila
print(f"🚀 [Req {id_requisicao}] Iniciando processamento na GPU...")
inicio = time.time()
try:
# Chamada assíncrona definindo parâmetros de memória e retenção
response = await client.generate(
model=modelo,
prompt=prompt,
keep_alive="2m", # Mantém o modelo na GPU por 2 minutos
options={
"num_ctx": 2048, # Reduz a janela de contexto para poupar VRAM
"temperature": 0.2
}
)
duracao = time.time() - inicio
texto = response['response'].strip()
print(f"✅ [Req {id_requisicao}] Concluída em {duracao:.2f}s ({len(texto)} caracteres).")
return f"Req {id_requisicao}: {texto[:80]}..."
except ResponseError as e:
print(f"❌ [Req {id_requisicao}] Erro do Ollama (HTTP {e.status_code}): {e.error}")
return None
except Exception as e:
print(f"🚨 [Req {id_requisicao}] Erro inesperado: {e}")
return None
async def main():
client = AsyncClient(host="http://localhost:11434")
# Conjunto de requisições simultâneas para simular tráfego concorrente
prompts = [
"Defina o conceito de herança em Orientação a Objetos.",
"Explique o que é polimorfismo em Python.",
"O que é o conceito de encapsulamento?",
"Qual a função do parâmetro 'self' em métodos Python?",
"Diferencie métodos estáticos (@staticmethod) e de classe (@classmethod).",
"O que são métodos mágicos (dunder methods) em Python?"
]
print(f"--- Disparando {len(prompts)} requisições (Limite de GPU = {LIMITE_CONCORRENCIA_GPU}) ---\n")
# Instancia as tarefas assíncronas
tarefas = [
processar_requisicao_com_controle_vram(client, i + 1, prompt)
for i, prompt in enumerate(prompts)
]
# Executa o lote mantendo a fila sob controle
resultados = await asyncio.gather(*tarefas)
print("\n--- Todas as requisições da fila foram concluídas com sucesso ---")
if __name__ == "__main__":
asyncio.run(main())
Capítulo 8: Conclusão e Considerações Finais
A integração de modelos de linguagem locais com Python e Ollama representa uma mudança fundamental na forma como desenvolvemos e implantamos aplicações orientadas à inteligência artificial. Ao migrar de APIs em nuvem dependentes de assinaturas para uma infraestrutura local e privada, os desenvolvedores e organizações ganham controle total sobre a segurança dos seus dados, garantem operação offline e eliminam completamente custos recorrentes por token.
Ao longo deste guia, exploramos como o Ollama simplifica o gerenciamento de LLMs locais por meio de uma API REST leve e de uma SDK oficial para Python intuitiva. Vimos que é possível ir muito além de interações simples de chat: recursos avançados como streaming de respostas em tempo real, chamadas assíncronas concorrentes, saídas estruturadas com JSON Schema e suporte a chamadas de funções (Tool Calling) fornecem a base necessária para construir desde utilitários de CLI privados até agentes autônomos e microsserviços de produção com FastAPI.
Embora o desafio do gerenciamento de hardware e memória (VRAM/RAM) continue sendo um fator crítico, a adoção de boas práticas de resiliência — como controle de concorrência, tratamento defensivo de erros e estratégias de retry com backoff exponencial — garante a estabilidade de sistemas baseados em IA local. Com o ecossistema de modelos abertos evoluindo rapidamente, a capacidade de executar e orquestrar LLMs em sua própria máquina torna-se uma habilidade indispensável para qualquer engenheiro de software moderno.


Comentários
Postar um comentário