ПРАКТИЧЕСКАЯ ЛАБОРАТОРИЯ

Локальный RAG в Dify: собираем workflow и проверяем качество ответов

Уровень: средний Время: 60 минут Результат: локальный Dify, RAG-пайплайн и таблица сравнения

Задача не в том, чтобы один раз получить убедительный ответ по загруженному PDF. Нужен воспроизводимый сервис: документы индексируются по понятным правилам, поиск можно настраивать отдельно от генерации, а качество проверяется на одном и том же наборе вопросов. Ниже мы развернём такой контур локально, соберём приложение в Dify и проведём контролируемое сравнение трёх конфигураций поиска без выдуманных результатов.

Конкретный случай

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

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

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

Что именно мы строим

Контур состоит из пяти частей:

  1. локальный Dify, запущенный в контейнерах через Docker и Docker Compose;
  2. модель генерации текста — локальная или доступная через совместимый интерфейс;
  3. отдельная модель для эмбеддингов;
  4. база знаний с документами и настроенным поиском;
  5. контрольный набор вопросов, по которому сравниваются варианты поиска.
Вопрос пользователя
        ↓
поиск фрагментов в базе знаний
        ↓
отобранные фрагменты + инструкция
        ↓
LLM
        ↓
ответ + указание источников

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

Перед стартом: фиксируем эксперимент

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

rag-lab/
├── corpus/
│   ├── returns.md
│   ├── delivery.md
│   ├── plans.md
│   └── escalation.md
├── questions.csv
├── experiment-log.csv
└── README.md

В README.md запишите дату эксперимента, названия файлов, выбранные модели и параметры поиска. В questions.csv создайте столбцы:

id,question,expected_document,must_contain,must_not_claim,answerable

Пример структуры строки без выдуманного ответа:

Q01,"Когда можно вернуть товар?","returns.md",
"срок и условия возврата","несуществующие исключения","yes"

Поля must_contain и must_not_claim описывают проверяемые признаки, а не эталонный литературный ответ. Это позволяет оценивать смысл и не штрафовать модель за другую формулировку.

Какие вопросы включить

Для первой проверки достаточно 12–20 вопросов четырёх типов:

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

Шаг 1. Проверяем локальную машину

Для лабораторного запуска понадобится 64-битная система, Git, Docker с поддержкой Compose и свободное место для контейнеров, моделей и индекса. Если генерация тоже выполняется локально, требования к памяти зависят от выбранной модели. Начинайте с модели, которая уверенно помещается в доступную память: медленная, но стабильная конфигурация полезнее постоянно завершающегося процесса.

git --version
docker --version
docker compose version
docker info

Последняя команда должна завершиться без ошибки подключения к Docker daemon. Затем посмотрите, не заняты ли стандартные веб-порты:

ss -ltn | grep -E ':(80|443|3000|5001|11434)\b' || true

Если порт занят, не завершайте неизвестный процесс вслепую. Сначала определите владельца порта, затем либо освободите его, либо измените публикуемый порт Dify в конфигурации.

Шаг 2. Разворачиваем Dify

Клонируйте официальный репозиторий Dify в отдельный каталог. Конкретный тег версии сохраните в журнале эксперимента: работа с плавающей веткой затрудняет повторение результата.

git clone https://github.com/langgenius/dify.git
cd dify
git tag --sort=-version:refname | head
git checkout <выбранный-тег>
cd docker
cp .env.example .env

Не подставляйте в статью или общий репозиторий реальные ключи. Файл .env должен оставаться локальным. Перед запуском проверьте хотя бы значения публичных портов, URL интерфейса, пароль базы данных и секреты приложения. Для лаборатории можно использовать сгенерированные случайные значения:

openssl rand -hex 32

Команда печатает секрет в терминал. Сохраните его только в предназначенном для этого поле .env; не добавляйте вывод в историю эксперимента.

Запустите стек:

docker compose up -d
docker compose ps

Статус основных сервисов должен перейти в running или healthy. Если какой-то контейнер перезапускается, соберите диагностический вывод:

docker compose ps
docker compose logs --tail=200
docker compose config --quiet

После успешного запуска откройте адрес установки, соответствующий порту из .env, обычно http://localhost/install. Создайте локального администратора и завершите первоначальную настройку.

