Pular para o conteúdo principal
Arquitetando Software em 2026 · 5 de 7 partesEngineering19 min de leitura

Todo novo requisito tem um custo. Torne-o visível.

O ranking do Bullpen precisava de usernames, e usernames precisam de moderação. Em vez de recusar um requisito que o plano nunca teve, levantei o custo de quatro respostas, de "agora não" ao Jev, da TypeSafe, no Vercel AI Gateway com uma fila de revisão, e deixei o custo decidir. Este bônus entre a Parte 4 e a Parte 5 mostra as opções, o escopo, as regras que mantêm a mudança dentro dele e o que o modelo mediu.

← Todos os Posts
2/4

Quando adicionei login ao Bullpen na Parte 4, fiz uma escolha de propósito: o nome de ninguém no GitHub apareceria no site. Cada jogador recebe um rótulo neutro, "Player" mais quatro caracteres de um hash, como "Player 6FEF". O ADR-0020 registra isso. Não era para ser a experiência final. Era para impedir que uma identidade de login virasse uma identidade pública.

Então chegou um requisito que o plano desta série não tinha: os jogadores querem seus próprios nomes no ranking, e esses nomes precisam de uma moderação forte, inteligente e eficiente. Parece uma mudança pequena na tela. Não é. Ela reabre uma decisão que a Parte 4 tomou de propósito, e coloca um texto escolhido por desconhecidos na frente de todo mundo que lê o ranking.

Eu poderia ter dito não. O plano estava fechado, e a próxima parte é sobre a abertura do mercado. Mas a resposta a um novo requisito nunca é só "não". É "entendi, e isto é o que custa". Um preço não é uma promessa de construir. É a informação necessária para decidir, e às vezes ela diz "agora não". Aí quem é dono do requisito escolhe. No Bullpen eu sou quem pede e quem constrói, o que torna fácil pular essa etapa, então este post registra a conversa inteira.

Este é um bônus entre a Parte 4 e a Parte 5, fora das seis partes numeradas. A decisão aqui é de outro tipo: não "como isto deve ser construído", mas "isto deve ser construído agora, e a que preço". A resposta que construí modera usernames com o Jev, um modelo de decisão da TypeSafe, pelo Vercel AI Gateway, com uma fila de revisão por trás. O caminho até ela importa mais do que o modelo.

O requisito, do jeito que chegou

O requisito surgiu em 16 de outubro, enquanto o login ainda estava sendo construído. A Parte 4 entregou o login em 26 de outubro com os rótulos neutros e deixou os nomes para depois. Um ranking de "Player 6FEF" funciona, mas ninguém entra numa liga para ser um código hexadecimal.

Então a necessidade, escrita do jeito que eu gostaria de recebê-la de outra pessoa:

  • Quem: todo jogador com login, e todo visitante que lê o ranking.
  • O quê: um nome que cada jogador escolhe, mostrado onde a posição dele aparecer.
  • A condição: um nome que insulta, ameaça ou se passa por outra pessoa nunca chega à página, e um nome normal não espera dias por uma pessoa.
  • Por que agora: o login está no ar, então os jogadores existem, e o ranking é o primeiro lugar onde eles veriam os nomes uns dos outros.

"Forte, inteligente e eficiente" não é algo que um teste consiga verificar, então o primeiro trabalho foi transformar isso em números. Fixei estas metas em 1º de novembro, antes da construção e antes de qualquer nome chegar ao modelo, do mesmo jeito que a Parte 3 fixou primeiro suas metas de cold start. Nenhum modelo garante zero nomes ruins em produção, então a meta de força vale para um conjunto de teste fixo, e os erros em produção são contados à parte:

PalavraO que significa aquiMeta
ForteNenhum nome de um conjunto rotulado de nomes a rejeitar é aceito sem uma pessoa0 no conjunto de teste; erros em produção contados à parte
InteligenteNomes limpos não se acumulam na frente de uma pessoaNo máximo 1 em cada 20 nomes limpos enviados para revisão
EficienteRápido para responder, barato para rodar, pouco trabalho humanop95 da chamada ao modelo abaixo de 500 ms; menos de US$ 0,0001 por decisão; menos de 10 nomes por semana em revisão, e a fração de todos os envios revisados

