ADR-0019: Resposta estruturada do agente
- Status: Accepted
- Data: 2026-05-27
- Responsáveis pela decisão: Waldemar Szemat
Contexto e definição do problema
Seção intitulada “Contexto e definição do problema”Os avaliadores de correção da recusa e da escalonamento decidem “o agente recusou?” / “o agente escalou?”. Decidir isso correspondendo substrings de prosa livre contra tabelas de marcadores apenas em inglês é frágil de três maneiras concretas:
- As tabelas de marcadores são apenas em inglês. O harness inclui as localidades es-419 e pt-BR (ADR-0001), mas os avaliadores não conseguem enxergar uma recusa nessas localidades a menos que a prosa contenha por acaso as substrings em inglês. Uma expressão regular multilíngue distinta existia em paralelo na camada de guardrails - duas camadas de marcadores paralelas que derivam de forma independente.
- Adicionar um novo subtemplate de recusa (há seis:
input-malformed,out-of-scope-dosing,out-of-scope-diagnosis,out-of-scope-interpretation,out-of-scope-pii,out-of-scope-meta) exige ampliar a tabela de marcadores; uma substring omitida pontua silenciosamente uma recusa como uma resposta. - O sinal do portão de avaliação é, estruturalmente, uma correspondência de substrings n-de-N sobre prosa que o modelo pode legitimamente parafrasear. Duas recusas semanticamente idênticas podem pontuar 1.0 e 0.0 conforme a escolha de palavras.
A evolução para um domínio de agente estruturado precisava que o contrato fosse robusto antes de empilhar a recuperação por RAG e um avaliador focado apenas na recuperação por cima. Como tornamos o discriminador estrutural em vez de baseado em prosa, mantendo inalterada a renderização existente do texto do assistente na SPA?
Fatores de decisão
Seção intitulada “Fatores de decisão”- Correção estrutural: o discriminador não deve depender da escolha de palavras da prosa nem da localidade.
- Portabilidade entre provedores: o contrato deve funcionar em Groq, OpenAI, Anthropic e no stub em processo. A cobertura do modo JSON varia acentuadamente entre provedores.
- Compatibilidade retroativa: os testes existentes, o portão de avaliação e a SPA devem continuar funcionando durante a migração. Sem reescrita de “big bang”.
- Sinal de avaliação: o tipo estruturado mapeia diretamente para as expectativas must_refuse / must_escalate que os casos de avaliação já carregam, de modo que o avaliador se torna uma verificação de discriminador de uma única linha.
- Orçamento de spans: os atributos de trace para a resposta estruturada devem caber no orçamento de observabilidade existente (política de spans apenas com metadados da ADR-0006).
Opções consideradas
Seção intitulada “Opções consideradas”- Opção A: Manter a prosa + ampliar as tabelas de marcadores de substrings por localidade + adicionar camadas de expressões regulares por localidade.
- Opção B: Migrar cada adaptador para o uso nativo de ferramentas com uma
única ferramenta
agent_replycujo esquema imponha o envelope. - Opção C: Adicionar um esquema Pydantic
AgentReply, solicitar o modo JSON por provedor (cada adaptador degrada com cortesia para a melhor superfície de JSON disponível), validar na camada do agente, consumir o discriminador na camada do avaliador e descontinuar a correspondência de substrings em três etapas.
Resultado da decisão
Seção intitulada “Resultado da decisão”Opção escolhida: Opção C - esquema Pydantic + modo JSON por provedor + descontinuação escalonada da camada de substrings.
A única razão determinante é a assimetria entre provedores do
suporte ao uso de ferramentas: a Anthropic oferece uso de ferramentas de forma
nativa, os provedores compatíveis com OpenAI (Groq, OpenAI) oferecem
response_format={"type":"json_schema",...} diretamente, e o caminho do
cliente stub não precisa de nenhum dos dois. A Opção B forçaria os
adaptadores compatíveis com OpenAI a tomar um desvio desnecessário para o uso de
ferramentas apenas para igualar a Anthropic. A Opção C deixa cada adaptador
usar seu idioma nativo, e a camada do agente valida uma forma agnóstica ao
provedor na saída.
A resposta estruturada é um pequeno envelope validado com quatro campos:
kind- um entrerefusal,answerouescalation(o discriminador que os avaliadores leem).text- a mensagem do assistente renderizada na localidade (não vazia).citations- a lista de ids de chunks da KB que respaldam a resposta.rationale- uma breve explicação interna (com comprimento limitado).
A camada do agente valida o envelope e define a resposta estruturada no
estado do agente a partir do nó de geração de resposta e dos helpers de
emissão de recusa / resposta / escalonamento; a requisição de completion e os
tipos de resultado carregam os campos de modo JSON, encadeados pelo
transporte compartilhado. Os avaliadores leem o discriminador kind
estruturado diretamente; as tabelas de marcadores de substrings, os helpers
de correspondência de prosa e seus ramos de fallback não existem na camada de
avaliação, e o call-site posterior aos guardrails consome a resposta
estruturada.
A postura do modo JSON por provedor:
- Groq / OpenAI: nativo
response_format={"type":"json_schema","json_schema":{...,"strict":true}}por meio do construtor de cargas úteis compartilhado compatível com OpenAI. - Anthropic: sem sinalizador nativo; injetar um preâmbulo de modo JSON na mensagem de sistema e analisar a resposta com tolerância. A migração para o uso de ferramentas é adiada (cascatas para a Anthropic são raras; a abordagem do preâmbulo mantém esse provedor utilizável sem uma reescrita mais profunda do adaptador).
- Stub: emite um envelope de resposta predefinido reutilizando as heurísticas existentes sensíveis à localidade.
- Fallback: relé transparente; o campo de requisição do modo JSON sobrevive inalterado ao longo de primário → fallback → último recurso.
Confirmação
Seção intitulada “Confirmação”- Um teste dedicado fixa a forma do esquema e o ciclo de ida e volta.
- Cada teste de adaptador verifica a forma da carga útil do modo JSON na requisição e o campo estruturado na resposta.
- Cada teste de integração do grafo verifica que o
kindda resposta estruturada corresponde ao caminho esperado. - A camada de avaliação não tem referências a tabelas de marcadores de substrings (nenhum fallback de correspondência de prosa permanece).
Consequências
Seção intitulada “Consequências”Positivas
Seção intitulada “Positivas”- O discriminador é invariante à localidade e sobrevive a qualquer paráfrase que o modelo emita.
- Adicionar um novo subtemplate de recusa é um ramo de tipo de resposta de uma única linha, não uma ampliação da tabela de marcadores.
- Os avaliadores se tornam triviais: uma única verificação de igualdade do discriminador por caso.
- O envelope estruturado é o portador natural para a posterior evolução dos spans de citação e para o avaliador de recall da recuperação.
- O sinal de avaliação se torna estrutural; paráfrases de mesma semântica pontuam de forma idêntica.
Negativas
Seção intitulada “Negativas”- As respostas da Anthropic sob o modo JSON pagam ~50 tokens de entrada por turno pelo preâmbulo do esquema. As cascatas para a Anthropic são raras em uma configuração com OpenAI como primário, de modo que o custo acumulado é pequeno, mas diferente de zero.
- Uma nova superfície de abstração (o envelope de resposta e seu enum de tipo) é adicionada à API pública do pacote do agente.
- A camada de transporte ganha dois campos de requisição opcionais e um campo de resultado opcional; os adaptadores de provedor devem respeitá-los ou documentar por que não o fazem.
Neutras
Seção intitulada “Neutras”- O contrato de renderização da SPA não muda: o conteúdo da última mensagem
ainda recebe o
textrenderizado na localidade. O envelope estruturado é observável no estado do agente e nos spans de trace, mas não no fluxo SSE (os tipos de evento SSE são ampliados separadamente no streaming de tokens). - As chamadas em modo JSON são executadas a uma temperatura determinística; os caminhos de forma livre mantêm um padrão baixo.
Prós e contras das opções
Seção intitulada “Prós e contras das opções”Opção A: ampliar as tabelas de marcadores por localidade
Seção intitulada “Opção A: ampliar as tabelas de marcadores por localidade”- Boa, porque não requer mudanças nos adaptadores.
- Ruim, porque as tabelas de marcadores crescem com cada nova localidade e cada novo subtemplate; a deriva entre os marcadores da camada de avaliação e a expressão regular da camada de guardrails piora.
- Ruim, porque o sinal de avaliação permanece acoplado à prosa; as paráfrases ainda pontuam mal.
Opção B: uso nativo de ferramentas em todos os provedores
Seção intitulada “Opção B: uso nativo de ferramentas em todos os provedores”- Boa, porque o esquema é imposto no limite do provedor.
- Ruim, porque Groq / OpenAI ganham um desvio de uso de ferramentas que não precisam (já oferecem json_schema diretamente).
- Ruim, porque a taxa de acionamento do uso de ferramentas (o modelo decidindo
quando chamar a ferramenta) é um modo de falha adicional que não existe com um
response_formatforçado.
Opção C (escolhida): esquema Pydantic + modo JSON por provedor + descontinuação escalonada
Seção intitulada “Opção C (escolhida): esquema Pydantic + modo JSON por provedor + descontinuação escalonada”- Boa, porque cada adaptador usa seu idioma nativo.
- Boa, porque a migração é escalonada e reversível em cada checkpoint.
- Boa, porque o envelope estruturado é o portador natural para os posteriores spans de citação e o avaliador focado apenas na recuperação.
- Ruim, porque a falta de um sinalizador nativo na Anthropic força uma postura de preâmbulo de prompt + análise tolerante que outros adaptadores não precisam.