Проверка шага: интерфейс открывается после перезагрузки страницы, вход работает, а docker compose ps не показывает циклических перезапусков.

Шаг 3. Подключаем локальные модели

Для полностью локального контура удобно использовать Ollama или другой сервер, который Dify поддерживает напрямую либо через совместимый API. Нужны две роли:

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

ollama pull <chat-model>
ollama pull <embedding-model>
ollama list
curl http://127.0.0.1:11434/api/tags

В интерфейсе Dify откройте настройки поставщиков моделей, добавьте Ollama и зарегистрируйте обе модели с правильными типами. Адрес 127.0.0.1 внутри контейнера указывает на сам контейнер, а не на хост. Поэтому используйте адрес, доступный из сети Docker, например http://host.docker.internal:11434, если он поддерживается вашей системой.

На Linux имя host.docker.internal может потребовать явного сопоставления с host gateway. Добавьте его только нужным сервисам в локальном Compose override:

services:
  api:
    extra_hosts:
      - "host.docker.internal:host-gateway"
  worker:
    extra_hosts:
      - "host.docker.internal:host-gateway"

После изменения конфигурации примените её:

docker compose up -d
docker compose exec api getent hosts host.docker.internal

Если Ollama слушает только 127.0.0.1 хоста, контейнер всё равно не подключится к нему. Разрешите серверу слушать интерфейс, доступный Docker, но не публикуйте порт модели в недоверенную сеть. Ограничьте доступ локальным firewall.

Проверка шага: тест модели в Dify возвращает ответ, а тест embedding-модели завершается без ошибки соединения. Не переходите к индексации, пока обе проверки не проходят.

Шаг 4. Готовим документы

Качество RAG чаще ломается на корпусе, а не на модели. Перед загрузкой:

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

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

Стартовая конфигурация индексации

Токен — единица текста, которую обрабатывает модель. Не смешивайте символы, слова и токены в журнале параметров: если интерфейс Dify показывает размер в токенах, фиксируйте именно токены.

Шаг 5. Создаём базу знаний

  1. В разделе баз знаний создайте новый набор с нейтральным названием, например support-manual-v1.
  2. Загрузите подготовленные файлы из corpus/.
  3. Выберите embedding-модель, уже проверенную на предыдущем шаге.
  4. Настройте разбиение и просмотрите несколько получившихся чанков до запуска полной индексации.
  5. Запустите обработку и дождитесь завершения всех файлов.
  6. Зафиксируйте параметры и список документов в README.md.

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

Проверяем поиск отдельно от генерации

До сборки приложения откройте тест поиска по базе знаний. Выполните по одному запросу каждого типа из контрольного набора и смотрите не на готовый ответ, а на найденные фрагменты.

Если правильного фрагмента нет, смена инструкции для модели не исправит поиск. Сначала работайте с документом, разбиением, фильтрами и порогом релевантности.

Шаг 6. Собираем RAG-workflow

Создайте в Dify приложение типа Workflow или Chatflow — название режима зависит от нужной формы диалога и версии интерфейса. Для воспроизводимого однократного вопроса достаточно цепочки из четырёх узлов:

Start
  ↓
Knowledge Retrieval
  ↓
LLM
  ↓
Answer / End

Узел Start

Добавьте обязательную строковую переменную question. Если приложение диалоговое, используйте системную переменную пользовательского запроса, которую предлагает выбранный тип приложения.

Узел Knowledge Retrieval

Подключите созданную базу знаний и передайте в поиск пользовательский вопрос. Для первого запуска используйте базовую конфигурацию без дополнительных фильтров. Зафиксируйте:

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

Узел LLM

Передайте в модель вопрос и результаты поиска. Используйте явный промпт:

Ты отвечаешь только по фрагментам из базы знаний.

Правила:
1. Не добавляй факты, которых нет во фрагментах.
2. Если данных недостаточно, ответь:
   «В базе знаний нет достаточных данных для ответа».
3. Не превращай предположение в факт.
4. Укажи названия использованных источников.
5. Если источники противоречат друг другу, сообщи об этом.
6. Ответ должен быть кратким и содержать существенные условия.

Вопрос:
{{question}}

Фрагменты базы знаний:
{{результат_узла_поиска}}

