Перейти к содержанию

RAG-синхронизация: git → OpenWebUI knowledge base

Автоматическая загрузка markdown-документации из git-репозитория alatyr-infra-kb в OpenWebUI knowledge collection на ragserver для использования локальными AI-агентами.

Дата снимка: 05.08.2026 Развернуто на: ragserver (192.168.1.200) Источник: GitHub oswold1979/alatyr-infra-kb (private) Назначение: OpenWebUI knowledge collection alatyr-infra-kb (ID 191ae5b9-2520-492e-b6ee-937646008658)


Быстрая справка

Параметр Значение
Git-репо git@github-alatyr-kb:oswold1979/alatyr-infra-kb.git
Локальный clone /opt/rag-kb/ (владелец oswold:oswold)
Директория скрипта /opt/rag-sync/ (700, oswold)
Скрипт /opt/rag-sync/sync-to-openwebui.sh
State-файл /opt/rag-sync/state.json
Лог /opt/rag-sync/sync.log
Lock-файл /opt/rag-sync/.lock
Файлы окружения /opt/rag-sync/.env (600) — токен OpenWebUI
Аутентификация в GitHub Deploy key (SSH, ED25519, read-only)
Cron */10 * * * * (каждые 10 минут, user cron oswold + flock) — ✅ 07.08.2026

Архитектура

┌─────────────────────┐         ┌─────────────────────┐
│  Perplexity chat    │         │      GitHub         │
│  (пишу файлы)       │───push─▶│  alatyr-infra-kb    │
└─────────────────────┘         │      (private)      │
                                └──────────┬──────────┘
                                           │ SSH pull
                                           │ (deploy key)
                     ┌─────────────────────────────────────────┐
                     │  ragserver (192.168.1.200)              │
                     │                                         │
                     │  cron: sync-to-openwebui.sh (каждые 5м) │
                     │      │                                  │
                     │      ├─ git pull /opt/rag-kb/           │
                     │      ├─ diff + hash → state.json        │
                     │      ├─ curl → OpenWebUI API            │
                     │      │                                  │
                     │      ▼                                  │
                     │  OpenWebUI (Docker :3000)               │
                     │      │                                  │
                     │      ├─ POST /api/v1/files/ (upload)    │
                     │      ├─ POST /api/v1/knowledge/         │
                     │      │       {id}/file/add (attach)     │
                     │      │                                  │
                     │      ▼                                  │
                     │  Ollama (host systemd :11434)           │
                     │      └─ nomic-embed-text                │
                     │           → 768-dim vectors             │
                     │      │                                  │
                     │      ▼                                  │
                     │  Qdrant (Docker :6333)                  │
                     │      └─ collection open-webui_knowledge │
                     │                                         │
                     └─────────────────────────────────────────┘

Компоненты

1. Git-репозиторий и Deploy Key

Что: Приватный репо oswold1979/alatyr-infra-kb на GitHub, доступ по SSH через Deploy Key (read-only, привязан к репо).

Как создан:

sudo ssh-keygen -t ed25519 \
  -f /home/oswold/.ssh/id_ed25519_alatyr_kb \
  -C "ragserver-alatyr-kb-deploy-key" \
  -N ""

Публичный ключ добавлен в github.com/oswold1979/alatyr-infra-kb/settings/keys с Allow write access: НЕТ.

SSH-конфиг~/.ssh/config пользователя oswold:

# Deploy key для alatyr-infra-kb (read-only)
Host github-alatyr-kb
    HostName github.com
    User git
    IdentityFile ~/.ssh/id_ed25519_alatyr_kb
    IdentitiesOnly yes

URL для клона: git@github-alatyr-kb:oswold1979/alatyr-infra-kb.git (не github.com а псевдоним из config).

Fingerprint нашего deploy key: SHA256:q7olwXJsWFx/FarxezWpdaDkonbLZHdKTLnRnE77lwI

Первый клон:

sudo mkdir -p /opt/rag-kb
sudo chown oswold:oswold /opt/rag-kb
git clone git@github-alatyr-kb:oswold1979/alatyr-infra-kb.git /opt/rag-kb

2. Директория /opt/rag-sync/

Права 700, только oswold.

Содержимое:

/opt/rag-sync/
├── .env                       # OPENWEBUI_API_TOKEN и OPENWEBUI_URL (perms 600)
├── sync-to-openwebui.sh       # исполняемый скрипт синхронизации (700)
├── state.json                 # соответствие файл→file_id→sha256
├── sync.log                   # лог всех запусков
└── .lock                      # PID активного запуска (создаётся/удаляется скриптом)

