Blog

Migração de modelo sem quebrar o prompt em produção

Trocar o modelo parece a mudança mais barata de um sistema com LLM: uma string de configuração, um deploy, pronto. Só que o prompt em produção não é um texto neutro que qualquer modelo lê do mesmo jeito. Ele foi lapidado durante meses contra um modelo específico, e boa parte do que faz ele funcionar não está escrito: está no jeito daquele modelo interpretar uma instrução ambígua, no formato que ele já devolve sem que ninguém peça, na quantidade de exemplos que ele precisa antes de acertar. Trocar o modelo remove esse acordo tácito, e o que quebra não é o prompt inteiro, são as bordas: o JSON que agora vem embrulhado em markdown, a recusa que ficou mais conservadora, a resposta que dobrou de tamanho e estourou o orçamento de tokens. Este artigo mostra como migrar sem apostar: caracterizar o que o prompt depende do modelo antigo, rodar o candidato em tráfego sombra sem risco ao cliente, comparar por contrato e por juízo, adaptar o prompt em vez de reescrevê-lo do zero e fazer o corte com rollback pronto.

2026-07-26 / IA Aplicada / 14 min

01

O prompt não é portátil: ele foi ajustado contra um modelo

Todo prompt maduro carrega uma dívida invisível com o modelo em que nasceu. As instruções explícitas são a parte pequena; a parte grande é o comportamento que o modelo antigo entregava por padrão e por isso nunca precisou virar regra. Se aquele modelo já devolvia JSON limpo quando o prompt pedia "responda em JSON", ninguém escreveu "sem cercas de código, sem texto antes ou depois". Se ele já era conciso, ninguém colocou um teto de tamanho. Se ele raramente recusava, ninguém definiu o que fazer quando a recusa chega. Cada um desses silêncios é uma suposição, e a troca de modelo testa todas de uma vez. O resultado típico não é um sistema que para: é um sistema que continua respondendo, com uma fatia dos casos saindo errada de um jeito que só aparece na métrica dias depois.

Por isso a migração não começa escolhendo o modelo novo, começa listando as suposições. O exercício é ler o prompt e o código em volta perguntando, para cada comportamento de que o sistema depende, se ele está escrito ou se está apenas acontecendo. O que está escrito viaja com o prompt; o que está apenas acontecendo é dívida que vence na troca. Essa lista vira a espinha do teste de migração, porque cada suposição não escrita é exatamente um caso de regressão a verificar no candidato. Fazer isso antes de trocar transforma a migração de um mistério em uma checklist finita, e costuma render o efeito colateral de melhorar o prompt no modelo atual: uma suposição que virou instrução explícita reduz variância mesmo sem trocar nada.

Suposição tácitaComo quebra no modelo novoComo tornar explícita
O modelo devolve JSON puroVem embrulhado em cerca de código ou com um preâmbuloInstrução de formato mais parser tolerante e validação de schema
A resposta é curta por naturezaDobra de tamanho e estoura custo e janelaTeto explícito de tamanho e verificação no gate
Recusa é rara e previsívelFica mais conservador e recusa caso legítimoCasos de fronteira no dataset e política de fallback declarada
Chama a ferramenta certa sem hesitarDescreve a ação em texto em vez de emitir a tool callDescrição de tool mais afiada e asserção de tool call no eval
Segue a ordem das etapas do promptPula ou funde etapas em respostas longasSaída estruturada por campo em vez de texto sequencial

02

Caracterizar o comportamento atual antes de comparar

Comparar candidato com atual exige saber o que o atual faz, e essa informação quase nunca está documentada: está espalhada em logs. O primeiro passo concreto é montar um dataset de caracterização a partir do tráfego real, não de casos inventados. Uma amostra estratificada dos últimos dias, cobrindo os tipos de pedido na proporção em que aparecem mais os casos raros que importam, com a entrada completa e a resposta que o modelo atual deu. Esse conjunto não é um gabarito de respostas certas, é um retrato do comportamento vigente, e é contra ele que a pergunta da migração fica respondível: o candidato faz o mesmo trabalho, ou faz um trabalho diferente que ninguém pediu.

