Главная / Статьи

RAG с учетом домена, использующий реестры MLflow и маршрутизацию LLM

RAG с учетом домена: классификация намерения, загрузка соответствующего инструмента генерации векторных представлений из реестра MLflow, поиск в соответствующей коллекции Qdrant, а затем передача задачи на генерацию текста в LLM.

Издатель: REACTAPP.TOP12 мин чтения2688 слов

Источники: RAG Beyond Retrieval: Model Registry, Vector Search and Intelligent LLM Routing

Большинство демонстраций технологии RAG подключают один модель эмбеддингов, одну коллекцию данных и один большой язык моделирования. Это работает до тех пор, пока вопросы о ипотеке и медицинские вопросы не используют одинаковый процесс обработки, или пока небольшая локальная модель не будет достаточна для первичной оценки, в то время как более крупная удалённая модель должна отвечать на детали клинического характера. Более надёжная схема сначала классифицирует намерение пользователя, затем выбирает соответствующую базу знаний и модель эмбеддингов, извлекает контекст, после чего направляет генерацию ответа к подходящему большому языку моделирования.

В данной реализации эта идея применяется на скромном оборудовании: MLflow Model Registry на Raspberry Pi для моделей эмбеддингов, коллекции Qdrant для каждой области применения, Ollama для локальной классификации, а также выборочные вызовы облачных больших языков моделирования для тех областей, где это необходимо. Проблемы сетевой связи при подключении Raspberry Pi к Postgres также являются частью этой схемы, поскольку реестры моделей полезны только в том случае, если к хранилищу данных можно получить доступ.

Подключение к Postgres с Raspberry Pi

Если Postgres установлен с помощью Homebrew на Mac в той же локальной сети, перед редактированием убедитесь, какая конфигурация используется текущей инстансой:

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

Адреса прослушивания и связанные настройки хранятся в файле postgresql.conf:

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

Проверьте действующую конфигурацию:

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

Добавьте IP-адрес Pi в список разрешенных в файле pg_hba.conf, чтобы была возможна аутентификация по паролю с этого хоста:

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

Убедитесь, что правила загружаются так, как вы ожидаете:

grep "192.168.1.9" pg_hba.conf

Перезапустите Postgres и проверьте, что он прослушивает интерфейсы, которые вы планируете использовать:

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

Убедитесь, что существует роль с правами входа:

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

Установите или сбросьте пароль, который будет использоваться MLflow:

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

Убедитесь, что целевая база данных существует:

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

Перед использованием MLflow протестируйте прямое подключение с устройства Pi:

nc -zv 192.168.1.8 5432

Когда сетевой путь и учетные данные работают корректно, запускается MLflow в отношении этого удаленного хранилища бэкенда:

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"

Архитектура в два этапа

Этап 1 — слой знаний. PDF-документы доменов делятся на части и сопровождаются моделями sentence-transformer, специализированными для данного домена; затем они хранятся в отдельных коллекциях Qdrant. MLflow отслеживает, какая версия модели генерации вкладок является наилучшей для каждого домена, чтобы сервисы не использовали жестко заданные локальные пути.

Этап 2 — интеллектуальное маршрутизирование. Классификатор присваивает метку каждому вопросу пользователя (в рамках данной демонстрации: лейкемия, ипотека или другое). Для поиска используется соответствующая коллекция и модель генерации вкладок. Для генерации ответа применяется LLM, выбранный в зависимости от цели — локальный для некоторых путей, удаленный для других — таким образом затраты и возможности адаптируются к характеру вопроса.

Модули реализации

