Hcode
Capa Reescrever o prompt é o reflexo errado

Reescrever o prompt é o reflexo errado

Mateus Queirós

Yo Claude

O capítulo 04 do tutorial oficial de prompt engineering da Anthropic abre com um prompt que qualquer um já escreveu num dia de pressa:

  Yo Claude. {EMAIL} <----- Make this email more polite...

A resposta que volta começa assim:Dear Claude. O notebook aponta o diagnóstico:“Claude thinks ‘Yo Claude’ is part of the email it’s supposed to rewrite! You can tell because it begins its rewrite with ‘Dear Claude’.”A saudação que era instrução foi lida como dado, porque nada no texto dizia onde o e-mail começava e onde ele terminava.

Reescrever é o reflexo errado. O sintoma diz qual peça estrutural está faltando.

A peça aqui é uma tag. Mesmas três linhas, uma marcação a mais:

  Yo Claude. <email>{EMAIL}</email> <----- Make this email more polite...

Depois disso a saída para de cumprimentar você. A doc de boas práticas recomenda envolver cada tipo de conteúdo na sua própria tag, com nomes como<instructions>,<context>e<input>, porque o modelo não tem como inferir uma fronteira que ninguém desenhou. Quem cuida de governança devia ler esse defeito com mais apreço. Instrução e conteúdo de terceiro no mesmo bloco de texto, sem delimitador, é a superfície em que entrada não confiável vira comando. Num tutorial isso custa um e-mail malformado, mas num fluxo que processa documento de cliente é por essa mesma fresta que conteúdo de terceiro vira injeção.

Setenta por cento

O caso que carrega o resto do post está no cookbook oficial , um classificador que encaixa tickets de suporte de seguradora em 10 categorias, algumas confundíveis de propósito entre si, comoBilling InquirieseBilling Disputes. O notebook mede o mesmo problema três vezes sem trocar o modelo (claude-haiku-4-5), a temperatura (temperature=0.0) nem o test set, o conjunto de 68 tickets rotulados à mão que serve de gabarito. O que muda de uma rodada para a outra é uma peça estrutural.

A primeira versão não tem exemplo nenhum e não tem nem system prompt, aquele bloco de instrução que fica fora da conversa e vale para a sessão inteira. Nenhuma delas passa esse parâmetro, e tudo vive no turno de usuário, dentro da mensagem que o código envia:

  You will classify a customer support ticket into one of the following categories:
<categories>
    {{categories}}
</categories>
...
Respond with just the label of the category between category tags.

Na chamada entram mais duas peças, fora do texto do prompt. Uma é o prefill, em que você começa a resposta do modelo por ele, e o pedaço escrito no lugar dele é o"<category>"que obriga a continuação a ser um rótulo. A outra éstop_sequences=["</category>"], a string que faz a geração parar assim que aparece, cortando qualquer coisa que viesse depois. Só com isso, a acurácia chega a ~70%, contra um baseline aleatório de ~10%, que é o chão de um chute uniforme entre 10 categorias.

A última linha do template complica uma leitura preguiçosa da tese, porque“Respond with just the label of the category between category tags”é uma restrição de saída escrita em palavras comuns e está ali desde o começo. Prosa serve. O que ela não faz sozinha é fixar a forma da resposta, e quem fixa é o prefill junto com a stop sequence. Essas duas peças vivem no código da chamada, entram num diff e alguém as aprova num PR, enquanto “pedimos para ser mais direto” não é revisável.

Vale registrar o limite do caso, porque o notebook declara que“the problem definition, data, and labels used in this example were synthetically generated by Claude 3 Opus”. Material gerado assim costuma ser mais bem-comportado que tickets reais e dá um teto de acurácia mais generoso. A progressão entre as rodadas continua informativa, já que tudo que muda entre elas é a peça, mas o número absoluto não é promessa para o seu domínio.

E se for só reescrever?

A objeção é justa, e é a primeira que ocorre a qualquer um. O prompt não precisa de peça nova, precisa só ficar mais claro. Talvez, e o cookbook tem como medir isso.

RodadaO que mudou no promptAcuráciabaseline aleatórionada, chute em 10 categorias~10%simple_classifytags, prefill, stop sequence~70%rag_classify+ 5 exemplos rotulados em<examples>94%rag_chain_of_thought_classify+<scratchpad>97%

Entre a segunda e a terceira linha da tabela não mudam modelo, temperatura, test set nem quantidade de instrução em prosa. O que entra são cinco exemplos rotulados, pares de ticket e categoria correta colados dentro do próprio prompt para o modelo ver como a tarefa se resolve antes de tentar, o que se chama de few-shot. E não são cinco quaisquer. São recuperados por similaridade: o sistema devolve os exemplos mais parecidos com o ticket a classificar. Escolhidos assim, valem mais que cinco fixos pelo motivo mais banal, que é o ticket de cobrança chegar acompanhado de casos de cobrança e não de sinistro. Essa troca vale 24 pontos de acurácia .

O notebook registra o salto assim,“RAG boosted our accuracy from ~70% to 94%”, e depois“Chain-of-thought reasoning pushed our accuracy to 97%”. Esse último salto vem do<scratchpad>, um espaço no prompt onde o modelo escreve o raciocínio antes da resposta final, o que se chama de chain-of-thought e custa tokens em toda chamada. A recomendação da doc de incluir de três a cinco exemplos tem esse número atrás.

No diretórioevaluation/do mesmo caso, uma configuração de Promptfoo varre a temperatura em cinco pontos entre 0.0 e 0.8, para ver quanto do resultado depende desse botão. Temperatura é o quanto o modelo se permite variar; em 0.0 ele fica o mais previsível possível. O simple oscila entre 69,1% e 72,1%, o chain-of-thought entre 94,1% e 95,6%, e o RAG cai de 94,12% em T=0.0 para 89,71% em T=0.8. A temperatura mexe no número, sim, mas 4,4 pontos no pior caso contra os 24 que a peça comprou decidem onde vale gastar a tarde.

Para quem precisa prestar contas do que mudou, “melhoramos o prompt” morre na primeira pergunta de acompanhamento, enquanto “adicionamos cinco exemplos rotulados e a acurácia foi de ~70% para 94% no mesmo test set de 68 tickets” sobrevive a uma auditoria. E sobrevive sobretudo à situação inversa, quando a acurácia cai e alguém precisa saber o que reverter.

Três correções, um sintoma

No exercício 7.1 do mesmo tutorial, quatro e-mails de suporte precisam virar A, B, C ou D, e quem confere o acerto é uma linha de código só:

  re.search(ans, response[-1])

Parser é o pedaço de código que lê a resposta do modelo e extrai dela o dado que interessa. Este aqui olha só o último caractere, que precisa ser a letra da categoria. Qualquer “Claro! A categoria é B, porque o cliente…” quebra o parser carregando a classificação certa dentro da frase. Nos exercícios 6.1 e 6.2, do capítulo anterior, o corretor é mais tolerante e procura o padrão na resposta inteira; a implacabilidade que interessa aqui é a do 7.1.

Ohints.pyguarda duas soluções oficiais para esse conjunto, separadas pela causa que atacam. A do 6.1 resolve por restrição de saída, e são duas peças, não uma frase.

  Do not include any extra words except the category.
  # prefill do turno do assistente
"("

A do 7.1 resolve por few-shot, com um bloco<examples>trazendo três pares de pergunta e resposta completos, mais o prefill"The correct category is:". Comparada à do 6.1, é outro prompt inteiro. O 6.2 fica no meio sem solução redigida, só com o enunciado, que pede“your favorite output formatting technique to make Claude wrap JUST the letter… in<answer></answer>tags”.

