Nesta etapa, realizaremos a transição do ambiente previamente configurado para a primeira interação programática com um modelo de linguagem de grande escala. O objetivo técnico é consolidar o uso do Cursor, dos Jupyter Notebooks e de um cliente compatível com a API da OpenAI para executar inferência inicial, compreendendo ao mesmo tempo a arquitetura de mensagens que governa o comportamento dos modelos de chat modernos.
Ao final deste processo, você terá percorrido o ciclo básico de uma aplicação orientada por LLM: entrada de dados, engenharia de mensagens, chamada de inferência e interpretação da saída.
Pré-requisito: antes de iniciar, conclua a configuração do ambiente descrita no
README.mddo repositório e valide que o projeto já está sincronizado no ambiente virtual.venv.
Na engenharia aplicada de LLMs, a fase inicial de experimentação exige um ambiente que favoreça iteração rápida, isolamento de dependências e observabilidade do código em execução. Para isso, utilizaremos o Cursor como IDE principal e os Jupyter Notebooks (.ipynb) como interface de experimentação.
Antes de executar qualquer notebook, valide se o ambiente do Cursor possui as extensões adequadas instaladas:
O Kernel é o processo responsável por executar o código do notebook. Em projetos com ambientes virtuais, selecionar o kernel incorreto é uma das causas mais comuns de falhas de importação e inconsistência de dependências.
.venv/bin/python
ou, em ambientes Windows, o equivalente dentro da pasta .venv.
Nota Técnica: sempre que o projeto for sincronizado novamente, ou quando houver troca de máquina, conflito de cache ou atualização de dependências, vale revalidar o kernel ativo.
Antes da primeira chamada de inferência, é recomendável validar se o endpoint que servirá o modelo está acessível. Em ambientes locais, isso é especialmente útil para detectar problemas de inicialização do Ollama ou incompatibilidades de rede entre o notebook e o serviço local.
A estratégia abaixo mantém o fluxo simples: criamos um cliente compatível com a API da OpenAI, apontamos para o endpoint local do Ollama e definimos funções utilitárias para verificar disponibilidade do serviço, confirmar a presença do modelo e executar chamadas com uma política mínima de repetição em caso de timeout inicial.
import os
from dotenv import load_dotenv
from IPython.display import Markdown, display
from openai import OpenAI, APIConnectionError, APITimeoutError, APIStatusError
from scraper import fetch_website_contents
load_dotenv()
URL_BASE_OLLAMA = "http://localhost:11434/v1"
TEMPO_LIMITE_PADRAO = 45.0
MODELO_PADRAO = "llama3.2:3b"
def verificar_modelo_disponivel(cliente: OpenAI, modelo: str = MODELO_PADRAO) -> bool:
"""
Confirma se o modelo informado está carregado no Ollama.
"""
try:
modelos = {item.id for item in cliente.models.list().data}
except (APIConnectionError, APITimeoutError, APIStatusError):
return False
return modelo in modelos
def gerar_chat_completion(
cliente: OpenAI,
mensagens: list[dict[str, str]],
modelo: str = MODELO_PADRAO,
tentativas: int = 2,
):
"""
Envia mensagens ao modelo com retry simples para mitigar timeout de aquecimento.
"""
ultima_excecao = None
for tentativa in range(1, tentativas + 1):
try:
return cliente.chat.completions.create(
model=modelo,
messages=mensagens,
)
except APITimeoutError as erro:
ultima_excecao = erro
if tentativa < tentativas:
print(f"⚠️ Timeout na tentativa {tentativa}/{tentativas}. Repetindo...")
else:
raise
raise ultima_excecao
A primeira utilidade relevante é a criação do cliente apontando para o endpoint local do Ollama. Em seguida, realizamos uma verificação simples para garantir que o serviço responde corretamente.
def criar_cliente_ollama(
url_base: str = URL_BASE_OLLAMA,
tempo_limite: float = TEMPO_LIMITE_PADRAO,
) -> OpenAI:
"""
Cria um cliente compatível com a API da OpenAI apontando para o Ollama local.
"""
return OpenAI(
base_url=url_base,
api_key="ollama", # Valor simbólico exigido pelo cliente
timeout=tempo_limite,
)
def verificar_endpoint_ollama(url_base: str = URL_BASE_OLLAMA) -> bool:
"""
Verifica se o endpoint OpenAI-compatível do Ollama está acessível.
"""
cliente = criar_cliente_ollama(url_base=url_base)
try:
cliente.models.list()
print(f"OK: Endpoint disponível em {url_base}")
return True
except (APIConnectionError, APITimeoutError):
print("Falha de conexão: confirme se o Ollama está ativo no ambiente local.")
return False
except APIStatusError as erro:
print(f"Falha HTTP ({erro.status_code}): revise a configuração do serviço.")
return False
Com a função definida, podemos executar a checagem inicial:
if verificar_endpoint_ollama():
pass
Observação: neste estágio, o objetivo não é ainda resolver uma tarefa de negócio, mas apenas confirmar que o ambiente consegue se comunicar com o servidor que expõe o modelo.
Com o endpoint validado, podemos executar uma chamada mínima ao modelo apenas para confirmar que o pipeline está funcional. O objetivo aqui é testar a estrutura mais simples possível de mensagens, utilizando apenas uma entrada do usuário.
mensagem = "Oi, beleza, tudo suave?"
mensagens_teste = [
{"role": "user", "content": mensagem}
]
mensagens_teste
Na sequência, instanciamos o cliente, confirmamos se o modelo está de fato disponível no Ollama e executamos a primeira inferência.
cliente_llm = criar_cliente_ollama()
# Para usar a API oficial da OpenAI, remova o base_url do cliente e carregue sua chave do arquivo .env
if not verificar_modelo_disponivel(cliente_llm, MODELO_PADRAO):
raise RuntimeError(
f"Modelo '{MODELO_PADRAO}' não encontrado no Ollama. Execute: ollama pull {MODELO_PADRAO}"
)
resposta_teste = gerar_chat_completion(
cliente=cliente_llm,
modelo=MODELO_PADRAO,
mensagens=mensagens_teste,
)
resposta_teste.choices[0].message.content
Esse teste simples já valida quatro pontos centrais do ambiente:
A seguir, integraremos um utilitário de extração de conteúdo web com a inferência do LLM. Esse padrão já representa a base arquitetural de muitos produtos reais: capturamos um conteúdo bruto, estruturamos a tarefa por meio de prompts e solicitamos ao modelo uma transformação útil sobre o material coletado.
Antes de definir a instrução enviada ao modelo, vale inspecionar o conteúdo textual retornado pela rotina de extração:
url_exemplo = "https://santclear.github.io/resume/"
conteudo_site = fetch_website_contents(url_exemplo)
print(conteudo_site[:2000])
Essa inspeção é importante porque permite verificar se o texto coletado é suficiente para a tarefa desejada, se há excesso de ruído e se a estratégia de extração adotada faz sentido para aquele tipo de página.
Modelos de chat modernos operam sobre uma sequência estruturada de mensagens, normalmente representadas como uma lista de dicionários contendo role e content. Compreender essa estrutura é fundamental para controlar comportamento, formato de saída e objetivo da resposta.
O Prompt de Sistema define o enquadramento comportamental do modelo. É nele que estabelecemos o papel do assistente, o tom da resposta, as restrições de saída e os critérios que devem orientar a inferência.
No exemplo abaixo, o modelo é instruído a atuar como um analista técnico de conteúdo web, com foco em resumo curto, objetivo e limpo de elementos de navegação.
PROMPT_SISTEMA = """
Você é um analista técnico de conteúdo web.
Leia o texto extraído de um website e produza um resumo curto, claro e objetivo.
Ignore menus, cabeçalhos, rodapés e elementos de navegação.
Responda em markdown, sem envolver a resposta em bloco de código.
"""
O Prompt de Usuário representa a instrução variável da tarefa. Em produtos reais, essa mensagem frequentemente é enriquecida com contexto adicional antes de ser enviada ao modelo.
PROMPT_USUARIO_BASE = """
A seguir está o conteúdo textual de um website.
Produza um resumo breve da página.
Quando houver anúncios, lançamentos ou atualizações relevantes, destaque os pontos principais.
"""
A separação entre essas duas camadas é o que permite transformar um único modelo em múltiplos comportamentos especializados.
A API espera receber as mensagens em uma lista estruturada como esta:
mensagens_exemplo = [
{"role": "system", "content": "Você é uma calculadora simples e direta."},
{"role": "user", "content": "1 / 13 = ?"}
]
resposta_exemplo = gerar_chat_completion(
cliente=cliente_llm,
modelo=MODELO_PADRAO,
mensagens=mensagens_exemplo,
)
resposta_exemplo.choices[0].message.content
Esse exemplo é deliberadamente simples para evidenciar a estrutura esperada pela API: uma sequência ordenada de mensagens, cada uma com seu papel explícito na interação.
No caso da sumarização, encapsulamos a criação das mensagens em uma função simples. Isso melhora a legibilidade do notebook e prepara o código para reutilização em funções maiores.
def montar_mensagens_resumo(conteudo_site: str) -> list[dict[str, str]]:
"""
Organiza as mensagens no formato esperado pela API de chat.
"""
return [
{"role": "system", "content": PROMPT_SISTEMA},
{"role": "user", "content": PROMPT_USUARIO_BASE + conteudo_site},
]
Podemos inspecionar diretamente a estrutura gerada:
montar_mensagens_resumo(conteudo_site)
Esse tipo de inspeção intermediária é útil durante a fase de desenvolvimento porque ajuda a verificar se o contexto está sendo montado exatamente como o modelo irá recebê-lo.
Agora reunimos todas as etapas em uma função única: coleta do conteúdo, montagem do contexto e chamada ao modelo. Mesmo sendo um exemplo simples, esse fluxo já representa a base de muitas aplicações reais de IA generativa.
def gerar_resumo_site(url: str, modelo: str = MODELO_PADRAO) -> str:
"""
Extrai o conteudo textual de um site e solicita ao modelo um resumo em markdown.
"""
conteudo_site = fetch_website_contents(url)
resposta = gerar_chat_completion(
cliente=cliente_llm,
modelo=modelo,
mensagens=montar_mensagens_resumo(conteudo_site),
)
return resposta.choices[0].message.content
Com a função definida, já podemos testar a geração do resumo em formato textual:
gerar_resumo_site("https://santclear.github.io/resume/")
Para melhorar a visualização no notebook, encapsulamos a exibição em uma segunda função que renderiza a resposta como Markdown:
def exibir_resumo_site(url: str) -> None:
resumo = gerar_resumo_site(url)
display(Markdown(resumo))
E então executamos a exibição renderizada:
exibir_resumo_site("https://santclear.github.io/resume/")
Esse pipeline consolida os blocos essenciais de uma aplicação baseada em LLM:
O utilitário fetch_website_contents funciona bem em páginas cujo conteúdo principal já está presente no HTML inicial. Em sites fortemente dependentes de JavaScript, renderização dinâmica ou mecanismos mais agressivos de proteção, essa abordagem pode não capturar a totalidade do conteúdo.
Nesses casos, a evolução natural da stack envolve ferramentas como Selenium ou Playwright, capazes de renderizar a página antes da extração.
A forma mais direta de observar essas diferenças é testar a mesma função em sites distintos:
exibir_resumo_site("https://iclnoticias.com.br/")
exibir_resumo_site("https://tvtnews.com.br/")
Ao comparar os resultados, o desenvolvedor passa a perceber que a qualidade da resposta do LLM depende não apenas do modelo, mas também da qualidade do texto de entrada obtido no processo de coleta.
Altere o PROMPT_SISTEMA para que o modelo assuma o papel de um consultor de produto SaaS e reanalise a mesma página sob a ótica de proposta de valor, clareza de posicionamento e maturidade comercial.
O objetivo deste exercício é observar, na prática, como a troca da camada de instrução altera a natureza da resposta, mesmo quando o conteúdo de entrada permanece idêntico.
Ao concluir esta etapa, você terá estabelecido a base operacional para os próximos capítulos: validar um endpoint, estruturar mensagens, executar inferência e reaproveitar esse padrão para múltiplos problemas de negócio.
A partir daqui, a progressão natural é evoluir de chamadas simples para aplicações mais estruturadas, integrando APIs, orquestração de contexto e, posteriormente, arquiteturas mais sofisticadas baseadas em agentes e recuperação de informação.