Um guard de IA não é só um prompt: contexto, Jev e avaliação em produção

Um guard de IA parece uma classificação binária: responde ou silencia. Em produção, ele é uma decisão de produto com custo de erro nos dois lados.
Um falso positivo interrompe uma conversa com uma resposta inútil, vaza uma ação para o canal errado ou contorna uma política. Um falso negativo deixa uma pessoa sem a ajuda que pediu. Quando o guard é a porta de entrada de um agente, a decisão também define se todo o restante do sistema será executado.
Este post nasceu de uma regressão real. Uma pessoa mencionava o bot e escrevia uma ordem curta para que ele aprendesse com uma conversa. O guard escolhia silence. O caso não exigia um modelo mais eloquente: exigia descobrir quais dados chegavam ao modelo e quais tinham desaparecido antes da decisão.
Depois da correção, a avaliação passou de dez para treze casos e cobriu os exemplos que haviam falhado. A mudança foi implantada. O que proponho nas seções seguintes é uma evolução arquitetural: como eu trataria esse mesmo tipo de guard hoje, combinando regras determinísticas, Jev como modelo de decisão e um modelo de raciocínio recente para a minoria de casos que realmente precisa dele.
O incidente: normalização apagou intenção
A entrada da triagem não recebia a mensagem original. Antes de chegar ao prompt, a menção ao bot era removida como parte da normalização.
Isso parece uma limpeza inofensiva até comparar duas frases:
| Mensagem original | Papel da menção | Decisão esperada |
|---|---|---|
@Assistente, registre o que decidimos nesta conversa. | chamada direta no início | responder ou iniciar o fluxo solicitado |
A @Assistente registrou o que decidimos ontem. | referência no meio | silenciar |
Depois de remover a menção, ambas perdiam um dos sinais que tornam a interpretação possível. A palavra no texto pode ajudar, mas ela não substitui o fato de que a pessoa chamou o bot e a posição em que isso ocorreu.
O problema, portanto, não era apenas de instrução. Era de contrato de dados. O classificador recebia uma representação incapaz de distinguir duas intenções que o produto precisava tratar de formas diferentes.
type TriageInput = {
messageId: string
channelId: string
threadId?: string
authorId: string
rawText: string
normalizedText: string
mentions: Array<{
subject: 'bot' | 'user' | 'group'
start: number
end: number
position: 'start' | 'middle' | 'end'
}>
botMentioned: boolean
botMentionPosition?: 'start' | 'middle' | 'end'
channelPolicyVersion: string
}
Não é necessário expor essa estrutura inteira a todos os modelos. Mas ela precisa existir, ser versionada e sobreviver à normalização. rawText permite auditoria; normalizedText reduz ruído; mentions preserva a semântica que a limpeza textual não consegue carregar sozinha.
Na correção, a triagem passou a receber a menção ao bot e sua posição. O prompt foi atualizado para dizer explicitamente que imperativos como “aprenda”, “registre”, “resuma” e “abra um bug” podem ser pedidos, mesmo sem interrogação. Também removi uma reação automática do caminho de silêncio: se a decisão é não responder, reagir visivelmente já é uma forma de resposta.
A arquitetura que eu usaria agora
Eu não colocaria um único LLM para decidir tudo. A estrutura abaixo separa regras que pertencem ao produto de inferências que dependem de linguagem.
evento do canal
│
▼
parser de menções + normalização semântica
│
├── regras determinísticas de política ──────────────► decisão final
│ (canal proibido, origem inválida, allowlist)
▼
Jev: decisão estruturada e probabilidades
│
├── confiança alta + sem conflito ───────────────────► decisão final
│
└── incerteza, conflito ou ação sensível
│
▼
modelo de raciocínio + saída estruturada
│
▼
decisão, auditoria e amostragem humana
Essa cascata não é sobre usar muitos modelos porque isso parece sofisticado. Ela atribui uma tarefa a cada mecanismo:
| Camada | Pergunta que responde | Resultado |
|---|---|---|
| Regras determinísticas | há uma condição objetiva de bloquear, permitir ou escalar? | decisão explicável e estável |
| Jev | dada a política e o estado, qual classe é mais provável? | escolha, distribuição de probabilidades e confiança |
| Modelo de raciocínio | a política entra em conflito com o contexto conversacional? | decisão estruturada para os poucos casos ambíguos |
| Revisão humana | a rubrica e os limiares ainda representam o uso real? | rótulos de referência e correções de política |
A principal regra é simples: a política não pode existir apenas no prompt. Se um canal nunca permite uma determinada ação, isso deve ser checado antes da chamada de modelo. Se uma operação exige confirmação humana, a resposta do classificador não pode removê-la. O modelo interpreta linguagem; o produto aplica controles.
Onde Jev encaixa bem
Jev é um modelo de decisão, não um chat. Ele recebe um estado e um conjunto de perguntas tipadas, devolvendo escolhas, scores ou probabilidades binárias. Isso combina bem com a triagem porque a saída desejada já é uma decisão estruturada, não uma explicação em prosa. A documentação também permite enviar até vinte perguntas sobre o mesmo estado em uma chamada, o que evita duplicar contexto entre classificadores independentes. A API do Jev documenta esse contrato de state, questions e distribuições de probabilidade.
Para este guard, eu formularia perguntas pequenas e ortogonais. Em vez de pedir “decida se responde”, separaria os sinais que depois podem ser auditados:
const decision = await jev.decide({
model: 'jev-1.13', // versão fixada para avaliações reproduzíveis
state: {
text: input.normalizedText,
botMentioned: input.botMentioned,
botMentionPosition: input.botMentionPosition,
inThread: Boolean(input.threadId),
channelPolicy: policy.publicSummary,
},
questions: {
addressee: {
type: 'choice',
instructions: 'Quem é o destinatário da mensagem?',
criteria: {
bot: 'A pessoa está falando diretamente com o bot.',
human: 'A pessoa está falando com outra pessoa.',
unknown: 'Não há evidência suficiente para decidir.',
},
},
intent: {
type: 'choice',
instructions: 'Qual é a intenção que controla a triagem?',
criteria: {
request: 'Pede uma ação, informação, registro, resumo ou aprendizado.',
reference: 'Apenas menciona ou descreve o bot.',
conversational: 'É conversa entre pessoas sem pedido ao bot.',
},
},
policyConflict: {
type: 'noul',
instructions: 'Há sinal de que o pedido viola a política deste canal?',
},
},
})
Os rótulos e os critérios fazem parte da implementação. Eles precisam ser curtos, mutuamente exclusivos e versionados junto da política. Um choice com opções sobrepostas produz uma aparência de precisão sem uma decisão realmente bem definida.
Também fixaria o modelo na produção e na suíte de avaliação. jev-latest é útil para experimentar melhorias, mas muda com o tempo; jev-1.13 representa uma build fixa. A recomendação é registrar model_version em toda decisão, inclusive quando o identificador usado já é fixo. Isso permite explicar por que a distribuição mudou e reproduzir uma regressão. A distinção entre versão fixada e alias móvel está documentada pelo Jev.
A probabilidade não é autorização automática
Uma resposta como request: 0.94 é evidência, não uma permissão. Eu transformaria probabilidades em uma decisão operacional somente depois de aplicar três verificações:
const confidence = decision.answers.intent.confidence
const margin =
decision.answers.intent.probabilities.request -
Math.max(
decision.answers.intent.probabilities.reference,
decision.answers.intent.probabilities.conversational,
)
const canAutoRespond =
decision.answers.addressee.choice === 'bot' &&
decision.answers.intent.choice === 'request' &&
decision.answers.policyConflict.noul < 0.05 &&
confidence >= thresholds.intentConfidence &&
margin >= thresholds.intentMargin
O limiar não deve nascer de intuição. Ele vem de um conjunto de validação rotulado por humanos. Para cada faixa de confiança, eu mediria precisão, recall, taxa de escalonamento e o custo do erro. Se o guard silencia um pedido legítimo, talvez o limiar de silêncio deva ser mais conservador que o limiar de resposta. Essas decisões são de produto, não métricas universais.
Quando um modelo de raciocínio vale a latência
Jev é apropriado para decisões bem delimitadas. Ele não produz uma investigação em linguagem natural nem usa ferramentas. Eu não o usaria como desculpa para reduzir uma situação complexa a um rótulo prematuro.
Casos que eu escalaria incluem:
- a mensagem contém uma menção, mas o destinatário e a intenção entram em conflito;
- a política depende do que foi dito anteriormente na thread;
- a mensagem pede uma ação consequente, como publicar, alterar dados ou abrir um ticket externo;
- a distribuição é pouco concentrada ou a margem entre classes é pequena;
- o texto pertence a um cluster novo que ainda não tem exemplos rotulados suficientes.
Para essa segunda camada, eu usaria um modelo de raciocínio recente com entrada estruturada e saída em JSON Schema. Os modelos de raciocínio atuais da OpenAI trabalham bem em fluxos com múltiplas etapas e uso de ferramentas; a documentação recomenda a Responses API para preservar o raciocínio entre chamadas de ferramenta. Veja o guia atual de modelos de raciocínio.
O objetivo desse modelo não é escrever uma defesa longa da sua decisão. É avaliar o caso contra a mesma rubrica, devolver motivos enumerados e declarar a evidência que usou.
const escalationSchema = z.object({
action: z.enum(['respond', 'silence', 'require_confirmation', 'human_review']),
intent: z.enum(['request', 'reference', 'conversation', 'unknown']),
policy: z.enum(['allowed', 'blocked', 'needs_review']),
evidence: z.array(
z.enum(['bot_mentioned_at_start', 'imperative_verb', 'thread_context', 'channel_rule']),
),
confidence: z.number().min(0).max(1),
})
O serviço deve validar essa estrutura e decidir o efeito. Se o modelo devolver respond, por exemplo, o executor ainda verifica permissões, idempotência e confirmação. Uma saída estruturada melhora a observabilidade e reduz ambiguidade de integração; ela não substitui autorização.
Em uma stack OpenAI, eu começaria com um modelo de custo menor nos casos de escalonamento rotineiros e reservaria um modelo de raciocínio de fronteira para disputas de política ou análises de thread mais longas. O modelo exato deve ser uma configuração versionada, não uma string espalhada pelo código. A página de modelos da OpenAI mantém os modelos disponíveis e orienta essa escolha entre capacidade, custo e latência. Consulte o catálogo atual.
A avaliação precisa testar a cascata inteira
No incidente original, aumentar o dataset de dez para treze casos foi o passo correto: os dois exemplos reais passaram a ter uma proteção contra regressão, e um contraexemplo proibido pela política preservou a fronteira.
Mas uma arquitetura de cascata precisa de uma suíte mais rica que “o prompt acertou?”. Eu dividiria a avaliação em quatro conjuntos.
| Conjunto | O que contém | O que mede |
|---|---|---|
| Regras | canais, permissões e operações com resposta objetiva | nenhuma regra foi delegada ao modelo por acidente |
| Pares contrafactuais | mesma frase com menção no início, meio ou fim | sensibilidade à evidência que realmente muda a decisão |
| Casos dourados | incidentes reais anonimizados e revisados | regressões de comportamento observadas em produção |
| Casos de fronteira | ironia, thread longa, política em conflito e ação consequente | taxa de escalonamento e segurança sob ambiguidade |
Os pares contrafactuais são particularmente importantes aqui. Se trocar a posição da menção deveria mudar a resposta, o teste precisa mudar apenas isso. Caso contrário, um acerto não prova que o modelo está usando o sinal certo; ele pode estar se apoiando em uma palavra acidentalmente correlacionada.
Também manteria uma matriz de versões:
dataset v12 × policy v7 × jev-1.13 × escalation-model v3
dataset v12 × policy v7 × jev-candidate × escalation-model v3
dataset v12 × policy v7 × jev-1.13 × escalation-model candidate
Cada alteração de política, modelo, limiar ou serialização do contexto executa a matriz. O relatório deve registrar decisões, probabilidades, caminho percorrido, latência e custo. Um score agregado é útil para detectar mudança; os casos que inverteram de decisão são o que explica a mudança.
Calibração: a parte que costuma faltar
Confiança só é útil se estiver razoavelmente calibrada para o domínio. Se o modelo responde com 0,90 em cem casos, aproximadamente noventa deles deveriam estar certos para aquela população. A métrica não exige perfeição, mas exige medição.
Eu faria amostragem estratificada por faixa de confiança e caminho da cascata. Decisões com 0,50–0,65 merecem revisão por serem candidatas naturais a escalonamento. Decisões acima de 0,95 também devem ser auditadas em uma pequena amostra, porque excesso de confiança pode esconder um erro sistemático. A cada revisão humana, o sistema ganha um rótulo para recalibrar limiares e enriquecer os casos dourados.
faixa de confiança 0.50────0.65────0.80────0.95────1.00
amostragem humana alta alta média baixa
efeito esperado escalar escalar automático automático + auditoria
Os limiares precisam ser específicos ao risco. Pode ser aceitável silenciar automaticamente uma referência com alta confiança. Pode não ser aceitável executar uma ação externa sem confirmação, mesmo quando a intenção é quase certa.
Observabilidade é parte do contrato
Para depurar uma decisão, eu preciso reconstruir o caminho sem registrar mais conteúdo do que o necessário. Um evento de triagem deveria conter, no mínimo:
type TriageDecisionEvent = {
messageId: string
traceId: string
inputContractVersion: string
policyVersion: string
rulesApplied: string[]
route: 'rule' | 'jev' | 'reasoning_model' | 'human_review'
model?: { id: string; version: string }
answers?: Record<string, unknown>
thresholds?: { intentConfidence: number; intentMargin: number }
finalAction: 'respond' | 'silence' | 'require_confirmation' | 'human_review'
latencyMs: number
}
O texto da mensagem deve seguir a política de retenção e acesso do produto. Em muitos casos, guardar um identificador, uma versão de entrada e a decisão já é suficiente para detectar um padrão; a inspeção do texto completo pode ficar restrita a uma ferramenta protegida e a um período curto de retenção.
A observabilidade também fecha o ciclo de avaliação. Uma alteração de inputContractVersion que reduz a taxa de resposta pode ser uma regressão do parser, não uma piora do modelo. Sem registrar a versão da transformação, a equipe tende a culpar o prompt pelo componente errado.
O resultado prático
A correção original foi pequena: preservar a menção, informar sua posição, esclarecer o prompt e adicionar casos de avaliação. Isso resolveu a falha observada porque corrigiu a representação que chegava ao guard.
A lição mais durável é maior. Um guard confiável é um sistema de decisão: tem contrato de entrada, regras que não podem ser negociadas, modelos com responsabilidades delimitadas, limiares calibrados, avaliações versionadas e sinais de produção que voltam para o dataset.
Jev pode reduzir custo e latência quando a pergunta é realmente uma escolha bem definida. Um modelo de raciocínio recente é valioso quando há contexto, conflito e consequências. Nenhum deles compensa contexto perdido ou política escondida em prosa. O ganho vem de saber exatamente qual decisão cada camada pode tomar — e de medir, em produção, quando ela erra.