Файл .env:

OPENWEBUI_API_TOKEN=sk-<REDACTED>
OPENWEBUI_URL=http://localhost:3000

Токен создан в OpenWebUI → Settings → Account → API Keys → "rag-sync-ragserver".

TODO: скопировать токен также в Vaultwarden как резервную копию (сейчас доступ к Vaultwarden с рабочего компа не работает — офисный провайдер режет TLS handshake).

3. OpenWebUI knowledge collection

Имя: alatyr-infra-kb ID: 191ae5b9-2520-492e-b6ee-937646008658 Создана автоматически скриптом при первом запуске через POST /api/v1/knowledge/create.

Что в неё синхронизируется — все *.md из корня репо, кроме файлов начинающихся с TODO- (потому что они меняются каждый день и засоряют RAG).

Файлов на 05.08.2026: 10

  • 00_INDEX.md
  • README.md
  • vps-hostkey-vm-mini.md
  • nginx-vps-configs.md
  • dns-alatyr-service.md
  • vaultwarden.md
  • amneziawg-tunnels.md
  • certbot.md
  • ragserver-workstation.md
  • crm-objects-spec.md

Скрипт sync-to-openwebui.sh

Расположение в репо: scripts/sync-to-openwebui.sh (версионируется) Расположение на ragserver: /opt/rag-sync/sync-to-openwebui.sh (копия, исполняемая)

Логика

  1. Loading .env — токен и URL
  2. Lock: если существует .lock и PID жив → выйти с ошибкой (иначе удалить stale lock)
  3. git pull --ff-only в /opt/rag-kb/
  4. Найти/создать коллекцию alatyr-infra-kb:
  5. GET /api/v1/knowledge/ → поиск по имени
  6. Если нет → POST /api/v1/knowledge/create
  7. Прогон всех *.md (кроме TODO-*):
  8. Считать sha256sum
  9. Сравнить с state.json
  10. Если новый → upload + attach + запись state
  11. Если изменился → remove-from-collection + delete-file + upload + attach + update state
  12. Если не изменился → skip
  13. Удалённые в репо (были в state, нет в файловой системе) → удалить из коллекции + удалить из state
  14. Логировать всё в /opt/rag-sync/sync.log
  15. Summary в конце: added=N, updated=N, removed=N, skipped=N, errors=N

Обновление скрипта

Скрипт лежит в самом git-репозитории (scripts/sync-to-openwebui.sh). Чтобы обновить:

  1. Правишь в alatyr-infra-kb/scripts/sync-to-openwebui.sh (например через Perplexity)
  2. git push в GitHub
  3. На ragserver — при следующем запуске скрипт сам делает git pull и подхватывает новую версию сам себя, но она применится только к следующему запуску (уже запущенный экземпляр работает старой версией)
  4. Скопировать новую версию из /opt/rag-kb/scripts/ в /opt/rag-sync/ и сделать исполняемой:
cp /opt/rag-kb/scripts/sync-to-openwebui.sh /opt/rag-sync/
chmod +x /opt/rag-sync/sync-to-openwebui.sh

TODO: автоматизировать это — в самом скрипте вначале проверять диффу между /opt/rag-kb/scripts/sync-to-openwebui.sh и /opt/rag-sync/sync-to-openwebui.sh, при различии — копировать и рестартовать себя. Аккуратно чтобы не зациклиться.


OpenWebUI RAG-стек (детали)

Компоненты (все на ragserver)

Компонент Где живёт Порт Роль
OpenWebUI Docker rag-openwebui 3000 Frontend + API + RAG-логика
PostgreSQL Docker rag-postgres 5432 Метаданные (пользователи, коллекции, файлы)
Qdrant Docker rag-qdrant 6333 HTTP, 6334 gRPC Векторное хранилище
Ollama Host systemd (ollama.service) 192.168.1.200:11434 Сервер моделей (LLM + embeddings)

Важно: Ollama НЕ в докере — работает как systemd-service на хосте. Слушает на 192.168.1.200:11434, не на localhost. Поэтому curl localhost:11434 из ragserver не сработает — надо использовать 192.168.1.200:11434.

Настройки RAG (env в OpenWebUI)

