Agents com Raciocínio Local usando DuckDB: Como Criar Agentes de IA Rápidos, Confiáveis e Conectados a Dados Reais

Published on: 2026-01-27
Post image
pt agents-com-duckdb duckdb-ai-agents agentes-de-ia-com-sql raciocinio-local-em-ia local-reasoning-ai duckdb-analytics ai-agents-com-dados-reais agentes-de-ia-com-banco-local duckdb-parquet-csv-json arquitetura-de-agentes-de-ia llm-com-sql agentes

Agentes de IA costumam soar convincentes mesmo quando não consultam dado algum. Esse comportamento cria respostas “bonitas”, porém desconectadas da realidade operacional, especialmente em perguntas sobre métricas, tendências e causas, como churn, receita e suporte.

Uma forma prática de tornar esses agentes mais úteis é oferecer um mecanismo de “raciocínio local”: o modelo planeja a análise, mas quem valida é um banco analítico executando consultas de verdade. Nesse cenário, o DuckDB funciona como um motor rápido e embutido que consulta arquivos reais (como Parquet, CSV e JSON) com SQL, reduzindo latência e aumentando a auditabilidade.

O que significa “agente que pensa localmente”

Um “agente que pensa localmente” é um sistema em que a parte conversacional não depende apenas de memória ou de generalizações. O modelo de linguagem atua como planejador e narrador, enquanto um componente determinístico consulta dados e devolve evidências. O termo “local” costuma significar “no mesmo processo ou serviço”, sem precisar de um data warehouse remoto para cada pergunta. Essa abordagem reduz incerteza porque o texto final nasce de resultados observáveis e repetíveis.

Esse tipo de agente costuma ser composto por duas camadas bem separadas. A primeira camada interpreta a pergunta e gera uma intenção analítica, como “contar”, “segmentar” ou “comparar períodos”. A segunda camada executa consultas e retorna tabelas pequenas e limitadas, para evitar respostas gigantes e caras. Por fim, o agente transforma esses números em uma explicação, deixando claro o que foi medido e com quais limites.

Por que o DuckDB se encaixa tão bem nesse papel

O DuckDB é um banco analítico in-process, ou seja, roda dentro do próprio programa em vez de depender de um servidor separado. Ele é especialmente eficiente para leituras e agregações, como somas, médias, agrupamentos, junções e window functions (funções de janela que calculam métricas considerando “partições” e “ordens” dentro do conjunto). Isso permite fazer análises típicas de BI diretamente sobre arquivos, muitas vezes em segundos. Além disso, por estar embutido, a implantação tende a ser mais simples do que manter infraestrutura de banco analítico remoto apenas para perguntas pontuais.

Outro ponto importante é o controle. Um agente com DuckDB pode registrar a consulta executada, o tempo gasto e até métricas de qualidade, o que facilita depuração. Em vez de “acreditar” que a resposta está correta, torna-se possível verificar o SQL e repetir a execução. Em termos práticos, DuckDB vira um “caderno de cálculo” confiável dentro do serviço, enquanto o modelo descreve o que foi encontrado com linguagem clara.

Arquitetura essencial: modelo planeja, DuckDB prova, agente explica

Uma arquitetura simples e robusta separa o fluxo em etapas pequenas e previsíveis. Primeiro, a pergunta vira um plano: quais dados usar, quais filtros aplicar e quais métricas calcular. Depois, o DuckDB executa a consulta e devolve linhas limitadas e o esquema (nomes e tipos de colunas) para evitar ambiguidades. Por fim, o modelo resume os resultados e explicita suposições e limitações, como período disponível e chaves de junção ausentes.

Esse desenho reduz o “teatro” de inteligência, porque a parte numérica fica a cargo do banco. Também diminui a chance de alucinações, como colunas inventadas, já que o agente pode consultar o esquema antes de escrever o SQL final. A previsibilidade aumenta quando cada etapa tem limites claros: tempo máximo de consulta, número máximo de linhas e lista controlada de tabelas. O resultado é um sistema que erra de forma rastreável e melhora com ajustes objetivos.

