Blog

Feature flag em fluxo de agente: ligar comportamento novo para poucos

A flag existe justamente porque ninguém tem certeza de que o comportamento novo funciona, e num serviço tradicional isso é barato: a requisição entra, o if decide, a resposta sai, e se a variante nova for ruim você desliga a chave e a próxima requisição já nasce curada. Num fluxo de agente nada disso vale. A unidade de trabalho não é a requisição, é a conversa, que dura minutos ou dias, guarda estado entre os turnos e às vezes já executou uma ferramenta com efeito no mundo real. Avaliar a flag a cada turno faz o agente trocar de personalidade no meio do atendimento, perder ferramentas que já anunciou ao modelo e produzir um histórico onde metade das mensagens seguiu uma regra e a outra metade seguiu outra. Este artigo mostra por que a flag no agente precisa ser decidida uma vez e congelada na conversa, como escolher o ponto de avaliação, como separar as flags que podem virar no meio das que não podem, como matar a variante ruim sem interromper quem está no meio dela e o que registrar para que a comparação entre variantes signifique alguma coisa.

2026-08-05 / IA Aplicada / 14 min

01

Por que a flag do agente não se parece com a flag de um endpoint

Uma feature flag comum protege um trecho de código sem estado: o efeito da decisão começa e termina dentro da mesma requisição. Se a variante nova responde errado, o dano se limita àquela resposta, e virar a chave conserta tudo que vier depois. Um fluxo de agente rompe as duas premissas ao mesmo tempo. A decisão do turno três depende do que o agente disse no turno um, das ferramentas que o modelo viu declaradas, do resumo que foi gravado na memória da conversa e, se houve tool use, de um efeito que já aconteceu do lado de fora. Ligar um comportamento novo aqui não é trocar um ramo do if, é mudar as regras de um jogo que já está em andamento.

O sintoma mais comum é o agente esquizofrênico: a flag é avaliada a cada turno, o rollout está em vinte por cento, e o mesmo cliente cai na variante nova numa mensagem e na antiga na seguinte porque o sorteio foi aleatório em vez de ancorado. O agente muda de tom, contradiz o que ele mesmo prometeu duas mensagens atrás e às vezes oferece uma ação que na variante antiga não existe. Mesmo com âncora determinística por conversa, resta o segundo sintoma, mais silencioso: o rollout muda de porcentagem enquanto conversas estão vivas, e uma conversa que começou na variante nova de repente passa a ser resolvida pela antiga, com um histórico que já assumia o comportamento novo. A flag correta no agente não responde "qual variante agora", responde "qual variante esta conversa está usando desde que começou".

DimensãoFlag em endpointFlag em fluxo de agente
Unidade de decisãoA requisição, sem estado entre elasA conversa inteira, com estado acumulado entre turnos
Quando avaliarA cada chamada, o custo de reavaliar é zeroUma vez, na criação da conversa, e congelar o resultado
Efeito de virar a chaveA próxima requisição já usa o valor novoConversas em andamento continuam na variante que escolheram
Dano de uma variante ruimLimitado à resposta errada daquela chamadaEstado corrompido, ferramenta já executada, histórico inconsistente
RollbackDesligar a flag resolve tudoDesligar impede novas conversas; as vivas precisam de política própria

02

Decidir uma vez: a flag é resolvida na criação da conversa

A inversão que resolve a maior parte dos problemas é mover a avaliação da flag do turno para o início da conversa. No primeiro contato, o sistema resolve todas as flags relevantes de uma vez, grava o resultado junto do estado da conversa e nunca mais pergunta ao serviço de flags durante aquele atendimento. O turno três lê a decisão gravada, não recalcula. Isso dá consistência de graça: o agente não pode mudar de comportamento no meio porque a variante já está escrita no estado, e nem uma mudança de rollout nem uma indisponibilidade do serviço de flags conseguem alterá-la.