RAG_EMBEDDING_ENGINE=ollama
RAG_EMBEDDING_MODEL=nomic-embed-text:latest
RAG_CHUNK_SIZE=1000                # символов на чанк
RAG_CHUNK_OVERLAP=200              # символов перекрытия
RAG_EMBEDDING_BATCH_SIZE=4         # ⚠ очень мало! ускоряется увеличением
RAG_TOP_K=5                        # найти топ-5 при поиске
RAG_RERANKING_MODEL=               # без реранкинга
VECTOR_DB=qdrant
QDRANT_URI=http://qdrant:6333      # через docker DNS
QDRANT_API_KEY=<хранится в docker-compose>

Модель эмбеддингов

nomic-embed-text — 137M параметров, генерирует 768-мерные векторы.

Хорошо работает с русским/английским текстом, но: - Требует Ollama (для загрузки/запуска) - Медленный на CPU без GPU: ~0.5-1 сек на чанк

Векторное хранилище (Qdrant)

Одна общая коллекция на все knowledge collections OpenWebUI:

  • open-webui_knowledge — все векторные точки от всех knowledge collections (не отдельные Qdrant-коллекции)
  • open-webui_files — отдельная коллекция для файлов
  • Метрика: Cosine
  • Размерность: 768 (соответствует nomic-embed-text)
  • on_disk: false (векторы в RAM для скорости)

API-ключ Qdrant хранится в env-переменной QDRANT_API_KEY контейнера OpenWebUI и QDRANT__SERVICE__API_KEY контейнера Qdrant.

TODO: ротировать QDRANT_API_KEY (утёк в перплексити-чат 05.08.2026 при отладке).


Производительность

Первый синк 05.08.2026 — 10 файлов, ~200 KB, заняло ~9 минут 36 секунд.

Почему медленно

Причины:

  1. Двойная генерация эмбеддингов — OpenWebUI при upload (POST /api/v1/files/) и потом при attach (POST /api/v1/knowledge/{id}/file/add) индексирует одно и то же дважды. Это баг архитектуры OpenWebUI, не наш.
  2. Ollama на CPU — i3-7100U без GPU, ~0.5-1 сек на чанк для nomic-embed-text
  3. RAG_EMBEDDING_BATCH_SIZE=4 — очень маленький batch, слабая параллельность
  4. crm-objects-spec.md (50 KB) — 63 чанка × 2 захода = ~126 вызовов Ollama = ~2.5 мин на один этот файл

Оценка последующих запусков

  • Если файлы не изменились — все skipped в скрипте, синк занимает ~2-5 сек (только git pull + сравнение sha256)
  • Если один файл изменился — синк занимает ~30-60 сек (в зависимости от размера файла и количества чанков)
  • Полная переиндексация (всё удалить и заново) — ~10 минут

Как ускорить (TODO)

По приоритету:

  1. 🟢 Увеличить RAG_EMBEDDING_BATCH_SIZE с 4 до 32 — ускорит эмбеддинги +50-100%
  2. 🟢 Увеличить RAG_CHUNK_SIZE с 1000 до 2000 — вдвое меньше чанков, но крупнее контекст. Может ухудшить precision, но должно быть OK для документации
  3. 🟡 Сменить RAG_EMBEDDING_ENGINE с ollama на sentence_transformers — встроенный движок с моделью all-MiniLM-L6-v2 (22 MB, 384-dim). Синк ускорится в 20-30 раз, но модель хуже для русского языка
  4. 🟡 Проверить если можно отключить двойную индексацию через API OpenWebUI (может есть флаг skip_embedding при upload)
  5. 🔵 Купить GPU для ragserver — ускорит эмбеддинги в 10-50 раз (но нужно ~500W БП и место в корпусе — сейчас i3 U-серии, вряд ли влезет)

Изменения в env — через docker-compose в /opt/rag-platform/docker-compose.yml, потом:

sudo docker compose -f /opt/rag-platform/docker-compose.yml up -d

Cron / systemd (планирование запуска)

✅ Установлен 07.08.2026 (задача 6)

User cron от юзера oswold (не root, не в /etc/cron.d/) — владелец /opt/rag-sync/ всё равно oswold, а user cron проще аудировать через crontab -l.

*/10 * * * * /usr/bin/flock -n /tmp/sync-to-openwebui.lock /opt/rag-sync/sync-to-openwebui.sh 2>&1 | tee -a /opt/rag-sync/cron.log