Переменную результата поиска вставьте через селектор переменных Dify, а не вводите её имя вручную: структура выхода узла может отличаться между версиями.

Для оценочного запуска уменьшите творческую вариативность модели, если поставщик позволяет менять temperature. Не пытайтесь компенсировать плохой поиск длинной инструкцией.

Узел Answer или End

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

Проверка шага: в режиме трассировки видны входной вопрос, найденные фрагменты, вход LLM и итоговый ответ. Удаление узла поиска должно лишить модель документов — это простой способ убедиться, что цепочка действительно использует базу знаний.

Шаг 7. Сохраняем воспроизводимую версию

Экспортируйте приложение в доступный Dify формат DSL и сохраните рядом с журналом:

rag-lab/
├── corpus/
├── questions.csv
├── experiment-log.csv
├── workflow-v1.yml
└── README.md

Экспорт workflow обычно не включает сами документы, секреты провайдера и локальные модели. Поэтому в README.md отдельно запишите:

Хеши позволяют проверить, что два запуска используют одинаковые документы:

find corpus -type f -print0 \
  | sort -z \
  | xargs -0 sha256sum > corpus.sha256

sha256sum -c corpus.sha256

Шаг 8. Готовим три конфигурации поиска

Меняйте только один существенный фактор за раз. Если одновременно заменить embedding-модель, размер чанка, top-k и промпт, причина изменения качества останется неизвестной.

Вариант Что фиксируем Что меняем Какой вопрос проверяем
A — базовый Корпус, модели, чанки, промпт Ничего: исходные top-k и порог Как ведёт себя стартовая настройка
B — больше кандидатов Всё из A Только top-k Помогают ли дополнительные фрагменты составным вопросам
C — более строгий поиск Всё из A Только порог или reranking Снижаются ли ложные находки на неответимых вопросах

Не считайте обозначения A, B и C готовыми рекомендациями. Числовые значения возьмите из вашего интерфейса, внесите в журнал и не меняйте во время одного прогона.

Шаг 9. Проводим тест без подгонки

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

  1. Скопируйте конфигурацию приложения или сохраните параметры A.
  2. Начните новую сессию, чтобы история предыдущего диалога не влияла на ответ.
  3. Запустите все вопросы из questions.csv.
  4. Запишите ответ, источники, задержку и наблюдаемую ошибку.
  5. Повторите для B и C, меняя только указанный параметр.
  6. Не исправляйте вопросы после просмотра неудачного ответа. Новые формулировки добавляйте как отдельные тесты следующей версии.

Шкала ручной оценки

Для неответимых вопросов оценка 2 ставится только за корректный отказ. Красивый ответ без основания получает 0.

Таблица сравнения ответов

Следующая таблица — шаблон для фактического прогона. Она намеренно не содержит придуманных результатов. Заполните её текстами и оценками из вашего локального Dify.

ID и вопрос Ожидаемый источник A: ответ / источник / оценка B: ответ / источник / оценка C: ответ / источник / оценка Вывод
Q01 — прямой факт Указать файл и раздел Заполнить после прогона Заполнить после прогона Заполнить после прогона Какая настройка нашла полный фрагмент
Q02 — перефразированный запрос Указать файл и раздел Заполнить после прогона Заполнить после прогона Заполнить после прогона Устойчив ли семантический поиск к формулировке
Q03 — составной вопрос Указать два фрагмента Заполнить после прогона Заполнить после прогона Заполнить после прогона Хватило ли top-k для двух условий
Q04 — данных нет Нет источника Заполнить после прогона Заполнить после прогона Заполнить после прогона Какая настройка корректно отказалась

Для полного набора удобнее использовать experiment-log.csv:

run_id,config_id,question_id,answer,retrieved_sources,
retrieval_ok,answer_score,unsupported_claim,refused_correctly,
latency_ms,notes

Сводные показатели

После ручной разметки для каждой конфигурации посчитайте:

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

Как интерпретировать результат