Guardrails no “tool” de SQL: segurança antes de poder

Expor execução de SQL para um agente sem proteção tende a causar problemas: consultas pesadas, vazamento de dados e até tentativas de escrita. Por isso, o “tool” (ferramenta) que executa SQL precisa impor regras simples e rígidas. As regras mais comuns incluem modo somente leitura, proibição de múltiplas instruções, limite de linhas retornadas e tempo máximo de execução. Também é comum restringir o acesso a um conjunto conhecido de tabelas e visões, evitando que o agente “ache” caminhos inesperados.

Além das proteções, é útil adicionar ganchos de explicabilidade. O termo explicabilidade aqui significa registrar o que aconteceu: SQL final, parâmetros, duração e contagem de linhas. Esse registro torna a análise auditável e permite reproduzir a resposta sem depender do texto gerado. Na prática, o DuckDB vira a fonte de verdade, e o agente passa a ser uma camada de interpretação controlada.

Modelo de dados local: arquivos brutos, visões estáveis e “contrato” de dataset

Um agente costuma funcionar melhor quando existe um “contrato” de nomes estáveis. Em vez de apontar o agente diretamente para caminhos de arquivo que mudam, cria-se visões com nomes fixos, como events, orders, customers e tickets. Uma view (visão) é uma consulta salva que se comporta como tabela, escondendo detalhes como localização do arquivo e filtros básicos. Assim, mesmo que o arquivo mude de pasta ou o formato seja atualizado, o agente continua consultando o mesmo nome lógico.

Esse padrão também ajuda a documentar o que existe e o que não existe. Se “refunds do provedor B” não estão no dataset local, essa ausência pode ser registrada de forma explícita, evitando inferências erradas. Em análises de churn, por exemplo, a definição do evento de churn (cancelamento, inatividade, expiração) precisa estar clara e refletida nas visões. A consistência semântica reduz divergências e melhora a confiança no resultado.

Ferramentas auxiliares: descoberta de esquema e amostragem segura

Grande parte dos erros em agentes analíticos vem de detalhes simples, como nomes de colunas e tipos de dados. Para reduzir isso, além da execução de SQL, é útil oferecer ferramentas específicas de descoberta. Um conjunto pequeno de funções para listar tabelas, descrever colunas e trazer amostras reduz alucinações e acelera a criação de consultas corretas. A amostragem também ajuda a identificar valores nulos e padrões de chaves de junção antes de executar agregações mais caras.

Essas ferramentas devem ser tão protegidas quanto a execução de SQL. Limites de linhas e filtros obrigatórios evitam que dados sensíveis sejam retornados em massa. Também é recomendável padronizar a resposta dessas ferramentas em JSON, pois fica mais fácil para o agente interpretar e para o sistema registrar. Com isso, o agente passa a “checar o terreno” antes de afirmar qualquer coisa.

Implementação profissional em Python: tool de DuckDB com limites e auditoria

O exemplo abaixo mostra um “tool” que executa SQL com guardrails (barreiras de segurança) e retorna resultados limitados. Ele inclui: bloqueio de escrita, bloqueio de múltiplas instruções, imposição de LIMIT, tempo máximo e coleta de métricas básicas. Também inclui funções de descoberta de esquema, úteis para evitar colunas inventadas. Esse tipo de implementação é adequado para serviços backend que recebem chamadas de um orquestrador de agente.

import duckdb
import json
import time
from typing import Any, Dict, List, Optional, Tuple

MAX_LINHAS = 200
TIMEOUT_MS = 3000

PALAVRAS_PROIBIDAS = [
    "insert ", "update ", "delete ", "create ", "drop ", "alter ", "copy ",
    "grant ", "revoke ", "truncate ", "attach ", "detach "
]

