Discourse AI: a resposta de saída estruturada não era JSON válido

Atualizei ontem[1] e agora há muitos erros nos logs:

Discourse AI: structured output response was not valid JSON, falling back to best-effort parsing (311 bytes) 

Inspecionei a resposta HTTP e não havia problema. Então tentei o texto no rails console:

...
discourse(prod)> p = JsonCompleter.new
=>
#<JsonCompleter:0x00007f48763b0270
...
discourse(prod)> p.parse text
=>
{"spam" => false,
 "reason" =>
  "..."}
discourse(prod)> p.parse DiscourseAi::Utils::BestEffortJsonParser.escape_control_characters(text)
(discourse):14:in '<main>': unexpected token "\\" (JsonCompleter::ParseError)

Acontece que o Discourse escapou os caracteres de quebra de linha antes de analisar[2], causando o erro.


  1. ↩︎

  2. code, introduzido por pr#41716 ↩︎

Isso é um aviso, não um erro.

Dito isso, estou mudando isso para o nível de log INFO, pois as pessoas continuam se preocupando com isso :face_exhaling:

Bem, tecnicamente verdadeiro, mas indica um bug no código. A solução alternativa funciona, então nenhum erro é gerado, mas é melhor corrigir a origem em vez de mudar para o nível INFO para encobrir, não é?

O JSON permite caracteres de quebra de linha como espaços em branco, mas não permite sequências de escape fora de strings; a função de escape, no entanto, as escapa. Para corrigir isso, você pode mudar para escapar apenas caracteres ASCII de 0x0 a 0x1f, inclusive, de acordo com a especificação JSON. (Não sei como escrever isso em Ruby, então sem PR.)

Para ilustrar, o seguinte é um JSON válido:

{
  "key": "value"
}

O seguinte é um JSON escapado, que é inválido e está causando o problema:

{\u000a  "key": "value"\u000a}

qual LLM você está usando?

llama-server -hf mradermacher/Qwen3-4B-Instruct-2507-GGUF:Q4_K_M

Um modelo de 4B vai ter dificuldade em cumprir as exigências; eles estão melhorando, mas este certamente está mais para o lado dos modelos antigos.

Se você for usar um de 4B, eu provavelmente recomendaria algo mais recente… tente o Qwen 3.5-4B ou o Gemma 4 E4B.

Acredito que o llama.cpp force o esquema, selecionando apenas tokens que estejam em conformidade com ele. Também capturei uma resposta com o tcpdump e verifiquei que a resposta retornada é um JSON válido.

No entanto, ela continha espaços e quebras de linha fora das strings (ou seja, era um JSON formatado), e essas quebras de linha foram escapadas pelo Discourse, tornando-o inválido (verifiquei no console do Rails).

Aliás, estou usando a função de detecção de spam.

Válido, trabalhando em uma correção.

A correção está aqui: