AGONTS

RAG-пайплайн: конфигурация, чанкинг, реранкер

Как устроен поиск по базе знаний под капотом — embedding, structure-aware чанкинг, cross-encoder rerank, конфиг через platform_settings.rag_config.

Этот документ описывает «инженерную» сторону RAG в AGONTS — как устроен пайплайн от загрузки файла до ответа агента, какие узлы можно крутить и какие defaults стоят. Пользовательскую сторону (загрузка, коллекции, агенты) смотрите в База знаний.

Архитектура

┌─────────────┐                                 ┌────────────┐
│   /memory   │  upload  ──┐                    │   /chat    │
│   (Web UI)  │            │                    │  (Web UI)  │
└─────────────┘            ▼                    └────────────┘
                  ┌────────────────┐                   │
                  │  agonts-api    │  ◀── tool call ───┘
                  │  Elysia 1.4    │
                  └────────────────┘
                        │      │
              parse  ◀──┘      └──▶  search
                │                       │
       ┌────────▼────────┐     ┌────────▼────────┐
       │   chunkText     │     │   embed query   │
       │  ByStrategy()   │     └────────┬────────┘
       └────────┬────────┘              │
                │                       │
       ┌────────▼────────┐     ┌────────▼────────┐
       │  TEI :8001      │     │  Qdrant search  │
       │  bge-m3 1024d   │     │  topK = 8       │
       └────────┬────────┘     └────────┬────────┘
                │                       │
       ┌────────▼────────┐     ┌────────▼────────┐
       │ Qdrant upsert   │     │  TEI :8004      │
       │ payload: text + │     │ bge-reranker-   │
       │ sectionPath     │     │ v2-m3 (rerank)  │
       └─────────────────┘     └────────┬────────┘

                               ┌────────▼────────┐
                               │  rag-hits block │
                               │  in LLM prompt  │
                               └─────────────────┘

Узлы

Embedding model

BAAI/bge-m3 (multilingual, 1024 dim, 8192 token ctx). Раздаётся через TEI 1.7 на cards-vm :8001, проксируется bun-runner на :4001/v1/embeddings.

Reranker

BAAI/bge-reranker-v2-m3 (cross-encoder). TEI 1.7 на cards-vm :8004, проксируется bun-runner на :4001/rerank.

Vector store

Qdrant 1.x в Docker на ag0nts.xyz, одна коллекция на каждую knowledge_collection (knowledgeQdrantCollectionName).

LLM

llama.cpp + Gemma 4 26B-A4B Q8_0 на cards-vm :8002 → bun-runner :4001/v1/chat/completions. Контекст 256k токенов.

Конфигурация: platform_settings.rag_config

Все ручки RAG лежат в JSONB-колонке rag_config на platform_settings (одна строка на workspace). Если поля нет — берём DEFAULT_RAG_CONFIG из @agonts/contracts. Поверх можно прокинуть env-переменные на api-контейнере.

Поля и defaults

ПолеDefaultЧто делает
chunkStrategystructure_awareКак резать текст (см. ниже).
chunkSize900Целевой размер чанка в символах (не токенах).
chunkOverlap120Overlap между соседними чанками в символах.
embeddingBatchSize8Сколько чанк-текстов отправляем за раз в /v1/embeddings.
searchTopK8TopK для Qdrant перед реранком (на коллекцию).
minScore0.2Cosine-cutoff: ниже не отдаём в LLM.
rerankEnabledtrueВключить cross-encoder реранкер.
rerankTopK4Сколько hits оставляем после реранка.
rerankUrlnullOverride URL реранкера (иначе совпадает с embedding baseUrl).
rerankModelnullИмя модели для рерanker'а (TEI его игнорирует — у него один модель).
metadataEnrichmenttrueПрефиксовать чанк строкой [Section: A › B › C]\n\n для structure_aware.

Env-overrides

Если что-то нужно крутить «горячо» без миграций / UI — задайте на api-контейнере:

RAG_CHUNK_STRATEGY=structure_aware
RAG_CHUNK_SIZE=900
RAG_CHUNK_OVERLAP=120
RAG_EMBEDDING_BATCH=8
RAG_SEARCH_TOPK=8
RAG_MIN_SCORE=0.2
RAG_RERANK_ENABLED=true
RAG_RERANK_TOPK=4
RAG_RERANK_URL=http://100.96.251.102:4001
RAG_RERANK_MODEL=bge-reranker-v2-m3
RAG_METADATA_ENRICHMENT=true

Порядок мердж-приоритета: DEFAULT_RAG_CONFIG → env → platform_settings.rag_config (сильнее всего).

Кэш: getRagConfigForWorkspace(workspaceId) мемоизирует на 30 секунд, ключ — workspaceId (или __platform__).

Где это в коде

packages/contracts/src/schemas/settings.ts          # Zod-схема + defaults
packages/db/src/schema.ts                           # platformSettings.ragConfig column
packages/db/drizzle/0056_platform_settings_rag_config.sql
apps/api/src/modules/shared/rag-config.ts           # getRagConfigForWorkspace()

Chunking-стратегии

В packages/db/src/rag/chunking.ts. Диспетчер — chunkTextByStrategy(text, strategy, options) возвращает массив TextChunk { chunkIndex, text, sectionPath? }.

