mozak.tech Engenharia de IA Corporativa 21%

Parte I — Conectividade Padronizada entre Sistemas e Modelos

1.8 — BI Copilot: Análise de Dados com MCP como Camada de Acesso (MCP BI Copilot)

Objetivo da Aula

Ao concluir esta aula, você será capaz de:

Implementar um BI Copilot usando MCP tools que respondem queries de negócio em linguagem natural

Calcular métricas de negócio (MRR, churn rate, LTV, CAC, ROI) diretamente no MCP server

Estruturar dados de vendas e campanhas para análise por LLM

Construir um agente que encadeia múltiplas tool calls para responder perguntas compostas

Comparar ROI entre canais de marketing e gerar recomendações acionáveis

Por que isso importa

Copilots para ERPs e sistemas de BI são o caso de uso mais solicitado em empresas hoje. A premissa é simples: CEO pergunta “como estamos em Q2?” e o sistema busca os dados, calcula as métricas e responde com contexto — sem BI analyst no loop.

O padrão BI Copilot com MCP é o caminho mais direto para isso: - Dados ficam no server MCP: sem expor schema de banco de dados para o LLM - Métricas são calculadas no server: o LLM recebe o resultado, não os dados brutos - Tools são bem nomeadas: o LLM sabe quando usar calcular_metrica('churn_rate') vs consultar_vendas() - System prompt define o persona: “analista de BI” vs “assistente geral”

O starter desta unidade é um exemplo completo e funcional. Sua missão é entendê-lo profundamente e estendê-lo.

Conceitos Fundamentais

Métricas de Negócio que Todo Dev de IA Precisa Conhecer

Para construir copilots de BI úteis, você precisa entender as métricas que o negócio usa. As 5 do starter são as mais comuns em SaaS:

Fundamento: Métricas SaaS para Justificar IA a Gestores

Ao construir copilots de BI ou justificar investimento em IA para gestores, você precisa falar o idioma deles. MRR (receita recorrente mensal) e ARR (anual) medem o negócio de assinatura. Churn é a taxa de cancelamento — 5% ao mês significa que metade da base some em um ano. LTV (lifetime value) é quanto um cliente vale no total; CAC (custo de aquisição) é o que você gastou para adquiri-lo. A régua de saúde: LTV/CAC > 3 e payback do CAC em menos de 12 meses. No contexto de IA: esses indicadores ajudam a calcular o ROI de features de IA (quanto LTV aumenta, quanto churn reduz) e a justificar o custo de compute para gestores que pensam em OPEX, não em latência.

MRR (Monthly Recurring Revenue)

MRR = receita_total / número_de_meses

Receita recorrente mensal. Para o starter: soma de todas as receitas dividida por 6 meses.

Churn Rate

churn_rate = total_churned / total_novos_clientes × 100

Percentual de clientes que cancelaram. Saudável em SaaS B2B: < 2%/mês. Acima de 5%: alarme vermelho.

LTV (Customer Lifetime Value)

LTV = (receita_total / total_clientes) × 12

Valor médio que um cliente gera em 12 meses. Simplificado — em produção considera churn implícito.

CAC (Customer Acquisition Cost)

CAC = total_gasto_marketing / total_clientes_novos

Quanto custa adquirir um cliente. Regra de ouro: LTV/CAC > 3 é saudável.

ROI de Marketing

ROI = (receita_gerada - gasto) / gasto × 100

Retorno sobre investimento em marketing. Calculado por canal para comparação.

Estrutura de Dados de BI

O starter usa duas coleções:

// Série temporal de vendas (por mês)

const vendas = [

  { mes: 'jan', receita: 120000, clientes_novos: 45, churn: 3, produto: 'Pro' },

  // ...

]



// Performance por canal de marketing

const campanhas = [

  { nome: 'Google Ads', gasto: 15000, leads: 320, conversoes: 45, receita_gerada: 63000 },

  // ...

]

Essa separação é deliberada: cada tool acessa uma coleção diferente. O LLM aprende a usá-las baseado nos nomes e descriptions: - consultar_vendas → acessa vendas - analisar_campanhas → acessa campanhas + calcula ROI - calcular_metrica → faz cálculo cross-collection

Filtragem por Intervalo de Meses