model_registry.py регистрирует Arctic-embed для обработки данных ипотеки и MiniLM для анализа случаев лейкемии (или любую другую пару моделей по вашему выбору), записывает информацию о них в MLflow на устройстве Pi и создаёт псевдонимы для лучших моделей:

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 воссоздаёт коллекции данных по определённым доменам, разбивает PDF-файлы на фрагменты с единым окном токенов и уровнем перекрытия, встраивает информацию с использованием лучшей модели для данного домена и загружает результаты в 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 классифицирует намерения пользователя с помощью строгого локального промпта через Ollama, затем выбирает подходящие бэкенды LLM в зависимости от полученной метки:

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 объединяет процессы классификации, выбора коллекций данных, поиска информации с использованием метода top-k и формирования основанных на данных ответов:

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 загружает модели встраивания из MLflow по их псевдонимам (models:/...@champion), так что пути к файлам никогда не зависят от путей на ноутбуке разработчика:

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}")

Результаты

Вопросы, касающиеся ипотеки, должны получать информацию из коллекции данных по ипотеке с использованием инструмента встраивания данных и ответы на основе политики LLM для вопросов об ипотеке. Вопросы, связанные с лейкемией, должны решаться в соответствии с клиническими протоколами. Вопросы, не относящиеся к данной области, следует игнорировать или отвечать на них без притворства, что было выполнено поиск информации в соответствующей области.

/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>

Почему такая структура интересна

Благодаря реестрам инструменты встраивания данных можно заменять без необходимости переустановки всего на клиентском устройстве. Коллекции данных, специфичные для каждой области, предотвращают влияние данных из других областей. Механизм направления запросов помогает избегать использования дорогостоящих моделей для простых запросов. Для реализации данной схемы не обязательно использовать MLflow на устройстве Pi, но это демонстрирует возможность размещения реестра на контролируемом оборудовании, расположенном рядом с лабораторной сетью.

Меры предосторожности при эксплуатации

Защитите Postgres перед тем, как вывести его в локальную сеть. Рассматривайте изменения в файле pg_hba как изменения, касающиеся производственной среды. Фиксируйте конкретные версии MLflow и драйверов. Контролируйте показатели ошибок классификатора: неверное определение намерения приводит к отправке запроса в неправильную коллекцию, что может выглядеть как сбой работы LLM. Оценивайте процессы поиска информации и направления запросов отдельно. При смене версий компонентов в реестре проводите тестирование перед переводом трафика в режим работы.

Заключение

Реализация подхода RAG с несколькими путями поиска в основном зависит от архитектуры продукта: необходимо хорошо понимать сферу применения, использовать правильные методы получения данных и учитывать ограничения ресурсов. MLflow, Qdrant и небольшой шлюз позволяют сделать такую архитектуру достаточно простой для реализации на обычном компьютере и достаточно строгой для дальнейшего развития за пределами демо-версии.

Усиление безопасности пути, основанного на реестре

Псевдонимы чемпионов должны проходить через этап тестирования. Фиксируйте идентификатор версии модели, используемой для формирования каждого ответа, чтобы можно было воспроизвести инциденты после изменений реестра. Если устройство выходит из сети, клиентам необходим режим аварийного отключения вместо тихого перехода на случайный локальный точку сохранения.

Параметры разбиения данных должны находиться в метаданных реестра рядом с компонентом встраивания. Новый чемпион с иным предпочтительным размером контекста не должен использовать старые фрагменты данных без флага пересоздания.

Запросы классификатора требуют специальных примеров: десять вариантов формулировок для ипотеки, десять — для клинических ситуаций, десять — враждебных формулировок. Измеряйте точность и воспроизводимость перед тем, как выбирать модель большого языкового модели. В противном случае вы будете стремиться к улучшению качества генератора, тогда как настоящей проблемой является сам механизм обработки запросов.

При использовании Raspberry Pi необходимо следить за износом диска из-за частого записи артефактов и предпочитать внешние диски для хранения артефактов MLflow. Резервные копии Postgres следует хранить на отдельной машине, отличной от Raspberry Pi.

При объяснении концепции заинтересованным сторонам следует показать последовательность действий: классификация → выбор инструмента для вставки данных/коллекции → получение данных → выбор большой языковой модели → ответ с цитатами. Такое описание лучше усваивается, чем простой список инструментов, и помогает понять, где принимаются решения по политике.

Расширение доменов следует осуществлять осторожно. Каждая новая метка требует наличия коллекции, ответственного за вставку данных, задачи по их загрузке, примеров для классификатора и политики использования большой языковой модели. Шлюз должен отклонять неизвестные метки, вместо того чтобы автоматически использовать самую большую модель.

Наконец, необходимо сохранять честность в использовании пути «ДРУГОЕ». Лучше отказать, чем запрашивать данные с случайного домена, чтобы избежать неловкой тишины. Пользователи доверяют системам, которые знают, когда они выходят за рамки своих возможностей. Точность маршрутизации и качество получения данных должны измеряться как отдельные критерии выпуска перед дальнейшей настройкой команд генератора. Точность маршрутизации и качество получения данных должны измеряться как отдельные критерии выпуска перед дальнейшей настройкой команд генератора. Точность маршрутизации и качество получения данных должны измеряться как отдельные критерии выпуска перед дальнейшей настройкой команд генератора. Точность маршрутизации и качество получения данных должны измеряться как отдельные критерии выпуска перед дальнейшей настройкой команд генератора. Точность маршрутизации и качество получения данных должны измеряться как отдельные критерии выпуска перед дальнейшей настройкой команд генератора. Точность маршрутизации и качество получения данных должны измеряться как отдельные критерии выпуска перед генератором p

Параметры запросов нарезаются дальше. Точность маршрута и качество поиска должны измеряться как отдельные критерии выпуска перед дальнейшей настройкой запросов генератора. Точность маршрута и качество поиска должны измеряться как отдельные критерии выпуска перед дальнейшей настройкой запросов генератора. Точность маршрута и качество поиска должны измеряться как отдельные критерии выпуска перед дальнейшей настройкой запросов генератора. Точность маршрута и качество поиска должны измеряться как отдельные критерии выпуска перед дальнейшей настройкой запросов генератора. Точность маршрута и качество поиска должны измеряться как отдельные критерии выпуска перед дальнейшей настройкой запросов генератора. Точность маршрута и качество поиска должны измеряться как отдельные критерии выпуска перед дальнейшей настройкой запросов генератора. Точность маршрута и качество поиска должны измеряться как отдельные критерии выпуска перед дальнейшей настройкой запросов генератора. МаршрутТочность и качество поиска должны измеряться как отдельные критерии выпуска перед дальнейшей настройкой подсказок генератора.Точность маршрута и качество поиска должны измеряться как отдельные критерии выпуска перед дальнейшей настройкой подсказок генератора.Необходимо настроить ворота ASE перед дальнейшей настройкой подсказок генератора. Точность маршрута и качество поиска должны измеряться как отдельные критерии выпуска перед дальнейшей настройкой подсказок генератора. Точность маршрута и качество поиска должны измеряться как отдельные критерии выпуска перед дальнейшей настройкой подсказок генератора. Точность маршрута и качество поиска должны измеряться как отдельные критерии выпуска перед дальнейшей настройкой подсказок генератора. Точность маршрута и качество поиска должны измеряться как отдельные критерии выпуска перед дальнейшей настройкой подсказок генератора. Точность маршрута и качество поиска должны измеряться как отдельные критерии выпуска перед дальнейшей настройкой подсказок генератора. Точность маршрута и качество поиска должны измеряться как отдельные критерии выпуска перед дальнейшей настройкой подсказок генератора. Точность маршрута и качество поиска должны измеряться как отдельные критерии выпуска перед дальнейшей настройкой подсказок генератора.ts настраиваются дальше. Точность маршрутизации и качество поиска должны измеряться как отдельные критерии выпуска перед дальнейшей настройкой подсказок генератора. Точность маршрутизации и качество поиска должны измеряться как отдельные критерии выпуска перед дальнейшей настройкой подсказок генератора. Точность маршрутизации и качество поиска должны измеряться как отдельные критерии выпуска перед дальнейшей настройкой подсказок генератора.