Accueil / Articles

RAG conscient du domaine avec des registres MLflow et un routage de modèles LLM

RAG conscient du domaine : classifier l’intention, charger le modèle d’incorporation approprié depuis un registre MLflow, rechercher la collection Qdrant correspondante, puis diriger la génération vers un LLM.

Éditeur : REACTAPP.TOP12 min de lecture2688 mots

Sources: RAG Beyond Retrieval: Model Registry, Vector Search and Intelligent LLM Routing

La plupart des démos RAG relient un module d’incorporation, une collection et un LLM. Cela fonctionne tant que les questions relatives aux prêts immobiliers et celles liées à la médecine partagent le même processus, ou tant qu’un petit modèle local suffit pour le triage tandis qu’un modèle distant plus grand doit répondre aux détails cliniques. Un schéma plus solide consiste d’abord à classifier l’intention, à sélectionner la base de connaissances et le modèle d’incorporation correspondants, à récupérer le contexte, puis à diriger la génération vers l’LLM approprié.

Cette architecture met en œuvre cette idée avec du matériel modeste : le MLflow Model Registry sur une Raspberry Pi pour les modules d’incorporation, des collections Qdrant par domaine, Ollama pour la classification locale, ainsi que des appels ciblés à des LLM en cloud pour les domaines qui en ont besoin. Les difficultés de réseau liées à la connexion de la Raspberry Pi à Postgres font également partie du contexte, car les registres de modèles ne sont utiles que si le stockage backend est accessible.

Connexion à Postgres depuis la Raspberry Pi

Si Postgres a été installé via Homebrew sur un Mac se trouvant dans le même réseau local, confirmez quelle configuration utilise l’instance en cours d’exécution avant de la modifier :

psql -U <your_user> -d postgres -c "SHOW config_file;"

Les adresses d’écoute et les paramètres associés se trouvent dans postgresql.conf :

cd /opt/homebrew/var/postgresql@15
sed -i '' "s/#listen_addresses = 'localhost'/listen_addresses = '*'/" postgresql.conf

Vérifiez la configuration effective :

grep -v "^#" postgresql.conf | grep listen_addresses
# listen_addresses = '*'

Incluez l’IP du Pi dans la liste blanche de pg_hba.conf afin d’autoriser l’authentification par mot de passe depuis cet hôte :

echo "host  all  all  192.168.1.9/32  md5" >> pg_hba.conf

Vérifiez que les règles sont chargées comme prévu :

grep "192.168.1.9" pg_hba.conf

Redémarrez Postgres et confirmez qu’il écoute sur les interfaces souhaitées :

brew services restart postgresql@15
lsof -i -P | grep LISTEN | grep postgres

Vérifiez que le rôle nécessitant des privilèges d’accès existe :

psql -U <admin_user> -d postgres -c "\du"

Definissez ou réinitialisez le mot de passe que MLflow utilisera :

psql -U <admin_user> -d postgres -c "ALTER USER root WITH PASSWORD 'root';"

Vérifiez que la base de données cible existe :

psql -U <admin_user> -d postgres -c "\l" | grep mlflow
psql -U <admin_user> -d postgres -c "CREATE DATABASE mlflow OWNER root;"

Testez la connectivité brute depuis le Pi avant d’intégrer MLflow :

nc -zv 192.168.1.8 5432

Lorsque le chemin réseau et les identifiants fonctionnent correctement, lancez MLflow en direction de ce stockage backend distant :

mlflow server \
  --backend-store-uri postgresql://root:root@192.168.1.8:5432/mlflow \
  --host 0.0.0.0 \
  --port 5000 \
  --allowed-hosts "192.168.1.9:5000" \
  --cors-allowed-origins "http://192.168.1.9:5000"

Architecture en deux étapes

Étape 1 — couche de connaissances. Les PDFs du domaine sont divisés en fragments et intégrés avec des modèles sentence-transformer spécifiques au domaine, puis stockés dans des collections Qdrant distinctes. MLflow suit quelle version de l’outil d’intégration est la meilleure pour chaque domaine, afin que les services n’aient pas besoin d’encoder manuellement des chemins locaux.

