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 | Что делает |
|---|---|---|
chunkStrategy | structure_aware | Как резать текст (см. ниже). |
chunkSize | 900 | Целевой размер чанка в символах (не токенах). |
chunkOverlap | 120 | Overlap между соседними чанками в символах. |
embeddingBatchSize | 8 | Сколько чанк-текстов отправляем за раз в /v1/embeddings. |
searchTopK | 8 | TopK для Qdrant перед реранком (на коллекцию). |
minScore | 0.2 | Cosine-cutoff: ниже не отдаём в LLM. |
rerankEnabled | true | Включить cross-encoder реранкер. |
rerankTopK | 4 | Сколько hits оставляем после реранка. |
rerankUrl | null | Override URL реранкера (иначе совпадает с embedding baseUrl). |
rerankModel | null | Имя модели для рерanker'а (TEI его игнорирует — у него один модель). |
metadataEnrichment | true | Префиксовать чанк строкой [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 8192bun-runner :4001 (proxy)
/opt/agents-machine/runner.ts — тонкий OpenAI-compat прокси, бьющийся
в три апстрима:
| Endpoint | Backend | Env |
|---|---|---|
/v1/chat/completions | LLAMA_URL (localhost:8002) | LLAMA_URL |
/v1/embeddings | EMBED_URL (localhost:8001) | EMBED_URL, EMBED_MODEL |
/rerank + /v1/rerank | RERANK_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
Список документов
-
Search-поле фильтрует таблицу напрямую —
qпараметр уходит вlistKnowledgeDocuments, который делаетilikeпоname. Без отдельного «Результаты поиска» блока.
-
URL-state:
/memory?tab=documents&q=<filename>авто-заполняет search input. Используется чатом для перехода по цитированному файлу. -
Колонки имеют фиксированную ширину через
colgroup+table-layout: fixed(изолировано на.docsTableчтобы не ломать другие admin-таблицы). -
Sidebar — счётчик документов в коллекции (badge) и trash-кнопка не пересекаются: при hover badge fade-out, trash появляется на его месте.
Раскрываемая строка
Клик по строке (вне чекбокса/иконок) или по 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-модалка
Открывается тремя путями:
- Клик по имени документа в строке.
- Eye-кнопка в actions row.
- «Просмотр» в 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
Кликабельные имена файлов в ответах
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
Тестирование качества
Заливаем 4 wiki-статьи как тестовый corpus в коллекцию «AI Wiki — RAG/Embeddings»:
rag-wiki-raw.clean.md— Retrieval-augmented generationembedding-wiki.clean.md— Word embeddingvectordb-wiki.clean.md— Vector databasesent-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.