class ErroConsultaSegura(ValueError):
    pass

def _validar_sql_apenas_leitura(sql: str) -> None:
    sql_limpo = sql.strip()
    if not sql_limpo:
        raise ErroConsultaSegura("SQL vazio não é permitido.")

    # Impede múltiplas instruções; aceita um ponto e vírgula apenas no final.
    sql_sem_final = sql_limpo.rstrip().rstrip(";")
    if ";" in sql_sem_final:
        raise ErroConsultaSegura("Apenas uma instrução SQL é permitida (sem múltiplos comandos).")

    sql_minusculo = sql_limpo.lower()
    if any(p in sql_minusculo for p in PALAVRAS_PROIBIDAS):
        raise ErroConsultaSegura("Operações de escrita/DDL não são permitidas neste tool.")

def _garantir_limit(sql: str, max_linhas: int) -> str:
    sql_minusculo = sql.lower()
    if " limit " in f" {sql_minusculo} ":
        return sql
    return f"{sql.rstrip()}\nLIMIT {max_linhas}"

def _abrir_conexao(caminho_db: str = ":memory:") -> duckdb.DuckDBPyConnection:
    con = duckdb.connect(database=caminho_db, read_only=False)
    con.execute(f"PRAGMA threads=4")
    con.execute(f"PRAGMA enable_progress_bar=false")
    con.execute(f"PRAGMA memory_limit='1GB'")
    con.execute(f"PRAGMA busy_timeout={TIMEOUT_MS}")
    return con

def preparar_dataset_exemplo(con: duckdb.DuckDBPyConnection) -> None:
    # Exemplo de visões; em uso real, apontam para arquivos locais (Parquet/CSV/JSON).
    # Ajustar caminhos conforme o ambiente.
    con.execute("""
        CREATE OR REPLACE VIEW orders AS
        SELECT * FROM read_parquet('data/orders.parquet')
    """)
    con.execute("""
        CREATE OR REPLACE VIEW customers AS
        SELECT * FROM read_parquet('data/customers.parquet')
    """)
    con.execute("""
        CREATE OR REPLACE VIEW events AS
        SELECT * FROM read_parquet('data/events.parquet')
    """)

def listar_tabelas(con: duckdb.DuckDBPyConnection) -> List[str]:
    linhas = con.execute("SHOW TABLES").fetchall()
    return [l[0] for l in linhas]

def descrever_tabela(con: duckdb.DuckDBPyConnection, nome: str) -> List[Dict[str, Any]]:
    _validar_sql_apenas_leitura(f"SELECT * FROM {nome} LIMIT 1")
    info = con.execute(f"DESCRIBE {nome}").fetchall()
    # DESCRIBE retorna: column_name, column_type, null, key, default, extra
    colunas = []
    for linha in info:
        colunas.append({
            "coluna": linha[0],
            "tipo": linha[1],
            "aceita_nulo": linha[2],
            "chave": linha[3],
            "padrao": linha[4],
            "extra": linha[5],
        })
    return colunas

def amostrar_linhas(con: duckdb.DuckDBPyConnection, nome: str, limite: int = 5) -> List[Dict[str, Any]]:
    limite = max(1, min(limite, 20))
    sql = f"SELECT * FROM {nome} LIMIT {limite}"
    _validar_sql_apenas_leitura(sql)
    rel = con.execute(sql)
    colunas = [d[0] for d in rel.description]
    linhas = rel.fetchall()
    return [dict(zip(colunas, l)) for l in linhas]