Étape 2 — routage intelligent. Un classificateur attribue une étiquette à chaque question posée par l’utilisateur (dans cette démonstration : leucémie, prêt immobilier, ou autre). La récupération des informations utilise la collection et l’outil d’intégration correspondants. La génération de réponses fait appel au LLM choisi en fonction de l’intention — local pour certains chemins, distant pour d’autres — de sorte que les coûts et les capacités soient adaptés à la question posée.

Modules d’implémentation

model_registry.py enregistre Arctic-embed pour les prêts immobiliers et MiniLM pour la leucémie (ou le couple que vous choisissez), les enregistre dans MLflow sur la Pi, et crée des alias pour les modèles préférés :

import mlflow
import mlflow.sentence_transformers
from mlflow import MlflowClient
from mlflow.models import infer_signature
from sentence_transformers import SentenceTransformer

mlflow.set_tracking_uri("http://192.168.1.9:5000")
mlflow.set_experiment("embedding-models")

EMBEDDERS = {
    "mortgage-embedder": {"source": "Snowflake/snowflake-arctic-embed-m-v1.5"},
    "leukemia-embedder": {"source": "all-MiniLM-L6-v2"},
}

sample_texts = ["MLflow tracks embedding models", "Qdrant stores vectors"]
client = MlflowClient()

for name, cfg in EMBEDDERS.items():
    model = SentenceTransformer(
        cfg["source"],
        trust_remote_code=cfg.get("trust_remote_code", False),
        config_kwargs=cfg.get("config_kwargs"),
        device=cfg.get("device"),
    )
    with mlflow.start_run(run_name=name):
        info = mlflow.sentence_transformers.log_model(
            model=model,
            name="embedder",
            signature=infer_signature(sample_texts, model.encode(sample_texts)),
            input_example=sample_texts,
            registered_model_name=name,
        )
    client.set_registered_model_alias(name, "champion", info.registered_model_version)
    print(f"{name}: v{info.registered_model_version}, dim={model.get_embedding_dimension()}")

ingest_data.py reconstruit les collections de domaines, divise les PDF en tronçons avec une fenêtre de tokens et un niveau d’overlap constants, les incorpore à l’aide du modèle préféré pour ce domaine, puis les met à jour dans Qdrant :

import os
from chonkie import Pipeline
from qdrant_client import QdrantClient
from dotenv import load_dotenv, find_dotenv

load_dotenv(find_dotenv())

client = QdrantClient(url="http://localhost:6333", api_key=os.environ["QDRANT_API_KEY"])
client.delete_collection("leukemia_details") # this will delete and recreate the collection.

(Pipeline()
 .fetch_from("file", dir="../", ext=[".pdf"])
 .process_with("liteparse")
 .chunk_with("recursive", chunk_size=512)
 .refine_with("overlap", context_size=100)
 .store_in("qdrant", collection_name="leukemia_details", client=client,
           embedding_model="sentence-transformers/all-MiniLM-L6-v2")
 .run())

count = client.count("leukemia_details", exact=True).count
print(f"Ingested {count} chunks into leukemia_details")

ai_gateway.py classe les intentions à l’aide d’une instruction locale stricte via Ollama, puis sélectionne les backends de LLM en fonction de chaque étiquette :

from openai import OpenAI
import mlflow

mlflow.set_tracking_uri("http://192.168.1.9:5000")

client = OpenAI(
    base_url="http://192.168.1.9:5000/gateway/mlflow/v1",
    api_key="no-api-key",
)

CLASSIFIER_PROMPT = (
    "Classify the user query into exactly one label: LEUKEMIA, MORTGAGE, or OTHER.\n"
    "LEUKEMIA = questions about leukemia (AML, CML, ALL, CLL), its symptoms, diagnosis, "
    "treatment, prognosis, or related blood cancers.\n"
    "MORTGAGE = questions about home loans, mortgage rates, refinancing, EMI, "
    "eligibility, lenders, or mortgage documents.\n"
    "OTHER = everything else, including general medical questions not about leukemia.\n"
    "Reply with only the label."
)

ROUTES = {
    "LEUKEMIA": "openai",
    "MORTGAGE": "gemma",
}