Quatro respostas, cada uma com um custo

Cada opção abaixo é uma resposta real ao requisito.

Agora não. Manter os rótulos neutros até depois da Parte 6. Custo: zero código, zero risco, e um ranking de "Player ####" por mais um mês. Os nomes chegariam depois do fim da série. Esse é o padrão honesto quando um requisito aparece no meio do plano, e era a resposta mais barata na mesa.

Agora, com uma blocklist. Uma lista de palavras proibidas, verificada no código. Custo: uma tarde, nada para rodar, nada externo. É fraca nas duas direções. Rejeita nomes inocentes que por acaso contêm um trecho proibido, e aceita qualquer coisa escrita com criatividade. Cada erro vira mais uma entrada numa lista que eu preciso manter.

Agora, só com revisão humana. Todo nome novo espera por mim. Custo: nenhuma dependência nova e nenhuma aceitação errada, mas um jogador que se cadastra às 21h espera até eu olhar, e a fila cresce a cada cadastro. É forte, mas não é inteligente nem eficiente, e coloca o meu tempo no caminho crítico.

Agora, com um modelo de decisão na frente de uma fila de revisão. Mandar cada nome novo ou alterado para um modelo que devolve uma probabilidade, aceitar acima de um limiar, e colocar o resto na fila para mim. Custo: uma dependência externa nova, um preço por chamada, um ADR novo, um novo ponto por onde dados saem do sistema, e tempo tirado da Parte 5.

Para essa última opção olhei o Jev, da TypeSafe, que está no Vercel AI Gateway desde 16 de setembro. Ele não escreve texto; responde perguntas tipadas com probabilidades. A TypeSafe diz que ele é "up to 193.6x faster and 444.6x cheaper than LLMs" nas próprias avaliações, o que este post trata como alegação até o Bullpen medir. Um modelo de linguagem geral pedindo saída estruturada também funcionaria, mas a resposta dele é texto gerado e moldado numa estrutura, não uma probabilidade, e o preço por token no mesmo gateway é maior. A documentação de decisões do gateway consegue até reexecutar automaticamente uma decisão incerta num modelo de linguagem. Não uso isso. Traria outro provedor, outro comportamento e outra decisão de privacidade, tudo sem eu escolher.

Quanto o Jev custa no Vercel AI Gateway

É a única opção com números que precisei pesquisar, então aqui estão, tirados da documentação e antes de qualquer construção.

CustoO que a documentação dizO que significa para o Bullpen
PreçoA entrada do Jev custa US$ 0,042 por milhão de tokens na página dele no AI Gateway, e a saída é gratuita. A decisão de exemplo da documentação usou 275 tokens de entrada e custou US$ 0,00001155Cerca de US$ 0,12 para 10.000 verificações, se cada uma custar o mesmo que o exemplo. A contagem real depende da pergunta e dos critérios, então a construção lê o valor do campo de custo do gateway
Plano gratuitoO crédito gratuito do AI Gateway cobre um subconjunto de modelos, sem valor informado; a página do Jev mostra um preço e nenhuma indicação de crédito gratuitoAinda não confirmado. A medição rodou sem nenhuma recusa por falta de crédito, mas a resposta não diz qual crédito pagou
DadosO Jev aceita retenção zero de dados e nenhum treinamento como opções por requisição, mas o gateway só oferece isso nos planos Pro e EnterpriseO Bullpen está no Hobby, então o nome vai para a TypeSafe sob os termos da TypeSafe. Um nome é público por definição, então a chamada envia o texto do nome e nada mais: nenhum id de usuário, nenhum e-mail, nenhuma sessão
LatênciaNenhum número publicado para o Jev no gatewayMedido abaixo: p95 de 215 ms a partir da minha própria máquina
FalhaO gateway responde 429 quando um limite de taxa é atingido, e recusa quando o crédito ou o orçamento acabaTratado abaixo: nenhuma falha consegue colocar um nome não verificado na página

A política de privacidade da TypeSafe diz que ela não treina com a entrada e não a compartilha além dos seus prestadores de serviço, mas não informa prazo de retenção, e a retenção zero dela é para clientes enterprise. O FAQ do gateway da Vercel diz que o gateway não guarda o conteúdo dos prompts. Para um nome que vai ficar público de qualquer jeito, isso basta. Para qualquer coisa privada, não bastaria.

