Два окна страницы поиска pacod-kb, светлое и тёмное, наложенные друг на друга. В обоих задан вопрос «How does the gateway keep provider keys?» с top K 5; в тёмном виден первый фрагмент, из раздела «How it is put together» проекта pacod-demo, с RRF 0.0164, идентификатором документа и байтами 16916–17985, и следующий, из pacod-gw, с RRF 0.0161.
Поиск днём и ночью. Простой вопрос к семи README проектов PACOD возвращает ранжированные фрагменты, у каждого путь заголовков, идентификатор документа и диапазон байтов, нужные для цитирования; оценки 0.0164 и 0.0161 (1/61 и 1/62) означают, что порядок задал один лишь векторный поиск.

pacod-kb загружает файлы и веб-страницы, преобразует их в канонический Markdown, индексирует для гибридного семантического и ключевого поиска и хранит каждую версию. Каждый документ проверяется на prompt injection, прежде чем что-либо попадёт в индекс, а моделям ниже по цепочке kb передаёт фрагменты, на которые можно сослаться, по документированному контракту метаданных. Работает на CPU, и поиск остаётся отзывчивым, пока документы загружаются.

База знаний для RAG

  • Лицензия: Apache-2.0
  • Версия: 1.0.0 · Стабильная
  • Языки: Python
  • Технологии: FastAPI, HTMX, docling, BGE-M3, Qdrant, SQLite FTS5, PostgreSQL, Docker
  • Открыть на GitHub · Создать issue

Возможности

Загрузка чего угодно

PDF, DOCX, HTML, Markdown, текст и JSON — загруженные вручную, положенные в папку, полученные по URL или отслеживаемые на изменения.

Канонический Markdown

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

Безопасность контента

Многоуровневая проверка на prompt injection и спам блокирует документ или отправляет его в карантин до индексации; результаты поиска ограждаются как недоверенные данные.

Гибридный поиск

Плотные векторы BGE-M3 в Qdrant плюс индекс ключевых слов, объединённые методом reciprocal rank fusion, — в проектах, которые никогда не видят документов друг друга.

Все версии сохраняются

Новая версия замещает старую, а та остаётся доступной для поиска по дате; если отслеживаемая страница возвращается к прежнему виду, восстанавливается более ранняя версия.

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

Необязательный реранкер на cross-encoder выдаёт оценку релевантности от 0 до 1, сопоставимую между запросами, так что min_score может отсеивать результаты, которые не отвечают на вопрос.

Контракт для потребителей

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

Рассчитан на CPU

Запросы идут впереди работы по загрузке и ждут не больше одного небольшого пакета; эмбеддинги можно получать и от общего сервиса.

Веб-интерфейс и CLI

Веб-интерфейс в общем стиле pacod, светлый или тёмный, для документов, проектов, задач, использования AI и его стоимости и участников; CLI kb для загрузки, поиска, отслеживания, пересборки и экспорта.

Диаграмма платформы с выделенным pacod-kb и приглушёнными остальными проектами: серая линия от людей к pacod-kb, зелёная пунктирная линия от pacod-kb к pacod-gw, под которым находятся провайдеры моделей, и золотая пунктирная линия к pacod-id; пунктирная рамка pacod-demo охватывает всех.
Место pacod-kb. Люди пользуются им напрямую (серый), его необязательные вызовы LLM идут через pacod-gw, который хранит ключи провайдеров (зелёный), а вход — через pacod-id, где выдаются роли в kb (золотой). pacod-demo запускает его вместе с остальными.

Скриншоты