REJECT_MESSAGE = (
    "Sorry, this setup can only answer Leukemia-specific medical questions "
    "and mortgage-related questions."
)


def classify(query: str) -> str:
    resp = client.chat.completions.create(
        model="ollama",  # local Ollama endpoint does the classification
        messages=[
            {"role": "system", "content": CLASSIFIER_PROMPT},
            {"role": "user", "content": query},
        ],
        temperature=0,
    )
    label = resp.choices[0].message.content.strip().upper()
    for intent in ROUTES:
        if intent in label:
            return intent
    return "OTHER"


def route(query: str) -> str:
    intent = classify(query)
    target = ROUTES.get(intent)
    if target is None:
        print(f"[intent={intent} -> rejected]")
        return REJECT_MESSAGE
    resp = client.chat.completions.create(
        model=target,
        messages=[{"role": "user", "content": query}],
    )
    print(f"[intent={intent} -> model={target}]")
    return resp.choices[0].message.content

#
# if __name__ == "__main__":
#     print(route("What are the first-line treatments for acute myeloid leukemia?"))
#     print(route("How does refinancing affect my monthly mortgage EMI?"))
#     print(route("What are the side effects of metformin?"))   # medical but not leukemia -> rejected
#     print(route("Write a haiku about monsoon in Hyderabad."))  # rejected

smart_rag.py associe la classification, le choix des collections, la récupération top-k et les réponses basées sur des données concrètes :

from qdrant_client import QdrantClient
import os

from ai_gateway import client, classify, ROUTES, REJECT_MESSAGE
from embedding_model_inference import EMBEDDERS
from dotenv import load_dotenv, find_dotenv

load_dotenv(find_dotenv())

COLLECTIONS = {
    "LEUKEMIA": {"collection": "leukemia_details", "embedder": "leukemia-embedder"},
    "MORTGAGE": {"collection": "mortgage", "embedder": "mortgage-embedder"},
}
TEXT_FIELD = "text"
EXIT_WORDS = {"bye", "exit", "quit"}


class SmartRAG:
    def __init__(self, qdrant_url="http://localhost:6333", top_k=5):
        self.qdrant = QdrantClient(url=qdrant_url, api_key=os.environ.get("QDRANT_API_KEY"))
        self.top_k = top_k

    def retrieve(self, query, intent):
        cfg = COLLECTIONS[intent]
        embedder = EMBEDDERS[cfg["embedder"]]
        prompt = "query" if "query" in embedder.prompts else None  # arctic needs its query prefix
        vec = embedder.encode(query, prompt_name=prompt, normalize_embeddings=True).tolist()
        hits = self.qdrant.query_points(cfg["collection"], query=vec, limit=self.top_k, with_payload=True).points
        return "\n\n".join(str(h.payload.get(TEXT_FIELD, h.payload)) for h in hits)

    def answer(self, query):
        intent = classify(query)
        if intent not in ROUTES:
            return REJECT_MESSAGE
        context = self.retrieve(query, intent)
        resp = client.chat.completions.create(
            model=ROUTES[intent],
            messages=[
                {"role": "system", "content": f"Answer using only this context:\n{context}"},
                {"role": "user", "content": query},
            ],
        )
        print(f"[intent={intent} -> {COLLECTIONS[intent]['collection']} -> {ROUTES[intent]}]")
        return resp.choices[0].message.content

    def run(self):
        while True:
            query = input("\nyou> ").strip()
            if query.lower() in EXIT_WORDS:
                break
            if query:
                print("\nassistant>", self.answer(query))


if __name__ == "__main__":
    SmartRAG().run()

embedding_model_inference.py charge les modèles d’incorporation depuis MLflow via des alias (models:/...@champion) afin que les chemins en ligne ne dépendent jamais du chemin sur l’ordinateur portable d’un développeur :

import mlflow
import mlflow.sentence_transformers

mlflow.set_tracking_uri("http://192.168.1.9:5000")

EMBEDDERS = {
    name: mlflow.sentence_transformers.load_model(f"models:/{name}@champion")
    for name in ("leukemia-embedder", "mortgage-embedder")
}