Используйте последовательную диагностику:

  1. Нужного фрагмента нет среди результатов. Проблема находится до LLM: корпус, разбиение, embedding, фильтр, top-k или порог.
  2. Нужный фрагмент найден, но расположен низко. Проверьте top-k и reranking.
  3. Нужный фрагмент передан модели, но ответ неверный. Проверьте инструкцию, размер контекста, параметры генерации и возможности модели.
  4. Ответ верный, но источник не отображается. Исправьте выход workflow и форматирование, не переиндексируя документы без причины.
  5. Система отвечает на вопросы вне корпуса. Проверьте порог, правило отказа и то, действительно ли модель получает только разрешённый контекст.

Лучшая конфигурация — не обязательно вариант с максимальным top-k. Большое число фрагментов может помогать составным вопросам, но одновременно приносить шум, конфликтующие версии и увеличивать задержку.

Проверка готового сервиса

Перед тем как считать работу завершённой, пройдите контрольный список:

Для проверки восстановления перезапустите стек:

docker compose restart
docker compose ps
docker compose logs --tail=100

Затем повторите один прямой, один составной и один неответимый вопрос. Это не заменяет полный прогон, но выявляет проблемы запуска и подключения моделей.

Что обычно ломается

Dify не видит Ollama

Признак: тест провайдера возвращает connection refused или timeout.

Проверка: убедитесь, что Ollama запущена, слушает доступный интерфейс, hostname разрешается внутри контейнера, а firewall не блокирует порт. Не используйте контейнерный localhost как адрес хоста.

Индексация зависла или завершилась ошибкой

Признак: документ остаётся в обработке, worker перезапускается или embedding-запросы падают.

Проверка: изучите логи worker, доступность embedding-модели, свободное место и память. Сначала загрузите один небольшой текстовый документ. Если он проходит, добавляйте корпус частями.

Правильный документ не находится

Причины: скан не распознан, важная таблица потеряла структуру, чанк обрезал условие, embedding-модель плохо работает с языком корпуса или порог слишком строгий.

Действие: найдите фразу в исходном тексте, затем в просмотре чанков, затем в тесте поиска. Такая последовательность показывает этап, на котором пропали данные.

В ответ попадают условия из другого раздела

Причины: слишком крупные чанки, несколько редакций документа, высокий top-k или отсутствие фильтра по статусу.

Действие: удалите дубли, добавьте версию и статус, уменьшите шум в результатах. Не просите модель самостоятельно угадывать актуальную редакцию.

Ответ выглядит хорошо, но не подтверждается источником

Это ошибка качества, даже если утверждение кажется правдоподобным. Пометьте unsupported_claim=yes, сохраните трассировку и проверьте, был ли факт в переданном контексте.

После изменения чанков качество стало «лучше на глаз»

Верните контрольный набор и сравните показатели на тех же вопросах. Без одинаковых входов такое впечатление нельзя использовать для выбора настройки.

Наблюдаемость и повторные прогоны

Наблюдаемость нужна не только в production. Для каждого запуска сохраняйте идентификатор конфигурации, вход, найденные документы, ответ, задержку и ошибку. Если Dify показывает трассировку узлов, используйте её для разбора, но важные параметры всё равно переносите в независимый журнал.

После обновления модели, документов или Dify запускайте тот же набор вопросов. Новый вариант принимается только если не ухудшает обязательные показатели: корректность, отказ при отсутствии данных и отсутствие неподтверждённых утверждений.

Сохраняйте последнюю рабочую конфигурацию, чтобы был возможен откат. Для Dify это означает как минимум сохранённый DSL, параметры моделей, копию настроек индексации и резервную копию данных согласно используемой схеме развёртывания.

Ограничения локального решения

Минимальный итог эксперимента

Работа завершена не тогда, когда чат впервые ответил правильно, а когда у вас есть полный комплект:

локальный Dify
+ доступные модели
+ зафиксированный корпус
+ проверенные чанки
+ экспортированный workflow
+ контрольные вопросы
+ три конфигурации поиска
+ заполненная таблица ответов
+ список ошибок и выбранная конфигурация

Такой результат можно повторить, проверить после обновления и передать другому инженеру. Dify экономит время на инфраструктурном каркасе, но качество RAG остаётся инженерной задачей: корпус, поиск, правила отказа и измеримый тест должны быть явными.

← Все практические инструкции · Лабораторный словарь →

Мы публикуем то, что можно проверить и повторить. Практические материалы Agent Lab Journal помогают собирать AI-системы вокруг реальных процессов, а не отдельных демонстраций. Обсудить задачу →