A âncora do sorteio precisa ser o identificador da conversa, não o do usuário e nem um número aleatório. O identificador do usuário parece atraente porque dá consistência entre conversas, mas amarra o cliente à mesma variante para sempre e enviesa a comparação: se um cliente heavy user caiu na variante nova, todas as conversas difíceis dele contam para o mesmo lado. Ancorar na conversa distribui melhor e ainda permite que o mesmo cliente participe das duas variantes ao longo do tempo. O aleatório puro é o pior dos três porque perde a reprodutibilidade: com um hash do identificador da conversa você recalcula a decisão em qualquer lugar do sistema e sempre chega no mesmo resultado, o que salva a análise quando o campo gravado se perde.

// agent/flags.js
// A flag do agente e resolvida UMA VEZ, na criacao da conversa, e o
// resultado vira parte do estado. Turnos seguintes leem, nunca reavaliam.

import { createHash } from 'node:crypto';

// Ancora deterministica na conversa: mesma conversa, mesmo bucket,
// em qualquer instancia e em qualquer momento futuro.
function bucketOf(conversationId, flagKey) {
  const h = createHash('sha256').update(flagKey + ':' + conversationId).digest();
  return h.readUInt16BE(0) % 100; // 0..99
}

// Chamada apenas no primeiro turno, quando a conversa e criada.
export function resolveFlagsForConversation(config, conversationId, context) {
  const decided = {};
  for (const [key, flag] of Object.entries(config)) {
    if (!flag.enabled) {
      decided[key] = flag.fallback;
      continue;
    }
    // Alvos explicitos (cliente interno, conta de teste) ignoram a fatia.
    if (flag.allowlist?.includes(context.accountId)) {
      decided[key] = flag.variant;
      continue;
    }
    decided[key] = bucketOf(conversationId, key) < flag.rolloutPct
      ? flag.variant
      : flag.fallback;
  }
  // Guardado junto do estado da conversa: e isto que os turnos leem.
  return { decidedAt: context.now, configVersion: config.version, values: decided };
}

// Turnos 2..N: le a decisao congelada. Se faltar, a conversa e antiga
// e nasceu antes da flag existir: cai no fallback, nunca sorteia de novo.
export function flagValue(conversationState, key, fallback) {
  return conversationState.flags?.values?.[key] ?? fallback;
}

Vale registrar junto da decisão a versão da configuração que a produziu. Sem isso, uma conversa iniciada segunda-feira e outra iniciada quarta podem ter caído na mesma variante por motivos diferentes, e a análise não consegue separar as duas populações quando alguém mexeu na porcentagem no meio. Com a versão gravada, cada conversa carrega a prova de qual configuração a decidiu, e o corte por versão vira uma consulta trivial.

03

Nem toda flag pode congelar: separe as três famílias

Congelar tudo na criação da conversa seria simples, mas está errado para uma parte das flags. Um kill switch de segurança que só pode ser congelado é inútil: se o comportamento novo está gerando resposta perigosa, você precisa que ele pare agora, inclusive nas conversas em andamento. A regra prática é classificar cada flag por uma pergunta: uma virada no meio da conversa quebra a coerência do que já foi dito ou executado? Se sim, congela. Se não, pode ser dinâmica. E existe uma terceira família, a que precisa ser dinâmica mesmo quebrando coerência, porque a alternativa é pior.

FamíliaQuando avaliarExemplosRisco de virar no meio
Congelada na conversaUma vez, na criação, gravada no estadoPrompt de sistema, conjunto de ferramentas, política de escalonamento, formato de respostaAlto: contradiz o histórico, invalida ferramenta já anunciada
Dinâmica por turnoA cada turno, lendo o valor atualRoteamento de modelo, limite de tokens, timeout, tamanho do retrievalBaixo: muda o custo e a latência, não a semântica do que foi dito
Kill switchA cada turno, com precedência sobre a congeladaDesligar uma ferramenta perigosa, cortar um comportamento que gerou incidenteAceito de propósito: parar agora vale mais que a coerência