QUERIES = {
    "leukemia-embedder": "What are the first-line treatments for acute myeloid leukemia?",
    "mortgage-embedder": "How does refinancing affect my monthly mortgage EMI?",
}

for name, embedder in EMBEDDERS.items():
    vecs = embedder.encode([QUERIES[name]], normalize_embeddings=True)
    print(f"{name}: shape={vecs.shape}, prompts={list(embedder.prompts) or None}")

Résultats

Une question relative à un prêt immobilier doit être récupérée dans la collection dédiée aux prêts immobiliers à l’aide de l’embedder correspondant, avec une réponse fournie selon les règles du modèle LLM dédié aux prêts immobiliers. Une question concernant une leucémie doit suivre le parcours clinique prévu. Les questions hors domaine doivent être ignorées ou répondues sans prétendre que la récupération d’informations dans ce domaine a réussi.

/Users/pavanmantha/.local/bin/uv run /Users/pavanmantha/Pavans/PracticeExamples/DataScience_Practice/mlflow-tutorials/mlflow_on_edge/.venv/bin/python /Users/pavanmantha/Pavans/PracticeExamples/DataScience_Practice/mlflow-tutorials/mlflow_on_edge/src/smart_rag.py
Downloading artifacts: 100%|██████████| 16/16 [00:19<00:00,  1.19s/it]
2026/09/06 12:48:35 INFO mlflow.sentence_transformers: 'models:/leukemia-embedder@champion' resolved as 'mlflow-artifacts:/3/models/m-f568ee233dd748f997ba66763e41653b/artifacts'
Downloading artifacts: 100%|██████████| 1/1 [00:00<00:00, 76.69it/s]
Loading weights: 100%|██████████| 103/103 [00:00<00:00, 15587.15it/s]
Downloading artifacts: 100%|██████████| 16/16 [01:29<00:00,  5.60s/it]
2026/09/06 12:50:06 INFO mlflow.sentence_transformers: 'models:/mortgage-embedder@champion' resolved as 'mlflow-artifacts:/3/models/m-c4221c844dfb45bfa15c6a4ecde56920/artifacts'
Downloading artifacts: 100%|██████████| 1/1 [00:00<00:00, 27.17it/s]
Loading weights: 100%|██████████| 197/197 [00:00<00:00, 8855.57it/s]
[transformers] BertModel LOAD REPORT from: /var/folders/1x/36nz44_569501n4xs0px2_8m0000gn/T/tmpef4y6tj5/model.sentence_transformer
Key                 | Status  |
--------------------+---------+-
pooler.dense.weight | MISSING |
pooler.dense.bias   | MISSING |

Notes:
- MISSING: those params were newly initialized because missing from the checkpoint. Consider training on your downstream task.
leukemia-embedder: shape=(1, 384), prompts=['query', 'document']
mortgage-embedder: shape=(1, 768), prompts=['query', 'document']
/Users/pavanmantha/Pavans/PracticeExamples/DataScience_Practice/mlflow-tutorials/mlflow_on_edge/src/smart_rag.py:20: UserWarning: Api key is used with an insecure connection.
  self.qdrant = QdrantClient(url=qdrant_url, api_key=os.environ.get("QDRANT_API_KEY"))

you> What are the phases of treatment for acute lymphoblastic leukemia?
[intent=LEUKEMIA -> leukemia_details -> openai]

assistant> The phases of treatment for acute lymphoblastic leukemia (ALL) are:

1. Induction - achieving remission.
2. Consolidation - eradicating residual disease with different agents to circumvent resistance.
3. Maintenance (in ALL) - prolonged low-dose therapy to suppress re-emergence of the clone.

you> What was 30-day Average SOFR referred to in Series issued before July 2023?
[intent=MORTGAGE -> mortgage -> gemma]

