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óricoSchema 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 realPadrõ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 é perdidoPadrão 2: Streaming com persistência no finalMessage
Não tentar salvar token a token (write amplification)
Acumular em respostaCompleta → salvar uma vez no finalMessageArmadilhas
⚠️ 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 vivaSe 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.