Разбор: - */10 — каждые 10 минут (не 5, т.к. первый синк 24 файлов занимает 15 мин, обычный delta-синк 20-30 сек) - flock -n /tmp/sync-to-openwebui.lock — mutex, чтобы параллельный запуск не попортил state - Скрипт сам пишет /opt/rag-sync/sync.log — cron.log нужен только для ошибок flock/cron-обёртки

Проверка:

crontab -l                             # строка на месте
tail -5 /opt/rag-sync/sync.log         # последний запуск
tail -5 /opt/rag-sync/cron.log         # пусто = ок

Вариант A (альтернатива): cron в /etc/cron.d/

Файл /etc/cron.d/rag-sync (требует sudo, мы не выбрали):

# rag-sync — синхронизация alatyr-infra-kb → OpenWebUI каждые 5 минут
*/5 * * * * oswold /opt/rag-sync/sync-to-openwebui.sh >> /opt/rag-sync/sync.log 2>&1

Плюс: - Одна строка - Автоматически запускается при старте системы

Минус: - Cron не отслеживает предыдущий запуск (но у нас есть lockfile)

Вариант B: systemd timer (аккуратнее)

/etc/systemd/system/rag-sync.timer:

[Unit]
Description=RAG sync from git

[Timer]
OnBootSec=2min
OnUnitActiveSec=5min
Persistent=true

[Install]
WantedBy=timers.target

/etc/systemd/system/rag-sync.service:

[Unit]
Description=Sync alatyr-infra-kb to OpenWebUI knowledge base

[Service]
Type=oneshot
User=oswold
ExecStart=/opt/rag-sync/sync-to-openwebui.sh

Активация:

sudo systemctl enable --now rag-sync.timer

Рекомендую B — systemd timer чище (persistent, лучше логи через journalctl, можно посмотреть next-run через systemctl list-timers).

TODO: установить один из вариантов (после того как сделаем ещё несколько тестов вручную).


Ручной запуск

Запустить синк прямо сейчас (не дожидаясь cron):

/opt/rag-sync/sync-to-openwebui.sh

Посмотреть последний лог:

tail -30 /opt/rag-sync/sync.log

Проверить state:

jq . /opt/rag-sync/state.json | head -30

Проверить сколько файлов сейчас в OpenWebUI коллекции:

source /opt/rag-sync/.env
curl -sSf -H "Authorization: Bearer $OPENWEBUI_API_TOKEN" \
  "$OPENWEBUI_URL/api/v1/knowledge/" | \
  jq '.items[] | select(.name=="alatyr-infra-kb") | {id, file_count}'
unset OPENWEBUI_API_TOKEN

Восстановление после сбоя

Сценарий 1: скрипт упал во время выполнения

Проверить lockfile:

cat /opt/rag-sync/.lock                       # покажет PID
ps -p $(cat /opt/rag-sync/.lock) 2>/dev/null  # проверить живой ли

Если процесс жив — подождать или убить:

kill $(cat /opt/rag-sync/.lock)

Скрипт при следующем запуске сам увидит stale lock и удалит его.

Сценарий 2: OpenWebUI пересоздан / потеряны все файлы

Синхронизация "с нуля":

# 1. Очистить state
> /opt/rag-sync/state.json

# 2. Запустить синк — он создаст коллекцию и загрузит все файлы
/opt/rag-sync/sync-to-openwebui.sh

Займёт ~10 минут (полная переиндексация всех файлов).

Сценарий 3: state.json потерян, но файлы в OpenWebUI есть

Плохой сценарий — скрипт увидит "новые" файлы и загрузит их дубликатом, а старые останутся.

Решение: сначала удалить старую коллекцию через API, потом запустить синк.

source /opt/rag-sync/.env
COLL_ID=$(curl -sSf -H "Authorization: Bearer $OPENWEBUI_API_TOKEN" \
  "$OPENWEBUI_URL/api/v1/knowledge/" | \
  jq -r '.items[] | select(.name=="alatyr-infra-kb") | .id')
curl -sSf -X DELETE -H "Authorization: Bearer $OPENWEBUI_API_TOKEN" \
  "$OPENWEBUI_URL/api/v1/knowledge/$COLL_ID/delete"
unset OPENWEBUI_API_TOKEN

# Также очистить связанные файлы (получить их из state.json если он ещё есть)
# либо через UI OpenWebUI → Files → удалить *.md

# И только потом:
> /opt/rag-sync/state.json
/opt/rag-sync/sync-to-openwebui.sh