O detalhe que costuma passar batido é a ferramenta declarada. Quando o agente anuncia ao modelo um conjunto de ferramentas no turno um e a flag remove uma delas no turno três, o modelo pode ter planejado uma sequência que agora não existe mais, e o resultado é uma chamada a uma ferramenta desconhecida ou um plano abandonado no meio. Se a remoção veio de um kill switch, o certo não é sumir com a ferramenta em silêncio: é devolver ao modelo um erro explícito e tratável, do tipo "esta ferramenta está indisponível agora", para que ele replaneje em vez de quebrar. A diferença entre um kill switch usável e um que gera incidente próprio está quase toda nessa resposta.

// agent/tools.js
// Kill switch tem precedencia sobre a decisao congelada, mas nao pode
// sumir com a ferramenta em silencio: o modelo ja pode ter planejado
// com ela. Devolver erro tratavel e o que permite o replanejamento.

export function toolsForTurn(conversationState, liveConfig) {
  // Conjunto congelado no inicio da conversa.
  const frozen = conversationState.flags.values.toolset ?? [];
  // Kill switches sao lidos agora, no turno atual.
  const killed = new Set(liveConfig.killedTools ?? []);
  return frozen.filter((tool) => !killed.has(tool.name));
}

export function executeTool(conversationState, liveConfig, call) {
  const killed = new Set(liveConfig.killedTools ?? []);
  if (killed.has(call.name)) {
    // Erro de dominio, nao excecao: volta como resultado da ferramenta
    // para o modelo replanejar dentro do mesmo turno.
    return {
      ok: false,
      error: 'tool_unavailable',
      message: 'Ferramenta indisponivel no momento. Prossiga sem ela.',
    };
  }
  return runTool(call);
}

04

Ligar para poucos: escolher quem entra sem enviesar a leitura

A porcentagem é o mecanismo mais visível do rollout, mas raramente é por onde se deve começar. Antes de dar cinco por cento do tráfego para a variante nova, vale rodar em uma lista explícita: contas internas, o time de suporte, um cliente que topou testar. Essa fase pega o erro grosseiro, o que aparece na primeira conversa, sem gastar nenhum cliente real. A lista explícita também precisa ser tratada à parte na análise, porque quem sabe que está testando se comporta diferente e contamina qualquer métrica de qualidade.

Quando a porcentagem entra, o cuidado é com o que a fatia representa. Um rollout de cinco por cento sobre todo o tráfego pega em maior proporção os assuntos frequentes e quase nunca os raros, que são justamente onde o comportamento novo costuma falhar. Segmentar antes de sortear ajuda: aplicar a fatia dentro de um tipo de atendimento, de um horário ou de um segmento de cliente dá uma amostra que representa aquele recorte, em vez de uma amostra dominada pelo caso mais comum. E existe o recorte inverso, o mais importante de todos: nem toda conversa deve ser elegível. Um cliente em processo de cancelamento, um atendimento que já passou por escalonamento, uma conversa marcada como sensível, todos esses ficam fora por decisão explícita, não por sorte do hash.

Elegibilidade antes do sorteio, decidida uma vez

  nova conversa
       |
       v
  [ e elegivel? ]  conta sensivel / ja escalada / cancelamento
       |  nao -> fallback (nunca entra no experimento)
       v sim
  [ esta na allowlist? ]  interno / cliente piloto
       |  sim -> variante nova (marcado como nao-amostra)
       v nao
  [ hash(conversationId + flagKey) % 100 < rolloutPct ]
       |  sim -> variante nova     -> grava no estado
       |  nao -> variante estavel  -> grava no estado
       v
  turnos 2..N leem o estado, nao reavaliam

  mudar rolloutPct daqui pra frente:
    afeta apenas conversas NOVAS; as vivas ficam onde estao