if (name === 'consultar_vendas') {

  const { mes_inicio, mes_fim } = args

  const meses = ['jan', 'fev', 'mar', 'abr', 'mai', 'jun']

  let dados = vendas

  if (mes_inicio) {

    const idx = meses.indexOf(mes_inicio)

    if (idx >= 0) dados = dados.slice(idx)

  }

  if (mes_fim) {

    const idx = meses.indexOf(mes_fim)

    if (idx >= 0) dados = dados.slice(0, idx + 1)

  }

  return { content: [{ type: 'text', text: JSON.stringify(dados) }] }

}

O LLM pode pedir “de março a junho” e a tool filtra o array. O array de meses como chave de índice é um truque simples mas eficaz para dados de série temporal.

Cálculo de ROI no Server (não no LLM)

const comROI = campanhas.map(c => ({

  ...c,

  roi: ((c.receita_gerada - c.gasto) / c.gasto * 100).toFixed(1) + '%',

  cpl: (c.gasto / c.leads).toFixed(2),

  taxa_conversao: (c.conversoes / c.leads * 100).toFixed(1) + '%',

}))
Decisão de Arquitetura: Determinístico no Código, LLM Só Interpreta

A heurística de particionamento de responsabilidades para sistemas com LLM: se a operação é determinística (cálculo numérico, agregação, regra de negócio com resultado único), ela vai no código — não no LLM. Se a operação é linguística, ambígua, ou requer interpretação de contexto, o LLM é o lugar certo. Os motivos são pragmáticos: (1) LLMs erram cálculos numéricos com frequência suficiente para ser problema em produção; (2) código determinístico é testável e auditável; (3) processar dados no servidor MCP antes de enviar ao modelo reduz tokens de contexto e custo. Corolário: nunca exponha o schema bruto do banco ao modelo — calcule as métricas que ele precisa no server e entregue dados já processados.

Decisão arquitetural importante: calcular ROI no server MCP, não deixar para o LLM. Razões: 1. Precisão: LLMs às vezes erram cálculos numéricos 2. Economia de tokens: LLM recebe resultado pronto, não dados brutos para calcular 3. Consistência: a fórmula fica num lugar só, não reimplementada pelo LLM a cada query

Aprofundamento Técnico

O Loop de Agente BI

async function consultarBI(pergunta: string): Promise<void> {

  const [ct, st] = InMemoryTransport.createLinkedPair()

  const server = criarBIServer()

  await server.connect(st)

  const client = new Client({ name: 'bi-copilot', version: '1.0.0' }, { capabilities: {} })

  await client.connect(ct)



  const { tools } = await client.listTools()

  const toolsAnthropic = tools.map(t => ({

    name: t.name, description: t.description ?? '',

    input_schema: t.inputSchema as Anthropic.Tool['input_schema'],

  }))



  const mensagens: Anthropic.MessageParam[] = [{ role: 'user', content: pergunta }]



  while (true) {

    const response = await anthropic.messages.create({

      model: 'claude-haiku-4-5-20251001',

      max_tokens: 500,

      system: 'Você é um analista de BI. Responda perguntas de negócio usando os dados disponíveis. Apresente resultados de forma clara com contexto.',

      tools: toolsAnthropic,

      messages: mensagens,

    })

    mensagens.push({ role: 'assistant', content: response.content })



    if (response.stop_reason === 'end_turn') {

      const t = response.content.find(b => b.type === 'text')

      if (t?.type === 'text') console.log(`R: ${t.text}`)

      break

    }



    const toolResults: Anthropic.ToolResultBlockParam[] = []

    for (const block of response.content) {

      if (block.type !== 'tool_use') continue

      const result = await client.callTool({ name: block.name, arguments: block.input as Record<string, unknown> })

      toolResults.push({ type: 'tool_result', tool_use_id: block.id, content: (result.content[0] as { text: string }).text })

    }

    mensagens.push({ role: 'user', content: toolResults })

  }



  await client.close()

}

O pattern aqui é o mesmo agent loop de antes, com uma diferença: await client.close() ao final. Em produção, você reutilizaria o client entre queries para não pagar o custo de setup a cada pergunta.

Queries Compostas e Encadeamento de Tools