Светлая страница документов pacod-kb для проекта kb_chunks: поле фильтра с All, Active, Superseded и Errors, кнопки экспорта и таблица из семи руководств на английском, pacod-kb, pacod-id, pacod-gw, pacod-demo, pacod-cms, pacod-benchmark и pacod-ap, с 4–28 фрагментами в каждом, все активны и загружены 2026-10-28.
Документы проекта: семь README проектов PACOD, загруженные как Markdown, каждый активен и с числом своих фрагментов. Идентификатор документа — SHA-256 его байтов, и каждый можно просмотреть, скачать как исходник или как сохранённый Markdown либо переиндексировать.
Светлая панель pacod-kb: плитки с 7 документами всего, 7 активными, 0 заменёнными и 0 ошибками; блок работы во главе с зелёной кнопкой Ingest document; блок обслуживания со Scan dropzone, Run watch, Regenerate from DB, Convert no-index и красной Rebuild full index; и строки настроек, где проверка содержимого включена, а преобразование детерминированное.
Панель: документы проекта по состояниям, повседневная работа, обслуживание (папка для приёма файлов, отслеживаемые веб-источники, полная перестройка индекса) и переключатели, действующие без перезапуска. Проверка содержимого включена; преобразование детерминированное, так что ни одна модель этих документов не касается.
Тёмная страница коллекций Qdrant в pacod-kb: форма создания коллекции с именем, 1024 измерениями и косинусным расстоянием и таблица с kb_chunks, 87 точками, кнопкой Export all и ссылкой Delete.
Коллекции kb в Qdrant, ночью. kb_chunks содержит 87 точек, по одной на каждый фрагмент семи документов, в виде 1024-мерных векторов BGE-M3 с косинусным расстоянием; в Qdrant, общем с другими продуктами, страница показывает только собственные коллекции kb.

Зачем он нужен

Большая часть работы в retrieval-augmented generation происходит до того, как модель прочтёт хоть слово: загрузить документы, поддерживать их в чистоте, найти нужный фрагмент и сообщить модели, откуда он взят. pacod-kb — именно эта часть, выделенная в отдельный сервис с HTTP API, веб-интерфейсом и CLI.

Его сформировали три требования:

  • Ответы должны быть цитируемыми. Каждый фрагмент несёт свой путь по заголовкам и смещения в байтах в стабильном тексте Markdown, так что потребитель может процитировать его, сослаться на него и получить его контекст.
  • Источники меняются. Ничего не перезаписывается: новая версия замещает старую, а та остаётся доступной для поиска по дате.
  • Загруженный текст — недоверенный ввод для модели. Перед индексацией он проверяется на prompt injection, а на выходе ограждается как данные.

Документ, шаг за шагом

Документ проходит один и тот же путь, был ли он загружен, положен в папку dropzone, получен по URL или повторно получен из отслеживаемого веб-источника:

  1. Приём. SHA-256 байтов источника — это идентификатор документа; уже известный идентификатор пропускается, поэтому один и тот же файл никогда не загружается дважды.
  2. Разбор. PDF через docling, с необязательным OCR для отсканированных страниц (если установлен tesseract, два движка сравниваются постранично), DOCX, HTML, Markdown, текст и JSON, ключи которого становятся заголовками. Результат — Markdown с front matter на YAML.
  3. Проверка языка. Принимаются английский, немецкий, болгарский, русский, сербский, македонский, хорватский и словенский; другие языки отклоняются до того, как что-либо будет записано. Текст, язык которого определить не удалось, принимается как unknown, с предупреждением.
  4. Сканирование детерминированного разбора на prompt injection и спам. Оно выполняется до того, как текст увидит какая-либо модель: преобразователь мог бы выполнить внедрённые инструкции, а преобразование удалило бы символы нулевой ширины и скрытую разметку, которые ищет проверка.
  5. Преобразование (необязательно). Проход LLM через pacod-gw приводит Markdown в порядок; PDF размером до 20 МиБ отправляется модели как файл, всё остальное — как извлечённый текст. Документ в карантине не отправляется никогда. Обрезанный, отказной или пустой ответ оставляет детерминированный разбор, а преобразованный текст проверяется второй раз.
  6. Очистка и структурирование. Фильтр шума убирает номера страниц, колонтитулы, уведомления о cookie и блоки навигационных ссылок. Заголовки повышаются в кодексах, регламентах и директивах (на восьми языках) и в научных работах; статья, полученная по URL, получает ещё автора, дату и издание.
  7. Сохранение document.md, манифеста с журналом жизненного цикла и снимка источника — в папках под KB_ROOT или в PostgreSQL.
  8. Индексация — только если статус документа ok. Чанки до 500 токенов, учитывающие заголовки, встраиваются BGE-M3, каждый с цепочкой [Title > Heading > Subheading] в начале (таблица получает ещё и абзац перед ней), в коллекцию Qdrant и индекс ключевых слов проекта. Чанки, не изменившиеся с прежней версии, сохраняют свои векторы.

