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

Веб-поиск в Open-WebUI: путь от энтузиазма к разочарованию

Введение

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

Open-WebUI1 — self-hosted интерфейс для работы с LLM — тоже поддерживает веб-поиск. Я попытался настроить его так, чтобы качество ответов было сопоставимо с DeepSeek Web. Спойлер: не получилось.

Что такое Open-WebUI

Open-WebUI — это веб-интерфейс для взаимодействия с языковыми моделями. Внешне напоминает ChatGPT: окно чата, история сессий, выбор модели. Но под капотом — self-hosted приложение, которое можно развернуть на своём сервере и подключить к любому LLM-провайдеру.

Основные возможности:

  • Мультипровайдер. Подключается к OpenRouter, Ollama, OpenAI, Anthropic и любому OpenAI-совместимому API. Можно использовать несколько провайдеров одновременно и переключаться между моделями в одном интерфейсе.
  • Сессии и история. Каждый чат — отдельная сессия с полной историей. Поиск по прошлым разговорам, папки для организации.
  • Knowledge Base. Загрузка документов (PDF, Markdown, DOCX) с RAG-поиском. В чате пишешь #имя_коллекции — модель ищет по загруженным документам.
  • Tools и Functions. Расширения из маркетплейса: выполнение кода, адаптивная память между сессиями, интеграция с внешними сервисами.
  • PWA. Работает как мобильное приложение через Add to Home Screen.
  • Веб-поиск. Интеграция с поисковыми движками для получения актуальной информации.

Open-WebUI по сути дела создает самостоятельную платформу для управления моделями и предоставляет веб-интерфейс для взаимодействия с ними. Один инстанс может обслуживать нескольких пользователей с разделением доступа.

Варианты установки

Официальная документация предлагает несколько вариантов развёртывания — от Docker Compose с PostgreSQL, PGVector и Redis до минимального контейнера со встроенной SQLite.

Сложный вариант: PostgreSQL + Redis

Первый инстинкт — развернуть «по-взрослому»:

  • PostgreSQL с расширением PGVector для векторного поиска по документам
  • Redis (Valkey) для кеширования и синхронизации конфигурации между workers
  • 3 uvicorn workers для обработки параллельных запросов

На бумаге выглядит солидно. На практике для single-user deployment это оказалось избыточным. Три контейнера вместо одного, дополнительная сеть для связи с базой, init-скрипт для расширения pgvector, конфигурация Redis.

Хуже того, multi-worker архитектура сломала синхронизацию конфигурации. API-ключ для веб-поиска, введённый через админ-панель, сохранялся в базу, отображался в UI, но при runtime оказывался None — worker, обрабатывающий запрос, не получал обновлённое значение.

Простой вариант: SQLite

Откат к одному контейнеру с SQLite всё исправил. Для одного пользователя этого достаточно с большим запасом:

  • Один контейнер ~500 MB RAM
  • SQLite — данные, конфигурация, история чатов в одном файле
  • Один worker — нет проблем с синхронизацией
  • Бекап — один файл webui.db в restic

В моём случае Open-WebUI работает за Tailscale, доступен только из VPN-сети. Quadlet-файл минимален:

[Container]
ContainerName=open-webui
Image=ghcr.io/open-webui/open-webui:main
Volume=%h/volumes/open-webui:/app/backend/data:Z
Network=caddy-private.network
EnvironmentFile=%h/.config/containers/systemd/open-webui/open-webui.env
EnvironmentFile=%h/volumes/open-webui/.env

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

У Open-WebUI есть архитектурная особенность, которая сначала вызывает недоумение: конфигурация живёт в двух местах, и они не всегда синхронизированы.

Переменные окружения задают начальные значения при первом запуске. Они определяют подключение к провайдерам, поисковому движку, базовые параметры RAG:

ENABLE_RAG_WEB_SEARCH=true
RAG_WEB_SEARCH_ENGINE=searxng
SEARXNG_QUERY_URL=http://searxng:8080/search?q=<query>&format=json
RAG_WEB_SEARCH_RESULT_COUNT=2