A amostra precisa de duas propriedades para não enganar. A primeira é cobertura das caudas: os casos raros são justamente onde a diferença entre modelos aparece, então uma amostra puramente aleatória, dominada pelo caso comum, dá uma sensação falsa de paridade. A segunda é ancoragem em resultado real onde ele existe: quando o sistema sabe depois se a resposta resolveu, porque houve escalonamento para humano, reclamação, retrabalho ou conversão, esse sinal deve viajar junto com o caso. Assim o dataset mistura casos com verdade conhecida, que viram asserção objetiva, e casos sem, que dependem de comparação. Uma última regra prática: o dataset carrega dado real de cliente e deve nascer já com redação de dados pessoais, porque ele vai ser lido por gente e enviado para um provedor novo, que é exatamente a operação que a migração está avaliando.

  • Amostra estratificada por tipo de pedido, na proporção real do tráfego, com sobrerrepresentação deliberada dos casos raros e caros.
  • Entrada completa como o modelo a recebe: prompt renderizado, contexto recuperado, histórico e definições de ferramenta, não apenas a mensagem do usuário.
  • Resposta atual salva junto, com o id da versão de prompt, o modelo e os parâmetros que a produziram, para a comparação ser entre coisas comparáveis.
  • Sinal de resultado quando existir: escalonou, foi corrigido, resolveu, para separar caso com verdade conhecida de caso que depende de juízo.
  • Redação de dados pessoais na origem, antes de o dataset sair do sistema, porque ele será lido por humanos e enviado a um provedor novo.

03

Tráfego sombra: rodar o candidato sem que o cliente veja

O dataset offline pega a regressão previsível, mas tráfego real tem formas que nenhuma amostra antecipa: mensagens truncadas, contexto gigante, sequências de ferramentas que só acontecem numa jornada específica. O tráfego sombra resolve isso rodando o candidato sobre o tráfego de produção ao vivo, em paralelo com o modelo atual, sem que a resposta dele chegue ao cliente. O que o usuário recebe continua vindo do modelo estável; a resposta do candidato é apenas gravada ao lado, formando pares comparáveis das mesmas entradas nas mesmas condições. É a única forma de medir o candidato contra a realidade sem que um erro dele custe alguma coisa.

Três cuidados fazem o tráfego sombra ser seguro em vez de uma nova fonte de incidentes. O primeiro é isolamento de efeito colateral: a execução sombra nunca pode disparar tool call que escreve, então as ferramentas com efeito precisam rodar num modo simulado que registra a intenção sem executar, e comparar intenções continua sendo suficiente para avaliar. O segundo é isolamento de recurso: a sombra não pode competir por cota nem por latência com o caminho do cliente, o que significa fila separada, execução assíncrona depois de responder ao usuário e permissão para descartar quando houver pressão. O terceiro é custo: sombra em cem por cento do tráfego dobra a fatura de inferência, então o normal é amostrar uma fatia, priorizando os tipos de caso onde a diferença importa mais.

Trafego sombra durante a migracao

  requisicao do cliente
        |
        +--> modelo ATUAL (estavel) --> resposta --> CLIENTE
        |
        +--> copia amostrada --> fila separada (assincrona)
                                      |
                                      v
                              modelo CANDIDATO
                              tools em modo simulado
                              (registra intencao, nao executa)
                                      |
                                      v
                              par {atual, candidato} gravado
                                      |
                                      v
                       comparacao: contrato + juiz + custo/latencia
                                      |
                          relatorio de paridade por caso de uso

04

Comparar por contrato antes de comparar por qualidade

Com os pares em mãos, a tentação é ir direto perguntar qual resposta é melhor. É a ordem errada, e cara. A maior parte das quebras de migração não é de qualidade, é de contrato: JSON que não parseia, campo obrigatório ausente, valor fora do enum, tool call que virou texto, resposta que estourou o teto de tamanho. Essas falhas são detectáveis por código, custam quase nada e são determinísticas. Rodá-las primeiro filtra o grosso do problema antes de gastar chamada de juiz, e dá um diagnóstico acionável: não é "o modelo novo é pior", é "em onze por cento dos casos ele embrulha o JSON em cerca de código", que é uma linha de instrução e um parser mais tolerante, não uma reescrita.