Загрузки из интерфейса или через API выполняются как задачи, одна за другой, на отдельном worker и никогда — в потоках, которые отвечают на поиск. В режиме базы данных задачи хранятся в PostgreSQL, а перезапуск помечает прерванные им задачи как завершившиеся ошибкой.

Архитектура

pacod-kb — это два сервиса, kb-pipeline и kb-ui, а также модели и хранилища, которыми они пользуются:

Часть Роль
Pipeline API (kb-pipeline, :8000) Загрузка, индексация, поиск, задачи, настройки; единственная часть, которая обращается к AI-шлюзу
Веб-интерфейс (kb-ui, :8080) FastAPI и HTMX; вход через pacod-id, сессии, участники, страницы; обращается к конвейеру с API-ключом
BGE-M3 Эмбеддинги размерности 1024 — в собственном процессе конвейера или от сервиса эмбеддингов pacod-ap
Qdrant Векторы: встроенный в конвейер или отдельный сервер, которым могут пользоваться и другие продукты
Индекс ключевых слов SQLite FTS5 (BM25) при хранении в файловой системе, полнотекстовый поиск PostgreSQL при хранении в базе данных
Документы и журнал учёта Папки под KB_ROOT или PostgreSQL; журнал учёта хранит настройки, промпты, использование и стоимость AI

Такое разделение сделано намеренно. Конвейер разбирает недоверенные документы и загружает произвольные URL, поэтому не получает ни одного из секретов pacod-id; интерфейс никогда не видит ключ шлюза. Каждый контейнер получает явный список переменных окружения, и этот список проверяет тест.

Каждый контейнер подключается к общей сети Docker pacod-net, где все могут обращаться друг к другу. Исключение — внутренний слушатель pacod-id (токены, ключи подписи, лента авторизации, admin API): он отвечает только в pacod-admin-net, внутренней сети без выхода наружу, под именем pacod-id-internal:8071. kb-ui подключён к этой сети; конвейер — нет, и обратиться к слушателю он не может.

Кто что сделал. Каждый вызов из интерфейса несёт X-KB-Actor — идентификатор вошедшего человека. Конвейер доверяет ему только вместе с API-ключом, записывает его в задачи, изменения настроек и учёт использования и передаёт шлюзу. Когда pacod-id удаляет учётную запись, kb завершает её сессии, повсюду заменяет этот идентификатор на deleted-user и только потом подтверждает удаление.

Поиск и что получает модель

Запрос выполняется в Qdrant и в индексе ключевых слов, а два ранжирования объединяются методом reciprocal rank fusion: каждый список добавляет 1/(60 + ранг) за каждый найденный им фрагмент. Фильтры по дате и истории применяются внутри обоих поисков, так что сотня замещённых фрагментов не может вытеснить тот единственный, который действует.

Оценка RRF упорядочивает один список результатов и ничего не значит при сравнении разных запросов. Оценка 0,0164 — это 1/61: первое место в одном списке и отсутствие в другом. Именно её по умолчанию получает большинство вопросов, заданных естественным языком, потому что ключевая половина поиска (KEYWORD_QUERY_MODE=legacy) требует совпадения каждого слова запроса. Режимы с анализом текста (or, and_or) вместо этого отбрасывают стоп-слова и отсекают окончания; они остаются выключенными, пока оценка не покажет, что они помогают.

Необязательный cross-encoder, BAAI/bge-reranker-v2-m3, переупорядочивает десять лучших кандидатов и даёт каждому релевантность от 0 до 1, сопоставимую между запросами, так что min_score может отсеивать результаты, которые не отвечают на вопрос. Он загружается в фоне, когда его включают, и ни один запрос его не загружает.

POST /query принимает вопрос и его фильтры:

{"query": "maximum fine for a data breach", "top_k": 5, "as_of": "2025-12-30"}

Ответ — {"results": [...], "reranked": false, "score_kind": "rrf"}. Каждый результат содержит:

Поле Для чего
chunk_id Стабильный идентификатор, кодирующий структуру заголовков, а не порядковый номер
source_id Идентификатор документа: SHA-256 байтов источника
heading_path Цепочка заголовков вроде ["Chapter 1", "§ 5"] — для цитирования и фильтров по разделам
byte_start, byte_end Смещения UTF-8 в сохранённом тексте Markdown
valid_until Когда эта версия была замещена, или null, пока она актуальна
text Текст, по которому построен эмбеддинг, ограждённый как недоверенные данные
score, rrf_score, rerank_score Оценка для упорядочивания и её составляющие

Смещения считаются в байтах, а не в символах, что важно для кириллицы:

body_bytes = body.encode("utf-8")
passage = body_bytes[result["byte_start"]:result["byte_end"]].decode("utf-8")

У каждого документа есть и манифест: заголовок, источник, тип документа, статус, проект, парсер или модель шлюза, создавшие текст, и журнал жизненного цикла (ingested, superseded, reinstated, security_flagged, review_flagged, llm_usage).

Версии и проекты

Новая версия, загруженная с --supersedes, или изменившийся отслеживаемый источник помечает старую версию полем valid_until — сразу в четырёх местах: во front matter, в манифесте, в индексе ключевых слов и в данных векторов. Поиск по умолчанию возвращает актуальные фрагменты; as_of — то, что действовало в заданный день; include_historical — всё. Если отслеживаемая страница вернулась к прежнему виду, более ранняя версия восстанавливается. В режиме файловой системы с GIT_AUTOCOMMIT=true каждая загрузка также фиксируется коммитом в git-репозитории в KB_ROOT.

Проект — это изолированная коллекция: собственная коллекция Qdrant и собственные строки в индексе ключевых слов. Поиск видит один проект, пересборка удаляет и заново заполняет индекс одного проекта (--all-projects пересобирает каждый в его собственную коллекцию), а переключение активного проекта отклоняется, пока задача стоит в очереди или выполняется.

Поиск, подтверждённый измерениями

Значения по умолчанию получены на стенде оценки поиска: 14 документов на шести языках (GDPR, Директива о защите данных в правоохранительной сфере, четыре конституции, статьи Википедии) и 92 написанных вручную запроса — перефразировки, точные термины, межъязыковые, на родном языке и без ответа. Результат засчитывается, если он взят из размеченного раздела, поэтому разметка переживает изменения в разбиении на чанки. Цифры измерены на процессоре машины разработчика (Apple M5, четыре потока), и документация подчёркивает, что это не продакшен-цифры:

Показатель Результат
Нужный раздел среди объединённых кандидатов для каждого запроса, у которого есть ответ
Нужный раздел в первой пятёрке, без реранкера 98,7 % (R@1 0,83, около 50 мс)
С реранкером, десять кандидатов R@1 0,91, около 2 с на запрос
Поиск во время загрузки документов (p50) раньше 24,3 с; теперь 0,41 с, когда задачи выполняются одна за другой
Встраивание при загрузке, пакеты, отсортированные по длине примерно в 2,2 раза быстрее, векторы не меняются

Оценка реранкера отличает ответы от того, что ответом не является: min_score 0.1 отсеивает 14 из 15 запросов без ответа и 2 из 70 правильных первых ответов, тогда как по оценкам RRF их было вообще не различить. Тем не менее по умолчанию он выключен: его выигрыш немного не дотянул до порога на отложенной половине запросов, а его задержку ещё нужно подтвердить на продакшен-процессоре.

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

Модель безопасности

Безопасность содержимого. Проверка работает с нормализованной копией текста (нормализация Unicode, приведение к нижнему регистру, склеивание слов, набранных вразрядку) и применяет таблицы правил: фразы-инъекции на английском, немецком, русском и болгарском, трюки с переопределением направления текста и символами нулевой ширины, блоки base64 и hex (они декодируются и проверяются ещё раз), скрытую разметку и признаки спама, такие как плотность ссылок и повторяющиеся строки. У каждого правила своя серьёзность. Высокая серьёзность — только у однозначной подмены инструкций («ignore previous instructions»), символа смены направления текста и инструкции, спрятанной в base64; фразы, которые встречаются и в технических текстах, например «enable developer mode», имеют низкую серьёзность, так что одно совпадение не может заблокировать документ. Тесты проверяют, что длинные законы, в том числе GDPR, проходят как чистые. Вердикт — один из трёх:

  • block: находка высокой серьёзности или высокий показатель угрозы. Загрузка завершается ошибкой, и ничего не сохраняется.
  • quarantine: документ сохраняется со статусом review, никогда не преобразуется и никогда не индексируется. Один только спам никогда не приводит к блокировке.
  • clean: документ индексируется.