База данных (таблица config в SQLite) хранит фактически используемые значения. После первого запуска Open-WebUI копирует значения env vars в базу — и дальше использует только базу. Изменение переменной окружения после первого запуска ничего не даёт — нужно менять через Admin Panel в веб-интерфейсе.

Это приводит к неочевидному поведению:

  1. Задаёшь RAG_WEB_SEARCH_RESULT_COUNT=5 в env
  2. Запускаешь контейнер, значение копируется в базу
  3. Меняешь в env на 2, рестартуешь контейнер
  4. Open-WebUI продолжает использовать 5 из базы

Часть настроек вообще недоступна через env и существует только в веб-интерфейсе: system prompt для поиска, режим Function Calling (Default FC / Native FC), параметры чанкинга документов. Это значит, что полностью воспроизвести конфигурацию из одних переменных окружения невозможно. При пересоздании контейнера с чистым volume часть настроек придётся вводить вручную.

На практике я храню env-файл в git как документацию начальных значений, а после развёртывания дохожу до нужной конфигурации через Admin Panel.

Зачем нужен веб-поиск в чате

Языковые модели уверенно отвечают на вопросы в рамках обучающей выборки. Но стоит спросить о событии после даты cutoff — и начинаются галлюцинации. Модель не скажет «я не знаю» — она сгенерирует правдоподобный, но выдуманный ответ.

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

Сравнение: DeepSeek Web vs Open-WebUI

Чтобы понять, к чему стремиться, достаточно сравнить ответы DeepSeek Web и Open-WebUI на один и тот же вопрос, требующий актуальной информации.

DeepSeek Web:

  • Формирует несколько поисковых запросов параллельно
  • Загружает десятки страниц за секунды
  • Синтезирует структурированный ответ с inline-цитатами
  • Указывает на противоречия между источниками
  • Результат — детализированный обзор с 10-15 ссылками

Open-WebUI с SearXNG:

  • Один поисковый запрос за раз
  • Загружает 2 страницы (больше не помещается в контекст)
  • Ответ поверхностный, часто на основе одного источника
  • Цитирование — если промпт явно требует, и то не всегда
  • Результат — краткий пересказ одной-двух страниц

Разница не в моделях — одна и та же модель через DeepSeek Web даёт существенно лучший результат, чем через Open-WebUI. Дело в инфраструктуре поиска: сколько запросов выполняется, сколько страниц загружается, как контент препарируется перед подачей в модель.

Варианты интеграции веб-поиска

Open-WebUI поддерживает несколько поисковых бэкендов. Я попробовал два.

Tavily

Tavily2 — коммерческий API для AI-поиска. Принимает запрос, возвращает очищенные результаты, оптимизированные для LLM. Интеграция тривиальна: API key в переменную окружения.

Плюсы:

  • Результаты уже подготовлены для LLM — чистый текст без навигации и рекламы
  • Настройка — одна переменная с ключом

Минусы:

  • Платный сервис с лимитами (1000 запросов/месяц на бесплатном тарифе)
  • Зависимость от внешнего API
  • Нет контроля над тем, какие движки используются

SearXNG

SearXNG3 — self-hosted метапоисковый движок. Агрегирует результаты из 246 поисковых движков (Google, Bing, DuckDuckGo, Wikipedia и др.), не хранит данные пользователей, отдаёт результаты в JSON через API.

Плюсы:

  • Self-hosted, никаких API ключей и лимитов
  • 246 поисковых движков с категориями (general, images, news, science)
  • JSON API из коробки
  • Уже развёрнут в инфраструктуре для других целей

Минусы:

  • Возвращает сниппеты, не полные страницы — Open-WebUI должен сам загружать контент
  • Качество сниппетов зависит от конфигурации движков

Для self-hosted deployment выбор очевиден — SearXNG. Не зависишь от внешнего API, не платишь за запросы, полный контроль над конфигурацией.

Сложности и попытки их решить

Проблема 1: сниппеты слишком короткие