Só o que passa no contrato merece comparação de qualidade, e mesmo aí a comparação precisa ser desenhada para não se enganar sozinha. Um juiz que recebe "resposta A" e "resposta B" tende a preferir a mais longa e a que vem primeiro, então a posição deve ser embaralhada por caso e o critério precisa ser explícito sobre o que conta: resolve o pedido, respeita a política, não inventa fato que não está no contexto. E o juízo deve ser calibrado contra um punhado de casos rotulados por gente antes de valer como métrica, senão a migração troca uma incerteza por outra. Um detalhe que costuma passar batido: se o modelo candidato for da mesma família do juiz, existe viés de autopreferência, e vale usar um juiz de outra família ou rotular à mão a fatia decisiva.

// compare/contract.js
// Ordem certa: checagens de contrato primeiro (baratas, deterministicas),
// juiz so no que sobrevive. A maioria das quebras de migracao e de
// formato, nao de qualidade.

const FENCE = /^```(?:json)?\s*([\s\S]*?)\s*```$/;

// Parser tolerante: aceita o JSON embrulhado, mas REGISTRA o desvio.
// Tolerar sem medir esconde a regressao que o gate precisa ver.
export function parseStructured(raw) {
  const text = String(raw ?? '').trim();
  const fenced = text.match(FENCE);
  const body = fenced ? fenced[1] : text;
  try {
    return { ok: true, value: JSON.parse(body), fenced: Boolean(fenced) };
  } catch {
    return { ok: false, value: null, fenced: Boolean(fenced) };
  }
}

export function checkContract(output, spec) {
  const violations = [];
  const parsed = parseStructured(output.text);

  if (!parsed.ok) violations.push('json_invalido');
  if (parsed.fenced) violations.push('json_em_cerca_de_codigo');

  if (parsed.ok) {
    for (const field of spec.requiredFields) {
      if (parsed.value[field] === undefined) violations.push('campo_ausente:' + field);
    }
    for (const [field, allowed] of Object.entries(spec.enums ?? {})) {
      const v = parsed.value[field];
      if (v !== undefined && !allowed.includes(v)) violations.push('enum_invalido:' + field);
    }
  }

  // Tool call que virou texto e a quebra mais silenciosa: o sistema
  // segue respondendo, so parou de agir.
  if (spec.expectsToolCall && !output.toolCalls?.length) violations.push('tool_call_ausente');
  if (spec.maxOutputTokens && output.usage.outputTokens > spec.maxOutputTokens) {
    violations.push('estourou_teto_de_tamanho');
  }

  return { pass: violations.length === 0, violations, parsed };
}

05

Adaptar o prompt em vez de reescrevê-lo

Quando o relatório aponta as diferenças, a reação errada é reescrever o prompt do zero para o modelo novo. O prompt atual codifica anos de casos de borda descobertos em produção, e uma reescrita joga esse conhecimento fora para resolver problemas de formato. A abordagem que funciona é cirúrgica: para cada violação de contrato recorrente, uma instrução explícita que fecha a suposição que estava tácita, e nada mais. JSON embrulhado vira uma linha proibindo cerca de código somada a um parser que a tolera; resposta longa demais vira um teto declarado; recusa em caso legítimo vira um exemplo de fronteira mostrando o comportamento esperado. Cada mudança dessas é pequena, verificável no dataset e reversível.

