Pular para o conteúdo
CESAR ZANIS
Tech Reference
Agora

IA & Agentes

Engenharia em produção além das demos: RAG, custos, proteção de dados e uso responsável de ferramentas de IA.

"A IA amplifica quem pensa. Não substitui quem pensa."

Cesar Zanis
01

A mudança de mentalidade (leia primeiro)

01

IA não substitui entendimento: amplifica

Gerar código está cada vez mais acessível. O valor da engenharia vai além de escrever um CRUD.

Profissionais precisam entender o negócio, desenhar soluções e coordenar ferramentas para entregá-las. A IA amplifica o bom julgamento; não o substitui.

A qualidade da resposta depende muito da qualidade do contexto fornecido.

02

O problema primeiro, a ferramenta depois

Antes de abrir qualquer IA, mastigue o problema de negócio na sua cabeça. Quem é o usuário? Qual a dor real? O que acontece se essa feature não existir?

Se você jogar "faz um sistema de cadastro" no LLM, vai receber um sistema de cadastro genérico que não serve para ninguém. Se você jogar "o usuário é um inspetor de campo que preenche formulários em PDA Honeywell sem internet estável, precisa salvar localmente sem perder dados mesmo se a bateria morrer no meio", a IA entrega ouro.

O contexto que você fornece é o diferencial. É sua experiência que faz o output ser útil.

03

IA para planejamento ≠ IA para implementação

Use modelos de raciocínio para explorar arquitetura: comparar alternativas, analisar trade-offs, revisar ADRs e questionar decisões. A decisão final continua sua.

Use agentes integrados ao editor para implementação: boilerplate, CRUD, testes e refatoração. Eles podem trabalhar com os arquivos reais do projeto, executar comandos e corrigir erros.

Não delegue à IA a responsabilidade pela arquitetura. Ela propõe; você decide.

02

Padrões de produção

RAG: quando e como fazer certo

A diferença entre um chatbot inútil e um assistente real.

RAG (Retrieval-Augmented Generation) é buscar contexto relevante antes de gerar a resposta. Sem RAG, o LLM responde com conhecimento de treinamento genérico. Com RAG bem feito, responde com o seu contexto específico (documentação, base de conhecimento, histórico de decisões).

O erro mais comum: usar apenas busca semântica (vetores) ignorando busca léxica (BM25/FTS). Para a maioria dos casos de uso, uma busca híbrida (semântica + léxica) supera qualquer uma das duas isoladas.

Python
# Arquitetura RAG mínima funcional
# 1. Indexação (roda offline, quando o conteúdo muda)
def index_document(doc: str, doc_id: str):
    # Chunking: blocos com overlap para não perder contexto
    chunks = chunk_with_overlap(doc, size=800, overlap=100)
    
    for i, chunk in enumerate(chunks):
        embedding = embed(chunk)  # via API: text-embedding-004
        vector_store.upsert(
            id=f"{doc_id}_{i}",
            vector=embedding,
            metadata={"text": chunk, "doc_id": doc_id}
        )

# 2. Retrieval (em tempo real, por query do usuário)
def retrieve(query: str, top_k: int = 5) -> list[str]:
    query_embedding = embed(query)
    
    # Busca semântica (vetores)
    semantic_results = vector_store.query(query_embedding, top_k=top_k)
    
    # Busca léxica (palavras exatas): não ignore isso
    lexical_results = full_text_search(query, top_k=top_k)
    
    # Rerank híbrido: RRF (Reciprocal Rank Fusion)
    return reciprocal_rank_fusion([semantic_results, lexical_results])

# 3. Generation: contexto injetado no prompt
def answer(query: str) -> str:
    context_chunks = retrieve(query)
    context = "\n\n".join(context_chunks)
    
    prompt = f"""Responda baseado APENAS no contexto abaixo.
Se a resposta não estiver no contexto, diga explicitamente.

CONTEXTO:
{context}

PERGUNTA: {query}"""
    
    return llm.generate(prompt)

Custo de LLM: o que não contam no playground

Meça separadamente os custos de entrada, saída e contexto.

O custo de LLM em produção não é o que parece no playground. Em produção, o custo vem de quatro frentes: tokens de sistema (fixos por request), histórico de conversa (cresce a cada turno), contexto RAG (por request) e output do modelo.

O que funciona na prática: comprimir o histórico ao invés de mandar 10 turnos completos, rotear por complexidade usando modelo pequeno para classificação e modelo grande só para resposta final, cache de resposta para queries idênticas ou similares (por embedding distance), e sempre definir maxOutputTokens explícito porque modelos são verbosos por natureza.

Python
# Controle de custo real em produção

# 1. Compressão de histórico: não mande tudo
def compress_history(history: list[dict], max_turns: int = 4) -> str:
    if len(history) <= max_turns:
        return format_history(history)
    
    # Resumo dos turnos antigos + últimas N mensagens completas
    old_turns = history[:-max_turns]
    recent_turns = history[-max_turns:]
    
    summary = llm.generate(
        f"Resuma em 2-3 frases os pontos principais desta conversa: {old_turns}",
        config={"maxOutputTokens": 150}  # resumo curto, modelo pequeno
    )
    
    return f"[Resumo anterior: {summary}]\n{format_history(recent_turns)}"

