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


## Введение

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

Open-WebUI[^1] — 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-файл минимален:

```ini
[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:

```bash
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

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

Плюсы:

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

Минусы:

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

### SearXNG

SearXNG[^3] — 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:

```bash
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-интерфейсе — это скорее приятное дополнение, чем полноценная замена специализированных сервисов.

## Источники

[^1]: [Open-WebUI — GitHub](https://github.com/open-webui/open-webui)

[^2]: [Tavily — AI Search API](https://tavily.com/)

[^3]: [SearXNG — Privacy-respecting metasearch engine](https://github.com/searxng/searxng)

