TL;DR

Terceiro artigo da série sobre construir o Bloom, um agente de coaching multiagente, em uma semana. A maior parte da “rigidez” percebida no sistema não vinha da arquitetura multiagente — vinha da camada de prompt, e corrigi-la ali foi bem mais barato do que reformar o design.

  • Diagnosticar antes de reescrever: quando o Bloom “parecia robótico”, a causa raiz quase sempre estava em decisões específicas de prompt, não no coordinator ou nos especialistas. Vale conferir a camada de prompt antes de tocar em arquitetura.
  • Prompt mais natural: definir estados por objetivo de informação (“descubra a barreira, em 1 a 3 turnos”) em vez de script fixo (“pergunte X, depois Y”) sobrevive muito melhor à variação da conversa. Exemplos few-shot mudam o tom mais do que regras em prosa — demonstração generaliza melhor do que descrição. E limites suaves (“2 a 4 frases”) batem limites rígidos, desde que o teto seja real, não uma régua que o modelo aprende a contornar.
  • Honestidade estrutural: o modelo de dados precisa aceitar “desconhecido” (null) para que o sistema nunca seja forçado a fabricar uma resposta só para avançar de estado. Pelo mesmo motivo, categorizações ambíguas (como o objetivo do usuário no onboarding) devem ficar em aberto em vez de forçadas para o balde mais provável.
  • Fallback merece o mesmo cuidado que o caminho feliz: um bug real fez o Bloom vazar o system prompt cru para o chat quando a chamada ao LLM falhava — lembrete de que o caminho de erro precisa de design conversacional próprio, não só um “isso nunca vai acontecer”.
  • Segurança comportamental não pode depender só de prompt: regras como “nunca envergonhe o usuário” são blindadas em código, como uma passagem determinística sobre a saída de cada especialista — nomeando explicitamente os padrões de distorção a bloquear (pensamento tudo-ou-nada, autoculpa, supergeneralização), em vez de uma instrução vaga de tom. Com loops multi-etapa, essa checagem precisa avaliar a trajetória inteira da conversa, não só a última mensagem.

A régua do primeiro artigo reaparece aqui: tom conversacional e segurança comportamental são uma fronteira entre o que é decidido pelo código e o que é decidido pelo modelo, e blindar em código é o que impede essa fronteira de ser cruzada silenciosamente.


O prompt antes da arquitetura

Artigo 3 de uma série sobre o que aprendi construindo um agente de coaching multiagente em uma semana


No fim do artigo anterior, deixei uma observação pendente sobre o fato de forçar o modelo a responder sempre em um JSON de controle ({"confirmed": true}) engessava o tom da conversa de forma mensurável. Isso não foi um acidente de arquitetura — foi um sintoma de algo que se repetiu várias vezes ao longo da construção do Bloom: a maior parte das queixas de ‘rigidez’ do sistema não residia na arquitetura multiagente, estava na camada de prompt.

Esse exercício de descobrir a raiz do problema, acabou sendo, sozinho, uma das economias mais importantes do projeto. E é sobre isso o que vou abordar neste artigo.


Diagnostique antes de reescrever arquitetura

Quando o Bloom começou a ‘parecer robótico’ nas primeiras versões, a tentação óbvia seria repensar todo o design do coordinator e dos especialistas. Talvez eu deveria incluir mais agentes? Talvez reduzir os estados, ou então testar um roteamento diferente? Investigar a fundo mostrou que a rigidez percebida vinha, na maior parte, de decisões específicas de prompt. Então fazia sentido aplicar uma correção barata primeiro, validar e só então decidir se uma reforma maior de fato se justifica.

Essa disciplina evitou empregar energia (e tokens) em algo desnecessário. Um sistema com agentes de inteligência artificial pode ‘parecer errado’ mas ao invés de tocar na arquitetura, vale a pena conferir como você está pedindo para o modelo se comportar.

Defina estados por objetivo de informação, não por script

Um dos ajustes mais eficazes nos prompts dos especialistas foi trocar instruções do tipo “pergunte X, depois pergunte Y, depois pergunte Z” por instruções do tipo “descubra a barreira do aprendiz e sua severidade, em 1 a 3 turnos”.

A diferença parece sutil, mas muda tudo. Um estado definido por script (uma pergunta fixa por etapa) colapsa uma estratégia de entrevista desenhada — pergunta aberta, reflexão, afirmação — numa ficha de cadastro burocrática. Um estado definido por objetivo de informação sobrevive a muito mais variação conversacional: o usuário pode chegar naquela informação com uma única frase, ou em três, dependendo de como a conversa flui, e o sistema continua funcionando.

