Función: clampRepetition() — defensa contra bucles periódicos
Firma
export function clampRepetition(text: string): string
Ubicación: functions/api/mcp/handlers/archivo-core.ts (línea ~755)
Propósito
Función determinista que detecta y trunca bucles periódicos sostenidos en texto generado por LLM. Diseñada específicamente para el footgun de Gemma 4 26B-A4B en AI Studio, que ocasionalmente degenera en repetición estocástica.
Algoritmo
- Validación rápida: rechaza strings < 80 caracteres (demasiado cortos para bucle significativo).
- Tokenización: divide el texto en palabras (whitespace splitting).
- Búsqueda de patrones periódicos: para cada posición
iy periodop(1 ≤ p ≤ 16):- Mide racha continuada donde
tokens[j] == tokens[j-p](case-insensitive). - Si racha ≥ max(18, 3×p) palabras → encontró bucle significativo.
- Mide racha continuada donde
- Truncado conservador:
- Deja prefijo sano + una sola instancia del patrón (tokens[0..i+p-1]).
- Limpia conectores colgantes (”… o la de”, ”… and the”).
- Añade
[…]para indicar truncado.
Comportamiento
Input degenerado (caso real s109)
"...la continuación de un proceso de automatización automático o la de la continuación
de un proceso de automatización automático o la de la continuación de un proceso de
automatización automático o la de..." (×40)
- Periodo detectado: p ≈ 10 palabras (“la continuación de un proceso…”)
- Racha: >30 repeticiones = 300+ palabras periódicas
- Output:
"...la continuación de un proceso de automatización automático o la de […]"(200 chars) - Cambio: 2837 → 200 caracteres, información preservada.
Input sano
"El wifi se configura accediendo a 192.168.1.1. El wifi requiere una contraseña."
- Repetición de “El wifi” = racha de 2 (< 18 mínimo).
- Output: texto intacto (no toca repetición legítima puntual).
Sub-componentes
MAX_PERIOD = 16: límite superior del patrón buscado. Evita O(n²) extremo.- Umbral de racha:
max(18, 3×p)palabras. Conservador por diseño — requiere ≥3 repeticiones completas del periodo, que no aparece en prosa legítima. - Limpieza de conectores: regex que elimina palabras función/conectores colgantes al truncar (“o”, “la”, “de”, “and”, “the”).
Función hermana: clampSubstringRepetition() (11-09-2026)
clampRepetition() trocea por espacios: no ve un patrón que se repite dentro de un mismo token, sin separadores. El 11-09-2026 Edu vio en el chat del Correo una respuesta como {"action":"responder","answer":"...resak-resak-resak-..."} cientos de veces — una subcadena corta repetida sin espacios, que ni clampRepetition() ni la defensa por carácter del Oráculo (solo compara un carácter contra el anterior) llegaban a detectar. Ver [[incident—20260911—correo-chat-gemma-substring-loop]].
export function clampSubstringRepetition(text: string): string {
if (!text || text.length < 40) return text;
const m = /(.{2,12}?)\1{9,}/s.exec(text);
if (!m || m.index === undefined) return text;
return text.slice(0, m.index).trim();
}
- Patrón: 2 a 12 caracteres repetidos ≥10 veces seguidas (regex de backreference
(.{2,12}?)\1{9,}). - Corte: justo antes de la primera repetición detectada — no intenta conservar una instancia del patrón, a diferencia de
clampRepetition(). - Umbral mínimo: 40 caracteres de longitud total (más bajo que los 80 de
clampRepetition(), porque el patrón que busca es más corto y la detección más barata). - Fallback del llamador: si tras el corte quedan menos de 20 caracteres útiles, quien la invoca sustituye el resultado por una respuesta honesta en vez de devolver un fragmento vacío o inútil.
Cómo se invoca
En synthesizeAnswer() (línea ~1383), como segunda pasada tras clampRepetition():
let finalAnswer = clampRepetition(answer || 'Sin respuesta del modelo.');
const clamped = clampSubstringRepetition(finalAnswer);
if (clamped.length < finalAnswer.length) {
finalAnswer = clamped.length >= 20 ? clamped : genericFallback();
if (clamped.length < 20) degraded = true;
}
Y en functions/api/correo/ask.ts (chat del Correo), sobre el texto ya rescatado del JSON del modelo:
const clamped = clampSubstringRepetition(answer);
if (clamped.length < answer.length) {
degraded = clamped.length < 20;
answer = degraded
? 'Se me ha ido la respuesta (el modelo entró en bucle). Pregúntamelo otra vez, mejor en una conversación nueva.'
: clamped;
}
Ambos consumidores aplican clampSubstringRepetition() siempre, después de clampRepetition().
Impacto
- Defensivo: no toca texto sano ni repetición legítima.
- Determinista: mismo input → mismo output (no aleatorio como sampling parameters).
- Barato: O(n×16) tokens, sub-ms en textos normales.
- Sintomático: no soluciona la causa de Gemma 4 (uso de defensas software en síntesis porque sampling params no están disponibles para Gemma en AI Studio).
Decisiones de diseño
¿Por qué no frequencyPenalty / presencePenalty?
La API de Google AI Studio solo confirma estos parámetros para Gemini 1.5+, no para Gemma 4. Usarlos en Gemma causaría HTTP 400, tumbando toda la síntesis.
¿Por qué clampRepetition en vez de prompt-based defense?
Gemma 4 degenera de forma estocástica con el mismo input. El prompt (“no repitas”, etc.) no lo evita porque el modelo entra en un estado de generación que ignora constraints semánticas. La defensa determinista en output es la única garantía confiable.
¿Y si el usuario quería el texto repetido?
Improbable — en síntesis de respuestas sobre documentación nunca el usuario quiere un muro periódico. Si se encuentran casos legítimos (poco probable), se ajusta el umbral de racha.
Véase también
- [[incident—20260605—gemma-4-help-loop]]
- [[feature—biblioteca—anti-degeneracion-gemma-4]]
- [[entity—functions—handler—synthesizeanswer]]
- [[entity—functions—tool—bib-ask]]
- [[incident—20260911—correo-chat-gemma-substring-loop]]