mozak.tech Engenharia de IA Corporativa 93%

Parte IV — Projeto Integrador: Do Conceito ao Produto Entregue

4.3 — Backend de Orquestração: API com Sessões de Agente e Integrações (Orquestração)

Objetivo da Aula

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

Projetar endpoints REST para sistema de chat com IA (upload, sessão, streaming)

Implementar persistência de histórico de conversa por sessão no banco

Integrar MCP como camada de ferramentas no backend Node.js/Python

Implementar streaming de respostas LLM via Server-Sent Events (SSE)

Gerenciar múltiplas sessões de usuário com isolamento de histórico

Por que isso importa

Um backend de IA sem gestão de sessão é um toy project — cada request ao LLM começa do zero. Histórico persistido no banco é o que transforma um chatbot em assistente. Streaming é o que transforma “esperando…” em experiência fluida. SSE é o padrão correto para streaming no contexto de APIs REST.

Conceitos Fundamentais

Design dos Endpoints

POST /api/documentos/upload

  Body: multipart/form-data com arquivo PDF/DOCX

  → Processa (extrai, chunka, embeds, indexa)

  → 202 Accepted + {job_id} (processamento assíncrono)



GET /api/documentos/status/{job_id}

  → {status: "processing|done|error", n_chunks: int}



POST /api/sessoes

  → Cria sessão vazia: {sessao_id: uuid, criado_em: ISO}



POST /api/sessoes/{sessao_id}/mensagens

  Body: {conteudo: string, streaming: bool}

  → Se streaming=false: JSON com resposta completa

  → Se streaming=true: SSE (text/event-stream)



GET /api/sessoes/{sessao_id}/historico

  → [{role, conteudo, timestamp}]



DELETE /api/sessoes/{sessao_id}

  → Remove sessão e histórico

Schema do Banco

-- Sessões de conversa:

CREATE TABLE sessoes (

    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

    usuario_id UUID REFERENCES usuarios(id),

    titulo VARCHAR(255),

    created_at TIMESTAMP DEFAULT NOW()

);



-- Mensagens do histórico:

CREATE TABLE mensagens (

    id SERIAL PRIMARY KEY,

    sessao_id UUID REFERENCES sessoes(id) ON DELETE CASCADE,

    role VARCHAR(20) CHECK (role IN ('user', 'assistant')),

    conteudo TEXT NOT NULL,

    tokens_usados INTEGER,

    created_at TIMESTAMP DEFAULT NOW()

);



-- Índice para busca de histórico:

CREATE INDEX idx_mensagens_sessao ON mensagens(sessao_id, created_at);

Streaming com SSE

// Endpoint Express com streaming:

app.post('/api/sessoes/:sessaoId/mensagens', async (req, res) => {

    const { conteudo, streaming } = req.body

    const { sessaoId } = req.params

    

    // Carregar histórico da sessão

    const historico = await db.query(

        'SELECT role, conteudo FROM mensagens WHERE sessao_id = $1 ORDER BY created_at',

        [sessaoId]

    )

    

    // Salvar mensagem do usuário

    await db.query(

        'INSERT INTO mensagens (sessao_id, role, conteudo) VALUES ($1, $2, $3)',

        [sessaoId, 'user', conteudo]

    )

    

    if (!streaming) {

        // Resposta completa (não-streaming):

        const response = await anthropic.messages.create({

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

            max_tokens: 1024,

            messages: [...historico.rows, { role: 'user', content: conteudo }],

        })

        const resposta = response.content[0].text

        await db.query(

            'INSERT INTO mensagens (sessao_id, role, conteudo) VALUES ($1, $2, $3)',

            [sessaoId, 'assistant', resposta]

        )

        return res.json({ resposta, tokens: response.usage.output_tokens })

    }

    

    // Streaming via SSE:

    res.setHeader('Content-Type', 'text/event-stream')

    res.setHeader('Cache-Control', 'no-cache')

    res.setHeader('Connection', 'keep-alive')

    

    let respostaCompleta = ''

    const stream = anthropic.messages.stream({

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

        max_tokens: 1024,

        messages: [...historico.rows, { role: 'user', content: conteudo }],

    })

    

    stream.on('text', (text) => {

        respostaCompleta += text

        res.write(`data: ${JSON.stringify({ delta: text })}\n\n`)

    })

    

    stream.on('finalMessage', async (msg) => {

        // Persistir resposta completa no banco

        await db.query(

            'INSERT INTO mensagens (sessao_id, role, conteudo, tokens_usados) VALUES ($1, $2, $3, $4)',

            [sessaoId, 'assistant', respostaCompleta, msg.usage.output_tokens]

        )

        res.write(`data: ${JSON.stringify({ done: true, tokens: msg.usage.output_tokens })}\n\n`)

        res.end()

    })

})

