Version : 4.15.0 Société : AISIA — Structure juridique en cours de création URL publique : https://aisia.fr/
---
1. Philosophie local-first 2. Les 4 runtimes locaux 3. Catalogue des 83 modèles locaux (52 activés) 4. Calcul de confiance 5. Self-consistency sampling 6. Routage local : sélection des modèles 7. Hot-reload à chaud 8. Ollama edge distribué 9. GPU Jetson Xavier AGX 10. Configuration complète 11. Monitoring et diagnostics 12. Fallback vers le cloud
---
Le mode local-first est un principe fondamental d'AISIA : **privilégier les modèles locaux** avant de recourir aux providers cloud.
| Aspect | Local-first | Cloud-only |
|---|---|---|
| Confidentialité | Données restent sur site | Données envoyées à un tiers |
| Latence | <1s pour modèles légers | 1-5s selon le provider |
| Coût | Gratuit (infra propre) | Payant (API tokens) |
| Disponibilité | Indépendant d'Internet | Dépendant du provider |
| Contrôle | Total | Dépendant des CGU |
Le mode local-first s'active quand :
1. AUTONOMOUS_LOCAL_ENABLED=true dans la configuration
2. Au moins un modèle local est disponible (health() == True)
3. La confiance de la réponse locale dépasse le seuil (0.75 par défaut)
Si la confiance est insuffisante, la requête est transmise aux providers cloud (fallback transparent pour l'utilisateur).
---
AISIA supporte 4 runtimes d'inférence pour les modèles locaux, chacun ayant ses forces :
Fichier : app/local_runtime/ollama.py
Le runtime principal, conçu pour la simplicité d'utilisation sur ARM64 (Raspberry Pi).
| Caractéristique | Valeur |
|---|---|
| Protocole | HTTP REST (/api/generate) |
| Endpoint défaut | http://localhost:11434 |
| Architectures | ARM64, AMD64 |
| Formats modèles | GGUF (quantisés) |
| Usage principal | Inférence CPU sur RPi |
Fichier : app/local_runtime/vllm.py
Runtime haute performance utilisant PagedAttention pour une inférence GPU optimisée.
| Caractéristique | Valeur |
|---|---|
| Protocole | HTTP REST compatible OpenAI (/v1/completions) |
| Architectures | AMD64, ARM64 (avec GPU CUDA) |
| Formats modèles | HuggingFace, AWQ, GPTQ |
| Usage principal | Inférence GPU (Jetson, NVIDIA) |
Fichier : app/local_runtime/llamacpp.py
Inférence optimisée pour CPU, écrite en C/C++. Idéal pour les modèles quantisés.
| Caractéristique | Valeur |
|---|---|
| Protocole | HTTP REST (/completion) |
| Architectures | Toutes (x86, ARM, Apple Silicon) |
| Formats modèles | GGUF |
| Usage principal | Inférence CPU optimisée |
Fichier : app/local_runtime/tgi.py
Runtime de Hugging Face pour la génération de texte à grande échelle.
| Caractéristique | Valeur |
|---|---|
| Protocole | HTTP REST (/generate) |
| Architectures | AMD64, ARM64 |
| Formats modèles | HuggingFace natif |
| Usage principal | Inférence distribuée |
Tous les runtimes implémentent l'interface LocalRuntimeAdapter :
class LocalRuntimeAdapter(ABC):
async def start(self) -> None: # Initialise la session HTTP
async def close(self) -> None: # Ferme la session
async def warmup(self) -> None: # Pré-chauffe le modèle
async def health(self) -> bool: # Vérifie la disponibilité
async def is_available(self) -> bool: # Test de base
async def generate(
self,
prompt: str,
max_tokens: int,
n_samples: int,
temperature: float = 0.7,
) -> list[str]: # Génère N échantillons
def status_dict(self, available: bool) -> dict: # Statut JSON
---
Les modèles sont définis dans local_models.yaml à la racine du projet.
- model_id: "ollama-llama3.2-3b"
display_name: "Llama 3.2 3B (Ollama)"
runtime: "ollama"
endpoint: "http://aisia_ollama:11434"
model_name: "llama3.2:3b"
priority: 100
enabled: true
preferred_tasks: ["general", "creative"]
modalities: ["text"]
max_context_tokens: 32768
confidence_threshold: 0.75
support_tier: "golden"
validation_status: "validated"
warmup: false
| Attribut | Type | Description |
|---|---|---|
model_id | string | Identifiant unique |
display_name | string | Nom d'affichage |
runtime | string | Runtime utilisé (ollama/vllm/llamacpp/tgi) |
endpoint | string | URL du serveur d'inférence |
model_name | string | Nom du modèle pour le runtime |
priority | int | Priorité de base (plus élevé = plus prioritaire) |
enabled | bool | Modèle activé (surchargeable via DB) |
preferred_tasks | list | Tâches préférées (general, coding, creative, reasoning, realtime) |
modalities | list | Modalités supportées (text, image, audio) |
max_context_tokens | int | Taille maximale du contexte |
confidence_threshold | float | Seuil de confiance spécifique (optionnel) |
support_tier | string | Niveau de support (golden, silver, catalog) |
validation_status | string | Statut de validation (validated, declared, deprecated) |
warmup | bool | Pré-chauffe au démarrage |
Si local_models.yaml n'existe pas, deux modèles par défaut sont créés :
{
"model_id": "ollama-primary",
"runtime": "ollama",
"model_name": settings.autonomous_primary_model, # llama3.2:3b
"priority": 100,
"preferred_tasks": ["general", "creative"],
},
{
"model_id": "ollama-fallback",
"runtime": "ollama",
"model_name": settings.autonomous_fallback_model, # llama3.2:1b
"priority": 90,
"preferred_tasks": ["reasoning", "coding", "general"],
}
---
Le calcul de confiance détermine si une réponse locale est suffisamment fiable pour être retournée à l'utilisateur sans passer par le cloud.
if cache_score > 0:
confidence = 0.7 cache_score + 0.3 consistency_score
else:
confidence = consistency_score
Deux composantes contribuent à la confiance :
Compare la réponse générée avec les réponses similaires stockées dans Qdrant.
def _cache_score(prompt, response, qdrant_store):
# 1. Cherche les K réponses les plus similaires dans Qdrant
results = qdrant_store.search_knowledge(
query=prompt,
limit=settings.autonomous_cache_lookup_limit, # 5
score_threshold=0.25,
)
# 2. Encode la réponse candidate
candidate_vector = qdrant_store.encode(response)
# 3. Encode les réponses cachées
cached_vectors = [qdrant_store.encode(text) for text in cached_texts]
# 4. Calcule le centroid des réponses cachées
centroid = np.mean(cached_vectors, axis=0)
centroid = centroid / np.linalg.norm(centroid)
# 5. Similarité cosinus avec le centroid
return np.dot(centroid, candidate_vector)
Un cache_score élevé signifie que la réponse est cohérente avec les meilleures réponses précédentes pour des questions similaires.
Mesure la cohérence entre les N échantillons générés par self-consistency.
def _consistency_score(samples, qdrant_store):
# Si embeddings disponibles : similarité cosinus paire à paire
# Sinon : similarité lexicale (Jaccard)
for i, first in enumerate(embeddings):
for second in embeddings[i + 1:]:
scores.append(np.dot(first, second))
return np.mean(scores)
Un score de cohérence élevé signifie que le modèle produit des réponses consistantes pour la même question (signe de confiance).
| Seuil | Variable | Défaut | Effet |
|---|---|---|---|
| Global | AUTONOMOUS_CONFIDENCE_THRESHOLD | 0.75 | Seuil par défaut |
| Par modèle | confidence_threshold dans YAML | null (hérite du global) | Seuil spécifique |
| Confiance | Interprétation | Action | |
| ----------- | ---------------- | -------- | |
| >= 0.75 | Bonne confiance | Réponse locale retournée | |
| 0.50 - 0.74 | Confiance modérée | Fallback cloud | |
| < 0.50 | Faible confiance | Fallback cloud |
| Variable | Défaut | Description |
|---|---|---|
AUTONOMOUS_CACHE_LOOKUP_LIMIT | 5 | Nombre de résultats Qdrant à consulter |
AUTONOMOUS_CACHE_SCORE_THRESHOLD | 0.92 | Seuil de similarité pour le cache |
Pour chaque requête locale, AISIA génère N échantillons (réponses) à partir du même prompt et mesure leur cohérence mutuelle.
| Variable | Défaut | Description |
|---|---|---|
AUTONOMOUS_SELF_CONSISTENCY_SAMPLES | 3 | Nombre d'échantillons |
AUTONOMOUS_MAX_TOKENS | 1024 | Tokens max par échantillon |
1. Le prompt est envoyé N fois au modèle local (temperature > 0 pour variabilité) 2. Le premier échantillon non-vide est sélectionné comme réponse principale 3. La cohérence entre les N échantillons est mesurée : - Avec embeddings Qdrant : similarité cosinus paire à paire - Sans Qdrant : similarité lexicale Jaccard
Quand Qdrant/embeddings ne sont pas disponibles :
def _lexical_similarity(first, second):
first_tokens = set(first.lower().split())
second_tokens = set(second.lower().split())
overlap = len(first_tokens & second_tokens)
union = len(first_tokens | second_tokens)
return overlap / union # Jaccard index
---
Le AutonomousLocalRouter utilise un système de scoring pour classer
les modèles candidats :
def _score_model(definition, features):
# Vérification des modalités
if not requested_modalities.issubset(supported_modalities):
return None # Modèle exclu
score = definition.priority # Score de base (100 par défaut)
# Bonus tâche préférée
if task_type in definition.preferred_tasks:
score += 100
elif "general" in definition.preferred_tasks:
score += 25
# Bonus contextuels
if features.get("has_code") and "coding" in preferred_tasks:
score += 10
if features.get("is_creative") and "creative" in preferred_tasks:
score += 10
if features.get("is_realtime") and "realtime" in preferred_tasks:
score += 10
if features.get("length_bucket") == "long" and max_context >= 32000:
score += 5
return score
Les modèles sont triés par score décroissant. AISIA essaie chaque modèle dans l'ordre jusqu'à obtenir une réponse suffisamment confiante :
for definition in candidates:
adapter = self.adapters.get(definition.model_id)
if adapter is None or not await adapter.health():
continue
samples = await adapter.generate(prompt, max_tokens, n_samples)
confidence = self._compute_confidence(samples, ...)
if confidence >= threshold:
return result # Réponse locale suffisamment confiante
if best_result is None or confidence > best_result.confidence:
best_result = result # Garder la meilleure tentative
Si aucun modèle n'atteint le seuil, la meilleure tentative est retournée
(avec confident=False), signalant au routeur principal de basculer
vers le cloud.
---
POST /admin/autonomy/reload
Authorization: Bearer TOKEN_ADMIN
async def hot_reload(self):
# 1. Invalide le cache des providers et modèles locaux
invalidate_providers_cache()
# 2. Ferme tous les adaptateurs existants (timeout 5s par adaptateur)
await self.close()
self.adapters = {}
# 3. Relit local_models.yaml depuis le disque
raw_models = get_active_local_models()
# 4. Crée les nouvelles définitions
self.local_models = [
LocalModelDefinition.from_dict(model)
for model in raw_models
if model.get("enabled", True)
]
# 5. Initialise les nouveaux adaptateurs
await self.start()
# 6. Incrémente le compteur de rechargement
self.reload_count += 1
self.last_reload_at = datetime.now(UTC)
# Timeout global : 30 secondes
{
"available": true,
"model_count": 74,
"available_model_count": 12,
"runtimes": ["llama.cpp", "ollama", "tgi", "vllm"],
"reload_count": 3,
"last_reload_at": "2026-04-19T10:30:00Z",
"last_route": {
"ok": true,
"confident": true,
"model": "llama3.2:3b",
"model_id": "ollama-llama3.2-3b",
"runtime": "ollama",
"confidence": 0.82,
"latency_ms": 1200.5,
"timestamp": "2026-04-19T10:29:30Z"
},
"models": [
{
"model_id": "ollama-llama3.2-3b",
"display_name": "Llama 3.2 3B",
"runtime": "ollama",
"available": true,
"last_error": ""
}
]
}
---
AISIA déploie 3 replicas Ollama sur des Raspberry Pi 4 du cluster :
Requête utilisateur
│
▼
AutonomousLocalRouter
│
┌────┴────┐────────┐
▼ ▼ ▼
RPi-01 RPi-02 RPi-03
Ollama Ollama Ollama
(llama3.2) (llama3.2) (llama3.2)
Dans stack-poc-global-reuse.yml, le service Ollama est configuré avec :
aisia.ollama=true/mnt/docker/AISIA/ollama/modelsLe routeur local essaie les modèles dans l'ordre de priorité. Chaque adaptateur Ollama pointe vers un endpoint spécifique ou vers le service Swarm (DNS round-robin automatique via le réseau overlay).
Les modèles doivent être téléchargés sur chaque noeud Ollama :
# Sur chaque noeud (via SSH ou script de maintenance)
ollama pull llama3.2:3b
ollama pull llama3.2:1b
ollama pull codellama:7b
ollama pull mistral:7b
Le script deploy/pull-models.sh automatise ce processus.
---
Le noeud Jetson héberge 6 modèles de taille 7B-8B optimisés pour GPU :
| Modèle | Taille | Runtime | Usage |
|---|---|---|---|
| Llama 3.1 8B | 8B | vLLM / Ollama | Général, raisonnement |
| CodeLlama 7B | 7B | Ollama | Code |
| Mistral 7B | 7B | Ollama | Général, multilingue |
| Llama 3.2 3B | 3B | Ollama | Rapide, général |
| Phi-3 Mini | 3.8B | Ollama | Compact, raisonnement |
| Gemma 2B | 2B | Ollama | Ultra-rapide |
# En LAN
ssh slambert@192.168.1.200
Hors LAN (via jump host)
ssh -J slambert@lambda.freeboxos.fr slambert@192.168.1.200
# Label GPU sur le noeud Jetson
docker node update --label-add aisia.gpu=true nvidia
docker node update --label-add aisia.role=gpu nvidia
Le service aisia_gpu est déployé avec la contrainte :
placement:
constraints:
- node.labels.aisia.gpu == true
---
| Variable | Défaut | Description |
|---|---|---|
AUTONOMOUS_LOCAL_ENABLED | false | Active le mode local-first |
AUTONOMOUS_LOCAL_HOST | http://localhost:11434 | URL Ollama |
AUTONOMOUS_PRIMARY_MODEL | llama3.2:3b | Modèle principal |
AUTONOMOUS_FALLBACK_MODEL | llama3.2:1b | Modèle de secours |
AUTONOMOUS_LOCAL_TIMEOUT_S | 15 | Timeout requête locale |
AUTONOMOUS_CONFIDENCE_THRESHOLD | 0.75 | Seuil de confiance global |
AUTONOMOUS_SELF_CONSISTENCY_SAMPLES | 3 | Échantillons self-consistency |
AUTONOMOUS_MAX_TOKENS | 1024 | Tokens max par réponse |
AUTONOMOUS_CACHE_LOOKUP_LIMIT | 5 | Nombre de résultats cache |
AUTONOMOUS_CACHE_SCORE_THRESHOLD | 0.92 | Seuil similarité cache |
LOCAL_MODELS_YAML | ./local_models.yaml | Chemin du catalogue modèles |
| Fichier | Description |
|---|---|
local_models.yaml | Catalogue des 83 modèles locaux (52 activés) |
providers.yaml | Registre des providers (inclut les locaux) |
GET /admin/autonomy/status
Authorization: Bearer TOKEN
L'objet last_route dans la réponse de statut montre la dernière
décision de routage local :
{
"ok": true,
"confident": true,
"model": "llama3.2:3b",
"model_id": "ollama-llama3.2-3b",
"runtime": "ollama",
"confidence": 0.82,
"attempted_models": ["ollama-llama3.2-3b"],
"latency_ms": 1200.5,
"error": "",
"timestamp": "2026-04-19T10:30:00Z"
}
Si un modèle affiche available: false, vérifiez :
1. Endpoint accessible : curl http://endpoint:port/
2. Modèle téléchargé : ollama list sur le noeud
3. Espace disque : les modèles GGUF font 2-8 Go
4. Timeout : le modèle peut être trop lent au démarrage
Les modèles peuvent être activés/désactivés via l'interface admin.
Ces surcharges sont stockées dans aisia_local_model_overrides (MariaDB)
et prennent priorité sur local_models.yaml.
---
Le fallback cloud est activé quand :
1. Aucun modèle local disponible : tous les modèles sont hors ligne
- error: "Aucun modèle local compatible avec la requête"
2. Confiance insuffisante : le meilleur modèle ne dépasse pas le seuil
- confident: false, confidence: 0.62
- La réponse locale est retournée avec le flag confident=False
- Le routeur principal (main.py) décide de passer au cloud
3. Aucune réponse exploitable : les modèles n'ont rien généré
- error: "Aucun modèle local n'a produit de réponse exploitable"
Le fallback est transparent : l'utilisateur reçoit une réponse sans savoir
si elle provient d'un modèle local ou cloud. L'information est disponible
dans les métadonnées de la réponse (provider_id, runtime).
Pour analyser le taux de fallback :
runtime: "ollama" vs les providers cloudai_requests_total{provider="ollama-*"} avec les providers cloudconfidence moyen dans les logs du routeur localDocument AISIA v4.21.0