Volver a la wiki

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.

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:

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:

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;

Beneficios:

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:

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:

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

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

Véase también

Subir