Необязательное второе мнение LLM через шлюз (по умолчанию выключено) запрашивается, только если правила не заблокировали документ; на него отводится 15 секунд, и при сбое оно не блокирует документ. Заведомо безопасный материал может обойти проверку с kb ingest --trusted. На выходе /query заключает каждый фрагмент в ограждение для недоверенных данных.

Загрузка по URL. Принимаются только http и https; допускается не больше пяти перенаправлений, и каждый переход проверяется; загрузка останавливается на 50 МБ или через 180 секунд. Загрузка по URL никогда не читает локальный файл. Два значения по умолчанию подходят для ноутбука, а не для сервера: задайте WEB_FETCH_ALLOW_PRIVATE=false, чтобы отклонять адреса, которые разрешаются в частные сети (защита от server-side request forgery), и ALLOW_INSECURE_TLS=false, чтобы после ошибки TLS не выполнялся повторный запрос без проверки сертификата.

Люди. Интерфейс выполняет вход через pacod-id по OIDC с PKCE, на собственных страницах pacod-id в оформлении kb. Он проверяет токен доступа, хранит токены в зашифрованной серверной сессии и выдаёт браузеру cookie, в котором есть только случайный секрет. Роли kb — это манифест, который интерфейс передаёт в pacod-id при каждом запуске; роль человека затем берётся из ленты авторизации pacod-id. Без роли в kb человек видит страницу «нет доступа»; если копия ленты в интерфейсе старше 60 секунд, страницы отвечают 503, а не гадают. Выход завершает сессию в pacod-id, а сессия, отозванная там, завершается и в kb.

Роль Может
viewer Читать и искать
editor Загружать, переиндексировать и удалять документы; запускать задачи; управлять проектами
admin AI Management, промпты, переключатели во время работы, участники

Администраторы приглашают людей и меняют роли на странице участников через admin API pacod-id: в списке только роли kb, а последнего администратора удалить нельзя.

API конвейера сравнивает свой ключ за постоянное время и по умолчанию закрыт: без PIPELINE_API_SECRET каждый маршрут, кроме /health, отвечает 503.

Развёртывание и эксплуатация

docker-compose.yml содержит собственные сервисы kb, kb-pipeline и kb-ui. docker-compose.standalone.yml содержит копии общих компонентов для kb, которые включаются профилями Compose: Qdrant, PostgreSQL, pacod-id с базой данных рядом с базой kb, перехватчик почты и шлюз для разработки — pacod-gw-lite перед mock-провайдерами pacod-gw, так что ничего не покидает машину. make up загружает оба файла; для этого рядом с kb нужны клоны pacod-id и pacod-gw, а на одной машине может работать только одна копия каждого общего компонента. Mock-провайдер отвечает эхом промпта, поэтому стек разработки оставляет преобразование выключенным; AI Management → Test model всё равно проходит весь путь, включая стоимость.

В продакшене kb направляют на настоящий шлюз (pacod-gw с организацией, рабочим пространством и ключом для kb или pacod-gw-lite с ключами провайдеров), ставят pacod-id за TLS или используют уже существующий и убирают профили для разработки.