Exemplos few-shot movem tom mais do que regras

Adicionar 2 ou 3 turnos de exemplo por especialista fez mais pela naturalidade da conversa do que qualquer quantidade de instruções em prosa dizendo “seja caloroso” ou “seja empático”. Regras descrevem comportamento em abstrato; exemplos demonstram o comportamento concreto — e demonstração generaliza melhor no diálogo do que descrição.

Isso tem uma implicação prática direta para quem está ajustando o tom de qualquer agente: antes de escrever mais uma frase de instrução no system prompt, vale testar se um exemplo bem escolhido não resolve o mesmo problema com menos atrito.

Limites suaves superam limites rígidos — mas o limite precisa ser real

“Entre 2 e 4 frases” produziu respostas melhores do que um limite rígido de palavras. Um hard cap é tecnicamente satisfeito enquanto a resposta ainda parece densa, empacotada, artificial por causa do modelo que aprende a comprimir sem necessariamente ficar mais natural.

Mas o contraponto importa tanto quanto a regra: sem limite nenhum, as respostas do Bloom evoluíram para textos longos, cheios de emoji, estruturados em bullets aninhados — um muro de texto disfarçado de mensagem de chat. (Vou voltar a esse ponto específico no próximo artigo, porque a mesma dinâmica reapareceu, com outra causa raiz, quando testamos modelos diferentes.)

Assim como acontece na criação dos filhos, o modelo não deve ficar “sem limites”, deve ser “flexível e real”. Um teto que orienta sem se tornar uma régua que o modelo aprende a contornar tecnicamente.

Avance de estado só com resposta genuína, nunca por “turno concluído”

Um estado da conversa só deveria avançar quando a informação exigida foi de fato capturada — não simplesmente porque um turno se passou e é hora de seguir em frente. A alternativa funcional foi migrar o modelo de dados para aceitar explicitamente “desconhecido” (null), em vez de forçar o sistema a preencher algo.

Isso parece um detalhe técnico pequeno, mas resolve um problema comportamental real: sem essa opção, o sistema é estruturalmente forçado a fabricar ou chutar uma resposta que o usuário nunca deu, só para poder seguir adiante no fluxo. Dar ao sistema a opção de armazenar honestamente um dado nulo em vez de forçar um chute é, por sí só, uma benção.

Nunca force uma categorização que as palavras do usuário não sustentam

No onboarding do Bloom, a categoria do objetivo do usuário (por exemplo, um objetivo profissional versus pessoal) nunca é assumida — só é registrada quando as próprias palavras da pessoa deixam isso claro. Se está ambíguo, o sistema deixa em aberto, em vez de escolher o balde mais parecido.

Essa regra vale para qualquer etapa de classificação sob incerteza em um fluxo de onboarding ou intake: a tentação de “resolver” uma ambiguidade escolhendo a opção mais provável é, na prática, uma forma de inventar informação que o usuário não deu.

Caminhos de fallback precisam de design conversacional próprio

Quando a chamada ao LLM falhava, ou o sistema rodava em modo sem API key, o caminho de fallback simplesmente devolvia o conteúdo cru do arquivo de system prompt — vazando instrução interna diretamente para o chat do usuário. Um bug que gerava um pesadelo na experiência do usuário (UX).

   [Usuário] 👤  Quero começar meu planejamento.
   [Coach]   🤖 Olá! Aqui está o meu plano para você...
   
   ⚠️ [Aviso do Sistema: Falha na Conexão com LLM, Fallback Ativado]
   
   [Coach]   🤖 # Planning Specialist Instructions
                You are the Planning Specialist for Bloom. Your mission is to help the 
                learner co-create a weekly learning plan that feels realistic and owned...
                Today's date is {today}. Use this as the ground truth...
                The learner's primary goal is: "{primary_goal}"
                Weekly time budget: {weekly_time_budget_hours}

O ponto geral aqui é que o caminho de fallback de qualquer sistema agentico precisa ser escrito com a mesma intenção conversacional que o caminho principal, e não tratado como um ‘isso nunca deveria acontecer mesmo’ que ninguém desenha de verdade. E se prepare porque isso vai acontecer, só não com a mesma frequência do caminho feliz.

Blindar invariantes em código, não só em prompt

