Dans des secteurs aussi sensibles que la finance, la santé ou le droit, les entreprises montrent une préférence croissante pour héberger en interne les capacités de questions-réponses intelligentes. Les modèles de langage généraux, bien que puissants, ne répondent pas à l'exigence fondamentale de confidentialité des données privées. C'est dans ce contexte que Langchain-Chatchat, un système de Q&R basé sur une base de connaissances et déployable localement, s'impose progressivement comme un choix clé pour construire des asssistants IA dédiés aux entreprises.
Fondé sur une architecture RAG (Retrieval-Augmented Generation), il permet aux utilisateurs de télécharger des fichiers privés tels que des PDF ou des Word, assure automatiquement l'analyse textique, le stockage vectoriel et combine le tout avec un modèle de langage fonctionnant localement pour fournir des réponses précises. L'ensemble du processus s'effectue sans appel API externe, garantissant ainsi que les connaissances ne quittent pas le domaine de l'entreprise.
Cependant, lorsque ce système passe de l'environnement de test à la production, un problème concret émerge : un déploiement sur un seul nœud ne peut supporter une forte concurrence, et une panne entraîne une interruption de service. Sans parler des latences élevées pour les accès inter-régionaux ou des temps de récupération en cas de sinistre. Comment rendre un système IA aussi gourmand en ressources à la fois stable et performant ? La réponse réside dans le déploiement multi-actif.
Le multi-actif n'est pas une simple duplication
Beaucoup considèrent à tort le multi-actif comme le simple fait de faire tourner plusieurs copies du même service à différents endroits. En réalité, un véritable déploiement multi-actif exige que chaque nœud puisse traiter indépendamment les requêtes en lecture/écriture, avec une cohérence finale des données. Cela implique la coordination de mécanismes complexes comme l'équilibrage de charge, la synchronisation d'état et le basculement sur panne.
Prenons l'exemple d'un système de base de connaissances pour une banque nationale : le siège de Pékin met à jour un nouveau document sur le procsesus d'approbation de crédit. Les employés des succursales de Shanghai, Shenzhen et Chengdu doivent y accéder instantanément. Avec une architecture maître-esclave traditionnelle, les autres nœuds devraient peut-être attendre une fenêtre de synchronisation planifiée. Avec un déploiement multi-actif couplé à un mécanisme de synchronisation incrémentielle piloté par événements, tous les nœuds peuvent reconstruire leur index en quelques minutes, assurant une cohérence des résultats de recherche pour l'ensemble des employés.
Décryptage de l'architecture : des modules à la synergie
L'un des atouts de Langchain-Chatchat réside dans sa segmentation claire en modules : interface frontend, service backend, base de données vectorielle, moteur d'inférence LLM, couche de stockage, tous découplés. Cette structure offre naturellement une facilité de déploiement distribué.
Dans un scénario multi-actif, il ne suffit pas de déployer simplement une « stack complète » par zone géographique. L'enjeu est de coordonner ces instances indépendantes pour qu'elles fonctionnent comme un tout cohérent.
Comment synchroniser les bases de données vectorielles ?
C'est l'une des questions centrales. La base vectorielle détient la « mémoire » des connaissances ; une incohérence entre les nœuds pourrait conduire à des situations embarrassantes comme « impossible de trouver à Shenzhen une politique publiée à Pékin ».
Actuellement, trois approches principales existent :
- Stockage partagé + montage local
Tous les nœuds montent un système de fichiers distribué (comme NFS ou MinIO). La base vectorielle lit et écrit directement à cet emplacement. L'avantage est une cohérence naturelle des données, mais l'inconvénient est une forte dépendance au réseau ; une perturbation au niveau du stockage affecte tous les nœuds. - Stockage indépendant + réplication asynchrone
Chaque nœud maintient sa propre copie de la base vectorielle. Un service de synchronisation centralisé déclenche la reconstruction des index inter-nœuds. Par exemple, en utilisant Kafka pour publier un événement « mise à jour de document », chaque nœud consomme l'événement, télécharge le fichier original et recalcule les embeddings. Cette approche offre une meilleure tolérance aux pannes, adaptée aux déploiements inter-régionaux. - Base de données vectorielle distribuée native
Utilisation de bases vectorielles supportant nativement la distribution comme Milvus ou Weaviate, partagées par un cluster. Bien que l'architecture soit simple, elle exige une maintenance avancée et présente un risque de point unique (sauf si la base vectorielle est elle-même en multi-actif).
En pratique, la deuxième approche est recommandée : elle offre un bon compromis entre cohérence et disponibilité. C'est particulièrement pertinent pour les combinaisons courantes en contexte chinois comme Chroma et FAISS, qui n'ont pas de capacités distribuées natives, rendant la réplication asynchrone la voie la plus praticable.
Exemple de code (Python) : Écoute d'une file d'attente de messages pour déclencher la mise à jour locale de l'index.
import hashlib
import datetime
from kafka import KafkaConsumer
from langchain_community.document_loaders import UnstructuredFileLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.embeddings import HuggingFaceEmbeddings
from langchain_community.vectorstores import Chroma
# Initialisation du modèle d'embeddings
embedding_model = HuggingFaceEmbeddings(model_name="BAAI/bge-small-zh-v1.5")
def calculate_checksum(filepath):
"""Calcule le hash SHA256 d'un fichier."""
with open(filepath, "rb") as f:
return hashlib.sha256(f.read()).hexdigest()
def update_index_if_changed(filepath):
"""Vérifie le hash et reconstruit l'index si nécessaire."""
file_hash = calculate_checksum(filepath)
# Vérification dans un registre local (ex: base SQLite)
if not local_registry.has_processed(file_hash):
loader = UnstructuredFileLoader(filepath)
raw_documents = loader.load()
splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=50)
document_chunks = splitter.split_documents(raw_documents)
# Mise à jour de la base vectorielle locale (écrasement)
vector_db = Chroma(persist_directory="./local_vectordb", embedding_function=embedding_model)
vector_db.add_documents(document_chunks)
vector_db.persist()
# Enregistrement du hash traité
local_registry.record_processed(file_hash, datetime.datetime.now())
Logique : Ce code peut être déployé comme un démon sur chaque nœud, écoutant les notifications de mise à jour provenant de la file d'attente centrale. L'index n'est reconstruit que lorsque le contenu du fichier a réellement changé, évitant ainsi des calculs inutiles.
Comment allouer les ressources pour l'inférence LLM ?
Un autre point critique est la consommation de ressources pour l'inférence LLM. Par exemple, ChatGLM-6B, même quantifié en INT4, nécessite environ 6 Go de mémoire VRAM. Si plusieurs nœuds partagent un seul GPU, la latence de réponse augmentera de manière drastique.
L'idéal est d'équiper chaque nœud actif de ressources d'inférence dédiées. Voici des pistes d'optimisation :
- Utiliser des modèles légers : tels que Qwen-Max ou Phi-3-mini, pour un équilibre performance/ressources.
- Activer le traitement par lots (batching) et le batching continu (continuous batching) pour améliorer le débit.
- Pour les zones à faible utilisation, prévoir un mode d'inférence CPU par défaut, plus lent mais fonctionnel.
Kubernetes est la plateforme idéale pour gérer ces ressources hétérogènes. Voici un fragment de configuration typique :
apiVersion: apps/v1
kind: Deployment
metadata:
name: chatchat-deployment-pekin
labels:
app: chatchat
zone: pekin
spec:
replicas: 2
selector:
matchLabels:
app: chatchat
zone: pekin
template:
metadata:
labels:
app: chatchat
zone: pekin
spec:
containers:
- name: chatchat-backend
image: monregistre/chatchat:0.3
ports:
- containerPort: 7860
env:
- name: VDB_STORAGE
value: "/mnt/vectordb"
volumeMounts:
- name: vectordb-volume
mountPath: /mnt/vectordb
- name: llm-server
image: monregistre/llm-inference:chatglm3-cuda12
resources:
limits:
nvidia.com/gpu: 1
requests:
memory: "8Gi"
cpu: "4"
volumes:
- name: vectordb-volume
nfs:
server: stockage-entreprise.internal
path: /data/chatchat/pekin
---
apiVersion: v1
kind: Service
metadata:
name: chatchat-service-pekin
spec:
selector:
app: chatchat
zone: pekin
ports:
- protocol: TCP
port: 7860
targetPort: 7860
type: NodePort
Cette configuration déploie deux répliques sur le nœud de Pékin, avec le conteneur LLM demandant explicitement un GPU NVIDIA. Des déploiements similaires pour shanghai et shenzhen peuvent être créés, et un équilibrage de charge global peut unifier l'accès.
Note : Si les ressources GPU sont limitées, il peut être judicieux de découper le service d'inférence en un micro-service cluster distinct, capable de mise à l'échelle élastique à la demande, plutôt que de l'intégrer dans chaque nœud.
Comment garantir l'expérience utilisateur ?
Le multi-actif n'est pas qu'une question technique, c'est aussi une question d'expérience utilisateur. Imaginez un utilisateur du Guangdong en plein dialogue avec un assistant IA ; le système bascule soudainement sa requête vers un nœud de Shanghai, et l'historique de conversation est perdu. L'expérience serait certainement dégradée.
Par conséquent, l'état des sessions doit être géré de manière centralisée. La solution recommandée est d'intégrer un cluster Redis pour stocker les données de session :
from redis.cluster import RedisCluster
import json
# Connexion au cluster Redis
startup_nodes = [
{"host": "redis-node-1", "port": 6379},
{"host": "redis-node-2", "port": 6379},
{"host": "redis-node-3", "port": 6379}
]
redis_conn = RedisCluster(startup_nodes=startup_nodes, decode_responses=True)
def save_user_session(user_identifier, conversation_history):
"""Sauvegarde l'historique de conversation avec un TTL."""
key = f"session:{user_identifier}"
redis_conn.setex(key, 1800, json.dumps(conversation_history)) # TTL de 30 min
def retrieve_user_session(user_identifier):
"""Récupère l'historique de conversation."""
key = f"session:{user_identifier}"
data = redis_conn.get(key)
return json.loads(data) if data else []
Ainsi, quel que soit le nœud vers lequel un utilisateur est routé, il peut restaurer son contexte tant qu'il a accès au cluster Redis. Un délai d'expiration raisonnable est également défini pour éviter une occupation mémoire prolongée.
De plus, l'utilisation d'outils comme Istio ou OpenTelemetry permet d'implémenter un traçage distribué inter-zones, facilitant l'identification des goulots d'étranglement en latence et des appels anormaux.
Considérations clés pour le déploiement réel
1. Fréquence de synchronisation des données vs compromis de coût
Une synchronisation en temps réel complet est coûteuse, surtout lorsque les documents sont fréquemment mis à jour. Il est conseillé d'adopter une stratégie de « priorité à l'incrémentation + vérification complète périodique » :
- Lors d'un changement de document, un événement est émis pour déclencher une synchronisation asynchrone ;
- Chaque nuit, une comparaison complète est exécutée pour réparer les écarts potentiels.
Cela permet de garantir une cohérence au quotidien sans surcharger le système.
2. Arbitrage en cas de partition réseau
Un déploiement multi-sites est inévitablement confronté à des fluctuations réseau. Lorsqu'un nœud est temporairement isolé, faut-il continuer à fournir des données obsolètes ou refuser les requêtes ?
Selon le théorème CAP, un choix doit être fait. Pour un système de base de connaissances, on privilégie généralement la disponibilité et la tolérance au partitionnement (AP), c'est-à-dire autoriser une brève incohérence des données au profit de la disponibilité du service. La cohérence sera rattrapée une fois le réseau rétabli.
3. La sécurité et la conformité ne sont pas négociables
Même au sein d'un réseau interne, la sécurité ne doit pas être relâchée. Il est recommandé de :
- Activer le chiffrement TLS pour toutes les communications inter-nœuds ;
- Chiffrer de bout en bout les documents sensibles avant leur transfert ;
- Centraliser la conservation des journaux d'audit pour répondre aux exigences de conformité (ex: RGPD).
4. Le système de monitoring doit être au rendez-vous
Un déploiement multi-actif sans monitoring revient à « conduire à l'aveugle ». Les indicateurs de base à surveiller comprennent :
| Indicateur | Seuil d'alerte | Outil suggéré |
|---|---|---|
| État de santé des nœuds | 3 battements de cœur consécutifs échoués | Prometheus + Alertmanager |
| Latence de synchronisation vectorielle | > 5 minutes | Exporter personnalisé |
| Temps de réponse (P95) | > 2 secondes | Grafana |
| Utilisation GPU | Continue > 85% | Node Exporter + cAdvisor |
La visualisation via des tableaux de bord en temps réel permet de maîtriser l'état global et de réagir rapidement.
À quoi ressemble l'architecture finale ?
Nous pouvons schématiser une topologie de déploiement multi-actif typique :
[Équilibreur de charge global (DNS/GSLB)]
|
------------------------------------------------
| | |
[Nœud Pékin] [Nœud Shanghai] [Nœud Shenzhen]
|---------------| |----------------| |-----------------|
| Interface Web | | Interface Web | | Interface Web |
| Base Vect (Chroma)| | Base Vect (Chroma)| | Base Vect (Chroma)|
| Inférence LLM (GPU)| | Inférence LLM (GPU)| | Inférence LLM (GPU)|
| Stockage Partagé| | Stockage Partagé| | Stockage Partagé|
| (NFS/MinIO) | | (NFS/MinIO) | | (NFS/MinIO) |
|---------------| |----------------| |-----------------|
| | |
-------------|Synchronisation événementielle via Kafka|-----------------
↑
[Service de synchronisation centralisé]
(Écoute des modifications de documents, diffusion d'événements)
↑
[Cluster Redis (Sessions)]
[Prometheus + Grafana (Monitoring)]
Cette architecture atteint plusieurs objectifs importants :
- Accès par proximité : les utilisateurs se connectent au nœud le plus proche, réduisant la latence ;
- Isolation des pannes : la défaillance d'un nœud n'affecte pas l'ensemble du système ;
- Extension élastique : l'ajout d'une nouvelle zone géographique ne nécessite que le déploiement d'un nouveau nœud et son intégration au réseau de synchronisation ;
- Conformité facilitée : les données peuvent être maintenues dans des zones géographiques spécifiques selon les besoins.