Repare no que a solução 6.1 tem de desconfortável, porque ela começa com “Do not”, o enquadramento que a doc desaconselha (“tell Claude what to do instead of what not to do”). O material oficial resolve o exercício desobedecendo à recomendação da própria Anthropic e o output sai certo assim mesmo, porque o que salvou a resposta foi o prefill(abrindo a forma no lugar do modelo. Daí sai a régua, que é ler a saída errada e instalar a peça que fecha a fronteira que o modelo não tinha como enxergar. Quando cada correção fica etiquetada pela causa, dá para escrever um changelog de prompt que alguém ainda entende seis meses depois, e sem isso toda alteração vira “ajuste de redação” na descrição do commit.

E a fatura?

Sobre prompt inchado inflando a resposta e o custo por chamada, não há número público. O apoio documental é qualitativo e nada além disso. A doc observa que o estilo de formatação do prompt pode influenciar o estilo da resposta e dá como exemplo que tirar markdown do prompt tende a reduzir o volume de markdown na saída. Daí em diante vale como impressão de quem opera prompt em produção, com o peso que uma impressão tem. Meça na sua própria conta antes de tomar como fato.

O número duro fica do outro lado, no dos structured outputs. A doc o declara sem eufemismo. Usar um schema, a descrição formal do formato que a resposta precisa ter, faz a API injetar um system prompt adicional explicando esse formato ao modelo. Isso aumenta a contagem de tokens de input em toda chamada. E a primeira requisição com um schema novo ainda paga a latência de compilar a gramática, que depois fica em cache por 24 horas desde o último uso. Medir tokens de saída por chamada junto com a acurácia é o mínimo defensável, porque custo que ninguém mede só aparece no fechamento do mês.

Structured outputs não resolve isso?

Vale reconstruir a objeção na melhor versão dela, a da própria documentação . Structured outputs restringe a geração token a token contra uma gramática compilada, ou seja, a API traduz o schema num conjunto de regras e, a cada passo, só deixa o modelo escolher entre as continuações que mantêm a resposta dentro do formato. A promessa é literal na página,“Always valid: No more JSON.parse() errors”e“No retries needed for schema violations”. Se a saída já sai garantida nessa camada, escrever instrução de formato à mão vira trabalho manual para um problema que a API resolve na camada certa, e quem faz esse argumento tem a doc do lado dele.

O contra-argumento vive na mesma página, e é por isso que vale. Duas linhas de output, as duas válidas contra o mesmo schema:

  {"category": "Billing Disputes"}
{"category": "Billing disputes"}

“Structured outputs don’t guarantee the capitalization of string enum and const values.”O enum é a lista fechada de valores que o campo aceita. A doc é específica sobre onde a divergência costuma cair,“typically in the first letter of a word following a space”, o que atinge rótulos como os desse classificador, e recomenda comparar valores de enum de forma case-insensitive. Um roteador a jusante que compare string exata quebra com as duas linhas acima parseando perfeitamente. Há ainda um segundo buraco. A API devolve emstop_reasono motivo pelo qual parou de gerar, e quando ele érefusaloumax_tokensa doc avisa que a saída pode não bater com o schema. Schema e peça de prompt operam em camadas diferentes. E como “o schema garante” é a frase que faz um time desligar validação a jusante, continuar checandostop_reasondepois de adotar structured outputs é código necessário, não zelo.

No fim das contas

Diagnosticar por causa só funciona se existir um test set e alguém disposto a rodar a medição. O cookbook tem 68 tickets rotulados exatamente por isso, e montar um conjunto desses é o trabalho chato que o método cobra adiantado. Abaixo de um punhado de chamadas por dia, ou num protótipo que ainda vai mudar de forma semana que vem, reescrever no olho é mais barato e provavelmente certo. A régua vale quando existe algo a jusante que quebra, um roteador ou um relatório que alguém assina embaixo. E o caso condutor tem o limite já declarado, com dados e rótulos sintéticos e um teto mais generoso que o real.

Quantos exemplos rotulados você teria hoje, se precisasse provar amanhã que a última mudança no seu prompt melhorou alguma coisa?