Chega o ponto em que prompt bem escrito não é suficiente. “Nunca envergonhe o usuário” é exatamente o tipo de regra que não deveria emergir de forma probabilística — porque, se depender só do prompt, ela vai evaporar silenciosamente em algum turno, em algum contexto que ninguém previu.

O Bloom mantém um filtro de segurança hardcoded como uma passagem final sobre a saída de cada especialista, independentemente do que o prompt diz. É uma rede de segurança determinística por cima de um comportamento que, no resto do tempo, é gerado probabilisticamente.

O contraste fica claro num exemplo real do projeto. Um tracker de hábito prescritivo, diante de uma sessão de estudo perdida, dispara algo como: “Alerta: você perdeu sua sessão das 14h. Seu streak de 12 dias foi quebrado. Reagende agora.” O Bloom, na mesma situação, responde algo como: “Notei que você perdeu a sessão desta tarde. A vida acontece — foi energia, trabalho inesperado, ou questão de horário?” Mesmo evento, duas posturas emocionais opostas — uma pune, a outra valida e mantém a pessoa engajada em co-criar um novo plano.

Nomeie os padrões de distorção que você está protegendo contra

Em vez de uma instrução vaga do tipo “seja empático” ou “seja encorajador”, o filtro de segurança do Bloom nomeia explicitamente os padrões que quer bloquear: pensamento tudo-ou-nada (“perdi uma aula, já fracassei no curso”), rotulação e autoculpa (“eu sou preguiçoso mesmo”), supergeneralização (“eu nunca consigo seguir uma agenda”).

Nomear o padrão específico e checar por ele explicitamente funciona muito melhor do que confiar numa instrução genérica de tom. É o mesmo princípio dos exemplos few-shot lá em cima: concreto generaliza melhor do que abstrato.

Aqui está a implementação real da classe SafetyFilter no arquivo safety.filter.ts contendo os padrões regex bloqueados:

export class SafetyFilter {
  private blockedPatterns: { label: string; regex: RegExp }[] = [
    {
      label: 'shame_or_guilt',
      regex: /\b(you should have|why didn't you|you failed to|why did you miss|you ought to)\b/i,
    },
    {
      label: 'absolutist_claims',
      regex: /\b(this always works|guaranteed to work|never fail|foolproof)\b/i,
    },
    {
      label: 'identity_assumptions',
      regex: /\b(as a developer|since you are a|because you are a)\b/i,
    },
    {
      label: 'medical_advice',
      regex: /\b(diagnose|clinical|therapy|depression|anxiety disorder|prescription|treatment)\b/i,
    },
    {
      label: 'productivity_extremism',
      regex: /\b(sleep less|no excuses|work harder|hustle harder|grind 24\/7|don't stop)\b/i,
    },
    {
      label: 'overdependence',
      regex: /\b(i know what's best for you|i will decide|you must obey|trust me completely)\b/i,
    },
  ];

  public scan(text: string): SafetyScanResult {
    const flags: string[] = [];
    for (const pattern of this.blockedPatterns) {
      if (pattern.regex.test(text)) {
        flags.push(pattern.label);
      }
    }
    return { passed: flags.length === 0, flags };
  }
}

Uma vez com loops multi-etapa, avalie a trajetória

Por fim, um ponto que só fica visível depois que o sistema ganha loops de múltiplas etapas (como vimos no artigo anterior): uma sequência de passos individualmente aceitáveis pode compor, no conjunto, um resultado prescritivo ou nocivo que nenhuma checagem de passo único pegaria. A avaliação de segurança precisa olhar para a trajetória inteira da conversa, não só para a última mensagem antes de responder.


O mesmo padrão, em nova roupagem

Prompt bem desenhado — objetivo de informação em vez de script, exemplos em vez de regras, limites suaves mas reais, avanço de estado condicionado a resposta genuína — resolve a maior parte da sensação de rigidez que um sistema agentico pode produzir. Mas invariantes de segurança comportamental não podem depender só disso.

A régua do primeiro artigo desta série reaparece aqui, só que vestida de outra roupa: tom conversacional e segurança comportamental são uma fronteira entre o que é decidido pelo código e o que é decidido pelo modelo — e blindar em código é exatamente o que garante que essa fronteira não seja rompida silenciosamente, turno a turno, sem que ninguém tenha decidido nada.

Fica uma pergunta em aberto para o último artigo desta série: e quando o próprio modelo por trás do agente muda — de um LLM grande para um modelo menor, local? Esse equilíbrio cuidadosamente construído no prompt sobrevive à troca, ou regride silenciosamente? É exatamente esse o assunto que fecha a série.