CreaRack-SL

Feature: defensa integral contra degeneración Gemma 4

Resumen ejecutivo

PR #94 (2026-06-05) implementa tres capas de defensa contra el footgun estocástico de Gemma 4 26B-A4B en AI Studio: bucles periódicos de repetición que degradan Help Widget, Oráculo y Tutor.

  • Defensa 1: clampRepetition() — truncado determinista de patrones periódicos.
  • Defensa 2: maxOutputTokens dinámico por modo — Help 768, Oracle/Tutor 1024 (antes 2048 fijo).
  • Defensa 3: HELP_RELEVANCE_FLOOR 0.4 — rechaza síntesis sobre grounding irrelevante.

Actualización 11-09-2026 (PR #177): añade una cuarta capa — clampSubstringRepetition() — para un bucle que las tres anteriores no cazaban (repetición de una subcadena corta sin espacios), y corrige un fallo distinto en el chat del Correo: cuando el JSON de Gemma no se cerraba, el usuario recibía el JSON crudo entero como respuesta. Ver [[incident—20260911—correo-chat-gemma-substring-loop]].

Contexto

Gemma 4 ocasionalmente degenera en bucles de repetición cuando se le pide sintetizar sobre grounding flojo o tangencial:

  • Genera un segmento de N palabras.
  • Lo repite una y otra vez.
  • Continúa hasta agotar maxOutputTokens.
  • Síntomas: latencia >40s (timeout del Oráculo) o muro de texto inútil en Help.

Limitación fundamental: frequencyPenalty y presencePenalty de Gemini 1.5 no están disponibles en Gemma 4 en AI Studio. Una llamada con estos params causa HTTP 400.

Por eso la solución es determinista en software (output post-processing + acotación de tokens + validación de relevancia).

Defensa 1: clampRepetition()

Ubicación: functions/api/mcp/handlers/archivo-core.ts línea 755.

Detecta colas periódicas sostenidas (≥3 repeticiones del periodo) y trunca dejando una instancia + […].

Caso real validado (s109): muro de “la continuación de un proceso de automatización automático o la de…” repetido 40 veces → 2837 → 200 caracteres, contenido preservado.

Conservadora por diseño:

  • Require ≥18 palabras periódicas (3 repeticiones de periodo 6, o 18 palabras si periodo 1).
  • Texto sano con repetición puntual pasa intacto.

Ver: [[entity—biblioteca—function—clamp-repetition]]

Defensa 2: maxOutputTokens dinámico

Antes: maxOutputTokens: 2048 fijo para todos los modos.

Ahora:

const maxOutputTokens = mode === 'help' ? 768 : 1024;
  • Help Widget (user_facing): 768 tokens (~200 palabras). Límite real de una respuesta útil.
  • Oracle (staff, consultas internas): 1024 tokens (~350 palabras).
  • Tutor (conversacional): 1024 tokens (~250 palabras).

Beneficios:

  • Acota latencia máxima (Help <2s, Oracle <5s típicamente).
  • Reduce daño por degeneración: si Gemma entra en bucle, se queda sin tokens antes de generar un muro de 2000+ caracteres.
  • Realista: Help no necesita 2048 tokens (era desperdicio).

Defensa 3: HELP_RELEVANCE_FLOOR 0.4

Solo en modo user_facing (Help Widget via Django proxy).

Cuando el mejor chunk recuperado tiene relevancia < 0.4 (es ruido):

Antes:

// Intentaba sintetizar sobre grounding irrelevante
const result = await synthesizeAnswer(..., matches);
// → Gemma degenera/divaga

Ahora:

const HELP_RELEVANCE_FLOOR = 0.4;
if (userFacing && (matches[0]?.score ?? 0) < HELP_RELEVANCE_FLOOR) {
  return JSON.stringify({
    answer: "No tengo información sobre eso en la ayuda. Prueba a reformular, o pregunta por una función concreta...",
    sources: [],
  });
}

Aclaración importante: No captura preguntas con grounding tangencial de score medio (p.ej. “wifi” ~0.66 — hay chunks relacionados pero no específicos). Esos:

  1. Pasan el floor (0.66 > 0.4).
  2. Se procesan normalmente.
  3. Si Gemma divaga de todos modos, clampRepetition() lo corta.

Cobertura:

  • Sí: Help Widget (flag user_facing=true).
  • No: staff bib_ask (sin flag) ni Oráculo (endpoint propio sin flag) — conservan comportamiento anterior.

Defensa 4: clampSubstringRepetition() + rescate del JSON sin cerrar (11-09-2026)

PR #177 (2026-09-11) añade una segunda capa de detección de bucles, y cierra un fallo distinto en el chat del Correo (no en el Oráculo): cuando el modelo entraba en bucle, el JSON de la respuesta nunca se cerraba y el usuario recibía el JSON crudo entero como texto.

Fallo 1 — bucle por subcadena sin espacios: clampRepetition() trocea por palabras; no detecta un patrón corto (2-12 caracteres) repetido sin separadores dentro de un mismo token ("...resak-resak-resak-...", visto por Edu el 11-09-2026 en el chat del Correo). La nueva función clampSubstringRepetition() — ver [[entity—biblioteca—function—clamp-repetition]] — cubre este hueco con una regex de backreference ((.{2,12}?)\1{9,}), aplicada siempre después de clampRepetition().

Fallo 2 — JSON sin cerrar en el chat del Correo: si el modelo entraba en bucle dentro del valor de "answer" y agotaba tokens antes de cerrar el JSON, callGemma() en functions/api/correo/ask.ts no conseguía parsear nada y devolvía el texto crudo completo (incluyendo {"action":"responder","answer":"...) como respuesta al usuario. El fix rescata el valor de "answer" con una regex tolerante a comillas sin cerrar, y aplica el mismo corte por subcadena.

Resultado: si tras cortar la subcadena repetida queda menos de 20 caracteres útiles, tanto el Oráculo como el chat del Correo devuelven una respuesta honesta (“se me ha ido la respuesta…”) en vez de un fragmento vacío, marcada con degraded: true en la respuesta del chat del Correo (el cliente no debería reenviarla como historial).

Ver: [[entity—biblioteca—function—clamp-repetition]] · [[incident—20260911—correo-chat-gemma-substring-loop]]

Cambios secundarios

Reindentado Prettier en archivo.ts

El cambio de functions/api/mcp/handlers/archivo.ts incluye reindentado extenso (salda deuda de formato preexistente). No es cambio funcional — prettier se reaplicó a las secciones tocadas por el fix.

Ejemplos:

  • Saltos de línea en multilinea chains .prepare().bind().
  • Indentación consistente en destructuring y condicionales.

Modo de operación post-fix

  1. Usuario hace pregunta en Help Widget con grounding débil/ausente.

    • Floor de relevancia → “No tengo información…” (respuesta honesta).
  2. Usuario hace pregunta con grounding relevante (score >0.4).

    • Síntesis normal.
    • Si Gemma degeneraría, clampRepetition() lo trunca antes de devolverlo; clampSubstringRepetition() cubre además el bucle por subcadena sin espacios.
  3. Oracle staff query.

    • Sin floor de relevancia (staff sabe lo que hace).
    • maxOutputTokens=1024 acota latencia máxima.
    • clampRepetition() + clampSubstringRepetition() como red de seguridad si degenera.
  4. Chat del Correo (correo/ask.ts).

    • Si el JSON de Gemma no parsea (footgun de bucle sin cerrar), se rescata el valor de "answer" con regex tolerante en vez de mostrar el JSON crudo.
    • clampSubstringRepetition() corta la subcadena repetida; si no queda nada útil, respuesta honesta con degraded: true.

Testing & validación

  • clampRepetition(): validado contra caso real s109 (2837→200 chars).
  • clampSubstringRepetition(): validado contra el caso real de Edu (11-09-2026, “resak-resak-…”), texto sano, y confirmación de que el corte por palabras de clampRepetition() sigue vivo — test/clamp-substring-repetition.test.ts, 3 casos, suite completa 187/187.
  • Integración: Help Widget sigue respondiendo correctamente; Oráculo sin timeouts sistemáticos.
  • Regresión: texto sano no se ve afectado.

Deuda técnica y alternativas descartadas

frequencyPenalty / presencePenalty

Descartadas: API Studio solo soporta en Gemini 1.5+, no Gemma 4. Usarlos causa HTTP 400.

Prompt engineering más agresivo

Insuficiente: el bucle ocurre en un estado estocástico que ignora constraints semánticas. El prompt “no repitas” no lo previene de forma consistente.

Cambio de modelo (Gemini 1.5)

Pospuesto: fuera de scope de este fix. Gemma 4 es el modelo actual; defensa software es la solución inmediata.

Futuro

  • Monitoreo: rastrear frecuencia residual de degeneraciones post-fix (via logging en synthesizeAnswer).
  • Corpus: mejorar chunks para “dispositivos wifi” (gap identificado s109).
  • Migración Gemini 1.5: cuando se ejecute, frequencyPenalty se convierte en defensa secundaria (clampRepetition puede retirarse).

Véase también

  • [[incident—20260605—gemma-4-help-loop]]
  • [[entity—biblioteca—function—clamp-repetition]]
  • [[entity—functions—handler—synthesizeanswer]]
  • [[entity—functions—tool—bib-ask]]
  • [[incident—20260911—correo-chat-gemma-substring-loop]]