O agente que o gestor confia não é o que responde mais rápido — é o que entende que "quantos laudos o fulano produziu?" é uma pergunta de múltiplas fontes, não uma query simples.
O analista que estava sempre ocupado
Em qualquer instituição com dados estruturados, existe uma figura familiar: o analista de dados. Ele conhece o banco. Sabe em qual tabela mora cada informação. Sabe escrever o JOIN que ninguém mais sabe. E por isso, toda demanda de dado passa por ele.
A instituição pública de saúde de grande porte com quem trabalhamos não era diferente. A equipe de gestão chegava com perguntas aparentemente simples: "quantos laudos o Dr. Fulano produziu esse mês?", "qual é a média do grupo de cabeça e pescoço?", "como a produção de MRI se compara ao semestre passado?". Perguntas legítimas, perguntas que qualquer gestor tem o direito de responder por conta própria.
O problema não era falta de dados. O dado estava no banco. O problema era que havia um humano no meio do caminho — e esse humano tinha outras prioridades, reuniões, e um limite de horas no dia.
O ciclo era previsível: o gestor mandava a solicitação, o analista colocava na fila, escrevia o SQL na semana seguinte, devolvia uma planilha, o gestor fazia perguntas adicionais, o ciclo recomeçava. O tempo de atendimento era de dias para perguntas simples, semanas para as complexas. E o analista era um gargalo que crescia proporcionalmente com a ambição da gestão.
Este é o quarto especialista do sistema que descrevemos no case do agente conversacional. Quando dissemos, naquele post, que "o agente de Dados merece uma história própria" — esta é essa história.
O dado estava no banco — e mesmo assim era inacessível
A raiz do problema tinha uma camada técnica que qualquer desenvolvedor reconhece, mas que os gestores raramente articulam de forma clara.
Os dados de que precisavam não estavam numa fonte só. Os profissionais — médicos, técnicos, coordenadores — viviam cadastrados num sistema. Os laudos que eles produziam viviam em outro. As escalas de trabalho, em um terceiro. A qualidade dos exames, em um quarto. Para responder "quantos laudos o Dr. Fulano produziu esse mês?", você precisa de pelo menos dois sistemas: um para identificar quem é o Dr. Fulano (seu ID interno) e outro para contar os laudos associados a esse ID.
Isso parece trivial. Não é. Quando você tem mais de trinta fontes de dados com esquemas distintos, a pergunta simples do gestor esconde um problema de integração que o analista resolve de memória — porque passou anos memorizando qual tabela está em qual banco, com qual chave, com qual naming convention. O gestor não sabe que fez uma pergunta "difícil". Para ele, é só uma pergunta.
O trabalho central deste projeto foi justamente esse: mapear todas essas fontes, criar um agente que soubesse navegar por elas de forma coordenada, e entregar ao gestor a experiência de ter um analista sênior disponível via chat.
O problema de acesso a dados em organizações maduras raramente é falta de dado. É falta de uma camada de integração que torne o dado consultável por quem não escreve SQL.
Por que n8n não foi suficiente
O ponto de partida foi, como em outros projetos desta série, o n8n. Workflows que funcionavam para casos simples: uma pergunta previsível, uma query fixa parametrizada, uma resposta. Quando a pergunta era sempre a mesma — ou variava em poucas dimensões — o n8n era suficiente.
O limite apareceu quando as perguntas começaram a mudar de forma. Um analista humano adapta o SQL à pergunta. Um workflow fixo não consegue isso. Para perguntas com estrutura variável — diferente período, diferente critério de agrupamento, diferentes entidades — seria necessário construir um workflow separado para cada variante. A manutenção escalava de forma linear com a diversidade das perguntas. E a diversidade das perguntas era, por definição, ilimitada.
O diagnóstico foi o mesmo do projeto de RAG: não é um problema de prompt. É um problema de arquitetura. Um agente que gera SQL em runtime — adaptado à pergunta, não a um template — era o caminho certo. O n8n virou o PoC que mostrou onde o teto de complexidade estava, e motivou a migração para LangGraph.
A cadeia que imita um cientista de dados
O agente de Dados é um microservice FastAPI independente dentro do sistema multi-agente. Internamente, ele roda um pipeline LangGraph com uma cadeia de cinco etapas que replica, de forma automatizada, o raciocínio que um cientista de dados faria manualmente.
A sequência é esta:
Query Augmenter → SQL Writer → Database Executor → Python Analyzer → Streamer
Cada nó tem uma responsabilidade clara, e a separação entre eles não é decorativa — ela define onde os erros se manifestam e onde as salvaguardas precisam ser colocadas.
O Query Augmenter é o porteiro. Ele recebe a pergunta do gestor e avalia se ela é específica o suficiente para gerar uma query precisa. Uma pergunta fechada — "quantos laudos o Dr. Fulano produziu em maio?" — passa direto para o SQL Writer. Uma pergunta aberta — "como está a produção?" — dispara um conjunto de perguntas de aprofundamento: produção de quem? Em qual período? De qual instituto ou modalidade? Sem esse filtro, o SQL Writer gera queries que retornam "algo" — e "algo" raramente é o que o gestor queria.
O SQL Writer recebe a pergunta augmentada e os DDLs das tabelas relevantes e gera o SQL correspondente. Apenas SELECT — nunca UPDATE, INSERT ou DELETE. Essa é uma salvaguarda não-negociável, implementada tanto no prompt quanto na validação antes da execução.
O Database Executor executa a query no banco real e captura o resultado. Aqui mora um dos loops centrais da arquitetura.
O Python Analyzer recebe o resultado da query e o processa com código Python executado no shell da VPS — não num sandbox de terceiros, não num serviço externo. Python local, com acesso às bibliotecas instaladas no ambiente. Esse nó existe por um motivo específico: o resultado de uma query SQL pode ter centenas ou milhares de linhas, e enviar esse volume diretamente ao LLM para interpretação é um desperdício — de tokens, de janela de contexto, e de qualidade de resposta. O Python Analyzer faz o trabalho analítico primeiro: calcula agregações, identifica outliers, computa derivadas, formata o resultado com contexto interpretativo. O LLM recebe uma análise, não uma tabela crua.
O Streamer monta a resposta final e a entrega ao usuário via SSE, com o mesmo padrão de streaming nó a nó descrito no case do agente conversacional.
O Python Analyzer não é uma otimização de custo. É o que separa "retornar dados" de "entregar análise". Um analista sênior não te manda uma planilha bruta — ele te manda uma planilha com os números que importam destacados.
Como o agente aprende o schema sem alucinar
Um dos problemas clássicos de NL-to-SQL é a alucinação de schema: o LLM gera SQL com referência a colunas ou tabelas que não existem, ou usa nomes ligeiramente errados que quebram a query. Com trinta fontes de dados e centenas de tabelas, esse risco não é trivial.
A abordagem que adotamos — e que recomendamos como padrão para qualquer projeto de agente de dados — é direta: ir nas tabelas relevantes, exportar o DDL do banco, e adicionar esse DDL ao contexto do LLM no momento da geração de SQL. Não ao contexto de todas as chamadas — só quando necessário.
A diferença está na forma de organização. Criamos uma fonte canônica separada: uma pasta contendo os DDLs de todos os sistemas integrados, cada um num arquivo próprio com metadados de contexto (nome do sistema, propósito, chaves de integração com outros sistemas). O agente não carrega esse catálogo completo em cada chamada. Ele identifica, durante o augmento da query, quais sistemas são relevantes para a pergunta — e injeta apenas os DDLs dessas tabelas no contexto do SQL Writer.
# Durante o augmento, o agente identifica as fontes relevantes
relevant_sources = augmenter.identify_sources(query)
# Ex.: ["sistema_laudos", "sistema_profissionais"]
# No SQL Writer, apenas os DDLs dessas fontes são injetados
ddl_context = "\n\n".join([
ddl_catalog.load(source)
for source in relevant_sources
])
A consequência prática: o LLM recebe o schema exato do que precisa usar, sem ruído de centenas de tabelas irrelevantes. A qualidade do SQL gerado aumenta significativamente. E o custo de tokens por chamada permanece controlado — porque injetar o DDL de trinta sistemas em cada query seria um desperdício proporcional.
O schema é contexto. Injetar apenas o contexto necessário para cada pergunta não é otimização prematura — é o que torna possível ter trinta fontes sem explodir a janela de contexto.
O loop de SQL: quando zero resultados não é uma resposta
O SQL Writer gera a query. O Database Executor executa. E às vezes o resultado é zero linhas.
Zero linhas pode significar coisas muito diferentes: pode ser que o dado realmente não existe, pode ser que a query usou um critério de filtro muito restrito, pode ser que o formato de data estava diferente do esperado, pode ser que o JOIN usou a chave errada. Um analista humano veria o zero e tentaria uma abordagem diferente. O agente precisa fazer o mesmo.
O loop funciona assim: se a execução retorna zero resultados, o resultado (incluindo a query gerada) é devolvido ao LLM com uma instrução explícita para tentar uma abordagem diferente. O LLM observa o que foi tentado, raciocina sobre o motivo do zero, e gera uma nova query. O ciclo se repete até obter algum resultado — ou até atingir o limite de quatro iterações.
Quatro é um número deliberado. Não é arbitrário. É o resultado de um incidente em produção que merece ser contado.
O incidente que custou caro e o que ele ensinou
Alguns dias após o deploy em produção, o sistema entrou em loop infinito. Uma sequência de queries estava gerando zero resultados em todas as iterações — e o agente continuava tentando, indefinidamente. A cada iteração, uma chamada ao banco. A cada iteração, uma chamada ao LLM. O custo de API disparou de forma inesperada em poucas horas.
O bug foi resolvido rapidamente. Mas o incidente deixou um aprendizado que vai além deste projeto: qualquer sistema que tem um loop precisa de uma condição de parada explícita, configurável e monitorada. Não como feature de safety — como parte do design.
O limite de quatro iterações no loop de SQL não é o único ponto de parada. O mesmo princípio se aplica ao nó de augmentação: se o gestor responde às perguntas de aprofundamento de forma insatisfatória por mais de uma rodada, o agente encerra e informa que não tem informações suficientes para gerar uma query confiável. O sistema não persiste indefinidamente tentando extrair clareza de uma pergunta irrecuperável.
A terminologia que adotamos internamente é "segurança de stoppage": todo loop no sistema tem um limite explícito, todo agente tem um critério de desistência. É o tipo de coisa que parece óbvia depois do incidente e invisível antes.
O desafio que mais custou tempo: simples versus complexo
O incidente do loop foi dramático mas resolvido em horas. O problema que consumiu mais iterações ao longo de semanas foi mais sutil: ensinar o LLM a distinguir uma pergunta simples de uma pergunta complexa.
Uma pergunta simples tem um caminho direto para o banco: um filtro, uma agregação, uma tabela. Uma pergunta complexa exige múltiplas fontes, JOINs entre sistemas, transformações, contexto adicional. O agente precisa saber qual tratamento dar a cada uma — porque o tratamento errado gera uma experiência ruim de formas opostas.
Quando o agente tratava uma pergunta simples como complexa, ele disparava múltiplas perguntas de aprofundamento — "você quer incluir laudos cancelados?", "qual período exato?", "apenas este instituto ou todos?" — para uma pergunta que o gestor considerava trivial. Experiência irritante. O gestor sentia que o sistema era burocrático.
Quando tratava uma pergunta complexa como simples, ele gerava um SQL que ignorava as nuances — retornava um número incorreto com aparência de resposta correta. Pior ainda: o gestor aceitava o resultado errado sem saber.
A calibração entre esses dois modos foi a parte que mais dependeu de exemplos reais da instituição. Não há atalho aqui: é necessário coletar perguntas reais dos usuários, classificá-las manualmente, e usar esse dataset para ajustar o prompt do Augmenter com exemplos concretos de cada classe. O prompt genérico nunca calibra corretamente — o vocabulário e as convenções de cada organização são muito específicos.
Guardrails: o que o agente não pode fazer
Num sistema de acesso a banco de dados em produção, a lista do que o agente não pode fazer é tão importante quanto o que ele pode.
A restrição mais fundamental é operacional: o agente só gera SELECT. Toda query gerada é validada antes da execução com um parser simples que rejeita qualquer statement que não seja SELECT. O LLM é instruído no prompt a nunca gerar outra coisa — mas a validação no código não depende de instrução de prompt. São camadas independentes.
def validate_sql_safety(query: str) -> bool:
"""
Valida que a query é apenas SELECT.
Não depende apenas do prompt — é validação em código.
"""
normalized = query.strip().upper()
allowed_prefixes = ("SELECT", "WITH")
if not any(normalized.startswith(p) for p in allowed_prefixes):
raise SQLSafetyError(f"Query rejeitada: não é SELECT. Recebido: {normalized[:50]}")
return True
A segunda camada é de profundidade de investigação: o loop de SQL tem limite de quatro iterações. O Augmenter tem limite de rodadas de clarificação. O Python Analyzer tem timeout para execução de código. Cada componente que pode entrar em loop tem seu próprio limitador.
A terceira camada é o escopo de acesso: o agente acessa apenas as tabelas para as quais tem credenciais configuradas. Não há acesso a sistemas fora do escopo configurado, independentemente do que a pergunta do gestor pedir.
Como replicar essa arquitetura
Se você está construindo um agente de dados para uma organização com múltiplas fontes estruturadas, estas são as decisões que mais importam:
1. Comece pelo mapeamento das fontes, não pelo código. Antes de escrever uma linha de agente, documente cada fonte de dados: nome do sistema, propósito, tabelas principais, chaves de integração com outros sistemas, volume de dados. Esse documento vai virar a base do prompt de contexto do SQL Writer — e vai economizar semanas de depuração de queries incorretas.
2. Decida se um índice de DDLs é suficiente ou se você precisa de recuperação semântica. Para sistemas com poucos bancos e esquemas estáveis, um diretório de DDLs com injeção dinâmica por pergunta funciona bem. Para sistemas com dezenas de bancos ou esquemas que mudam frequentemente, considere um banco vetorial separado para recuperação semântica dos DDLs — a busca por similaridade pode identificar as tabelas relevantes mais eficientemente do que regras manuais. A escolha depende da volatilidade e escala do ambiente.
3. Interprete antes de apresentar. Se o resultado do SQL pode ter mais de algumas dezenas de linhas, não passe para o LLM diretamente. Use Python (ou qualquer linguagem que você controla) para agregar, filtrar e formatar o resultado antes da síntese. O LLM recebe análise, não dados brutos. A qualidade da resposta muda radicalmente.
4. Construa o loop de SQL com limite explícito desde o início. Não adicione o limite depois do primeiro incidente de loop. Defina na arquitetura inicial quantas tentativas são razoáveis (três a cinco é um intervalo defensável), implemente o contador, e monitore quantas queries chegam ao limite — é um sinal de que o esquema ou o prompt precisam de ajuste.
5. Calibre simples versus complexo com exemplos reais. A distinção entre pergunta simples e complexa não é universal — ela é específica ao vocabulário e às convenções da organização. Colete 20 a 30 perguntas reais dos usuários antes do deploy, classifique-as manualmente, e use-as como few-shot examples no prompt do Augmenter.
6. Valide SQL no código, não apenas no prompt. O prompt instrui o modelo. A validação em código garante. Para restrições de segurança — apenas SELECT, sem acesso a determinadas tabelas — a implementação no código não depende do comportamento do LLM. As duas camadas são independentes e complementares.
O que faríamos diferente
A execução de Python no shell da VPS foi a decisão mais pragmática e a que tem o maior acúmulo de dívida técnica. O código é executado no mesmo ambiente do serviço, sem sandbox de isolamento. Para o volume e a confiabilidade atuais, funcionou. Para um ambiente com múltiplos usuários simultâneos ou com menor controle sobre as perguntas que chegam, a ausência de isolamento é um risco real. A próxima iteração provavelmente move a execução de Python para um ambiente de sandbox adequado — não necessariamente um serviço externo, mas ao menos um processo isolado com recursos limitados.
A calibração do Augmenter para perguntas simples versus complexas foi o trabalho mais artesanal do projeto. Cada ajuste era um ciclo de teste com usuários reais, coleta de feedback, e revisão de prompt. Não há como escapar desse ciclo — mas ele poderia ser sistematizado mais cedo. Uma sugestão: construa um harness de avaliação automatizado antes do deploy, com um conjunto de perguntas etiquetadas como simples ou complexas. Isso torna cada iteração de prompt mensurável em vez de dependente de impressão qualitativa.
O documento de lógica de negócios — que descreve como a instituição funciona, quais sistemas existem, quais são as convenções de nomenclatura, quais são as chaves de integração entre sistemas — foi sendo construído de forma incremental ao longo do projeto. Ele deveria ser o primeiro artefato, antes de qualquer código. É o documento mais difícil de manter atualizado e o mais crítico para a qualidade das respostas.
O princípio que fecha este ciclo
Este agente é o quarto especialista do sistema que construímos para a instituição. O primeiro foi o RAG corporativo, que serve documentos. O segundo e terceiro foram os especialistas de Laudos e RH. O quarto — este — serve operação: dados estruturados, múltiplas fontes, perguntas que nenhum dashboard antecipa.
O que conecta os quatro não é a tecnologia. É a convicção de que o trabalho começa pela observação do processo como ele é, não como queremos que seja. No caso do RAG, a observação revelou que os títulos dos documentos já eram o índice. No caso dos dados, a observação revelou que o gargalo real não era ausência de SQL — era ausência de integração entre fontes, e ausência de uma camada que fizesse o trabalho analítico que até então só humanos sabiam fazer.
A camada de integração e indexação das fontes de dados é o trabalho mais crítico e mais subestimado em qualquer projeto de agente de dados. Sem ela, o agente é rápido em gerar SQL incorreto.
A redução do tempo de atendimento de semanas para segundos não é o resultado de um modelo melhor. É o resultado de entender onde o tempo estava sendo gasto, por quê, e construir a camada que eliminava o gargalo real — que era humano, não técnico.
Qual a diferença entre este agente de dados e o agente de RAG corporativo?
Os dois agentes atendem à mesma instituição e fazem parte do mesmo sistema, mas servem tipos fundamentalmente diferentes de conhecimento.
O RAG corporativo serve conhecimento documental: POPs, políticas, protocolos, manuais. O usuário pergunta sobre o que um documento diz, quando ele vence, quem é responsável. O conteúdo vive em PDFs num Drive. A busca é sobre texto não-estruturado com metadados semi-estruturados.
O agente de Dados serve conhecimento operacional: laudos produzidos, escalas de trabalho, volumes por período, comparações entre grupos. O conteúdo vive em bancos de dados relacionais com schema definido. A busca é sobre dados estruturados que precisam ser consultados com SQL.
Os dois são complementares. Um pergunta "o que diz o protocolo?". O outro pergunta "quantos exames foram realizados de acordo com esse protocolo?". Juntos cobrem o conhecimento institucional de formas que cada um isolado não consegue.
Por que o agente executa Python local em vez de usar um serviço de sandbox externo?
A decisão foi pragmática e tem tradeoffs claros. Python executando no shell da própria VPS é a opção mais simples de implementar, sem dependência de serviços externos, sem latência de rede adicional, e sem custo de API de terceiros.
O custo é a ausência de isolamento: o código Python roda no mesmo processo e ambiente do serviço. Para o volume e o perfil de usuários controlados deste projeto, o risco foi avaliado como aceitável. Para ambientes com usuários menos controlados ou com maior escala, a ausência de sandbox seria um risco técnico real.
A próxima iteração consideraria mover a execução para um processo isolado com recursos limitados — não necessariamente um serviço externo, mas com isolamento de processo adequado. A decisão atual é uma dívida técnica consciente, não uma omissão.
Como o agente lida com perguntas que cruzam múltiplos sistemas?
Este é o coração do problema de integração de múltiplas fontes. O agente de Dados mantém um catálogo de DDLs de todos os sistemas integrados, com metadados sobre as chaves de junção entre eles.
Durante o augmento da pergunta, o agente identifica quais sistemas são necessários para respondê-la. Para uma pergunta como "quantos laudos o Dr. Fulano produziu?", o Augmenter identifica que são necessários dois sistemas: o de profissionais (para mapear o nome ao ID interno) e o de laudos (para contar os registros associados a esse ID). Os DDLs de ambos são injetados no contexto do SQL Writer, que então gera um JOIN entre as tabelas relevantes.
A qualidade desse processo depende diretamente da qualidade do documento de lógica de negócios — que descreve quais são as chaves de integração entre sistemas, quais são as convenções de nomenclatura, e quais são os casos onde a integração é mais frágil.
O que acontece quando o agente gera uma query que retorna zero resultados?
O agente entra num loop de refinamento com limite explícito de quatro iterações. Em cada iteração, o SQL gerado e o resultado zero são devolvidos ao LLM com instrução para tentar uma abordagem diferente — filtro menos restritivo, critério de data diferente, JOIN alternativo.
Se após quatro iterações o resultado ainda for zero, o agente encerra o loop e devolve ao usuário uma mensagem explicando que não encontrou dados correspondentes à pergunta, junto com as queries que foram tentadas. Isso permite ao usuário reformular a pergunta com mais contexto.
O limite de quatro iterações nasceu de um incidente em produção: sem esse limite, uma sequência de queries com zero resultados causou um loop infinito que disparou um volume inesperado de chamadas ao banco e ao LLM. A "segurança de stoppage" — limitadores explícitos em todos os loops do sistema — é agora uma convenção de design, não um patch.
Como o agente distingue uma pergunta simples de uma pergunta complexa?
O Query Augmenter classifica cada pergunta antes de processá-la. Perguntas simples têm um caminho direto: um critério de filtro, uma agregação, uma ou duas tabelas. Perguntas complexas envolvem múltiplos sistemas, critérios compostos, ou análises que requerem transformações sobre o resultado.
Para perguntas simples, o Augmenter verifica se os parâmetros essenciais estão presentes (período, entidade de referência, critério de agrupamento) e passa a query augmentada direto para o SQL Writer sem fazer perguntas adicionais.
Para perguntas complexas ou ambíguas, o Augmenter devolve um conjunto de perguntas de aprofundamento ao usuário antes de acionar qualquer nó costoso. Esse comportamento é calibrado com exemplos reais da instituição — o vocabulário e as convenções de cada organização determinam o que é "simples" ou "complexo" naquele contexto específico. Um prompt genérico nunca calibra corretamente esse limiar.