def duckdb_query(sql: str, params: Optional[Dict[str, Any]] = None, max_linhas: int = MAX_LINHAS) -> Dict[str, Any]:
    params = params or {}
    _validar_sql_apenas_leitura(sql)
    sql = _garantir_limit(sql, max_linhas)

    con = _abrir_conexao()
    try:
        # Em produção, visões seriam preparadas uma vez e a conexão poderia ser reutilizada com cuidado.
        preparar_dataset_exemplo(con)

        inicio = time.time()
        rel = con.execute(sql, params)
        colunas = [d[0] for d in rel.description]

        linhas = rel.fetchmany(max_linhas)
        duracao_ms = int((time.time() - inicio) * 1000)

        return {
            "sql_executado": sql,
            "duracao_ms": duracao_ms,
            "colunas": colunas,
            "linhas_retorno": len(linhas),
            "linhas": [dict(zip(colunas, l)) for l in linhas],
        }
    finally:
        con.close()

if __name__ == "__main__":
    resultado = duckdb_query("""
        SELECT
            customer_id,
            COUNT(*) AS total_pedidos
        FROM orders
        GROUP BY customer_id
        ORDER BY total_pedidos DESC
    """)
    print(json.dumps(resultado, ensure_ascii=False, indent=2))

Como era sem esse padrão e como fica com o loop de evidências

Sem um loop de evidências, o modelo recebe uma pergunta e produz uma narrativa com base em padrões prováveis. Mesmo quando a linguagem parece correta, não existe garantia de que as métricas correspondem a um período, uma segmentação ou uma definição específica. Isso gera respostas difíceis de auditar, porque não há consulta, não há versão do dado e não há trilha de execução. Quando algo dá errado, a correção vira tentativa e erro em texto.

Com DuckDB no centro, o fluxo muda para “planejar, medir, explicar”. O agente descreve o que será medido, executa SQL limitado e então resume os resultados. Se a resposta estiver estranha, a inspeção do SQL mostra rapidamente se faltou filtro de data, se houve duplicidade por junção ou se a definição de churn foi mal aplicada. A melhoria vira um ajuste no dado ou no SQL, em vez de um ajuste vago de “prompt”.

Padrões de consulta para perguntas comuns: churn, coortes e segmentação

Perguntas como “o churn aumentou após uma mudança de preço?” exigem cuidado com definições. Churn pode significar cancelamento explícito, expiração sem renovação ou inatividade acima de um limite. Coorte é um agrupamento por um marco de tempo, como semana de ativação, para comparar comportamentos entre grupos semelhantes. Segmentação é dividir o conjunto por atributos como plano, região ou canal, para identificar onde o efeito é maior.

O SQL abaixo ilustra um caminho típico: construir uma base de clientes com atributos, mapear eventos relevantes e calcular uma taxa por período e segmento. Em bases reais, parte dessa lógica fica encapsulada em visões para reduzir repetição e risco de inconsistência. A ideia é manter consultas curtas, com resultados pequenos e rastreáveis. Em casos complexos, o agente pode executar duas ou três consultas sequenciais, registrando cada uma.

WITH base_clientes AS (
  SELECT
    c.customer_id,
    c.plano,
    c.regiao,
    DATE_TRUNC('week', c.data_ativacao) AS semana_ativacao
  FROM customers c
),
cancelamentos AS (
  SELECT
    e.customer_id,
    MIN(CAST(e.event_time AS DATE)) AS data_cancelamento
  FROM events e
  WHERE e.event_name = 'subscription_canceled'
  GROUP BY e.customer_id
),
janela AS (
  SELECT
    b.plano,
    b.regiao,
    b.semana_ativacao,
    COUNT(*) AS clientes_na_coorte,
    SUM(CASE WHEN ca.data_cancelamento BETWEEN DATE '2025-01-01' AND DATE '2025-01-31' THEN 1 ELSE 0 END) AS churn_jan
  FROM base_clientes b
  LEFT JOIN cancelamentos ca
    ON ca.customer_id = b.customer_id
  GROUP BY 1,2,3
)
SELECT
  plano,
  regiao,
  semana_ativacao,
  clientes_na_coorte,
  churn_jan,
  ROUND(100.0 * churn_jan / NULLIF(clientes_na_coorte, 0), 2) AS taxa_churn_pct
