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.