A regra que evita o efeito sanfona é validar cada ajuste nos dois modelos, não só no candidato. Uma instrução adicionada para consertar o modelo novo pode piorar o antigo, e como o antigo continua servindo o cliente durante toda a migração, uma regressão nele é dano imediato. O caminho seguro é tratar o prompt adaptado como uma versão candidata no mesmo esquema de versionamento que o resto: id imutável, dataset rodando contra as duas combinações de modelo e prompt, promoção só quando a nova combinação empata ou ganha e a antiga não piora. E quando a diferença for grande demais para ser fechada com ajustes pontuais, a decisão honesta é manter duas variantes do prompt, uma por modelo, no mesmo registro, com o custo de manutenção explícito em vez de fingir que uma serve para as duas.

  1. Agrupe as violações por tipo, não por caso: dez falhas de JSON embrulhado são um problema, não dez, e se resolvem com uma instrução.
  2. Feche cada suposição tácita com a menor mudança possível de prompt, uma por vez, medindo o efeito isolado no dataset.
  3. Rode cada ajuste nas duas combinações de modelo e prompt: o antigo ainda atende o cliente e não pode regredir durante a migração.
  4. Prefira mudar o parser e a validação quando o desvio for de formato tolerável, e mudar o prompt quando for de comportamento.
  5. Se restar diferença estrutural, mantenha duas variantes de prompt por modelo, com custo de manutenção declarado, em vez de forçar uma só.

06

O gate de paridade e o corte com rollback pronto

Migração precisa de um critério de parada escrito antes do resultado, senão a decisão vira negociação com o próprio viés. Esse critério é o gate de paridade, e ele é por caso de uso, não global. Uma média agregada esconde exatamente o que importa: o candidato pode ficar dois por cento melhor no geral e vinte por cento pior na fatia que envolve cobrança, e a média não conta isso. O gate declara, para cada caso de uso, o limite de queda tolerável em taxa de contrato válido, em qualidade julgada, em escalonamento para humano, e o teto de piora aceitável em custo e latência. Um caso de uso que não passa não bloqueia os outros: bloqueia a si mesmo, e o roteamento pode manter aquele caso no modelo antigo enquanto o resto migra.

Com o gate verde, o corte segue o mesmo desenho de qualquer mudança arriscada: fatia pequena e determinística por usuário para a conversa não trocar de modelo no meio, subida em degraus vigiada por métrica, e rollback como troca de ponteiro em vez de redeploy. A diferença é que aqui há um detalhe fácil de esquecer com custo alto: cache. Cache semântico e cache de prompt carregam respostas produzidas pelo modelo antigo, e servir uma dessas para tráfego que já está no modelo novo mistura os comportamentos e corrompe a medição. A chave de cache precisa incluir o identificador do modelo, senão o rollout mede uma mistura. O mesmo vale para as métricas: tudo quebrado por modelo, senão a comparação some no agregado.

// gate/parity.js
// Gate de paridade POR CASO DE USO, com limites escritos antes de ver
// o resultado. Media global esconde a fatia que regrediu.

export function parityGate(results, budgets) {
  const byUseCase = new Map();
  for (const r of results) {
    const bucket = byUseCase.get(r.useCase) ?? [];
    bucket.push(r);
    byUseCase.set(r.useCase, bucket);
  }

  const report = [];
  for (const [useCase, rows] of byUseCase) {
    const budget = budgets[useCase] ?? budgets.default;
    const n = rows.length;

    const contractDrop =
      rows.filter((r) => r.current.contractOk && !r.candidate.contractOk).length / n;
    const qualityDrop = rows.filter((r) => r.judge === 'current_better').length / n
      - rows.filter((r) => r.judge === 'candidate_better').length / n;
    const costRatio =
      rows.reduce((s, r) => s + r.candidate.costUsd, 0) /
      Math.max(rows.reduce((s, r) => s + r.current.costUsd, 0), 1e-9);
    const p95Ratio = percentile(rows.map((r) => r.candidate.ms), 95) /
      Math.max(percentile(rows.map((r) => r.current.ms), 95), 1);

    const failures = [];
    if (contractDrop > budget.maxContractDrop) failures.push('contrato');
    if (qualityDrop > budget.maxQualityDrop) failures.push('qualidade');
    if (costRatio > budget.maxCostRatio) failures.push('custo');
    if (p95Ratio > budget.maxLatencyRatio) failures.push('latencia');

    // Caso de uso reprovado nao bloqueia os outros: fica no modelo
    // antigo enquanto o resto migra.
    report.push({ useCase, n, contractDrop, qualityDrop, costRatio, p95Ratio, pass: failures.length === 0, failures });
  }
  return report;
}