assistant> *   Question: "What was 30-day Average SOFR referred to in Series issued before July 2023?"
    *   Constraint: Use *only* the provided context.

    *   Chunk 1: Discusses "Excess inclusion income", Residual Holders, taxable income, Trust REMIC taxable income, etc. No mention of SOFR or July 2023.
    *   Chunk 2: Discusses "Backup withholding", Non-U.S. Persons, Ginnie Mae Guaranty, State/Local/Foreign tax. No mention of SOFR or July 2023.
    *   Chunk 3: Discusses "Residual Holder's share of expenses", REITs, RICs, phantom income. No mention of SOFR or July 2023.
    *   Chunk 4: Discusses "Principal Distribution Amount", Ginnie Mae Project Loan Certificates, Underlying Certificates. No mention of SOFR or July 2023.
    *   Chunk 5: Discusses "OID on a Teaser Security", "Initial Distribution Period", "Excess Interest Payment", "Accrual of Original Issue Discount". No mention of SOFR or July 2023.

    *   I have scanned all five provided text nodes.

you>

Pourquoi cette structure est intéressante

Les registres permettent de remplacer les embedders sans avoir à rédéployer chaque client. Les collections par domaine évitent la contamination due aux voisins les plus proches provenant d’autres domaines. L’acheminement des intentions empêche que des modèles coûteux soient utilisés pour du trafic trivial. Il n’est pas nécessaire d’exécuter MLflow sur une Pi pour ce schéma, mais cela démontre que le registre peut fonctionner sur du matériel contrôlé situé près du réseau de laboratoire.

Précautions opérationnelles

Sécurisez Postgres avant de le mettre en ligne sur un réseau local. Traitez les modifications du fichier pg_hba comme des modifications en environnement de production. Fixez les versions de MLflow et des pilotes. Surveillez les taux d’erreur des classificateurs : une intention incorrecte envoie la requête dans la collection erronée et peut donner l’impression d’un « mauvais LLM ». Évaluez séparément les processus de récupération des données et d’acheminement. Lorsque des mises à jour sont apportées au registre, effectuez une comparaison avec un système de test ou une version en mode ombre avant d’interrompre le trafic en direct.

Conclusion

RAG au-delà d’un seul chemin de récupération relève principalement de l’architecture du produit : il faut connaître le domaine, effectuer la récupération avec les paramètres adéquats et générer les résultats en respectant des contraintes précises. MLflow, Qdrant et un petit gateway permettent de rendre cette architecture suffisamment concrète pour être utilisée sur un poste de travail, tout en assurant une discipline suffisante pour évoluer au-delà d’une simple démonstration.

Renforcement du chemin géré par le registre

Les alias de champion doivent passer par l’étape de préparation. Enregistrez l’ID de la version du modèle qui sert chaque réponse afin de pouvoir reconstituer les incidents après un changement dans le registre. Si le serveur tombe en panne, les clients ont besoin d’un mode de fermeture en cas d’erreur plutôt que de basculer silencieusement vers un point de contrôle local aléatoire.

Les paramètres de segmentation doivent figurer dans les métadonnées du registre à côté de l’élément d’intégration. Un nouveau champion ayant une longueur de contexte préférée différente ne doit pas réutiliser de vieux segments sans un indicateur de reconstruction.

Les prompts des classificateurs méritent des ensembles prédéfinis : dix formulations liées aux prêts immobiliers, dix formulations cliniques, dix formulations adversariales de type « autre ». Mesurez la précision et le rappel avant d’ajuster le choix du LLM. Sinon, vous chercherez à améliorer la qualité du générateur alors que le véritable problème réside dans le routage.

Pour les déploiements sur Raspberry Pi, surveillez l’usure du disque due aux écritures fréquentes d’artefacts et préférez des disques externes pour les racines des artefacts MLflow. Conservez les sauvegardes de Postgres sur une machine différente du Pi.

Lorsque vous expliquez la conception aux parties prenantes, décrivez la séquence : classifier → sélectionner l’embedder/collection → récupérer les données → choisir le LLM → répondre en citant des sources. Cette représentation est plus efficace qu’une simple liste d’outils et montre clairement où se prennent les décisions stratégiques.

Étendez les domaines avec prudence. Chaque nouvelle étiquette nécessite une collection, un responsable de l’embedder, une tâche d’ingestion, des exemples pour le classificateur et une politique relative au LLM. Le gateway doit refuser les étiquettes inconnues plutôt que de recourir automatiquement au modèle le plus volumineux.

