
Arquitetura de Software
IA multi-tenant para chatbots: gateway, limites e operacao
Um desenho pratico para transformar modelos generativos em uma capacidade governada da plataforma, com isolamento, quotas, prompts versionados e operacao previsivel.
O problema de colocar IA em um SaaS nao termina quando a primeira chamada ao modelo funciona. Ele comeca quando cada funcionalidade escolhe seu proprio SDK, o frontend informa o tenant e ninguem consegue explicar qual cliente consumiu a fatura.
Pedir que cada cliente forneca uma chave da OpenAI, Anthropic ou de outro provedor acelera o prototipo, mas transfere atrito, suporte e risco para quem deveria apenas usar o produto. Tambem dificulta oferecer planos, limites claros, auditoria e uma experiencia consistente.
Uma arquitetura mais madura centraliza as credenciais na organizacao e trata IA como uma capacidade interna da plataforma. O cliente consome acoes previstas no plano; a aplicacao controla autorizacao, volume, custo, seguranca e roteamento. O objetivo nao e esconder o custo, mas transforma-lo em uma experiencia de produto previsivel e sustentavel.
O desenho deste artigo serve para chatbots e outros recursos generativos em CRMs, atendimento, copilotos internos, sistemas de clinicas, e-commerce e ferramentas operacionais. Amazon Bedrock e uma implementacao possivel, nao o centro da arquitetura.
O problema que a arquitetura resolve
O acoplamento direto costuma surgir assim:
Frontend -> API do provedor de IA -> resposta
Esse fluxo falha por motivos importantes:
- uma chave exposta no navegador pode ser usada fora do produto;
- o
tenantIdrecebido do cliente pode ser adulterado; - cada funcionalidade passa a escolher modelo, prompt e parametros de forma independente;
- nao existe um ponto unico para rate limit, orcamento, bloqueio emergencial ou auditoria;
- mudar de provedor exige alteracoes espalhadas pelo codigo;
- custo e uso permanecem invisiveis ate a fatura chegar.
Em SaaS, isolamento de tenant e um requisito arquitetural: um tenant nao pode acessar recursos de outro, mesmo quando a infraestrutura e compartilhada. A SaaS Lens da AWS trata esse limite como parte fundamental do desenho.
A ideia central: um AI Gateway interno
O gateway e a unica porta de saida da aplicacao para modelos de IA. Ele nao e apenas um wrapper de SDK: e a camada que aplica politicas de produto, seguranca e operacao antes de qualquer chamada externa.
O gateway recebe um contexto confiavel da sessao autenticada e um comando de negocio. Ele nao recebe uma chave do cliente e nao aceita que o frontend decida qual modelo usar.
| Responsabilidade | Decisao do gateway |
|---|---|
| Identidade | Deriva accountId, userId e permissoes da sessao ou JWT validado. |
| Produto | Verifica se o plano permite a funcionalidade e se a IA esta ativa para o tenant. |
| Limites | Controla acoes mensais, orcamento, concorrencia e requisicoes por janela de tempo. |
| Prompt | Resolve uma versao imutavel e aprovada para a funcionalidade. |
| Modelo | Escolhe modelo preferido e fallback a partir de uma politica server-side. |
| Seguranca | Redige dados sensiveis quando necessario, aplica guardrails e valida ferramentas. |
| FinOps | Registra tokens, componentes de preco e custo real ou estimado por tenant. |
| Observabilidade | Produz logs estruturados, metricas e traces sem vazar conteudo sensivel. |
Principios que mantem o desenho desacoplado
1. O dominio nao conhece SDKs de provedores
Inbox, campanhas, resumos e classificacao de leads devem depender de um contrato interno. Bedrock, OpenAI e Anthropic vivem atras de adaptadores. Assim, trocar modelo, adicionar fallback ou testar outro fornecedor nao altera a regra de negocio.
Em Kotlin, um contrato minimo e autocontido pode ser assim:
enum class AiFeature {
REPLY_SUGGESTION,
CONVERSATION_SUMMARY,
LEAD_CLASSIFICATION
}
data class AiContext(
val accountId: UUID,
val userId: UUID,
val roles: Set<String>,
val requestId: UUID
)
data class GenerateCommand(
val feature: AiFeature,
val promptKey: String,
val variables: Map<String, String>,
val conversationId: UUID?,
val idempotencyKey: String
)
data class ProviderRequest(
val systemPrompt: String,
val userPrompt: String,
val maxOutputTokens: Int,
val temperature: Double,
val metadata: Map<String, String>
)
data class AiUsage(
val inputTokens: Int,
val outputTokens: Int,
val cacheReadTokens: Int = 0,
val cacheWriteTokens: Int = 0,
val provider: String,
val model: String
)
data class GenerateResult(
val text: String,
val usage: AiUsage
)
interface AiProvider {
val id: String
suspend fun generate(request: ProviderRequest): GenerateResult
}
O controller chama um servico de aplicacao, como AiGatewayService, que transforma a requisicao autenticada em um comando e delega a execucao ao AiGateway. Apenas o modulo de infraestrutura conhece BedrockRuntimeClient, OpenAIClient ou qualquer SDK equivalente.
2. O tenant vem da identidade, nunca do payload
O cliente pode informar conversationId, mas nao pode escolher livremente accountId, plano, modelo ou limite. O backend extrai o tenant da sessao e ainda valida que a conversa pertence a ele.
Na pratica, aplique defesa em profundidade:
- valide autenticacao e papeis;
- derive o tenant no servidor;
- filtre toda consulta por
account_id; - valide relacoes entre entidades, como conversa e contato;
- habilite RLS ou mecanismo equivalente no banco;
- mantenha testes negativos de acesso entre tenants.
3. Venda acoes; opere com tokens e custo
Tokens sao uteis para engenharia e faturamento interno, mas nao sao uma boa unidade de produto para a maioria dos clientes. Um plano pode oferecer, por exemplo:
| Funcionalidade | Unidade exibida | Peso interno inicial |
|---|---|---|
| Sugerir resposta | 1 acao | 1 |
| Classificar lead | 1 acao | 1 |
| Resumir conversa | 2 acoes | 2 |
| Gerar campanha | 3 acoes | 3 |
| Analisar historico longo | 5 acoes | 5 |
Esses pesos sao uma abstracao comercial, nao uma equivalencia de custo. Duas execucoes cobradas como uma acao podem usar modelos, contextos e quantidades de tokens diferentes. Por isso, a franquia que o cliente ve e o ledger financeiro que a plataforma opera precisam ser independentes.
O produto pode mostrar "740 de 2.000 acoes de IA usadas". Internamente, cada execucao registra componentes de uso, modelo, preco vigente, custo e duracao. Reavalie os pesos com dados reais de margem, qualidade e suporte; nao os use como substitutos da medicao financeira.
O preco nao deve ficar hard-coded. Mantenha um catalogo versionado por provider, model, region, modalidade e vigencia, e grave no evento o snapshot aplicado. Para uma chamada com cache de prompt, uma aproximacao e:
custo_inferencia =
(input_tokens_sem_cache / 1_000_000 * preco_input)
+ (cache_write_tokens / 1_000_000 * preco_cache_write)
+ (cache_read_tokens / 1_000_000 * preco_cache_read)
+ (output_tokens / 1_000_000 * preco_output)
custo_total = custo_inferencia
+ embeddings
+ reranking
+ ferramentas
+ capacidade_provisionada_rateada
+ outros_componentes_aplicaveis
Nem todo provedor ou modelo usa todos esses componentes. O Amazon Bedrock Pricing separa modalidades, modelos e niveis de servico; o Cost and Usage Report do Bedrock tambem distingue tokens de entrada, saida, leitura e escrita de cache quando aplicavel. A regra operacional e registrar cada componente, em vez de esconder tudo em uma unica estimativa.
4. Prompt e um artefato versionado
Nao deixe o prompt espalhado em if e concatenacao de strings. Um prompt deve ter chave, versao, variaveis, modelo pretendido, parametros de inferencia, dono e conjunto de avaliacoes.
reply-suggestion@v4
- objetivo: sugerir resposta curta para atendimento
- variaveis: customer_name, conversation_context, tone
- modelo: classe economica
- temperature: 0.3
- max_output_tokens: 350
- avaliacao: 42 conversas anonimizadas
- status: approved
No Amazon Bedrock, o Prompt Management permite criar prompts reutilizaveis com variaveis, testar variantes e salvar versoes. A integracao deve apontar para uma versao aprovada e imutavel, em vez de depender do rascunho atual.
Para preservar portabilidade, mantenha no dominio uma tabela ai_prompt_versions e, quando usar Bedrock, associe sua versao interna ao ARN da versao do prompt. Em outro provedor, a mesma versao pode apontar para um template mantido no Git. O dominio continua usando reply-suggestion@v4.
Fluxo de uma requisicao bem governada
O fluxo abaixo evita que custo, acesso e conteudo dependam de boas intencoes do frontend.
O diagrama de sequencia mostra quem participa. O fluxo de decisao abaixo mostra por que uma chamada avanca, e bloqueada ou precisa de reconciliacao:
A sequencia recomendada e:
- gerar ou propagar um
requestIde exigiridempotencyKeypara operacoes repetiveis; - validar sessao, papel e posse dos recursos consultados;
- verificar
ai_enabled, kill switch global, limite mensal, orcamento e rate limit; - reservar a acao de forma atomica para impedir estouro sob concorrencia;
- resolver o prompt e a politica de modelo no servidor;
- montar apenas o contexto necessario, com limite de historico e redaction quando aplicavel;
- chamar o adaptador com timeout, limite de saida e circuit breaker;
- persistir o evento de uso idempotente e liquidar ou devolver a reserva;
- retornar uma resposta normalizada e uma mensagem clara quando houver bloqueio.
Nao faca retry cego depois de timeout: o provedor pode ter concluido a inferencia e cobrado a chamada. Prefira idempotencia no fornecedor quando disponivel e reconciliacao de eventos no gateway.
Modelo de dados minimo
As tabelas podem viver no mesmo banco transacional da aplicacao. Para alto volume, os eventos podem seguir depois para uma plataforma analitica, mas a decisao de permitir ou negar uso precisa ser transacional e rapida.
| Tabela | Responsabilidade principal |
|---|---|
ai_tenant_settings |
Ativacao de IA, plano, limite mensal, teto de custo e configuracao de seguranca por tenant. |
ai_feature_policies |
Peso em acoes, modelo preferido e fallback, limites de tokens e permissoes exigidas. |
ai_usage_periods |
Contadores atomicos por tenant e periodo: reservado, consumido e custo acumulado. |
ai_requests |
Estado por idempotency_key: iniciado, concluido, bloqueado ou falho. |
ai_usage_events |
Ledger imutavel com componentes de uso, custo, latencia, modelo, prompt e bloqueios. |
ai_prompt_versions |
Chave, versao, checksum, status de aprovacao e referencia do provedor. |
ai_model_price_catalog |
Precos, modalidade, regiao, vigencia e moeda. |
Um evento deve ter, no minimo, account_id, request_id, feature, provider, model, prompt_version, componentes de uso, cost_microusd, latency_ms, status e created_at. Armazene conteudo somente quando houver necessidade legitima, com redaction, retencao curta e controle de acesso. O padrao deve ser registrar metadados, nao transcricoes completas.
Onde o gateway fica na arquitetura
O AI Gateway nao e uma entidade de dominio. Ele funciona como um servico de aplicacao: recebe um caso de uso, coordena autorizacao, ownership, quota, prompt, roteamento e ledger, e delega a inferencia para uma porta de provedor.
interfaces/http
-> application/AiGatewayService
-> domain
-> ports
-> infrastructure/adapters
O controller conhece HTTP, autenticacao e DTOs. O servico de aplicacao conhece o fluxo do caso de uso. O dominio concentra regras e invariantes. Adaptadores implementam persistencia, rate limit, provedores e observabilidade.
Essa separacao mantem SDKs, detalhes HTTP e infraestrutura fora das regras de negocio. O desenho combina DDD tatico leve, servicos de aplicacao e ports and adapters; ele nao depende de adotar todos os padroes de DDD para funcionar.
A orquestracao no gateway
O trecho abaixo mostra o ponto em que as politicas se encontram. As interfaces representam portas consumidas pela camada de aplicacao e podem ser implementadas com PostgreSQL, Redis, Bedrock, OpenAI ou outro servico.
class AiGateway(
private val authorization: AiAuthorization,
private val quota: AiQuotaService,
private val prompts: PromptRegistry,
private val router: ModelRouter,
private val usage: UsageLedger,
private val providers: Map<String, AiProvider>
) {
suspend fun generate(
context: AiContext,
command: GenerateCommand
): GenerateResult {
authorization.assertAllowed(context, command.feature)
quota.assertNotGloballyDisabled()
quota.checkRateLimit(
context.accountId,
context.userId,
command.feature
)
val reservation = quota.reserve(
accountId = context.accountId,
feature = command.feature,
idempotencyKey = command.idempotencyKey
)
try {
val prompt = prompts.resolveApproved(command.promptKey)
val route = router.resolve(
context.accountId,
command.feature,
prompt
)
val request: ProviderRequest = prompt.render(
variables = command.variables,
limits = route.limits,
metadata = mapOf("requestId" to context.requestId.toString())
)
val result = providers
.getValue(route.provider)
.generate(request)
usage.recordSuccess(
context,
command,
prompt,
route,
result.usage
)
quota.commit(reservation)
return result
} catch (error: Exception) {
quota.release(reservation)
usage.recordFailure(context, command, error)
throw error
}
}
}
Em producao, reserve, commit e release precisam ser idempotentes. Uma opcao simples usa INSERT ... ON CONFLICT no registro da requisicao e UPDATE condicional no periodo de uso, na mesma transacao quando o banco permitir. Redis pode complementar o rate limit de curta duracao, mas nao deve ser a unica fonte do limite mensal faturavel.
O que nao fazer
Alguns atalhos anulam as protecoes do gateway:
- nao aceite
provider,model,accountIdou limite de tokens enviados pelo frontend como decisao final; - nao registre prompts, documentos recuperados ou respostas completas por padrao;
- nao repita chamadas apos timeout sem idempotencia e reconciliacao de cobranca;
- nao permita que tool calling execute comandos sem schema, allowlist e autorizacao server-side;
- nao trate guardrail, prompt de sistema ou resposta estruturada como mecanismo de autorizacao;
- nao use apenas Redis como fonte da franquia faturavel;
- nao permita fallback para um modelo com politica de dados, regiao ou capacidades incompativeis.
Seguranca: o modelo nao autoriza nada
Prompts de sistema ajudam a orientar comportamento, mas nao controlam acesso. A OWASP alerta que prompt injection pode manipular comportamento, expor informacao e influenciar o uso de funcoes conectadas; RAG e fine-tuning nao eliminam esse risco.
Trate toda entrada do usuario, documento recuperado e resposta do modelo como conteudo nao confiavel:
- mantenha instrucoes de sistema somente no servidor;
- separe conteudo externo do contexto de controle;
- limite ferramentas por funcionalidade e tenant;
- valide parametros com schemas e allowlists no codigo;
- cheque autorizacao no executor da ferramenta, nunca no modelo;
- exija aprovacao humana para acoes irreversiveis ou de alto impacto;
- limite historico, anexos e resposta;
- teste prompt injection e vazamento cross-tenant.
Ao usar Bedrock, Guardrails pode aplicar filtros de conteudo, temas negados, palavras, informacoes sensiveis e verificacoes de grounding. Ele e uma camada util, mas tem limites que precisam entrar no threat model.
Os filtros de informacoes sensiveis, por exemplo, atuam sobre texto e nao detectam PII dentro dos parametros de uma saida tool_use nas APIs indicadas pela AWS. Esses parametros ainda precisam de validacao e autorizacao no executor. A verificacao de contextual grounding tambem possui casos de uso suportados especificos; valide a compatibilidade antes de aplica-la como protecao generica de chatbot.
Para credenciais, use identidades de workload: em AWS, uma role IAM da aplicacao; em provedores diretos, um segredo em cofre gerenciado. Nunca entregue segredo ao navegador, aplicativo mobile ou automacao pertencente ao tenant.
Observabilidade e operacao
IA sem telemetria e uma fonte de incidentes caros e dificeis de explicar. Instrumente cada chamada com requestId, identificador de tenant controlado ou pseudonimizado, feature, provider, model, promptVersion, duracao, status e uso.
O OpenTelemetry permite exportar traces, logs e metricas sem acoplar a observabilidade a uma unica ferramenta. Para atributos de IA generativa, acompanhe as semantic conventions de GenAI, cujo nivel de estabilidade pode evoluir.
Metricas minimas:
- solicitacoes, erros e bloqueios por tenant, feature, modelo e motivo;
- latencia p50, p95 e p99 por rota;
- tokens de entrada, saida, leitura e escrita de cache;
- custo diario, mensal e por tenant;
- taxa de fallback entre modelos;
- bloqueios de seguranca, redactions e falhas de ferramentas;
- saldo de acoes e uso proximo do teto.
Defina alertas para aumento abrupto de custo, erro do provedor, latencia alta, consumo anomalo por tenant e esgotamento global de orcamento. O gateway precisa de kill switch global e por feature: em um incidente, desativar geracao de campanha nao deve derrubar todo o chatbot.
Runbook de implementacao
Este roteiro comeca pequeno e preserva espaco para crescer.
Fase 1: delimitar a primeira capacidade
Escolha uma funcionalidade de baixo risco e valor claro. Para um CRM, "sugerir resposta" e um ponto de partida melhor que um agente com permissao para alterar dados.
- defina publico, permissao, entrada maxima, resposta esperada e peso em acoes;
- escolha um modelo economico e um fallback compativel;
- escreva de 20 a 50 casos com dados anonimizados ou sinteticos;
- defina bloqueios e mensagens que o usuario vera.
Criterio de saida: a funcionalidade tem contrato, prompt versionado, limite de saida e testes de qualidade.
Fase 2: criar as fronteiras do codigo
- crie
AiGateway,AiProvider,PromptRegistry,AiQuotaService,UsageLedgereModelRouter; - implemente somente um adaptador de provedor no inicio;
- garanta que UI e dominio dependam apenas do gateway;
- leia tenant e permissoes da identidade autenticada.
Criterio de saida: uma busca pelo SDK do provedor fora do modulo de infraestrutura nao retorna resultados.
Fase 3: criar o ledger e os limites
- migre as tabelas de configuracao, periodo de uso, requisicao, eventos e precos;
- implemente reserva atomica com
idempotencyKeyunico por tenant e operacao; - aplique rate limit por tenant, usuario e feature;
- adicione teto de custo interno independente da franquia de acoes.
Criterio de saida: requisicoes concorrentes nao ultrapassam o limite e retries nao duplicam consumo.
Fase 4: versionar prompts e politicas
- cadastre chave, versao, checksum, autor, aprovacao e avaliacao;
- armazene modelo, parametros e limite de tokens em politica server-side;
- conecte a versao do dominio a uma versao imutavel do provedor;
- rode casos dourados, formatos e cenarios adversariais no CI.
Criterio de saida: e possivel explicar qual prompt, politica e modelo geraram uma resposta.
Fase 5: endurecer a seguranca
- redija PII antes de logs e conforme a politica de envio ao provedor;
- configure guardrails ou filtros equivalentes para entrada e saida;
- valide no backend qualquer acao solicitada via tool calling;
- teste injection direta, documento recuperado malicioso e acesso cross-tenant;
- defina aprovacao humana para acoes de alto risco.
Criterio de saida: nenhum teste cross-tenant, de autorizacao ou de ferramenta privilegiada passa sem controle server-side.
Fase 6: observar e lancar com seguranca
- publique dashboards de custo, uso, latencia, erros e bloqueios;
- adicione alertas e kill switches testados;
- comece com feature flag para um tenant interno ou grupo piloto;
- estabeleca orcamento diario e rollback de prompt e modelo;
- revise semanalmente qualidade, custo por acao e casos de suporte.
Criterio de saida: a equipe consegue desativar uma rota, identificar a versao ruim e responder a uma cobranca inesperada em poucos minutos.
Checklist de aceite
| Cenario | Resultado esperado |
|---|---|
Usuario altera accountId no request |
O backend ignora o campo e usa o tenant da sessao. |
| Tenant sem IA contratada | Retorno de regra de produto, sem chamada ao provedor. |
| Limite mensal atingido | Reserva negada atomicamente; nenhum token e enviado. |
| Retry com a mesma chave | Retorna resultado anterior ou estado, sem nova cobranca. |
| Timeout depois do envio | A requisicao entra em reconciliacao; nao ocorre retry cego. |
| Provedor indisponivel | Fallback compativel ou erro controlado; reserva e conciliada. |
| Prompt novo degrada qualidade | CI ou canary impede promocao; rollback usa versao anterior. |
| Documento RAG contem instrucao maliciosa | O conteudo nao confiavel nao ganha privilegios. |
| Tenant A consulta conversa do tenant B | A consulta e bloqueada antes de montar o prompt. |
| Tool calling pede uma acao proibida | O executor rejeita a chamada por politica server-side. |
| Erro em producao | O trace aponta request, feature, modelo e versao sem expor conteudo sensivel. |
Quando usar Bedrock e quando chamar o provedor direto
O desenho funciona nos dois cenarios. Chamar o provedor diretamente costuma simplificar o MVP. Bedrock tende a fazer sentido quando AWS ja e a plataforma operacional, quando IAM e billing centralizado sao requisitos ou quando recursos como Prompt Management e Guardrails reduzem trabalho da plataforma.
O ponto decisivo e nao deixar essa escolha atravessar o dominio. O produto fala em "gerar sugestao de resposta"; o gateway traduz isso para modelo e provedor. Assim, um CRM, um SaaS de clinicas ou outro chatbot pode comecar com um fornecedor, adicionar Bedrock depois e manter a regra de negocio intacta.
Codigo executavel
A prova de conceito que acompanha este artigo esta disponivel no GitHub, no laboratorio multi-tenant-ai-gateway do repositorio iury-engineering-labs.
O laboratorio implementa o gateway em Kotlin e expoe dois endpoints Ktor: um para sugerir respostas e outro para consultar o estado de uma requisicao idempotente. A separacao entre controller, servico de aplicacao, dominio e adaptadores em memoria demonstra autorizacao server-side, reserva de quota, prompt versionado, roteamento, ledger de uso e o estado pendente de reconciliacao apos um timeout incerto, sem exigir credenciais de nuvem.
Para executar os testes:
gradle :labs:multi-tenant-ai-gateway:apps:gateway-kotlin:test
Conclusao
Centralizar a credencial da organizacao nao e apenas uma decisao de seguranca. E o que transforma IA em uma capacidade governavel: com planos compreensiveis, limites justos, custos rastreaveis e liberdade para trocar de modelo.
Comece com uma funcao, um gateway pequeno, um ledger confiavel e uma versao de prompt avaliada. A partir dai, cada novo caso de uso herda as mesmas protecoes em vez de reinventa-las.
Em SaaS, o modelo e uma dependencia. A capacidade de IA pertence a plataforma.
Referencias
- AWS SaaS Lens: Tenant Isolation
- AWS Prescriptive Guidance: Multi-tenant SaaS authorization and API access control
- Amazon Bedrock Pricing
- Amazon Bedrock Cost and Usage Report
- Amazon Bedrock Prompt Management
- Amazon Bedrock Guardrails
- Amazon Bedrock: sensitive information filters
- Amazon Bedrock: contextual grounding checks
- OWASP GenAI: Prompt Injection
- OpenTelemetry
- OpenTelemetry GenAI Semantic Conventions
- Codigo executavel: multi-tenant-ai-gateway
Comentários
Em breveAs respostas da comunidade chegam em breve.