Aprofundamento Técnico

Integração MCP no Backend

// Servidor Express registra ferramentas MCP:

import { MCPServer } from '@anthropic-ai/mcp'



const mcpServer = new MCPServer({

    tools: [

        {

            name: "buscar_documentos",

            description: "Busca trechos relevantes nos documentos indexados",

            inputSchema: {

                type: "object",

                properties: {

                    query: { type: "string" },

                    sessao_id: { type: "string" },

                },

                required: ["query"],

            },

            handler: async ({ query, sessao_id }) => {

                const embedding = await gerarEmbedding(query)

                const chunks = await buscarChunksSimilares(embedding, sessao_id)

                return chunks.map(c => `[${c.arquivo}]\n${c.conteudo}`).join('\n\n')

            },

        },

    ],

})



// Criar agente com MCP:

async function criarAgenteMCP(mensagens, sessaoId) {

    return anthropic.messages.create({

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

        max_tokens: 2048,

        system: `Você é assistente de análise de documentos. 

Use a ferramenta buscar_documentos para encontrar informações relevantes.

Cite sempre a fonte (arquivo e página) nas respostas.`,

        tools: mcpServer.tools,

        messages: mensagens,

    })

}

Limitar Histórico por Contexto

const MAX_HISTORICO_TOKENS = 4000  // reservar espaço para resposta



async function carregarHistoricoLimitado(sessaoId: string) {

    // Carregar mensagens mais recentes que cabem no contexto:

    const mensagens = await db.query(

        `SELECT role, conteudo, tokens_usados 

         FROM mensagens 

         WHERE sessao_id = $1 

         ORDER BY created_at DESC 

         LIMIT 20`,

        [sessaoId]

    )

    

    let tokens_acumulados = 0

    const historico = []

    for (const msg of mensagens.rows.reverse()) {  // reverter para ordem cronológica

        tokens_acumulados += msg.tokens_usados || estimarTokens(msg.conteudo)

        if (tokens_acumulados > MAX_HISTORICO_TOKENS) break

        historico.push({ role: msg.role, content: msg.conteudo })

    }

    return historico

}

Exemplos Anotados

Exemplo 1: Flow Completo de uma Mensagem

1. POST /api/sessoes/abc-123/mensagens {conteudo: "Qual o prazo de rescisão?"}

2. Backend carrega histórico (2 mensagens anteriores)

3. Backend usa buscar_documentos("prazo rescisão") → chunk da Cláusula 5

4. Backend monta mensagens: [histórico] + [contexto do chunk] + [pergunta]

5. Streaming: Claude responde "30 dias conforme Cláusula 5"

6. Backend persiste resposta no banco

7. Frontend recebe SSE: delta token a token → exibe em tempo real

Padrões e Armadilhas

Padrões

Padrão 1: Persistir ANTES de enviar ao LLM

Salvar mensagem do usuário antes de chamar o LLM

Se o LLM travar → histórico do usuário não é perdido

Padrão 2: Streaming com persistência no finalMessage

Não tentar salvar token a token (write amplification)

Acumular em respostaCompleta → salvar uma vez no finalMessage

Armadilhas

⚠️ Armadilha 1: Histórico ilimitado no contexto

Conversa de 100 turnos → contexto excede 200k tokens → erro ou custo explosivo

Solução: janela deslizante de N turnos OU sumarização do histórico antigo

⚠️ Armadilha 2: SSE sem heartbeat

Conexão SSE longa sem dados → proxy/load balancer fecha por timeout

Enviar event: data: {"heartbeat": true}\n\n a cada 15s para manter viva
⚗ 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

Esta unidade guia a Entrega 2 (Backend API). Use exercicios.md e o README em entrega-2-backend-api/ para: 1. Implementar os 5 endpoints REST 2. Configurar PostgreSQL com pgvector + tabela de sessões/mensagens 3. Testar streaming com cliente HTTP (curl ou Postman)

Agora você está pronto para o lab.