Para a query “Qual campanha tem melhor ROI? Onde devo aumentar investimento?”, o LLM tipicamente:

Chama analisar_campanhas para obter ROI por canal

Analisa os dados recebidos

Responde com recomendação baseada nos números

Para “Qual nosso churn rate e LTV?”, o LLM: 1. Chama calcular_metrica('churn_rate') 2. Chama calcular_metrica('ltv') (em paralelo, Claude suporta tool calls paralelas) 3. Integra os dois resultados na resposta

O system prompt “Apresente resultados de forma clara com contexto” instrui o LLM a não só retornar números, mas contextualizá-los (“Churn de 3,6% está acima do ideal de 2% para SaaS B2B”).

System Prompt do Analista de BI

system: 'Você é um analista de BI. Responda perguntas de negócio usando os dados disponíveis. Apresente resultados de forma clara com contexto.'

Este system prompt simples tem 3 efeitos: 1. Persona: “analista de BI” → o LLM usa linguagem de negócios, não técnica 2. Instrução de uso de tools: “usando os dados disponíveis” → o LLM busca antes de responder 3. Formato: “de forma clara com contexto” → rejeita respostas apenas numéricas

Para produção, enriqueca:

Você é um analista de BI sênior. Use os dados disponíveis para responder perguntas.

Sempre apresente: (1) os números exatos, (2) comparação com período anterior quando relevante,

(3) sua interpretação do que os números significam, (4) uma recomendação acionável.

Se os dados não forem suficientes para responder, diga explicitamente o que falta.

Exemplos Anotados

Exemplo 1: Calculando ROI Real por Canal

// Server-side: calcular todas as métricas relevantes, não só ROI

const analisarCampanhas = () => {

  return campanhas.map(c => ({

    nome: c.nome,

    investimento: `R$${c.gasto.toLocaleString('pt-BR')}`,

    leads: c.leads,

    conversoes: c.conversoes,

    receita: `R$${c.receita_gerada.toLocaleString('pt-BR')}`,

    // Métricas calculadas no server — não deixar o LLM fazer as contas

    roi: ((c.receita_gerada - c.gasto) / c.gasto * 100).toFixed(1) + '%',

    cpl: `R$${(c.gasto / c.leads).toFixed(2)}`,  // Custo por Lead

    cpa: `R$${(c.gasto / c.conversoes).toFixed(2)}`,  // Custo por Aquisição

    taxa_conversao: (c.conversoes / c.leads * 100).toFixed(1) + '%',

    // Ranking implícito: o LLM vai comparar naturalmente

  }))

}



// Resultado para o LLM (exemplo):

// [

//   { nome: 'Indicações', roi: '4900.0%', cpa: 'R$27.78', taxa_conversao: '30.0%' },

//   { nome: 'Email Marketing', roi: '2600.0%', cpa: 'R$52.63', taxa_conversao: '8.4%' },

//   { nome: 'LinkedIn', roi: '287.5%', cpa: 'R$363.64', taxa_conversao: '23.2%' },

//   { nome: 'Google Ads', roi: '320.0%', cpa: 'R$333.33', taxa_conversao: '14.1%' },

// ]

// LLM verá que Indicações tem ROI 15x maior que LinkedIn e fará recomendação clara

Exemplo 2: Query Composta — Saúde Financeira

// Para responder "Estamos saudáveis financeiramente?", o LLM encadeia:



// Tool call 1:

await client.callTool({ name: 'calcular_metrica', arguments: { metrica: 'churn_rate' } })

// Retorna: "Churn rate médio: 4.0%/mês"



// Tool call 2:

await client.callTool({ name: 'calcular_metrica', arguments: { metrica: 'ltv' } })

// Retorna: "LTV estimado: R$30965 (baseado em 12 meses)"



// Tool call 3:

await client.callTool({ name: 'calcular_metrica', arguments: { metrica: 'cac' } })

// Retorna: "CAC médio: R$44"



// O LLM então calcula mentalmente LTV/CAC = 30965/44 ≈ 703 (excelente)

// e responde com análise completa, incluindo preocupação com churn de 4%

Exemplo 3: Extensão — Tool de Previsão Simples

// Adicionar ao server MCP:

{

  name: 'prever_receita',

  description: 'Prevê receita para os próximos N meses usando tendência linear dos últimos dados',

  inputSchema: {

    type: 'object',

    properties: {

      meses_futuro: { type: 'number', minimum: 1, maximum: 12 }

    },

    required: ['meses_futuro']

  },

}



// Handler:

if (name === 'prever_receita') {

  const { meses_futuro } = args as { meses_futuro: number }

  

  // Regressão linear simples nos últimos 3 meses

  const ultimos = vendas.slice(-3)

  const crescimento_medio = ultimos.reduce((acc, v, i) => {

    if (i === 0) return acc

    return acc + (v.receita - ultimos[i-1].receita) / ultimos[i-1].receita

  }, 0) / (ultimos.length - 1)

  

  const ultima_receita = vendas[vendas.length - 1].receita

  const previsoes = Array.from({ length: meses_futuro }, (_, i) => ({

    mes: `M+${i+1}`,

    previsao: Math.round(ultima_receita * Math.pow(1 + crescimento_medio, i + 1))

  }))

  

  return { content: [{ type: 'text', text: JSON.stringify({

    crescimento_mensal: (crescimento_medio * 100).toFixed(1) + '%',

    previsoes

  }) }] }

}

Padrões e Armadilhas

Padrões

Padrão 1: Calcule métricas derivadas no server, não no LLM LLMs cometem erros em cálculos numéricos. ROI, taxas percentuais, médias — calcule no TypeScript. O LLM interpreta e recomenda, não calcula.

Padrão 2: Use descriptions para guiar qual tool usar quando

// Ruim: "Retorna dados de vendas"

// Bom: "Retorna dados de vendas mensais: receita, clientes novos e churn. Use para análises temporais e tendências."

A description é o único guia que o LLM tem para escolher entre tools.

Padrão 3: System prompt define o nível de análise esperado Um system prompt de “analista de BI sênior” produz respostas muito mais úteis que “assistente”. Investir em bom system prompt reduz tokens por resposta (o LLM vai direto ao ponto).

Armadilhas

⚠️ Armadilha 1: Criar um client por query em produção

// LENTO: cria e destrói conexão a cada pergunta

async function consultarBI(pergunta: string) {

  const [ct, st] = InMemoryTransport.createLinkedPair()  // overhead

  // ...

  await client.close()  // desperdício

}



// RÁPIDO: reutilize o client (inicialize uma vez)

const { client } = await inicializarBIServer()

// Depois para cada query:

async function consultarBI(client: Client, pergunta: string) { ... }

⚠️ Armadilha 2: max_tokens: 500 pode truncar respostas analíticas Para queries complexas (“compare todas as campanhas e faça recomendação”), 500 tokens é pouco. Use 1000-2000 para copilots de BI. O custo adicional de tokens de output é mínimo comparado ao valor da resposta completa.

⚠️ Armadilha 3: Dados sem unidades no JSON

// RUIM: LLM não sabe o que é "120000"

{ receita: 120000 }



// BOM: contexto claro

{ receita_brl: 120000, receita_formatada: 'R$120.000' }

Inclua unidades e formatos no JSON retornado. Previne o LLM de interpretar incorretamente.

⚗ Laboratório prático — mozak.tech
Se não for realizar o laboratório, pule para o próximo capítulo.

Ponte para o Lab

O starter desta unidade é um BI Copilot completo e funcional — sem TODOs de código. A atividade é compreender e estender.

Rodando o starter:

npx ts-node starter.ts

Queries que ele responde (testar todas): 1. “Como foi o crescimento de receita de março a junho?” → usa consultar_vendas 2. “Qual campanha tem melhor ROI? Onde devo aumentar investimento?” → usa analisar_campanhas 3. “Qual nosso churn rate e LTV? Estamos saudáveis financeiramente?” → usa calcular_metrica 2x

Extensões para praticar: - Adicione a tool prever_receita(meses) do Exemplo 3 acima - Adicione tool comparar_periodos(mes1, mes2) que retorna crescimento percentual entre dois meses - Expanda calcular_metrica para incluir nps_estimado ou arr (ARR = MRR × 12) - Modifique o system prompt e observe como a qualidade das respostas muda

Agora você está pronto para o lab.