function percentile(values, p) {
  const sorted = [...values].sort((a, b) => a - b);
  if (!sorted.length) return 0;
  return sorted[Math.min(sorted.length - 1, Math.floor((p / 100) * sorted.length))];
}

Depois do corte, a migração não acabou: ela entra no período em que a diferença lenta aparece. Regressões de formato surgem em horas, mas mudanças de tom, de tamanho médio de resposta e de taxa de escalonamento levam dias para se separar do ruído. Vale manter o modelo antigo acessível por uma janela combinada, o gate de paridade rodando em amostra contínua e as métricas quebradas por modelo enquanto durar essa janela. Só depois disso o modelo antigo sai do manifesto e o prompt adaptado vira o novo estável, com uma lista de suposições agora explícitas que torna a próxima migração incomparavelmente mais barata.

FAQ

Perguntas frequentes

Por que trocar o modelo quebra um prompt que estava funcionando bem?

Porque o prompt não é portátil: ele foi lapidado contra um modelo específico e boa parte do que faz ele funcionar nunca virou instrução escrita. Se o modelo antigo já devolvia JSON limpo, ninguém precisou proibir cerca de código; se já era conciso, ninguém pôs teto de tamanho; se raramente recusava, ninguém definiu o que fazer com a recusa. Cada silêncio desses é uma suposição tácita, e a troca de modelo testa todas de uma vez. O resultado típico não é um sistema que para, é um sistema que continua respondendo com uma fatia dos casos saindo errada de um jeito que só aparece na métrica dias depois. Por isso a migração começa listando o que o sistema depende e não está escrito, porque cada suposição não escrita é exatamente um caso de regressão a verificar no candidato.

Como testar o modelo novo sem arriscar o cliente?

Com tráfego sombra: o candidato roda sobre o tráfego real de produção em paralelo com o modelo atual, mas a resposta dele nunca chega ao cliente, apenas é gravada ao lado formando pares comparáveis. Isso pega as formas que nenhum dataset antecipa, como mensagens truncadas, contexto gigante e sequências raras de ferramentas. Três cuidados tornam a sombra segura: as ferramentas com efeito colateral rodam em modo simulado, registrando a intenção sem executar, para a execução sombra não escrever nada; a sombra usa fila separada e execução assíncrona depois de responder ao usuário, para não competir por cota nem por latência com o caminho do cliente; e o tráfego é amostrado em vez de espelhado por inteiro, porque sombra em cem por cento dobra a fatura de inferência.

Qual critério usar para decidir que a migração pode subir?

Um gate de paridade por caso de uso, com limites escritos antes de ver o resultado. Média agregada esconde o que importa: o candidato pode ficar dois por cento melhor no geral e vinte por cento pior na fatia que envolve cobrança. O gate declara, por caso de uso, a queda tolerável em taxa de contrato válido, em qualidade julgada e em escalonamento para humano, mais o teto de piora em custo e latência; um caso de uso que não passa fica no modelo antigo enquanto o resto migra, em vez de bloquear tudo. A ordem de avaliação também importa: checagens de contrato primeiro, porque são determinísticas, baratas e pegam a maioria das quebras, e juiz apenas no que sobreviveu, com posição embaralhada e critério calibrado contra rótulos humanos.

Migrar de modelo é um projeto de engenharia, não uma troca de string

Trocar o modelo parece uma linha de configuração, mas o prompt em produção carrega meses de suposições tácitas sobre como aquele modelo específico se comporta, e a troca testa todas de uma vez nas bordas: JSON que passou a vir embrulhado, resposta que dobrou de tamanho, tool call que virou texto, recusa em caso legítimo. Caracterizar o comportamento atual com uma amostra real, rodar o candidato em tráfego sombra sem risco ao cliente, comparar por contrato antes de comparar por qualidade, adaptar o prompt com mudanças cirúrgicas validadas nos dois modelos e cortar por trás de um gate de paridade por caso de uso, com rollback como troca de ponteiro, transforma a migração de uma aposta numa operação medida. Posso conduzir essa migração no seu sistema com LLM, do harness de sombra ao gate de paridade, para você trocar de modelo sem descobrir a regressão pelo cliente.