
Arquitetura de Software
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 acoplamento direto costuma surgir assim:
Frontend -> API do provedor de IA -> resposta
Esse fluxo falha por motivos importantes:
tenantId recebido do cliente pode ser adulterado;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.
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. |
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.
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:
account_id;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.
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.
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:
requestId e exigir idempotencyKey para operacoes repetiveis;ai_enabled, kill switch global, limite mensal, orcamento e rate limit;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.
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.
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.
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.
Alguns atalhos anulam as protecoes do gateway:
provider, model, accountId ou limite de tokens enviados pelo frontend como decisao final;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:
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.
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:
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.
Este roteiro comeca pequeno e preserva espaco para crescer.
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.
Criterio de saida: a funcionalidade tem contrato, prompt versionado, limite de saida e testes de qualidade.
AiGateway, AiProvider, PromptRegistry, AiQuotaService, UsageLedger e ModelRouter;Criterio de saida: uma busca pelo SDK do provedor fora do modulo de infraestrutura nao retorna resultados.
idempotencyKey unico por tenant e operacao;Criterio de saida: requisicoes concorrentes nao ultrapassam o limite e retries nao duplicam consumo.
Criterio de saida: e possivel explicar qual prompt, politica e modelo geraram uma resposta.
Criterio de saida: nenhum teste cross-tenant, de autorizacao ou de ferramenta privilegiada passa sem controle server-side.
Criterio de saida: a equipe consegue desativar uma rota, identificar a versao ruim e responder a uma cobranca inesperada em poucos minutos.
| 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. |
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.
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
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.
As respostas da comunidade chegam em breve.