O dinheiro é pouco. Os custos reais são a dependência, os dados saindo do Bullpen e a semana que isso tira da Parte 5. Esse último é o que o dono do requisito deve pesar, porque é o custo que move uma data. Planejei este bônus para 9 de novembro, com a Parte 5 em 16 de novembro e a Parte 6 em 23 de novembro. Ele saiu em 10 de novembro, e as duas partes seguintes andaram uma semana cada, para 23 e 30 de novembro. Não foi a construção que as moveu: ela levou cerca de duas horas de trabalho com o agente em 1º de novembro. As etapas que só eu podia fazer levaram nove dias: criar uma role no banco, definir uma variável, rodar a medição com uma chave. Coloquei preço no código e esqueci de colocar preço na minha própria atenção.

A direção, e por quê

Escolhi a quarta opção: Jev pelo AI Gateway, um limiar, uma fila de revisão e um caminho manual para mim.

A blocklist falha em "forte" e "inteligente" ao mesmo tempo. A revisão só humana falha em "eficiente", e o custo dela cresce a cada jogador que se cadastra. "Agora não" era uma resposta justa, e eu a teria escolhido se a opção do modelo tivesse saído cara. Saiu por centavos, mais uma dependência que consigo conter num único lugar. A contenção importa mais do que o preço, então ela ocupa a maior parte do que vem abaixo.

O que mudaria a decisão. Três coisas. O conjunto rotulado mostrar o modelo aceitando nomes que ele deve rejeitar, o que falha em "forte". A fila de revisão crescer além da meta, o que falha em "eficiente". Ou os termos da TypeSafe proibirem o envio de nomes; na leitura de 1º de novembro, não proíbem. Qualquer uma delas leva o Bullpen de volta à revisão humana até existir uma opção melhor, e esse caminho é construído de qualquer forma.

O escopo, dentro e fora

Um requisito que chega no meio do plano tende a crescer enquanto é construído. Então o escopo foi escrito antes de qualquer código, e a construção é verificada contra ele:

  • Dentro: uma pergunta booleana por nome novo ou alterado, "Este nome pode ser mostrado publicamente?", com critérios escritos para verdadeiro e falso. Aceitar só acima de um limiar; todo o resto vai para revisão. Um único módulo no código chama o gateway. Um caminho manual me deixa aprovar ou rejeitar os nomes da fila, e ele funciona quando o modelo não funciona.
  • Fora: qualquer coisa no caminho do dinheiro (ordens, preços, poder de compra); verificações de dados de mercado; qualquer decisão que produza um número; qualquer outra funcionalidade de IA. Nenhuma saída do modelo altera uma conta.

Antes de perguntar a qualquer modelo, regras simples rodam no código: de 3 a 20 caracteres, letras, dígitos, underscores e hífens, únicos sem diferenciar maiúsculas. Um nome que quebra uma delas é um erro de formulário e não custa nada.

O limiar vem de um conjunto rotulado, não de um chute. O conjunto tem 146 nomes: 106 limpos e 40 que devem ser rejeitados. Escrevi todos antes de o modelo ver qualquer um, e nenhum veio de um jogador real. O conjunto de rejeição é codificado e leve, com leetspeak e preenchimento em vez de insultos escritos por extenso, então as linhas de insultos usam marcadores e são um teste fraco; prefiro dizer isso a publicar um arquivo de insultos num repositório público. A regra do limiar: o menor valor que não aprova nenhum nome a rejeitar, com uma margem, a menos que isso mande mais de 1 em cada 20 nomes limpos para revisão; nesse caso, eu escolho a troca. Aplicada à medição, a regra dá 0,90: a maior nota de um nome a rejeitar foi 0,88, e 0,90 acrescenta uma pequena margem.

Quando algo dá errado

