Workflow Automation

Construire un SaaS multi-tenant scalable avec Dify : Magasins de vecteurs isolés et limitation de débit dynamique

Le déploiement d'applications de grands modèles de langage (LLM) via des plateformes comme Dify a simplifié l'intégration de l'IA dans les flux de travail des entreprises. Cependant, lors du passage d'un prototype mono-tenant à une architecture SaaS multi-tenant, deux défis critiques émergent : l'isolation des données et l'équité des ressources. Si les données du tenant A se retrouvent dans les résultats de récupération du tenant B, ou si un seul consommateur intensif dégrade l'expérience de tous les autres, la plateforme échoue. Ce guide explore comment concevoir un système multi-tenant robuste en utilisant Dify, en mettant l'accent sur les magasins de vecteurs isolés et une limitation de débit sophistiquée basée sur l'usage.

La nécessité de magasins de vecteurs isolés

Par défaut, les bases de données vectorielles (comme Weaviate, Qdrant ou Milvus) utilisées dans les pipelines RAG (Génératif Augmenté par Récupération) partagent souvent un espace de noms ou une collection unique. Dans un environnement multi-tenant, c'est un cauchemar en termes de sécurité. Une requête pour "tenant_123" ne doit jamais retourner de documents appartenant à "tenant_456". Bien que Dify permette un filtrage de métadonnées personnalisé, s'appuyer uniquement sur le filtrage au niveau applicatif est fragile. L'approche la plus sûre est l'isolation architecturale au niveau de la couche de stockage.

Pour les tenants à haute sécurité, nous recommandons de mettre en œuvre une stratégie "base de données par tenant" ou "collection par tenant". Cela peut être réalisé en injectant un filtre dynamique dans le flux de travail de récupération de Dify. Voici un exemple de la manière dont vous pourriez modifier la logique de récupération au sein d'un plugin personnalisé Dify ou via un middleware API pour imposer une isolation stricte des espaces de noms.


from dify import DifyClient
from contextlib import contextmanager

def execute_isolated_retrieval(tenant_id: str, query: str, client: DifyClient):
    """
    Exécute une requête de récupération RAG strictement limitée à un tenant spécifique.
    Cela garantit que la recherche dans la base de données vectorielle filtre par la métadonnée 'tenant_id'.
    """
    # Dans Dify, vous pouvez passer des variables au nœud de base de connaissances.
    # Assurez-vous que votre jeu de données est configuré pour mapper 'metadata.tenant_id' à cette variable.
    
    params = {
        "inputs": {
            "query": query,
            "tenant_id": tenant_id  # Critique : Cela agit comme la clé de filtre
        },
        "response_mode": "blocking"
    }
    
    try:
        response = client.chat_messages(params)
        return response["answer"], response.get("metadata", {}).get("sources")
    except Exception as e:
        raise SecurityError(f"Échec de la récupération de données isolées pour le tenant {tenant_id} : {e}")

Pour une isolation encore plus stricte, vous pouvez exploiter la prise en charge par Dify de différents connecteurs de bases de données vectorielles par espace de travail si vous exécutez plusieurs instances Dify. Cependant, si vous exécutez une seule instance Dify pour de nombreux tenants, assurez-vous que votre pilote de base de données vectorielle est configuré pour appliquer une clause `filter` obligatoire basée sur le `tenant_id` passé dans les entrées de l'application. Cela prévient les attaques par "injection de prompt" qui tentent de contourner les filtres de récupération.

Mise en œuvre d'une limitation de débit basée sur l'usage

L'isolation n'est que la moitié de la bataille ; l'autre moitié est la gestion des ressources. Dans un modèle SaaS, vous offrez souvent des plans par niveaux (par exemple, Gratuit, Pro, Entreprise). Le niveau Gratuit peut avoir une utilisation de jetons limitée, tandis que le niveau Entreprise a un débit élevé. La limitation de débit native de Dify est souvent statique, il faut donc mettre en œuvre une limitation de débit dynamique à fenêtre glissante au niveau de la passerelle API ou au sein d'une couche middleware personnalisée.

Nous recommandons d'utiliser Redis pour cela, car il fournit les opérations atomiques nécessaires pour la limitation de débit à haute concurrence. La logique doit vérifier le niveau d'abonnement de l'utilisateur, déterminer son quota et décrémenteur le compteur. Si le quota est dépassé, la requête est rejetée avec un statut `429 Trop de requêtes`.


import redis
import time

r = redis.Redis(host='localhost', port=6379, db=0)

# Configurations des niveaux : [max_requetes_par_minute, max_jetons_par_jour]
TIERS = {
    "free": [10, 1000],
    "pro": [100, 10000],
    "enterprise": [1000, 100000]
}

def check_rate_limit(user_id: str, tier: str, estimated_tokens: int = 100) -> bool:
    """
    Vérifie si l'utilisateur est dans sa limite de débit en utilisant une fenêtre glissante pour les requêtes
    et un compteur quotidien pour les jetons.
    """
    now = time.time()
    one_minute_ago = now - 60
    today_key = f"token_usage:{user_id}:{int(now // 86400)}"
    request_key = f"req_window:{user_id}:{int(now)}"

    max_req, max_tok = TIERS.get(tier, TIERS["free"])

    # 1. Vérifier le quota de jetons (quotidien)
    current_tokens = r.get(today_key) or 0
    if int(current_tokens) + estimated_tokens > max_tok:
        return False
    
    # 2. Vérifier le taux de requêtes (Fenêtre glissante 1 minute)
    # L'utilisation d'un ZSET (Ensemble trié) pour une fenêtre glissante précise est meilleure, 
    # mais pour la simplicité, nous utilisons ici un compteur à fenêtre fixe.
    pipe = r.pipeline()
    pipe.incr(request_key)
    pipe.expire(request_key, 60)
    current_requests = pipe.execute()[0]
    
    if current_requests > max_req:
        return False

    # Si dans les limites, mettre à jour le nombre de jetons
    r.incrby(today_key, estimated_tokens)
    # Expirer la clé des jetons à la fin de la journée
    r.expireat(today_key, int((now // 86400 + 1) * 86400))
    
    return True

Intégrez cette vérification dans votre wrapper API Dify avant d'envoyer la requête au LLM. En estimant l'utilisation des jetons avant la requête (basée sur la longueur du prompt et la sortie attendue), vous pouvez bloquer proactivement les tenants qui consomment excessivement sans gaspiller les ressources de calcul du backend.

Conclusion

Construire un SaaS multi-tenant sur Dify nécessite de passer au-delà de la simple ingénierie de prompts vers une conception d'infrastructure robuste. En imposant une isolation stricte des magasins de vecteurs via le filtrage de métadonnées et en mettant en œuvre une limitation de débit dynamique par niveaux, vous assurez la sécurité, l'équité et la scalabilité. Commencez par auditer votre pipeline de récupération de données pour détecter les fuites potentielles, puis ajoutez des contraintes de ressources basées sur votre modèle économique. À mesure que votre base d'utilisateurs grandit, ces décisions architecturales détermineront si votre plateforme reste performante ou s'effondre sous le poids de son propre succès.

Share: