Перейти к содержимому
TelegramGitHub

Data Pipelines with Apache Airflow, глава 14: Проект с GenAI и семейными рецептами

Главная мысль: чтобы LLM отвечала по вашим данным, не нужно её переобучать. Достаточно RAG: найти нужные куски текста в векторной базе и подать их модели. Airflow готовит и обновляет эти данные.

DAG загрузки рецептов: preprocess, create_collection, compare, delete и save в Weaviate

Есть семейные рецепты. Хочется положить их в систему, где можно спросить: “Есть ли в рецепте дрожжи?”, “Переведи на другой язык”, “Чем заменить ингредиент?”. Проект называется Recipe Vault.

Fine-tuning значит дообучить LLM на своих данных. У этого пути четыре проблемы:

ПроблемаСуть
ЦенаНужно менять миллиарды параметров. Нужны мощные машины
Повторное обучениеКаждая новая порция рецептов требует нового обучения
ХостингСвою модель надо где-то держать
Источник неясенЗнания модели смешиваются. Не видно, что из ваших рецептов, а что из общих данных

RAG (retrieval-augmented generation) даёт LLM доступ к внешней “библиотеке”. Модель не обучается заново.

Поток RAG: вопрос, embedding, поиск в Weaviate, prompt, LLM и ответ

  1. Пользователь задаёт вопрос.
  2. Embedding model превращает вопрос в вектор (набор чисел).
  3. Векторная база ищет самые близкие по смыслу куски.
  4. Вопрос и найденные куски собирают в prompt.
  5. LLM отвечает на основе этого контекста.

Идея vector space простая. Каждому тексту соответствует точка. Похожие по смыслу тексты лежат рядом. Книга показывает это на примере с двумя осями: острое или мягкое, сладкое или солёное. В реальности у text-embedding-3-large около 3 тысяч измерений. Они не про вкус, а про смысл.

Близость считают как cosine similarity. Важно направление вектора, а не точные значения. Одинаковое направление даёт 1.

СервисРоль
MinIOХранилище файлов. Сюда попадают рецепты в JSON
WeaviateОткрытая векторная база
vectorflowСвой Python-пакет с CLI для шагов обработки
recipe_vaultПриложение на Streamlit: загрузка рецептов и чат
AirflowОркестрация загрузки данных

Всё поднимается через Docker Compose, как и в главе 13.

Все tasks запускают один образ через DockerOperator. Внутри образа CLI с командами preprocess, create, compare, save. Airflow только оркеструет, тяжёлая работа идёт в контейнере.

Общие параметры оператора собраны в словарь и передаются как **docker_kwargs. Так код не дублируется. Основные параметры:

  • image: образ контейнера.
  • environment: переменные окружения (доступ к MinIO, Weaviate, OpenAI).
  • docker_url: адрес Docker daemon. В проекте это socket proxy, так безопаснее.
  • network_mode: сеть, где видны все сервисы.
  • auto_remove: удалять контейнер после успеха.

Расписание: ежедневно. Дата в пути MinIO задаётся шаблоном data_interval_start. Для старых дат запускают backfill.

  • Читает JSON за день из MinIO.
  • Делит каждый рецепт на два chunk: ингредиенты и инструкции. В каждый кладёт название рецепта.
  • Для каждого recipe_name и chunk считает UUID функцией generate_uuid5.
  • Сохраняет один DataFrame в preprocessed.parquet.

Про размер chunk. Если взять весь рецепт целиком или целую книгу, трудно понять источник. Если резать на предложения, на один вопрос вернётся много кусков одного рецепта. Книга выбирает середину: два chunk на рецепт.

UUID строится из текста. Тот же текст даёт тот же UUID. Любая правка, даже в одной цифре, даёт новый UUID. Это нужно для проверки на дубли.

Collection в Weaviate похожа на таблицу. Для неё задают:

  • имя (recipes);
  • vectorizer_config: какая embedding model и как подключиться (OpenAI API или Azure OpenAI);
  • properties: поля recipe_uuid, recipe_name, chunk_uuid, chunk. UUID-поля не векторизуются.

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