O corolário do congelamento é que subir a porcentagem não converte ninguém. Ao passar de cinco para vinte por cento, as conversas que já estavam rodando continuam onde estavam, e só as novas sentem a mudança. Isso é desejável, mas tem uma consequência operacional: o efeito de um aumento demora a aparecer nas métricas, porque leva o tempo de vida médio de uma conversa até que a nova fatia esteja de fato representada. Subir a porcentagem e olhar o painel dez minutos depois mede quase nada, sobretudo em canais assíncronos onde uma conversa pode ficar aberta por dias.

05

Desligar a variante ruim sem interromper quem está no meio dela

Quando a variante nova se mostra ruim, existem três políticas possíveis para as conversas que já estão nela, e escolher errado transforma um problema em dois. Deixar todas terminarem é a opção mais coerente e a mais lenta: o dano continua acontecendo, limitado à fatia, até a última conversa fechar. Converter todo mundo para a variante estável agora é o mais rápido e o mais violento: um agente que anunciou uma ferramenta ou prometeu um formato pode não conseguir cumprir, e o cliente vê a mudança. A terceira, que costuma ser a resposta certa, é a conversão em ponto seguro: a conversa continua na variante nova até chegar a um estado onde a troca não contradiz nada, tipicamente logo depois de uma resposta completa e antes do próximo turno, e ali migra.

  1. Bloqueie a entrada primeiro: zere a fatia para que nenhuma conversa nova caia na variante ruim. Isso é instantâneo e não quebra ninguém.
  2. Classifique o dano: se a variante gera resposta perigosa ou executa ação errada, é kill switch e as conversas vivas precisam parar agora, aceitando a incoerência.
  3. Se o dano for de qualidade e não de segurança, marque as conversas vivas para conversão em ponto seguro em vez de trocar no meio de um turno.
  4. Defina o ponto seguro pelo estado, não pelo tempo: depois de uma resposta final entregue, sem tool call pendente e sem plano em aberto.
  5. Registre a conversão no estado da conversa, com o motivo e o instante, para que a análise não misture o trecho antigo com o novo na mesma amostra.
  6. Ao converter, injete no contexto uma nota curta de que a capacidade mudou, para o modelo não reafirmar algo que ele prometeu sob a variante anterior.

A conversão em ponto seguro exige que a conversa saiba dizer se está num ponto seguro, o que é outra forma de dizer que o estado do agente precisa ser explícito. Se o sistema não consegue responder "existe tool call pendente" ou "existe plano em aberto", a única política honesta é deixar terminar, porque qualquer conversão será às cegas. Isso costuma ser o argumento decisivo para modelar o estado do agente como uma máquina pequena e legível em vez de deixá-lo implícito no histórico de mensagens: sem isso, o rollback fica sem opção intermediária entre esperar e quebrar.

06

Medir a variante: sem atribuição por conversa, o rollout é fé

A flag só vale se a comparação entre variantes for confiável, e no agente a maioria das métricas úteis só existe no nível da conversa. Custo por resposta engana porque a variante nova pode gastar mais por turno e resolver em menos turnos, saindo mais barata no total. Qualidade por mensagem engana pelo mesmo motivo. As métricas que decidem são de desfecho: a conversa foi resolvida sem humano, quantos turnos levou, quanto custou do início ao fim, se o cliente voltou nas próximas horas com o mesmo assunto. Todas elas precisam da variante gravada no registro da conversa para poderem ser quebradas por lado.

MétricaUnidade corretaPor que a unidade errada mente
CustoSoma de toda a conversaPor turno, a variante que resolve em menos turnos parece cara
Resolução sem humanoDesfecho da conversaPor mensagem, não existe: escalonamento é evento único no fim
LatênciaPercentil por turno, comparado só entre turnos equivalentesMédia entre variantes com número de turnos diferente compara coisas distintas
Retorno do clienteJanela após o fechamento da conversaDentro da conversa não aparece: o problema volta depois, em outro atendimento
Erro de ferramentaPor conversa, contando se houve pelo menos umPor chamada, um retry ruidoso vira dez erros e domina o total