A regra para toda falha é a mesma: o jogador mantém o rótulo neutro até um nome ser aprovado, e o ranking nunca cai porque a moderação caiu.

  • O modelo estoura o tempo (2 segundos) ou está indisponível: o nome vai para a fila de revisão.
  • A resposta vem malformada, vazia ou de outro modelo: o nome vai para a fila de revisão.
  • O gateway limita a taxa, a chave está ausente, ou o crédito ou o orçamento acabou: o nome vai para a fila de revisão.
  • A probabilidade fica exatamente no limiar: só acima dele aprova, então o empate vai para uma pessoa.
  • Um nome bom é sinalizado: ele espera na fila, e eu aprovo.
  • Um jogador troca um nome aprovado: ele volta a "Player ####" até o novo ser aprovado. O nome antigo não fica no ar enquanto o novo é verificado.
  • Minha revisão demora: o jogador fica com "Player ####" um pouco mais. Nada quebra.
  • O modelo erra com confiança: um nome ruim com nota acima de 0,90 é aprovado e fica público, e nunca chega à fila, porque a fila só vê o que o modelo duvida. Esta construção ainda não tem resposta para esse caso. É a falha mais cara desta lista, então a próxima mudança é um jeito de denunciar um nome aprovado e de eu tirá-lo do ar.

Mais um caso surgiu na revisão de código: cada troca é uma chamada paga e talvez uma entrada na fila, então um jogador trocando o nome em loop poderia gastar o orçamento. Agora um jogador pode salvar no máximo 5 nomes em 24 horas, verificado antes de perguntar ao modelo. Nomes presos como pendentes, porque uma decisão não pôde ser salva, aparecem na minha página de revisão depois de 10 minutos, em vez de esperar o jogador tentar de novo.

O que a mudança tocou

Um novo requisito também é uma mudança na arquitetura, então ele recebe a mesma lista de impactos de uma parte planejada.

  • Modelo: o bônus tem seu próprio momento na linha do tempo do CALM, entre a Parte 4 e a Parte 5. Ele adiciona um sistema externo, o AI Gateway, com a TypeSafe por trás, e dois nós de dados: o schema handles e uma view publicada de nomes aprovados. O app de trading ganha três arestas: para o gateway por HTTPS, para o schema handles como dono dele, e para a view de nomes aprovados, só leitura.
  • Dados: os nomes e a fila de revisão vivem num schema próprio, handles, de uma nova role, handles_owner. A regra da Parte 3 é um dono por transação, e uma decisão sobre um nome não tem nada a ver com uma ordem. O lado de trading consegue ler uma única coisa dele, handles.approved_v1, e mais nada; a role de handles não toca em trading. Testes de banco provam as duas coisas. A fila de revisão é uma tabela no Postgres, não infraestrutura nova: ela guarda um punhado de nomes pendentes, e o que ela precisa é durabilidade, não vazão. Não tem nada em comum com a carga da abertura do mercado de que a Parte 5 trata.
  • Ranking: o ranking é uma materialized view com suas próprias regras de atualização (ADR-0014). Uma migration a recria com um join na view de nomes aprovados, então um nome aprovado substitui o rótulo neutro na próxima atualização. Um nome pendente, na fila ou rejeitado nunca substitui. A view é atualizada quando uma leitura a encontra com mais de 5 minutos, então um nome aprovado aparece em cerca de 5 minutos depois de uma visita. Não precisou de um novo modelo de leitura.
  • Decisões: o ADR-0027 registra as quatro opções, a escolha e o que a mudaria. O ADR-0020 agora diz que os nomes não estão mais adiados.
  • Custo: uma nova linha no registro de custos para os gastos do gateway, e um orçamento de US$ 5 no AI Gateway que corta o gasto antes de me surpreender, com um alerta em 75%. Ao custo medido, US$ 5 cobrem cerca de 311.000 decisões, então o limite existe para um bug ou um abuso, não para o uso normal.
  • Partes seguintes: o teste de carga da Parte 5 agora tem mais uma chamada externa no caminho dos nomes para considerar. A Parte 6 ganha mais uma decisão para avaliar.

As regras que mantêm a mudança dentro do escopo

Um escopo escrito num post se apaga. Um escopo escrito como regras quebra um build. Estas são quatro das novas linhas no arquivo ADL do Bullpen:

text
ASSERT(ONLY apps/web/app/lib/moderation READS AI_GATEWAY_API_KEY)
ASSERT(ONLY apps/web/app/lib/moderation READS ai-gateway.vercel.sh)
ASSERT(ONLY trading WRITES trading)
ASSERT(trading READS handles ONLY THROUGH handles.approved_v1)

Uma quinta proíbe o código de trading e de dados de mercado de importar o módulo de moderação. Cada regra tem um fixture que a quebra de propósito, e cada fixture falha a verificação com o arquivo, a linha e a regra que cruzou. Com o bônus, o check:arch roda 12 verificações sobre 61 regras, todas passando.

As regras pegaram uma coisa durante a construção, e era uma instrução minha. Eu tinha pedido que o ADR-0027 fosse escrito como Proposed. Mas o ADR-0016, da Parte 2, diz que só uma decisão Accepted pode colocar um novo sistema externo no modelo, e a verificação de atualidade do modelo falhou no momento em que o gateway apareceu. A correção honesta não era enfraquecer a verificação. A decisão de fato tinha sido tomada em 1º de novembro, antes da construção, quando aprovei a direção e o escopo, então o ADR-0027 está como Accepted e diz isso. A proteção me fez registrar o que realmente aconteceu.

O que a construção mediu

Rodei os 146 nomes uma vez cada, a partir da minha própria máquina, uma requisição por vez. O ponto mais próximo do gateway respondeu de Portland. Nenhuma chamada falhou. Três das quatro metas se mantiveram, e uma não:

MetaResultadoMedido
0 nomes a rejeitar aceitosCumprida0 de 40 com limiar de 0,90
No máximo 1 em 20 nomes limpos em revisãoNão cumprida17 de 106, cerca de 1 em 6
p95 abaixo de 500 msCumpridap50 de 156 ms, p95 de 215 ms, o mais lento 311 ms
Menos de US$ 0,0001 por decisãoCumpridaUS$ 0,00001608, do próprio campo de custo do gateway; US$ 0,0023 para os 146

O custo ficou cerca de 39% acima do exemplo da documentação e ainda seis vezes abaixo da meta. Os números de latência descrevem essa execução, sequencial, a partir da minha máquina. Não são o que um jogador espera: uma função da Vercel vai medir o próprio número, e a página soma o próprio tempo por cima.

Uma ressalva vale para todas as linhas. Os mesmos 146 nomes escolheram o limiar e depois mostraram o quanto ele funcionou, então este é um conjunto de desenvolvimento, não uma prova de como o modelo lida com jogadores reais. Um segundo conjunto, rotulado à parte e nunca usado para escolher o limiar, seria o teste honesto. Os envios reais também seriam, e a Parte 6 os relata.

A meta não cumprida é a parte interessante, e as notas mostram de onde ela vem. Os nomes limpos tiveram notas entre 0,78 e 0,97, com mediana de 0,94. Os nomes a rejeitar em geral tiveram notas baixas, com mediana de 0,25. Mas 4 dos 40 passaram de 0,8, e o mais alto chegou a 0,88. Os dois grupos se sobrepõem, então nenhum limiar cumpre as duas metas neste conjunto:

LimiarNomes a rejeitar aceitosNomes limpos em revisão
0,8042
0,8524
0,90017
0,95087

Com 0,85 a fila cumpre a meta, e dois nomes que escrevi para serem rejeitados chegam à página sem ninguém olhar. Com 0,90 nada ruim passa, e cerca de um nome limpo em cada seis espera por mim. O conjunto limpo está cheio de piadas de mercado, como um trocadilho com stop-loss, um caçador de fortunas e uma aposta no vermelho, então a hesitação do modelo não é absurda. Mas é uma meta não cumprida, uma meta que eu mesmo fixei, e o post valeria menos se eu mudasse a meta depois de ver o resultado.

Então mantenho 0,90. Um nome aceito por engano é público e custa confiança; um nome mandado à fila por engano custa a um jogador uma pequena espera e a mim um clique. Essa decisão é minha, como dono do requisito, e o ADR-0027 a registra com os números. A condição de mudança continua valendo: se a fila real passar de 10 nomes por semana, a pergunta é reescrita e medida de novo, ou a decisão muda.