pacod-demo, работающий на demo.pacod.io, запускает kb вместе с остальными проектами pacod. Он загружает только файл приложения kb: kb хранит свою базу данных в общем PostgreSQL, а свои коллекции kb_ — в общем Qdrant, строит эмбеддинги через общий сервис эмбеддингов, а его интерфейс обслуживается за общим входным Caddy. Демо тоже оставляет преобразование выключенным.

  • Хранилище. KB_STORAGE_BACKEND — filesystem или database; переключение не переносит документы.
  • Данные. Каталоги данных задаются переменными (KB_ROOT, KB_MODEL_CACHE, KB_QDRANT_DATA, KB_POSTGRES_DATA). Имя без косой черты означает том Docker; на macOS make setup использует тома.
  • Процессор. TORCH_NUM_THREADS=0 следует квоте процессора контейнера. EMBED_BATCH_TOKENS (1024) ограничивает, сколько запрос может ждать за загрузкой.
  • Модели офлайн. Запустите один раз с доступом к сети, чтобы заполнить кэш моделей, затем задайте HF_HUB_OFFLINE=1.
  • Общие модели. С EMBEDDINGS_URL конвейер строит эмбеддинги через общий сервис эмбеддингов, а не загружает BGE-M3 сам: векторы те же, поэтому ничего не нужно встраивать заново, а конвейеру требуется примерно на 2 ГБ меньше памяти. RERANKER_URL делает то же самое для реранкера. Если модель этого сервиса когда-нибудь заменят, индекс kb придётся пересобрать.
  • Общий Qdrant. На Qdrant, которым пользуются и другие продукты, страницы Qdrant в интерфейсе показывают и затрагивают только коллекции kb (KB_QDRANT_COLLECTION_PREFIX, по умолчанию kb_).
  • Переключатели во время работы. Панель включает и выключает проверку содержимого, AI-преобразование и проверку языка без перезапуска; эти настройки не сохраняются, и при следующем запуске снова действуют переменные окружения.
  • AI. Администраторы выбирают модели и правят промпты в AI Management, где также видно использование и стоимость, о которых сообщил шлюз. Каждый вызов несёт непрозрачную атрибуцию (функция, задача, проект, человек); ответ 429 повторяется один раз, после его Retry-After. Если шлюз не настроен, AI выключен, а всё остальное работает.
  • Логи. Одно событие на строку в общем формате pacod, JSON или текст (LOG_FORMAT), на уровне LOG_LEVEL, без строк запроса в URL. Docker хранит пять файлов по 10 МБ на контейнер.

Резервные копии охватывают KB_ROOT или базу данных kb в PostgreSQL, хранилище Qdrant (или его можно пересобрать командой kb rebuild --all-projects), базу данных pacod-id вместе с её ключом шифрования ключей и .env.

Что проверено и ограничения

make check запускает ruff, mypy --strict и тесты, которым не нужны ни сеть, ни загрузка моделей, ни работающий pacod-id или шлюз: тесты интерфейса проходят вход по настоящему сценарию против поддельного pacod-id. Снимки таблиц маршрутов обоих API ловят непреднамеренные изменения, а CI при каждом push запускает набор оценки с эмбеддером-заглушкой, чтобы поймать дрейф в корпусе или в разбиении на чанки.

  • Цифры поиска получены на одной машине разработчика; make eval-docker повторяет их внутри образа конвейера с ограничением до заданного числа CPU, как на продакшен-хосте.
  • Конвейер и интерфейс работают с одним worker каждый: встроенный Qdrant держит блокировку файла, а работа интерфейса с сессиями рассчитана на один процесс.
  • Реранкер в собственном процессе конвейера примерно удваивает потребление им памяти; с RERANKER_URL конвейер не загружает реранкер вовсе.
  • Сербский принимается, но у детектора языка нет профиля для сербского, поэтому сербский текст может быть определён неверно или отклонён.
  • У kb нет эндпоинта метрик. График использования AI загружает Chart.js из CDN — это единственный внешний скрипт интерфейса.
  • Задокументировано для следующей переиндексации, но не выпущено: разреженные векторы BGE-M3 для ключевой половины поиска, доработка разбиения на чанки и пересборка в теневую коллекцию с последующим переключением.

Подробнее:

Быстрый старт

Запуск в Docker

make setup
make up
open http://localhost:8080/kb

Разработка и оценка

uv sync
make check
make eval

Работает с

  • pacod-id: Пользователи входят через pacod-id; роли viewer, editor и admin назначаются там, а страница участников kb работает через его admin API.
  • pacod-gw: Необязательный проход очистки через LLM и необязательное второе мнение при проверке идут через шлюз, который хранит ключи провайдеров, рассчитывает стоимость каждого вызова и атрибутирует его.
  • pacod-ap: Работая рядом с pacod-ap, kb может строить эмбеддинги и переранжировать через его общий сервис эмбеддингов — с теми же векторами BGE-M3.
  • pacod-demo: Запускает kb вместе с остальными продуктами на общих Qdrant, PostgreSQL и сервисе эмбеддингов, за общей точкой входа.