Volver a la wiki

Incident s109: Contaminación de contexto en Oráculo por respuestas degradadas

Resumen

El Oráculo de la UI (chat multi-turno en SearchInline.tsx) presentaba un bucle de realimentación de contexto: respuestas degradadas (fallback genérico ‘tu pregunta es muy amplia’) se reenviaban como historia válida, lo que hacía que en la misma conversación cacheada, al repetir la pregunta, el modelo veía el fallback anterior y lo repetía indefinidamente.

Impacto: usuario reutiliza conversación en caché → hace otra pregunta → Oráculo repite fallback anterior aunque debería responder bien.

Causa raíz confirmada: Edu (s109) verificó que en caja limpia (pestaña recargada) el Oráculo responde perfecto. El bucle solo ocurría reutilizando la conversación cacheada.


Causa raíz (análisis)

El flujo es:

  1. Usuario pregunta → Oráculo falla en síntesis o retrieval → devuelve fallback genérico HTTP 200.
  2. Cliente (SearchInline.tsx) filtraba errores de red (error?: boolean) pero no diferenciaba fallbacks.
  3. El fallback se guardaba como turno asistente válido en turns[].
  4. Al hacer otra pregunta, historyPayload incluía el fallback.
  5. El modelo LLM veía que “el Oráculo dijo X fallback antes” → reproducía el patrón en la respuesta nueva.

El fallback es técnicamente un HTTP 200 (no un error), pero semánticamente es una no-respuesta que contamina el contexto.


Fix aplicado (PR#97, 3 archivos)

1. archivo-core.ts — añade campo degraded

export interface AskResult {
  // ... campos existentes ...
  degraded?: boolean;  // true cuando se devuelve el fallback genérico
}

En synthesizeAnswer(), cuando no se puede extraer respuesta útil del modelo:

if (!answer) {
  degraded = true;  // marcar como degradada
  answer = '...fallback genérico...';
}

Retorna { ..., degraded }.

2. ask.ts — propaga degraded en respuestas

Dos casos más donde se devuelve fallback sin respuesta útil (low-relevance, no-match):

// No-match (retrieval falló)
return Response.json({
  degraded: true,
  // ...
});

// Low-relevance score
return Response.json({
  degraded: true,
  // ...
});

// Caso normal: propagar desde synthesizeAnswer
degraded: result.degraded ?? false,

3. SearchInline.tsx — excluye degraded del historyPayload

Antes:

const historyPayload = priorTurns
  .filter((t) => !('error' in t) || !t.error)  // solo exluye errores de red
  .map((t) => ({ role: t.role, content: t.content }));

Después:

const historyPayload = priorTurns
  .filter((t) => t.role === 'user' || (!t.error && !t.degraded))
  // explícitamente: incluye users, excluye assistants con error O degraded
  .map((t) => ({ role: t.role, content: t.content }));

Guarda degraded en el turno:

degraded: data.degraded,

Verificación


Impacto técnico

AspectoDetalle
Cambio de schemaAskResult + OracleResponse incluyen degraded?: boolean
Filtrado en clientehistoryPayload ahora excluye tanto error como degraded
Propagaciónarchivo-core → ask.ts → SearchInline.tsx
CompatibilidadCampo opcional (?); clientes viejos ignoran degraded (degrada a incluir fallback, menos malo que antes)

Contexto: cadena de urgencia del Oráculo

Este es el tercer y final escalón de la cadena de arreglos urgentes:

  1. PR#95: Mejorar retrieval (búsqueda de chunks).
  2. PR#96: Síntesis robusta (manejo de respuestas vacías/ininteligibles).
  3. PR#97 (este): History limpio (no realimentar fallbacks).

Las tres partes verificadas individualmente y en conjunto.


Véase también

Subir