В режиме по умолчанию Open-WebUI получает от SearXNG только сниппеты — 2-3 предложения на каждый результат. Этого катастрофически мало для осмысленного ответа. Модель фактически пересказывает обрывки из поисковой выдачи.

Попытка решения: включить загрузку полных страниц (BYPASS_WEB_SEARCH_WEB_LOADER=false). Open-WebUI начинает скачивать HTML, парсить и передавать полный текст в модель.

Проблема 2: контекстное окно переполняется

С включённой загрузкой страниц 10 результатов генерируют сотни тысяч токенов. Контекстное окно модели не резиновое.

Решение: уменьшить количество результатов с 10 до 2. Две полные страницы помещаются в контекст. Но информации из двух источников часто недостаточно для качественного ответа.

Проблема 3: сайты блокируют запросы

Многие сайты возвращают 403 Forbidden на запросы без User-Agent. Open-WebUI по умолчанию отправляет запросы с минимальными заголовками.

Решение: добавить Chrome User-Agent:

USER_AGENT=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ...

Это помогло с большинством сайтов, хотя CloudFlare-защита по-прежнему блокирует часть запросов.

Проблема 4: модель игнорирует часть результатов

В агентном режиме (Native Function Calling) модель получает инструменты search_web и fetch_url. Предполагается, что она ищет, загружает страницы и анализирует. На практике модель часто вызывала fetch_url только для одного URL из двух, пропуская потенциально релевантный результат.

Попытка решения: специальный system prompt с жёстким правилом:

After each search_web call, you MUST call fetch_url for EVERY URL in the results. Do not cherry-pick — fetch all of them.

Это помогло частично — модель стала загружать все URL, но не всегда следовала инструкции. Промпт — это просьба, не гарантия.

Проблема 5: одна итерация поиска

DeepSeek Web выполняет множество поисковых запросов — уточняет, переформулирует, ищет в разных направлениях. Open-WebUI в лучшем случае делает один-два запроса. System prompt предусматривал до 3 итераций, но на практике модель обычно останавливалась после первого поиска, считая информацию достаточной.

Решения нет. Это фундаментальное ограничение подхода: Open-WebUI полагается на то, что модель сама решит, когда искать дальше. У DeepSeek Web поисковый пайплайн реализован на уровне инфраструктуры, а не через промпт.

Проблема 6: нет параллельных запросов

Даже когда модель выполняет несколько поисков, они идут последовательно. Каждый search_web → ожидание → fetch_url → ожидание → анализ → следующий запрос. Время ответа растёт линейно с количеством итераций.

Решения нет. Архитектура Function Calling в Open-WebUI последовательная.

Итоги

Веб-поиск в Open-WebUI работает, но результат далёк от того, что дают специализированные сервисы вроде DeepSeek Web или Perplexity. Фундаментальные ограничения:

  1. Мало источников. 2 страницы вместо десятков — потому что контекстное окно конечно, а Open-WebUI не умеет выделять и ранжировать релевантные фрагменты из большого количества документов.
  2. Нет параллелизма. Последовательные запросы вместо параллельного сбора информации.
  3. Промпт, не пайплайн. Поведение определяется system prompt — модель может проигнорировать инструкции, остановиться после первого поиска или пропустить URL.
  4. Нет специализированной обработки. Нет извлечения ключевых фрагментов, ранжирования по релевантности, дедупликации. Полная страница целиком подаётся в контекст.

В результате я откатился к простой конфигурации: SearXNG с двумя результатами, загрузка полных страниц, bypass embedding. Это лучше, чем ничего — модель хотя бы не галлюцинирует о текущих событиях. Но для задач, требующих глубокого исследования с множеством источников, я по-прежнему использую DeepSeek Web или Perplexity.

Возможно, ситуация улучшится с развитием Open-WebUI — проект активно развивается, и поддержка MCP (Model Context Protocol) в последних версиях открывает новые возможности для интеграции с внешними инструментами. Но на данный момент веб-поиск в self-hosted LLM-интерфейсе — это скорее приятное дополнение, чем полноценная замена специализированных сервисов.

Источники