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_mesesReceita recorrente mensal. Para o starter: soma de todas as receitas dividida por 6 meses.
Churn Rate
churn_rate = total_churned / total_novos_clientes × 100Percentual 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) × 12Valor 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_novosQuanto custa adquirir um cliente. Regra de ouro: LTV/CAC > 3 é saudável.
ROI de Marketing
ROI = (receita_gerada - gasto) / gasto × 100Retorno 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 claraExemplo 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.
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.tsQueries 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.