Task сравнивает UUID из файла с UUID в базе. Для каждой записи три случая:

СлучайДействие
Рецепта нет в базеПометка create
Рецепт есть, но chunk UUID другиеПометка update
Всё совпалоЗапись удаляется из DataFrame

Результат лежит в compared.parquet. Стратегия подходит для этого проекта. В других проектах (например, с несколькими пользователями) правила обновления будут другими.

  • delete_outdated_objects удаляет из базы chunk с пометкой update (метод delete_many).
  • save_in_vectordb пишет новые и обновлённые записи пакетами. Векторы считаются автоматически по настройкам коллекции.
  • При ошибке записи поднимается ValueError, task падает. Исправьте причину и перезапустите task.
  • Weaviate сам векторизует вопрос тем же embedding model и ищет близкие chunks.
  • Prompt template (в книге на LangChain) задаёт правила. Отвечать только по найденному контексту. Если ответа нет, сказать об этом. Формат ответа единый.
  • В prompt три параметра: вопрос, история чата, контекст.
  • Если нужного рецепта нет, ассистент так и говорит. Он может предложить похожие из базы.

Проверено на Airflow 3.4: DAG загружается, функция chunk_id проверена в pytest. Для запуска DAG нужен Docker.

Как задают общие параметры для нескольких DockerOperator (имена и образ придуманы для примера):

import pendulum
from airflow.providers.docker.operators.docker import DockerOperator
from airflow.sdk import DAG
COMMON = {
"image": "my_pipeline_cli:latest",
"docker_url": "tcp://docker-socket-proxy:2375",
"auto_remove": "success",
}
with DAG(
dag_id="notes_ingestion",
schedule="@daily",
start_date=pendulum.datetime(2026, 1, 1, tz="UTC"),
) as dag:
split = DockerOperator(
task_id="split_notes",
command=["split", "s3://data/{{ data_interval_start | ds }}"],
**COMMON,
)
load = DockerOperator(
task_id="load_to_vector_db",
command=["load", "notes"],
**COMMON,
)
split >> load

Идея UUID по содержимому в виде функции. Здесь uuid5 из стандартной библиотеки:

import uuid
def chunk_id(text: str) -> str:
"""Same text gives the same id; any edit gives a new id."""
return str(uuid.uuid5(uuid.NAMESPACE_URL, text.strip()))
ТерминЗначение
GenAI / LLMГенеративный ИИ. Большая языковая модель
Fine-tuningДообучение модели на своих данных
RAGПоиск нужных документов и передача их LLM как контекста
Embedding modelМодель, которая превращает текст в вектор
Vector databaseБаза, которая хранит векторы и ищет по близости
Cosine similarityМера близости по направлению векторов
ChunkНебольшой кусок текста для векторизации
CollectionАналог таблицы в Weaviate
UUID по содержимомуКлюч, который зависит от текста. Нужен для проверки на дубли
Prompt templateШаблон инструкций для LLM
  • Airflow в GenAI-проектах отвечает за данные: сбор, очистку, chunking, embeddings, обновление базы. Качество ответов зависит от качества этих данных.
  • Делайте обновление идемпотентным. UUID по содержимому и шаг compare не дают плодить дубли.
  • Храните промежуточные результаты (preprocessed, compared) в Parquet. Падение одного task не ломает остальные, и его можно повторить.
  • Тяжёлый код запускайте в контейнерах через DockerOperator. Общие параметры собирайте в один словарь.
  • Размер chunk влияет на точность поиска. Выбирайте его под данные и проверяйте на вопросах.
  • Секреты (ключи OpenAI) передавайте через переменные окружения или connections. Не пишите их в код DAG.
  1. Назовите проблемы fine-tuning. Как RAG решает их?
  2. Чем плохи слишком большие и слишком маленькие chunks?
  3. Как UUID по содержимому помогает отличить новый рецепт от изменённого?
  4. Почему task create_collection можно запускать каждый день?

Предыдущая: Проект: транспорт NYC. Следующая: Airflow в production.