# 2. Roteamento por complexidade: modelo certo para cada tarefa
def route_to_model(query: str) -> str:
    # Classificação rápida e barata
    is_complex = classifier.predict(query)  # modelo local ou flash
    
    return "gemini-3.8-flash" if is_complex else "gemini-3.5-flash-lite"

# 3. Limite explícito de output
generation_config = {
    "maxOutputTokens": 600,   # suficiente para resposta útil
    "temperature": 0.3,       # ajuste a temperatura à tarefa e ao modelo
}

LGPD + IA: o que é obrigatório

Defina a base legal e limite os dados enviados ao necessário.

Antes de enviar dados pessoais a uma API de IA, defina a finalidade e a base legal aplicável, minimize os dados e revise retenção, acesso e transferências internacionais. O consentimento é uma das bases legais da LGPD, não a única.

Políticas de treinamento variam por produto e contrato. Por exemplo, dados da API da OpenAI não são usados para treinamento por padrão. Avalie anonimização, controles contratuais e processamento local conforme o caso. Processamento local também exige proteção de acesso, logs e backups.

Python
# Mascaramento básico antes de enviar para LLM externo
import re

def anonymize_for_llm(text: str) -> tuple[str, dict]:
    """Remove PII antes de enviar. Retorna texto limpo + mapa para substituição."""
    replacements = {}
    counter = {"cpf": 0, "email": 0, "phone": 0}
    
    # CPF
    def replace_cpf(m):
        key = f"[CPF_{counter['cpf']}]"
        replacements[key] = m.group(0)
        counter["cpf"] += 1
        return key
    
    # Email  
    def replace_email(m):
        key = f"[EMAIL_{counter['email']}]"
        replacements[key] = m.group(0)
        counter["email"] += 1
        return key
    
    text = re.sub(r'\d{3}\.\d{3}\.\d{3}-\d{2}', replace_cpf, text)
    text = re.sub(r'[\w.-]+@[\w.-]+\.\w+', replace_email, text)
    
    return text, replacements

# Uso
clean_text, pii_map = anonymize_for_llm(user_message)
llm_response = llm.generate(clean_text)

# Nunca logue pii_map: ele contém os dados originais
# Use apenas se precisar reconstituir a resposta com dados reais

IA Local vs IA Cloud

Não é "ou". É saber onde cada uma faz sentido.

Ferramentas locais como Ollama, vLLM e llama.cpp executam modelos em infraestrutura sob seu controle. Podem atender classificação, extração e cargas com restrições de tratamento de dados. Verifique dependências e telemetria em vez de presumir que dados nunca saem.

Plataformas como Vertex AI, AWS Bedrock e Azure OpenAI oferecem acesso gerenciado a modelos sem operar suas próprias GPUs. Compare capacidade, privacidade, latência e custo conforme a tarefa.

Um desenho híbrido pode usar modelo local para roteamento e modelo cloud para solicitações complexas, quando a política de dados permitir.

Abstraia o provedor por uma interface quando precisar de múltiplas clouds. Inversão de Dependência também se aplica aqui.

Python
# Abstração de provider: troca de cloud sem mexer na lógica
from abc import ABC, abstractmethod

class LLMProvider(ABC):
    @abstractmethod
    def generate(self, prompt: str, config: dict) -> str: ...

# Google Cloud
class VertexProvider(LLMProvider):
    def generate(self, prompt: str, config: dict) -> str:
        from vertexai.generative_models import GenerativeModel
        model = GenerativeModel(config.get("model", "gemini-3.5-flash-lite"))
        response = model.generate_content(prompt)
        return response.text

# AWS
class BedrockProvider(LLMProvider):
    def generate(self, prompt: str, config: dict) -> str:
        import boto3
        client = boto3.client("bedrock-runtime")
        # Claude via Bedrock
        response = client.invoke_model(
            modelId=config.get("model", "anthropic.claude-3-haiku"),
            body={"prompt": prompt, "max_tokens": config.get("max_tokens", 600)}
        )
        return response["body"].read().decode()

# Local (Ollama)
class OllamaProvider(LLMProvider):
    def generate(self, prompt: str, config: dict) -> str:
        import requests
        r = requests.post("http://localhost:11434/api/generate", json={
            "model": config.get("model", "llama3.2"),
            "prompt": prompt, "stream": False
        })
        return r.json()["response"]

# Roteamento inteligente
def get_provider(task_type: str) -> LLMProvider:
    if task_type in ("classify", "extract", "moderate"):
        return OllamaProvider()   # execução local; proteja acesso e logs
    if task_type == "generate":
        return VertexProvider()   # cloud, modelo grande
    return BedrockProvider()      # fallback multi-cloud

O critério que não falha: se você não consegue explicar o que o agente faz em uma frase, você não entendeu o suficiente para colocá-lo em produção. Simplicidade aqui não é limitação; é sinal de domínio.

Precisa de ajuda para estruturar uma solução de IA para o seu negócio?

Falar com Cesar Zanis