TODO: добавить в скрипт --reset флаг который делает эти операции.


Использование knowledge в OpenWebUI

Через UI

  1. Открыть OpenWebUI (http://192.168.1.200:3000 или через VPN)
  2. Создать новый чат
  3. В поле ввода написать # — появится список коллекций
  4. Выбрать #alatyr-infra-kb
  5. Задать вопрос:
#alatyr-infra-kb Какая версия nginx на VPS vm-mini и какие сайты обслуживает?

OpenWebUI найдёт 5 наиболее релевантных чанков (RAG_TOP_K=5), передаст LLM как контекст, тот ответит.

Через API (для внешних агентов)

curl -X POST "http://192.168.1.200:3000/api/chat/completions" \
  -H "Authorization: Bearer $OPENWEBUI_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "qwen2.5:7b",
    "messages": [{"role":"user","content":"версия nginx"}],
    "files": [{"type":"collection","id":"191ae5b9-2520-492e-b6ee-937646008658"}]
  }'

TODO: интегрировать с локальными скриптами и CRM — чтобы AI-агент мог использовать эту knowledge для контекстных ответов.


Известные проблемы

1. Двойная генерация эмбеддингов при upload+attach

Что: OpenWebUI индексирует файл 2 раза — при POST /api/v1/files/ и потом при POST /api/v1/knowledge/{id}/file/add.

Импакт: синк 2x медленнее чем мог бы быть.

Обход: нет (баг архитектуры OpenWebUI). Можно попробовать skip_embedding параметр при upload (не проверял).

TODO: проверить есть ли флаг skip_embedding

2. Ollama на CPU медленная

Импакт: 10 файлов индексируются ~10 минут.

Обход: увеличить batch, chunk size, или сменить engine.

TODO: см. раздел "Как ускорить"

3. Nomic-embed-text может быть недоступен для русского

Модель nomic-embed-text работает для русского удовлетворительно, но не оптимально. Специализированные модели типа mxbai-embed-large или multilingual-e5-large могут дать лучший поиск.

TODO: сравнить качество поиска с другими embed-моделями (например mxbai-embed-large тоже через Ollama)

4. QDRANT_API_KEY утёк в чат 05.08.2026

Появлялся в выводе docker exec ... env который я показал в Perplexity-чате.

TODO: ротировать QDRANT_API_KEY — сгенерировать новый, обновить в docker-compose обоих сервисов.


TODO (сводка)

🔴 Приоритет 1 — безопасность

  • Ротировать QDRANT_API_KEY (утёк в чат 05.08.2026)
  • Продублировать OpenWebUI API-токен в Vaultwarden (сейчас только в .env на ragserver)
  • Настроить бэкап /opt/rag-sync/ и /opt/rag-kb/ — сейчас не бэкапится

🟡 Приоритет 2 — автоматизация

  • Установить systemd timer для запуска синка каждые 5 минут (сейчас только вручную)
  • Добавить в скрипт --reset флаг для полной пересинхронизации
  • Добавить в скрипт auto-update самого себя из /opt/rag-kb/scripts/
  • Настроить monitoring — email/telegram если синк упал N раз подряд

🟢 Приоритет 3 — ускорение

  • RAG_EMBEDDING_BATCH_SIZE с 432
  • RAG_CHUNK_SIZE с 10002000
  • Проверить skip_embedding при upload — если работает, избавиться от двойной индексации
  • Сравнить качество поиска nomic-embed-text vs mxbai-embed-large vs multilingual-e5

🔵 Приоритет 4 — расширение

  • Интегрировать RAG в CRM — чтобы менеджеры могли задавать вопросы по знаниям через API
  • Расширить KB на другие проекты — CRM, коробочка, alatyr-service platform
  • Собрать веб-хук от git push → мгновенный синк (вместо ожидания cron)

Ссылки


Связанные файлы


История изменений

Дата Что
2026-08-05 Первая версия. Развёрнут deploy key SSH, скрипт sync-to-openwebui.sh (bash, 199 строк), первый синк 10 файлов за 9m36s, коллекция alatyr-infra-kb (191ae5b9...) создана в OpenWebUI. Обнаружены: двойная индексация OpenWebUI при upload+attach, Ollama на хосте systemd не в докере, слушает 192.168.1.200:11434, размерность 768. Утечка QDRANT_API_KEY в чат — надо ротировать. Cron/timer пока не установлен.