Uma armadilha específica do agente é a conversa que atravessa uma mudança de configuração. Se ela começou sob a versão dois da flag e foi convertida sob a versão três, ela não pertence limpo a nenhum dos dois lados, e incluí-la em qualquer um dos grupos suja a comparação. O caminho honesto é marcar essas conversas como contaminadas e analisá-las à parte: se forem poucas, saem da amostra principal; se forem muitas, isso em si é o sinal de que as mudanças estão acontecendo rápido demais para o tempo de vida das conversas, e o rollout precisa desacelerar em vez de continuar produzindo dados que ninguém consegue ler.

// analytics/variant.js
// Atribuicao por conversa: a variante e um campo do desfecho, nao um
// campo de cada mensagem. Conversa que atravessou mudanca de config
// nao pertence a nenhum lado e sai da amostra principal.

export function conversationOutcome(state) {
  const flags = state.flags ?? {};
  const converted = Boolean(state.flagConversion);

  return {
    conversationId: state.id,
    variant: flags.values?.agentFlow ?? 'stable',
    configVersion: flags.configVersion ?? null,
    // Marca a contaminacao em vez de descartar em silencio: o volume
    // de contaminadas e por si so um sinal sobre o ritmo do rollout.
    contaminated: converted,
    resolvedWithoutHuman: state.escalatedAt == null,
    turns: state.turns.length,
    totalCostUsd: state.turns.reduce((sum, t) => sum + (t.costUsd ?? 0), 0),
    toolErrored: state.turns.some((t) => t.toolError === true),
    closedAt: state.closedAt,
  };
}

// A comparacao usa somente as nao contaminadas; as demais viram um
// relatorio proprio, que serve para decidir se o rollout esta rapido demais.
export function compareVariants(outcomes) {
  const clean = outcomes.filter((o) => !o.contaminated && o.closedAt != null);
  const byVariant = new Map();
  for (const o of clean) {
    const g = byVariant.get(o.variant) ?? { n: 0, resolved: 0, cost: 0, turns: 0 };
    g.n += 1;
    g.resolved += o.resolvedWithoutHuman ? 1 : 0;
    g.cost += o.totalCostUsd;
    g.turns += o.turns;
    byVariant.set(o.variant, g);
  }
  return [...byVariant].map(([variant, g]) => ({
    variant,
    conversations: g.n,
    resolutionRate: g.resolved / g.n,
    avgCostUsd: g.cost / g.n,
    avgTurns: g.turns / g.n,
  }));
}

07

Higiene: a flag temporária que virou arquitetura permanente

Flag de agente apodrece mais rápido que flag comum porque cada uma bifurca o comportamento inteiro, não um trecho. Duas flags de fluxo já dão quatro combinações de prompt, ferramentas e política de escalonamento, e ninguém testou as quatro. Três dão oito. Como o efeito de cada uma só aparece em conversa real, a combinação que ninguém testou é também a que ninguém consegue reproduzir quando um cliente reclama. Por isso a flag de fluxo precisa de prazo desde o nascimento: uma data de expiração declarada na própria configuração e um alerta quando ela passar, tratando a flag vencida como dívida visível em vez de um if esquecido.

  • Declare a data de expiração junto da flag e faça o CI reclamar quando ela vencer, do mesmo jeito que reclamaria de um teste quebrado.
  • Limite quantas flags de fluxo podem estar abertas ao mesmo tempo: cada uma multiplica as combinações que ninguém testou.
  • Ao chegar em cem por cento, remova o ramo antigo no mesmo ciclo. Uma flag em cem por cento que permanece é código morto que ainda pode ser ligado por engano.
  • Guarde o valor da flag no estado das conversas encerradas mesmo depois de remover a flag do código, senão a análise histórica perde o corte.
  • Nunca aninhe uma flag de fluxo dentro de outra: se o comportamento novo depende do outro comportamento novo, é uma flag só, com duas etapas.