Что делает. Сначала разбираем текст в дерево секций по заголовкам:

  • ATX-стиль (# , ## , ### , ...).
  • Numeric-стиль (1.2.3 Title).
  • ALL-CAPS standalone (если строка короткая, в верхнем регистре, окружена пустыми).

Внутри каждой leaf-секции пакуем абзацы в чанки до chunkSize, с overlap'ом chunkOverlap. Слишком длинные абзацы режем по предложению.

Если metadataEnrichment: true, в начало текста чанка вставляется строка [Section: A › B › C]\n\n — embedding и LLM видят полный breadcrumb секции, и при поиске «по разделу» нужный chunk легче находится.

Почему это default. Корпоративные базы знаний (регламенты, FAQ, инструкции) почти всегда структурированы заголовками, и section-aware чанкер сохраняет логические границы документа.

Старый-добрый sliding window: фиксированный размер chunkSize с overlap'ом chunkOverlap. Разрезы на natural boundaries (двойной \n → одиночный \n → . → пробел).

Используем как fallback, если документ совсем без структуры (одно длинное полотно текста).

Жёсткий сплит по \n\n, потом merge абзацев пока не упрёмся в chunkSize. Хорошо для FAQ-стайл документов где 1 абзац = 1 факт.

Разрезаем по .!? границам, склеиваем по N предложений до chunkSize. Для коротких инструкций.

Метаданные в Qdrant payload

Каждый point несёт:

{
  "documentId": "uuid",
  "collectionId": "uuid",
  "sourceName": "Регламент-обработки-жалоб-3299.md",
  "chunkIndex": 0,
  "text": "[Section: 2. Первичная диагностика]\n\nПроверка баланса абонента...",
  "sectionPath": ["Регламент взаимодействия операторов и инженеров 2 линии", "2. Первичная диагностика"],
  "chunkStrategy": "structure_aware"
}

sectionPath — это ровно тот breadcrumb, который чат показывает рядом с цитатой («Раздел: ... › 2. Первичная диагностика»).

Reranking

После Qdrant-поиска по каждой коллекции собираем кандидатов, режем по minScore, сортируем по cosine, берём топ max(rerankTopK, searchTopK), шлём пачкой в TEI cross-encoder через POST /rerank.

Контракт endpoint'а (TEI 1.7 совместимо с vLLM /v1/rerank и Cohere /rerank):

// REQUEST
{
  "query": "что такое сбой по линии",
  "texts": [
    "...chunk 1 text...",
    "...chunk 2 text...",
    "...chunk N text..."
  ],
  "raw_scores": false,
  "model": "bge-reranker-v2-m3"  // optional, ignored by TEI single-model
}

// RESPONSE (TEI native)
[
  { "index": 0, "score": 0.997 },
  { "index": 2, "score": 0.512 },
  { "index": 1, "score": 0.001 }
]

rerankCandidates (packages/db/src/rag/rerank.ts) сначала пробует /rerank, фолбекит на /v1/rerank если 404. Парсит и TEI-стиль (массив), и Cohere-стиль ({ results: [...] }). Возвращает топ rerankTopK, помеченные reranked: true в payload поиска.

Если реранкер недоступен — возвращаем базовые cosine-hits без ошибки (graceful degrade, лог [knowledge-search] rerank skipped).

Почему это важно

Cosine-similarity на multilingual-embedding'ах часто путает близкие, но семантически разные куски (например, два «регламента»). Cross-encoder читает запрос и кандидата вместе и выдаёт честный score. На наших тестах rerank поднимает топ-1 с ~0.65 cosine до 0.99 score, а нерелевантные куски прибиваются к 0.0001.

Пример:

query: "что такое retrieval augmented generation"

[1] score=0.9997 source="rag-wiki-raw.clean.md" — определение RAG ✓
[2] score=0.9988 source="rag-wiki-raw.clean.md" [Section: Process] ✓
[3] score=0.9616 source="vectordb-wiki.clean.md" — vector DB вступление ✓

Embedding-инфраструктура

TEI #1 — embeddings

docker run -d --name tei-embed --gpus all --restart=unless-stopped \
  -p 127.0.0.1:8001:80 \
  -v /opt/tei-cache:/data \
  ghcr.io/huggingface/text-embeddings-inference:1.7 \
  --model-id BAAI/bge-m3 \
  --port 80 \
  --max-client-batch-size 64 \
  --max-batch-tokens 16384
  • 1024-dim вектор, 8192-токен max input.
  • BAAI/bge-m3 — multilingual (RU + EN + 100+ языков), state-of-the-art для русского.
  • --auto-truncate отключён — длинные чанки 413, поэтому держим chunkSize под 8k токенов с запасом (900 chars * ~3 chars/token ≈ 300 токенов).

TEI #2 — reranker

docker run -d --name tei-rerank --gpus all --restart=unless-stopped \
  -p 127.0.0.1:8004:80 \
  -v /opt/tei-cache:/data \
  ghcr.io/huggingface/text-embeddings-inference:1.7 \
  --model-id BAAI/bge-reranker-v2-m3 \
  --port 80 \
  --max-client-batch-size 32 \
  --max-batch-tokens 8192

bun-runner :4001 (proxy)

/opt/agents-machine/runner.ts — тонкий OpenAI-compat прокси, бьющийся в три апстрима:

EndpointBackendEnv
/v1/chat/completionsLLAMA_URL (localhost:8002)LLAMA_URL
/v1/embeddingsEMBED_URL (localhost:8001)EMBED_URL, EMBED_MODEL
/rerank + /v1/rerankRERANK_URL (localhost:8004)RERANK_URL, RERANK_MODEL
/v1/modelsагрегирует upstream + объявляет embed/rerank

Это даёт API-контейнеру одну точку входа http://100.96.251.102:4001, а DNS / route — обычный tailscale-IP cards-vm.

UI / UX в /memory

Документы вкладки /memory: KPI-карточки, drop-zone, search-фильтр и таблица с превью-кнопкой в строке.

Список документов

  • Search-поле фильтрует таблицу напрямуюq параметр уходит в listKnowledgeDocuments, который делает ilike по name. Без отдельного «Результаты поиска» блока.

    Поиск по имени фильтрует таблицу напрямую: 21 из 221 документа с 'Регламент' в названии.
  • URL-state: /memory?tab=documents&q=<filename> авто-заполняет search input. Используется чатом для перехода по цитированному файлу.

  • Колонки имеют фиксированную ширину через colgroup + table-layout: fixed (изолировано на .docsTable чтобы не ломать другие admin-таблицы).

  • Sidebar — счётчик документов в коллекции (badge) и trash-кнопка не пересекаются: при hover badge fade-out, trash появляется на его месте.

Раскрываемая строка

Раскрытая строка документа: 4 KPI-карточки сверху, метаданные двумя колонками, scrollable storage-key, quick actions.

Клик по строке (вне чекбокса/иконок) или по chevron'у в столбце 2 раскрывает DocumentDetailPanel:

  • 4 KPI-карточки: Размер / Фрагментов + плотность / Время индексации / Статус.
  • Метаданные двумя колонками: id, коллекция, формат, storage key (full-width scrollable code-блок) + timestamps загрузка / индексация / обновлено / latency.
  • Кнопки copy ID / copy storage-key с тостом «скопировано».
  • Error-карточка со stack-style блоком, если статус = error.
  • Quick actions: Просмотр / Скачать оригинал / Открыть в новой вкладке / Переиндексировать / Удалить.

Preview-модалка

Полноэкранная preview-модалка: формат-бэйдж, размер, фрагменты, открыть-в-новой-вкладке, скачать, monospace-рендер md.

Открывается тремя путями:

  1. Клик по имени документа в строке.
  2. Eye-кнопка в actions row.
  3. «Просмотр» в DocumentDetailPanel.

Render-логика по формату:

ФорматКак показываем
md, txt, json, csv<pre> monospace, fetch + cleanup.
pdf<iframe sandbox="allow-scripts allow-same-origin">.
png, jpg, jpeg, gif, webp, svg<img>.
остальныеhint «не предпросмотреть, открой в новой вкладке / скачай».

В тулбаре модалки: format-badge + размер + фрагменты + «Открыть в новой вкладке» + «Скачать оригинал».

Drop-zone сверху

Drop-zone (или кнопка «Загрузить файлы») переехала на самый верх вкладки «Документы» — между KPI-картами и search-баром. Раньше она жила под таблицей и пагинацией.

Инвалидация при переключении коллекций

Кнопка коллекции в сайдбаре дёргает setFilter(collectionId) и в use-memory-workspace-controller.ts эффект на [filter] запускает параллельно refreshCollections() + getKnowledgeStatus(). Без этого sidebar-counters и storage-stats оставались stale до следующего поллинга.

UI / UX в /chat

Агент отвечает по CDATA-XPON ONU из knowledge base: цитирует файл, показывает варианты модели, выводит правила установки. Агент даёт пошаговую диагностику интернета и цитирует файл с разделом 'Регламент взаимодействия операторов и инженеров 2 линии › 2. Первичная диагностика'.

Кликабельные имена файлов в ответах

Inline-код с именем файла стал гипер-ссылкой — клик открывает /memory с предзаполненным поиском.

MessageMarkdown (apps/web/src/entities/chat/ui/message-markdown.tsx) переопределяет рендер inline-<code>: если содержимое матчит [\p{L}\p{N}\p{M}._\-+%() ]+\.(md|pdf|txt|json|csv|xlsx|xls|docx|doc), обоz'ёт в <a target="_blank" href="/memory?tab=documents&q=<filename>">.

// agent ответ:
**Файл:** `Регламент-обработки-жалоб-3299.md`
//                ▲
//          инлайн-код становится ссылкой
//          → открывается /memory?q=Регламент-обработки-жалоб-3299.md
//          → таблица фильтруется ровно на этот документ
//          → клик → preview modal
После клика на имя файла в чате /memory открыл документы с уже заполненным поисковым запросом.

Тестирование качества

Заливаем 4 wiki-статьи как тестовый corpus в коллекцию «AI Wiki — RAG/Embeddings»:

  • rag-wiki-raw.clean.md — Retrieval-augmented generation
  • embedding-wiki.clean.md — Word embedding
  • vectordb-wiki.clean.md — Vector database
  • sent-wiki.clean.md — Sentence embedding

Прогон через KnowledgeService.search (минуя реранкер):

curl -sS -b cookies.txt -X POST https://ag0nts.xyz/_api/knowledge/search \
  -H "Content-Type: application/json" \
  -d '{"query":"what is retrieval augmented generation","collectionId":"...","topK":3}'

После реранка top-1 — ровно intro-параграф rag-wiki-raw.clean.md, top-2 — [Section: Process] оттуда же, top-3 — vector-database intro. Score 0.999 / 0.999 / 0.96. reranked: true в payload.

В чате: spam агента вопросом про CDATA-XPON ONU при подключённой коллекции Игра-Сервис — База знанийsearch_memory вызывается один раз с query="CDATA-XPON ONU", latency 428ms (включая embed + qdrant + rerank), агент отвечает по CDATA-ONU-Optical-Network-Unit-1668.md с правильным определением и section-path.

Что ещё стоит сделать

On this page