ajuste no guardraild COE

This commit is contained in:
T3782834
2026-08-31 21:11:57 -03:00
parent 1a31478bfe
commit 9ed4782f9d
139 changed files with 4473 additions and 120 deletions

View File

@@ -1,148 +1,78 @@
"""Prompt do rail COER (coerência do input do cliente).
"""Prompt do rail COER (coerência semântica do input do cliente).
Roda no INPUT, em paralelo com PINJ (mesmo pool), num 20b. Decide se a fala do
cliente é aproveitável. Saída BINÁRIA (`1` passa / `0` descarta) — o `reason` é
texto fixo; pedir motivo antes do dígito foi medido e não paga (+170 ms, empate).
O COER responde uma pergunta estreita: existe significado conversacional recuperável
na fala do cliente? Ele não decide intenção, completude de parâmetros, executabilidade,
escopo de negócio ou se uma solicitação deve ser aceita. Essas decisões pertencem ao
router, aos contratos transacionais, aos validadores e ao mecanismo de clarification.
Descarta SÓ por três motivos:
(a) incompreensível — transcrição quebrada, palavra solta, conversa paralela;
(b) negação ambígua — "não" colado num pedido de AÇÃO do atendente, sem a vírgula
que decidiria a leitura ("não quero cancelar" × "não, quero cancelar");
(c) idioma (2026-08-10) — frase INTEIRA em inglês é STT quebrado, não cliente
bilíngue: descarta mesmo se ela se entende ou responde à pergunta pendente.
Ressalva: passa quando o agente pediu o NOME do item — nome de serviço É em
inglês (`coer_ok_0023`). ⚠️ A regra só funciona no ENQUADRAMENTO, acima do
gate de histórico (dentro de (a): 0/9 nos casos de inglês; no topo: 9/9),
porque o gate concede 1 a quem responde e o catch-all a quem pede algo
legível. Travado em `tests/guardrails/test_coerencia.py`.
O resto passa e é tratado adiante (matcher, TOX, OOS, orquestrador): referência
vaga, nome deformado, xingamento, assunto fora de fatura, resposta curta. O
histórico entra no prompt porque é ele que resolve fala curta e negação sem vírgula.
Dois bugs de produção fechados, ambos com a mesma assinatura — o modelo reconhece
a fala e escapa por uma regra de allow antes de aplicar (b):
- 2026-08-07, "não" seco no degrau 2 da retenção: (b) disparava só por começar
com "não" e o modelo COMPLETAVA a elipse com a ação que o AGENTE ofereceu.
Conserto: (b) exige que a fala PEÇA algo, e o teste da subtração proíbe
completar com a oferta do agente (`coer_ok_0027`: 161/220 → 340/340);
- 2026-08-10, "não gostaria de falar com a atendente" (`coer_ambig_0014`, 2/9):
a causa é o VERBO, não o gate nem o histórico (sonda 2×2 — condicional +
histórico curto 2/10 × "não quero" + o histórico longo do trace 10/10).
Conserto: gate vale só para a fala que "SÓ responde a ela"; (b) diz que
entender o pedido não dispensa o teste; a glosa do 1º exemplo cobre o
condicional. Alvo → 7/9, suíte 176,0 → 180,7/189.
⚠️ Protocolo: decida por BATCH (3 amostras de `--repeat 3` da suíte inteira, banda
de ruído ±4). `--repeat` focado engana nos dois sentidos — a mesma variante deu
7/10 focado × 0/9 batch, e o prompt atual dá 7/9 batch × 3/9 focado.
Variantes medidas e REJEITADAS (não retentar sem motivo novo) — a suíte está numa
fronteira zero-soma, cada cláusula compra um caso e vende outro:
- "a recusa soar clara não fecha" → CONTRADIZ a exceção "a fala segue dizendo
qual leitura vale": mata `coer_ok_0003` (7/9 → 0-1/9) em 3 variantes;
- exceção no GATE ("fala com 'não' ainda passa por (b)") → mata `coer_ruido_0011`
(9/9 → 0/9): exceção explícita REFORÇA o gate para todo o resto;
- "gostaria" na lista de modais de (b) → 169,7/189;
- few-shot NÃO é mais alavanca (era em 2026-08-05, +3,4 p.p.): +3 exemplos = empate
exato por +132 tokens; só o do NOME em inglês = 189,7/201 (arrasta a regra (c));
tirar exemplos custa mais do que os tokens que ocupam — inclusive o "não quero
entender porque…", que o controle FOCADO media como "sem efeito" e em batch vale
`coer_ok_0010` inteiro (9/9 → 1/9).
Tamanho: 1289 → 1334 (2026-08-07) → **1451 tokens** (cl100k). Suíte: **191,7/201
(95,4%)**, 67 casos. Detalhe por caso e histórico: `tests/llm_tests/README.md`.
Remedido em 2026-08-12 ao desfazer o revert (41979c4d): 193,7/204 (95,0%), 68 casos
— o novo `coer_ruido_0022` ("um" respondendo "sanei sua dúvida?", STT que não pegou
o "sim" → golden 0, reperguntar) sai de 3/10 no prompt antigo para 9/9 em batch só
com o gate "SÓ responde a ela", sem mudança extra de prompt.
A classificação continua totalmente delegada ao LLM. Não há listas de frases,
regexes ou exceções de domínio para liberar/bloquear entradas específicas.
"""
from __future__ import annotations
def build_coer_prompt(text: str, context: str = "") -> str:
"""Monta o prompt do rail COER.
"""Monta o prompt semântico do rail COER.
Args:
text: fala do cliente a classificar.
context: bloco de histórico já formatado por
``prompts._context.format_context_block`` (para este rail a última
fala do agente é PRESERVADA — é a pergunta pendente).
context: histórico já formatado, incluindo a pergunta pendente do agente
quando disponível.
Returns:
Prompt cuja resposta esperada é um único caractere: ``1`` ou ``0``.
"""
return f"""Você filtra a fala do CLIENTE no atendimento de fatura do provedor. A fala vem de
transcrição de voz e pode chegar truncada ou trocada. O atendimento é em português:
frase inteira em INGLÊS é STT quebrado, não cliente bilíngue — responda 0 mesmo que
ela se entenda ou responda à pergunta do agente; só não vale quando o agente pediu o
NOME do item, que é em inglês.
return f"""Você é o guardrail de COERÊNCIA SEMÂNTICA da fala do CLIENTE em uma conversa.
PRIMEIRO olhe o histórico. Se o agente terminou com uma pergunta e a fala SÓ responde a ela
(sim/não, "ainda não", nome de serviço, valor, uma das opções oferecidas), responda 1
— mesmo curta, estranha ou com o nome deformado pelo STT. Se não há pergunta pendente,
julgue a fala sozinha pelos casos abaixo, sem dar desconto.
Sua única responsabilidade é decidir se a fala contém significado conversacional
recuperável o suficiente para que as próximas camadas do sistema possam trabalhar.
Responda 0 (descartar) SÓ nestes dois casos:
NÃO tente decidir aqui:
- qual é a intenção do cliente;
- se a intenção mudou em relação ao turno anterior;
- se uma transação deve continuar, ser abandonada ou encerrada;
- se faltam parâmetros para executar uma ação;
- se um valor, nome, data ou outro parâmetro é válido;
- se a solicitação pertence ao escopo do atendimento;
- se uma ação é permitida por regra de negócio;
- se a fala precisa de clarification ou desambiguação posterior.
(a) NÃO DÁ PARA ENTENDER — você não conseguiria dizer em uma frase, SEM INVENTAR, o
que o cliente quer, responde ou reclama: transcrição quebrada, frase cortada no
meio, palavra ou letra solta, frase que soa completa mas cujo pedido não faz
sentido, ou fala dirigida a OUTRA PESSOA (o cliente conversando com quem está do
lado, sem falar com o atendimento). Palavra do domínio (plano, fatura, valor,
cpf) dentro de frase sem sentido não salva a fala. Fala VAGA não é
incompreensível: se ela aponta para o que está na tela ("esse aí", "isso aqui",
"esse negócio", "os valores"), responda 1 — perguntar qual item é do fluxo.
E se a última fala do agente pediu um NOME de item/serviço, nenhuma fala curta
é incompreensível: ela é a tentativa de dizer o nome, por mais estranha que
soe → 1 (reconhecê-lo é da etapa seguinte, que tem a fatura).
Essas responsabilidades pertencem ao router, ao estado transacional, aos validadores
e ao mecanismo de clarification. Portanto, uma fala pode ser compreensível mesmo
sendo incompleta para execução, contendo negação, discordância, reclamação, múltiplas
intenções, informalidade, erro gramatical ou referência que precise ser resolvida pelo
contexto.
(b) NEGAÇÃO AMBÍGUA — a fala começa com "não" E PEDE ALGO depois; entender o que ela
pede não a salva, quem decide é o teste. Faça o teste: tire
esse "não" do início e olhe SÓ o que sobra na fala — nunca complete com a ação
que o agente ofereceu. Se não sobra pedido nenhum ("não", "não sanou"), é
resposta ao agente → 1, seja qual for a pergunta pendente. Se o que sobra é
pedido de ação do atendente (cancelar, tirar cobrança,
ajustar/diminuir a fatura, transferir para atendente, encerrar a conta,
parcelar), sobram duas leituras opostas — recusa ("não quero cancelar") ou
pedido ("não, quero cancelar") — e a vírgula que decidiria não veio na
transcrição: responda 0. Vale para qualquer verbo ("não quero/preciso/posso",
"não quero que vocês...", "não cancela").
Responda 1 se: vem vírgula, "porque" ou "mas" depois do "não"; há sujeito antes
do "não" ("eu não quero cancelar"); a fala segue dizendo qual leitura vale; ou o
que sobra sem o "não" não é ação do atendente (pagar, reconhecer, entender,
mudar de plano).
Use o histórico somente para interpretar elipses, respostas curtas e referências ao
turno anterior. Nunca complete a fala inventando uma intenção que não esteja apoiada
pela própria fala ou pelo contexto imediato.
Responda 1 em TODO o resto, inclusive:
- pedido, queixa, dúvida ou desabafo que você entende, mesmo com erro de transcrição,
gíria, xingamento, número solto ou assunto fora de fatura (outros filtros cuidam);
- nome de serviço estranho ou deformado, inclusive quando o agente pediu para repetir
o nome do serviço;
- pedido de tempo, "alô?", agradecimento, despedida.
Responda 1 quando for possível identificar, sem inventar, pelo menos um conteúdo
conversacional útil: uma intenção, pergunta, resposta, afirmação, negação, reclamação,
referência, escolha, valor, nome, pedido de esclarecimento, encerramento ou mudança de
assunto. Não exija que esse conteúdo já seja suficiente para executar uma ferramenta.
Dúvida se entendeu a fala → 1. Pergunta ou pedido claro dirigido ao atendimento, mesmo
fora do assunto de fatura → 1. Dúvida entre as duas leituras da negação → 0.
Responda 0 somente quando, mesmo considerando o contexto imediato, não houver
significado conversacional recuperável com segurança — por exemplo, transcrição
fragmentada, palavras desconexas, fala cortada antes de formar qualquer relação
semântica, ou conversa paralela sem solicitação dirigida ao atendimento.
Exemplos (ilustram a regra, não são lista de falas):
- "não quero parcelar a fatura" → 0 (sem a vírgula, pode ser "não, quero parcelar");
idem no condicional, "não gostaria de parcelar a fatura"
- "eu não quero parcelar a fatura" → 1 (o "eu" antes do "não" fecha a leitura)
- "não quero parcelar, quero só entender o valor" → 1 (a fala diz qual leitura vale)
- "não vou pagar essa multa" → 1 (pagar não é ação do atendente: a queixa é a mesma)
- "não", depois de "sanou sua dúvida?" → 1 (responde a pergunta pendente)
- "deixe zero", depois de "qual o nome do serviço?" → 1 (pode ser o nome que o STT
deformou — "Deezer"; reconhecer o nome é da etapa seguinte, que tem a fatura)
- "não quero entender porque a conta subiu tanto" → 1 (entender é dúvida, não ação)
- "olha o menino ali pegando o negócio lá" → 0 (não dá para dizer o que o cliente quer)
- "bota dois planos um em cima do outro pra cá" → 0 (soa ordem, não quer dizer nada)
- "está cobrando um" → 0 (cortada no meio: não dá para saber de quê)
Critério decisivo:
- compreensível mas incompleto/ambíguo para a regra de negócio -> 1;
- compreensível mas com possível mudança de intenção -> 1;
- compreensível mas sem todos os parâmetros -> 1;
- compreensível e contendo negação/discordância -> 1;
- impossível determinar qualquer conteúdo conversacional sem inventar -> 0.
Na dúvida entre "há significado, mas outra camada precisa esclarecer" e "não há
significado recuperável", escolha 1. O COER deve bloquear apenas incompreensibilidade
semântica real, não incerteza de negócio.
------------------------------------{context}
Fala do cliente:
{text}
------------------------------------
Responda APENAS um caractere: 1 (aproveitável) ou 0 (descartar).
Responda APENAS um caractere: 1 (semanticamente compreensível) ou 0
(semanticamente incompreensível).
"""