A comparação com uma blocklist mostra mais a fraqueza da lista do que a força do modelo. Uma lista simples de trechos bloquearia Espresso_Eddie, porque contém "die", e Bass_Line, porque contém "ass". Ela não lê contexto. O modelo aprovou os dois. O inverso também aconteceu: LunaLovegood e Pork_Belly não contêm nada que uma lista de palavras pegaria, e o modelo mandou os dois para revisão, com 0,90 e 0,87, então ali o modelo é o mais rigoroso dos dois. O que esta medição não mostra é a metade que mais importa: se o modelo pega nomes nocivos que uma blocklist deixaria passar. Os resultados impressos não trazem a nota de cada nome a rejeitar, então não posso afirmar isso.

A primeira semana em produção traz os dois números que um conjunto de teste não dá: a fração dos envios reais que foi para revisão, e o tamanho da fila. Vou relatá-los na Parte 6, junto com as outras decisões que ela avalia.

Torne o custo visível

O hábito útil não é dizer sim a novos requisitos. É se recusar a responder antes de o preço estar na mesa. Um preço não é uma aceitação. Às vezes ele justifica um não, às vezes um "agora não", e às vezes, como aqui, mostra que o requisito era mais barato do que parecia em dinheiro e mais caro em duas coisas que eu não tinha precificado: um nome em cada seis esperando por uma pessoa, e nove dias das minhas próprias etapas. Se a opção do modelo tivesse custado um mês em vez de uma semana, eu teria escolhido "agora não".

O que tornou o "agora" seguro não foi o modelo. Foi escrever o escopo antes de construir, depois transformá-lo em regras, para que o requisito pudesse chegar no meio do plano sem levar o plano junto. Quando o próximo chegar no seu sistema, pergunte quanto custa antes de perguntar se deve fazer, e defina quem decide.

Continue lendo

Curtindo? Talvez goste disso aqui.

Nada parecido — quer tentar outro ângulo?

Arquitetando Software em 2026 · 5 de 7

A parte 6 está em produção.

Novas partes saem às segundas-feiras, 9h (horário do Pacífico) — deixe seu e-mail e eu envio cada uma no dia em que sair. Só isso.

Isso foi útil?

Deixe uma avaliação ou uma nota rápida — me ajuda a melhorar.

Posts Relacionados

Engineering

Database per service agora é de graça. Isso não é motivo para usar

Um free tier que dobra muda as entradas de uma decisão, não a decisão. Na primeira semana, previ que o Bullpen dividiria preços e histórico em dois serviços com dois bancos de dados. Depois o Neon fez database per service parecer de graça, e eu precisei decidir se isso mudava alguma coisa. Este é o histórico de decisões: cinco decisões de dados e deploy, cada uma com suas condições, as opções que pesei, por que uma venceu e o que me faria voltar atrás.

Engineering

Arquitetura como código tem três funções: descrever, governar, lembrar

Na primeira semana do Bullpen, o modelo de arquitetura planejado ainda citava um provedor de ações que eu tinha recusado, e uma mudança no rodapé e um teste o seguiram. Nada falhou. Uso esse incidente e outros dois para defender que arquitetura como código tem três funções: descrever o sistema, governá-lo e lembrar por que ele tem a forma que tem. Depois sigo a primeira mudança real após as correções, o analytics de produto, pelas três: um registro de decisão antes que o modelo pudesse citá-lo, arquitetura construída e planejada que discordavam, e uma regra que compõe as duas em vez de editar qualquer uma delas.

AI

O modelo de IA Jev não consegue alucinar. Ainda assim errou 37% dos e-mails.

O modelo de IA Jev não consegue devolver uma resposta fora do schema que você dá a ele, e a cobertura do lançamento transformou isso na promessa de que ele não consegue alucinar. Em um benchmark independente de phishing, o mesmo modelo errou 37,4% de 2.000 e-mails quando recebeu uma única pergunta ampla, enquanto cinco perguntas estreitas a ele, combinadas em código por uma regressão logística com validação cruzada, chegaram a 95,1%. Li a documentação e os evals da TypeSafe, rodei o adaptador do Pydantic AI e mapeei para que o Jev foi construído, onde as afirmações do lançamento se sustentam e onde eu o colocaria — ou não — em um pipeline.