A remoção também é uma operação com estado. Apagar a flag do código enquanto existem conversas vivas que a gravaram no estado faz o leitor cair no fallback no meio do atendimento, exatamente a troca não anunciada que o congelamento existia para evitar. A ordem segura é a inversa da introdução: primeiro leve a fatia a cem por cento e espere o tempo de vida das conversas passar, depois torne o ramo novo o comportamento padrão sem consultar a flag, e só então apague a configuração. O campo gravado nas conversas antigas continua lá, inofensivo, e é ele que mantém a análise histórica possível.

FAQ

Perguntas frequentes

Por que não avaliar a feature flag a cada turno do agente, como faço num endpoint?

Porque a unidade de trabalho do agente é a conversa, não a requisição, e o estado atravessa os turnos. Avaliando a cada turno, uma mudança de porcentagem ou um sorteio não ancorado fazem o mesmo cliente cair na variante nova numa mensagem e na antiga na seguinte: o agente muda de tom, contradiz o que prometeu antes e pode oferecer uma ação que na outra variante não existe. Pior, se o conjunto de ferramentas mudar no meio, o modelo pode ter planejado uma sequência que deixou de ser possível. A saída é resolver todas as flags de fluxo uma única vez, na criação da conversa, gravar o resultado junto do estado e fazer os turnos seguintes lerem esse valor congelado em vez de perguntar de novo ao serviço de flags. Assim nem uma virada de rollout nem uma indisponibilidade do serviço conseguem mudar o comportamento no meio do atendimento.

Como desligar uma variante ruim sem quebrar as conversas que já estão nela?

Em duas etapas separadas. A primeira é sempre a mesma e é instantânea: zerar a fatia para que nenhuma conversa nova entre na variante ruim, o que não afeta ninguém que já está dentro. A segunda depende do tipo de dano. Se a variante gera resposta perigosa ou executa ação errada, é caso de kill switch e as conversas vivas precisam parar agora, aceitando a incoerência como preço; nesse caso, uma ferramenta removida deve devolver ao modelo um erro tratável do tipo indisponível, e não sumir em silêncio, para que ele replaneje. Se o dano for de qualidade e não de segurança, o melhor é a conversão em ponto seguro: a conversa continua na variante nova até chegar a um estado onde a troca não contradiz nada, tipicamente após uma resposta final entregue, sem tool call pendente e sem plano em aberto, e migra ali, com a conversão registrada no estado.

Qual é a métrica certa para comparar duas variantes de fluxo de agente?

As de desfecho, medidas por conversa inteira, não por mensagem. Custo por turno engana porque a variante nova pode gastar mais em cada turno e resolver em menos turnos, saindo mais barata no total; o número que decide é o custo somado do início ao fim da conversa. Da mesma forma, resolução sem humano é um evento único no desfecho e não existe por mensagem, e o retorno do cliente com o mesmo assunto só aparece numa janela depois do fechamento. Erro de ferramenta deve ser contado como "houve pelo menos um" por conversa, senão um retry ruidoso vira dez erros e domina o total. Para tudo isso funcionar, a variante precisa estar gravada no registro da conversa, e as conversas que atravessaram uma mudança de configuração precisam ser marcadas como contaminadas e analisadas à parte.

A flag do agente pertence à conversa, não ao turno

Um fluxo de agente quebra as duas premissas que tornam a feature flag comum barata: a decisão não termina na requisição e o estado atravessa os turnos. Resolver a flag uma vez na criação da conversa, ancorada no identificador dela e gravada junto do estado, dá consistência de graça e faz uma mudança de rollout afetar apenas as conversas novas. Separar as flags congeladas das dinâmicas e do kill switch define o que pode virar no meio, e a conversão em ponto seguro dá a opção intermediária entre esperar e quebrar quando a variante se mostra ruim. Com a variante gravada no desfecho e as conversas contaminadas fora da amostra, a comparação entre lados passa a significar alguma coisa. Posso montar esse controle de rollout no seu fluxo de agente, do congelamento na criação da conversa à atribuição por desfecho, para você ligar comportamento novo para poucos sem descobrir o problema pelo cliente.