FROM janela
ORDER BY taxa_churn_pct DESC
LIMIT 200

Híbrido: busca semântica com embeddings + SQL para correlação

Algumas perguntas não são bem definidas em termos de filtros exatos, como “reclamações que parecem confusão com reembolso”. Nesses casos, embeddings ajudam a encontrar textos semanticamente parecidos, porque transformam frases em vetores numéricos comparáveis por distância. A busca semântica encontra um subconjunto promissor de registros, mas ainda não entrega métricas confiáveis. O papel do SQL é agregar, correlacionar e checar impactos, como taxa de churn e volume por segmento.

Uma estratégia comum é armazenar a tabela de tickets com uma coluna de embedding e, após recuperar os IDs mais similares, usar DuckDB para cruzar com clientes e eventos. Em ambientes simples, a busca semântica pode ocorrer fora do DuckDB, enquanto o DuckDB faz as junções e contagens. O ponto central é manter a evidência numérica no SQL e a “interpretação do texto” na etapa semântica. Isso evita que um critério subjetivo vire métrica sem validação.

Cache e materialização: desempenho previsível em perguntas repetidas

Consultas analíticas podem repetir muito trabalho, como ler arquivos grandes e recomputar agregações. Cache é guardar resultados intermediários para reaproveitar, reduzindo tempo total em interações frequentes. No DuckDB, uma abordagem direta é usar um arquivo .duckdb local para persistir tabelas materializadas em vez de recalcular tudo em memória. Materialized view é a ideia de armazenar o resultado de uma consulta como uma tabela para acelerar leituras futuras.

Esse padrão funciona bem quando há “perguntas parecidas” ao longo do dia, como métricas por semana e por plano. Também ajuda quando o agente faz uma primeira consulta ampla e depois várias variações pequenas com filtros. A materialização precisa ser controlada para não crescer sem limite, usando políticas simples de expiração por data. O objetivo é reduzir variabilidade, evitando que uma resposta leve vire uma execução pesada em horário crítico.

Notas de confiança: incerteza tratada como dado e não como vergonha

Em análises reais, a maior maturidade está em reconhecer limites mensuráveis. Uma “nota de confiança” pode ser gerada com base em sinais como cobertura de junção, fração de nulos em chaves e volume de linhas no período. Por exemplo, se apenas 70% dos eventos possuem um customer_id válido, a taxa calculada pode estar subestimada. Se a chave de junção entre billing e customers não casa bem, parte da receita pode não ser atribuída a segmentos. Esses avisos não impedem o resultado, mas contextualizam a confiabilidade.

O sistema pode calcular esses indicadores em consultas curtas e incluir junto com a resposta. Isso mantém a honestidade operacional e reduz decisões baseadas em números frágeis. Também torna mais simples identificar se o problema está em qualidade de dados ou em lógica de consulta. Em vez de um agente “confiante”, surge um agente “rastreável”.

Encerramento: um agente útil é um agente que mostra trabalho

Um agente analítico confiável não depende de truques de texto para parecer inteligente. O valor aparece quando o modelo planeja consultas, o DuckDB executa sobre dados reais e o resultado é explicado com limites claros. Esse ciclo cria velocidade, porque as respostas saem rápido, e cria segurança, porque cada número tem origem verificável. O raciocínio local, nesse formato, deixa de ser uma promessa e vira um mecanismo concreto de evidência.

O DuckDB se encaixa bem por ser embutido, rápido e amigável a arquivos comuns, permitindo que o “pensamento” sobre dados aconteça dentro do próprio serviço. Com guardrails, descoberta de esquema, limites e registro de SQL, o sistema se mantém previsível mesmo sob perguntas difíceis. O fim desse caminho é um agente que não adivinha: consulta, confirma e explica com rastreabilidade. Assim, a confiança deixa de ser um sentimento e passa a ser um atributo observável do processo.