Enfin, veillez à ce que le chemin « AUTRE » reste fiable. Refuser est préférable à récupérer des données depuis un domaine aléatoire afin d’éviter un silence gênant. Les utilisateurs font confiance aux systèmes qui savent quand ils sont hors de portée. L’exactitude du chemin et la qualité de la récupération doivent être mesurées comme des critères d’approbation distincts avant d’ajuster davantage les prompts du générateur. L’exactitude du chemin et la qualité de la récupération doivent être mesurées comme des critères d’approbation distincts avant d’ajuster davantage les prompts du générateur. L’exactitude du chemin et la qualité de la récupération doivent être mesurées comme des critères d’approbation distincts avant d’ajuster davantage les prompts du générateur. L’exactitude du chemin et la qualité de la récupération doivent être mesurées comme des critères d’approbation distincts avant d’ajuster davantage les prompts du générateur. L’exactitude du chemin et la qualité de la récupération doivent être mesurées comme des critères d’approbation distincts avant d’ajuster davantage les prompts du générateur. L’exactitude du chemin et la qualité de la récupération doivent être mesurées comme des critères d’approbation distincts avant d’ajuster davantage les prompts du générateur.

Les prompts sont affinés davantage. L’exactitude des itinéraires et la qualité de la récupération doivent être mesurées en tant que critères d’approbation distincts avant d’affiner davantage les prompts du générateur. L’exactitude des itinéraires et la qualité de la récupération doivent être mesurées en tant que critères d’approbation distincts avant d’affiner davantage les prompts du générateur. L’exactitude des itinéraires et la qualité de la récupération doivent être mesurées en tant que critères d’approbation distincts avant d’affiner davantage les prompts du générateur. L’exactitude des itinéraires et la qualité de la récupération doivent être mesurées en tant que critères d’approbation distincts avant d’affiner davantage les prompts du générateur. L’exactitude des itinéraires et la qualité de la récupération doivent être mesurées en tant que critères d’approbation distincts avant d’affiner davantage les prompts du générateur. L’exactitude des itinéraires et la qualité de la récupération doivent être mesurées en tant que critères d’approbation distincts avant d’affiner davantage les prompts du générateur. L’exactitude des itinéraires et la qualité de la récupération doivent être mesurées en tant que critères d’approbation distincts avant d’affiner davantage les prompts du générateur. ItinéraireL’exactitude et la qualité de récupération doivent être mesurées comme des critères d’édition distincts avant d’ajuster davantage les prompts du générateur.Elles doivent être mesurées comme des étapes de déploiement distinctes avant que les prompts du générateur ne soient davantage ajustés.Les seuils de validation doivent être définis avant d’affiner davantage les prompts du générateur. La précision des itinéraires et la qualité de la récupération doivent être mesurées en tant que seuils de publication distincts avant d’affiner davantage les prompts du générateur. La précision des itinéraires et la qualité de la récupération doivent être mesurées en tant que seuils de publication distincts avant d’affiner davantage les prompts du générateur. La précision des itinéraires et la qualité de la récupération doivent être mesurées en tant que seuils de publication distincts avant d’affiner davantage les prompts du générateur. La précision des itinéraires et la qualité de la récupération doivent être mesurées en tant que seuils de publication distincts avant d’affiner davantage les prompts du générateur. La précision des itinéraires et la qualité de la récupération doivent être mesurées en tant que seuils de publication distincts avant d’affiner davantage les prompts du générateur. La précision des itinéraires et la qualité de la récupération doivent être mesurées en tant que seuils de publication distincts avant d’affiner davantage les prompts du générateur. La précision des itinéraires et la qualité de la récupération doivent être mesurées en tant que seuils de publication distincts avant d’affiner davantage les prompts du générateur.Ils sont ajustés davantage. La précision des itinéraires et la qualité de la récupération doivent être mesurées comme des critères d’approbation distincts avant que les prompts du générateur ne soient à nouveau ajustés. La précision des itinéraires et la qualité de la récupération doivent être mesurées comme des critères d’approbation distincts avant que les prompts du générateur ne soient à nouveau ajustés. La précision des itinéraires et la qualité de la récupération doivent être mesurées comme des critères d’approbation distincts avant que les prompts du générateur ne soient à nouveau ajustés.