RAG consciente del dominio con registros de MLflow y enrutamiento de LLM
RAG consciente del dominio: clasificar la intención, cargar el modelo de incrustación adecuado desde un registro de MLflow, buscar la colección correspondiente en Qdrant y, a continuación, dirigir la generación a un LLM.
Fuentes: RAG Beyond Retrieval: Model Registry, Vector Search and Intelligent LLM Routing
La mayoría de las demostraciones de RAG conectan un modelo de incrustación, una colección y un LLM. Eso funciona bien hasta que las preguntas relacionadas con hipotecas y asuntos médicos comparten el mismo proceso, o hasta que un modelo local pequeño sea suficiente para la triaje mientras que un modelo remoto más grande debe responder a los detalles clínicos. Un patrón más robusto clasifica primero la intención, selecciona la base de conocimientos y el modelo de incrustación correspondientes, obtiene el contexto y luego dirige la generación al LLM adecuado.
Esta implementación aplica esa idea con hardware modesto: MLflow Model Registry en una Raspberry Pi para los modelos de incrustación, colecciones Qdrant por dominio, Ollama para la clasificación local, y llamadas selectivas a LLM en la nube para los dominios que lo necesitan. Los problemas de red relacionados con conectar la Raspberry Pi a Postgres forman parte del desafío, ya que los registros de modelos solo son útiles si se puede acceder al almacenamiento backend.
Conectividad con Postgres desde la Raspberry Pi
Si Postgres se instaló mediante Homebrew en una Mac de la misma red local, confirme qué configuración utiliza la instancia en ejecución antes de editarla:
psql -U <your_user> -d postgres -c "SHOW config_file;"
Las direcciones de escucha y los ajustes relacionados se encuentran en postgresql.conf:
cd /opt/homebrew/var/postgresql@15
sed -i '' "s/#listen_addresses = 'localhost'/listen_addresses = '*'/" postgresql.conf
Verifique la configuración efectiva:
grep -v "^#" postgresql.conf | grep listen_addresses
# listen_addresses = '*'
Añada la IP del Pi a la lista blanca en pg_hba.conf para permitir la autenticación con contraseña desde ese host:
echo "host all all 192.168.1.9/32 md5" >> pg_hba.conf
Verifique que las reglas se carguen como se espera:
grep "192.168.1.9" pg_hba.conf
Reinicie Postgres y confirme que escuche en las interfaces que tiene previstas:
brew services restart postgresql@15
lsof -i -P | grep LISTEN | grep postgres
Confirme que exista un rol con privilegios de inicio de sesión:
psql -U <admin_user> -d postgres -c "\du"
Establezca o restablezca la contraseña que utilizará MLflow:
psql -U <admin_user> -d postgres -c "ALTER USER root WITH PASSWORD 'root';"
Asegúrese de que la base de datos destino exista:
psql -U <admin_user> -d postgres -c "\l" | grep mlflow
psql -U <admin_user> -d postgres -c "CREATE DATABASE mlflow OWNER root;"
Pruebe la conectividad directa desde el Pi antes de involucrar a MLflow:
nc -zv 192.168.1.8 5432
Cuando la ruta de red y las credenciales funcionan, inicie MLflow contra ese almacén remoto de backend:
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"
Arquitectura en dos etapas
Etapa 1: capa de conocimiento. Los PDF del dominio se dividen en fragmentos e incorporan modelos sentence-transformer específicos para ese dominio, luego se almacenan en colecciones separadas de Qdrant. MLflow registra qué versión del modelo de incrustación es la mejor para cada dominio, de modo que los servicios no incluyan rutas locales de forma fija.
Etapa 2: enrutamiento inteligente. Un clasificador etiqueta cada pregunta del usuario (en esta demostración: leucemia, hipoteca u otra). La recuperación de información utiliza la colección e el modelo de incrustación correspondientes. La generación de respuestas emplea el LLM seleccionado para esa intención: local en algunos casos y remoto en otros, de modo que el costo y las capacidades varíen según la pregunta.
Módulos de implementación
model_registry.py registra Arctic-embed para préstamos hipotecarios y MiniLM para leucemia (o la pareja que elija), los registra en MLflow en la Pi y asigna alias a los modelos ganadores:
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 reconstruye las colecciones de dominio, divide los PDF en fragmentos con una ventana y superposición de tokens consistentes, los incrusta con el modelo ganador correspondiente al dominio y los actualiza en 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 clasifica la intención mediante un prompt local estricto a través de Ollama, y luego selecciona los backends de LLM según la etiqueta:
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 integra la clasificación, la selección de colecciones, la recuperación top-k y las respuestas basadas en información concreta:
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 carga los modelos de incrustación desde MLflow mediante alias (models:/...@champion) para que las rutas en línea nunca dependan de la ruta del portátil del desarrollador:
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}")
Resultados
Una pregunta relacionada con hipotecas debe obtener la información de la colección correspondiente mediante el mecanismo de incrustación y dar una respuesta según las políticas del modelo LLM especializado en hipotecas. Una pregunta sobre leucemia debe seguir el procedimiento clínico establecido. Las preguntas fuera del dominio correspondiente deben omitirse o responderse sin fingir que se ha logrado recuperar información del dominio.
/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>
Por qué esta estructura es interesante
Los registros permiten que los mecanismos de incrustación se intercambien sin necesidad de volver a desplegarlos en cada cliente. Las colecciones por dominio evitan la contaminación causada por información de dominios diferentes. La ruta de intenciones impide que los modelos costosos se utilicen para tráfico trivial. No es necesario ejecutar MLflow en una Pi para este patrón, pero demuestra que los registros pueden funcionar en hardware controlado cercano a la red del laboratorio.
Precauciones operativas
Asegure Postgres antes de exponerlo en una red LAN. Trate los cambios en pg_hba como cambios de producción. Fije las versiones de MLflow y del controlador. Monitoree las tasas de error de los clasificadores: una intención incorrecta envía la consulta a la colección equivocada y puede parecer un “LLM defectuoso”. Evalúe por separado la recuperación de información y el enrutamiento. Cuando haya cambios en el registro, ejecute una prueba de integración o una comparación con un modelo en modo sombra antes de interrumpir el tráfico en tiempo real.
Cierre
RAG más allá de un único camino de recuperación de información depende en gran medida de la arquitectura del producto: conozca el dominio, obtenga los datos con la geometría adecuada y genere resultados con el presupuesto correspondiente. MLflow, Qdrant y una pequeña pasarela hacen que dicha arquitectura sea lo suficientemente concreta como para funcionar en un escritorio, y lo suficientemente disciplinada como para evolucionar más allá de una demostración.
Reforzamiento del camino basado en el registro
Los alias de los campeones deben pasar por la fase de pruebas. Registre el ID de la versión del modelo que proporciona cada respuesta para poder reproducir los incidentes tras un cambio en el registro. Si Pi se desconecta, los clientes necesitan un modo de cierre ante fallos en lugar de recurrir silenciosamente a un punto de control local aleatorio.
Los parámetros de particionamiento deben encontrarse en los metadatos del registro junto al componente de incrustación. Un nuevo campeón con una longitud de contexto preferida diferente no debe reutilizar fragmentos antiguos sin una marca de reconstrucción.
Los prompts del clasificador merecen conjuntos fijos: diez formulaciones relacionadas con hipotecas, diez formulaciones clínicas y diez formulaciones “otras” de tipo adversarial. Mida la precisión y el recuerdo antes de ajustar la elección del LLM. De lo contrario, intentará mejorar la calidad del generador cuando el verdadero defecto radica en la ruta de procesamiento.
Para implementaciones en Raspberry Pi, vigile el desgaste del disco debido a las frecuentes escrituras de artefactos y prefiera discos externos como ubicación principal para los artefactos de MLflow. Guarde las copias de seguridad de Postgres en una máquina diferente a la del Pi.
Al explicar el diseño a las partes interesadas, describa la secuencia: clasificar → seleccionar el embedder/colección → recuperar → elegir el LLM → responder con citas. Esa descripción resulta más efectiva que una lista de herramientas y deja claro dónde se toman las decisiones de política.
Amplíe los dominios con cuidado. Cada etiqueta nueva requiere una colección, un responsable del embedder, un proceso de ingesta, ejemplos para el clasificador y una política para el LLM. La pasarela debe rechazar las etiquetas desconocidas en lugar de recurrir automáticamente al modelo más grande.
Finalmente, mantenga honesto el camino “OTRO”. Es mejor rechazar que buscar en un dominio aleatorio para evitar silencios incómodos. Los usuarios confían en los sistemas que saben cuándo están fuera de su alcance. La precisión de la ruta y la calidad de la recuperación deben medirse como umbrales de lanzamiento separados antes de ajustar aún más las instrucciones del generador. La precisión de la ruta y la calidad de la recuperación deben medirse como umbrales de lanzamiento separados antes de ajustar aún más las instrucciones del generador. La precisión de la ruta y la calidad de la recuperación deben medirse como umbrales de lanzamiento separados antes de ajustar aún más las instrucciones del generador. La precisión de la ruta y la calidad de la recuperación deben medirse como umbrales de lanzamiento separados antes de ajustar aún más las instrucciones del generador. La precisión de la ruta y la calidad de la recuperación deben medirse como umbrales de lanzamiento separados antes de ajustar aún más las instrucciones del generador. La precisión de la ruta y la calidad de la recuperación deben medirse como umbrales de lanzamiento separados antes de ajustar aún más las instrucciones del generador p
Se ajustan aún más los comandos. La precisión de la ruta y la calidad de la recuperación deben medirse como umbrales de lanzamiento separados antes de ajustar aún más los comandos del generador. La precisión de la ruta y la calidad de la recuperación deben medirse como umbrales de lanzamiento separados antes de ajustar aún más los comandos del generador. La precisión de la ruta y la calidad de la recuperación deben medirse como umbrales de lanzamiento separados antes de ajustar aún más los comandos del generador. La precisión de la ruta y la calidad de la recuperación deben medirse como umbrales de lanzamiento separados antes de ajustar aún más los comandos del generador. La precisión de la ruta y la calidad de la recuperación deben medirse como umbrales de lanzamiento separados antes de ajustar aún más los comandos del generador. La precisión de la ruta y la calidad de la recuperación deben medirse como umbrales de lanzamiento separados antes de ajustar aún más los comandos del generador. La precisión de la ruta y la calidad de la recuperación deben medirse como umbrales de lanzamiento separados antes de ajustar aún más los comandos del generador. La rutaLa precisión y la calidad de recuperación deben medirse como umbrales de lanzamiento separados antes de ajustar aún más las instrucciones del generador.Deben medirse como puntos de lanzamiento separados antes de ajustar aún más las instrucciones del generador. La precisión de la ruta y la calidad de la recuperación deben medirse como puntos de lanzamiento separados antes de ajustar aún más las instrucciones del generador. La precisión de la ruta y la calidad de la recuperación deben medirse como puntos de lanzamiento separados antes de ajustar aún más las instrucciones del generador. La precisión de la ruta y la calidad de la recuperación deben medirse como puntos de lanzamiento separados antes de ajustar aún más las instrucciones del generador. La precisión de la ruta y la calidad de la recuperación deben medirse como puntos de lanzamiento separados antes de ajustar aún más las instrucciones del generador. La precisión de la ruta y la calidad de la recuperación deben medirse como puntos de lanzamiento separados antes de ajustar aún más las instrucciones del generador. La precisión de la ruta y la calidad de la recuperación deben medirse como puntos de lanzamiento separados antes de ajustar aún más las instrucciones del generador. La precisión de la ruta y la calidad de la recuperación deben medirse como puntos de lanzamiento separados antes de ajustar aún más las instrucciones del generador.Se deben activar las compuertas de control antes de ajustar aún más los prompts del generador. La precisión de la ruta y la calidad de la recuperación deben medirse como compuertas de lanzamiento separadas antes de ajustar aún más los prompts del generador. La precisión de la ruta y la calidad de la recuperación deben medirse como compuertas de lanzamiento separadas antes de ajustar aún más los prompts del generador. La precisión de la ruta y la calidad de la recuperación deben medirse como compuertas de lanzamiento separadas antes de ajustar aún más los prompts del generador. La precisión de la ruta y la calidad de la recuperación deben medirse como compuertas de lanzamiento separadas antes de ajustar aún más los prompts del generador. La precisión de la ruta y la calidad de la recuperación deben medirse como compuertas de lanzamiento separadas antes de ajustar aún más los prompts del generador. La precisión de la ruta y la calidad de la recuperación deben medirse como compuertas de lanzamiento separadas antes de ajustar aún más los prompts del generador. La precisión de la ruta y la calidad de la recuperación deben medirse como compuertas de lanzamiento separadas antes de ajustar aún más los prompts del generador.Se ajustan aún más los TS. La precisión de la ruta y la calidad de la recuperación deben medirse como umbrales de lanzamiento separados antes de ajustar aún más las instrucciones del generador. La precisión de la ruta y la calidad de la recuperación deben medirse como umbrales de lanzamiento separados antes de ajustar aún más las instrucciones del generador. La precisión de la ruta y la calidad de la recuperación deben medirse como umbrales de lanzamiento separados antes de ajustar aún más las instrucciones del generador.