Skip to content

AI Router

Назначение

Центральный сервис работы с LLM в платформе Unishift. Маршрутизирует запросы к нескольким провайдерам (OpenAI, Anthropic, Google, Ollama и OpenAI-compatible эндпоинты вроде Yandex AI Studio), управляет чатами и knowledge base (RAG) на основе hybrid-поиска: PostgreSQL FTS + pgvector, слияние через Reciprocal Rank Fusion (RRF), опциональный LLM-rerank и переформулировка запросов.

Порты

  • HTTP: 8101
  • БД: ai_router_db

Ключевые возможности

  • Мультипровайдерность: OpenAI, Anthropic, Google, Ollama за единым API (Yandex AI Studio / Alice AI — через vendor openai + custom base_url)
  • Capabilities провайдера: chat, embed, planner, diagram, fill
  • Sidecar-провайдеры llama-planner / llama-diagram / llama-fill — upsert при старте из LLAMA_PLANNER_URL / LLAMA_DIAGRAM_URL / LLAMA_FILL_URL
  • Чаты с историей сообщений, стримингом ответа и reasoning (thinking)
  • Function calling — вызов инструментов из чата (tool calls)
  • Diagram-агент: инструменты draw.io / BPMN (create_drawio, update_drawio, …)
  • RAG / Knowledge base:
    • ingest с очисткой текста, чанкованием и эмбеддингами
    • hybrid retrieval (FTS + vector) + RRF
    • score threshold, LLM-rerank, query rewriting, multi-hop
    • цитирование источников и anti-hallucination system prompt
    • space-scoped ACL: scope space-{uuid} привязан к workspace; поиск фильтруется по spaces, доступным текущему пользователю
    • автосинхронизация заметок (documents → RAG) через NATS
  • CRUD провайдеров для администраторов (модели, API-ключи, base URL)
  • Учёт вызовов: сохранение истории, токенов, длительности и статистики по пользователю
  • Двойная аутентификация: JWT (пользователи) и Service Token (сервисы)
  • Ролевая модель: управление провайдерами требует роль admin
  • Публикация NATS-событий: стриминг чанков, статусы обработки, tool-call'ы
  • Swagger UI, health checks

RAG / Knowledge base

Пайплайн

Ingest                         Retrieval                         Generation
───────                        ─────────                         ──────────
clean text ──► chunk ──►       rewrite query ──►                 system prompt
embed ──► store                FTS + vector ──► RRF ──►          (anti-hallucination
(pgvector + tsvector)          [rerank] ──► [multi-hop]          + cite sources)
  1. Ingest — документ чистится (HTML/шум), режется на чанки с overlap, для каждого чанка считаются эмбеддинги и заполняется content_tsv (FTS).
  2. Retrieval — сырой вопрос опционально переписывается LLM; параллельно ищутся кандидаты по FTS и по cosine similarity; списки сливаются RRF; при включённом rerank LLM оставляет top-N; при слабом recall — второй hop.
  3. Generation — в чат с привязанными scopes подмешивается жёсткий system prompt («отвечай только по документам», «цитируй Source»), tool search_knowledge_base возвращает чанки в формате с заголовком источника.

Ingest

POST /api/v1/rag/documents

ПолеТипОбязательноеОписание
scopestringдаОбласть знаний (space-{uuid} или legacy id)
titlestringдаЗаголовок документа (попадает в метаданные чанков)
contentstringдаТекст / markdown / HTML
content_typestringнетtext (default), markdown или html
source_idstringнетВнешний id (UUID заметки) — upsert по (scope, source_id)
space_iduuidнетWorkspace id (должен совпадать с space-* scope)
metadataobjectнетДоп. поля чанков: category, author, date, …

При ingest:

  • upsert: если задан source_id — по (scope, source_id); иначе по (scope, title) (legacy). В ответе updated: true при обновлении. document_count увеличивается только при создании.
  • для space-* scopes требуется доступ пользователя к соответствующему space (через spaces API + SERVICE_TOKEN)
  • HTML/script/style вырезаются, whitespace нормализуется
  • markdown режется по заголовкам #; html парсится в markdown-ish (заголовки/списки) и тоже режется по заголовкам; text — по абзацам. Крупные секции дополнительно режутся по RAG_CHUNK_MAX_SIZE с overlap.
  • в metadata чанка всегда пишутся title, content_type, scope, ingested_at
  • FTS-индекс (content_tsv) обновляется триггером БД
  • векторы пишутся в document_chunk_embeddings_{dim} (не в колонку чанка), dim берётся из embed-провайдера, привязанного к scope

Embeddings (multi-dim + per-scope)

Разные модели эмбеддингов несовместимы. Поддерживаемые размерности: 256 / 768 / 1536 — отдельные таблицы document_chunk_embeddings_{256|768|1536} + HNSW.

СущностьПоля
Embed-провайдер (type=embed)embedding_dim, default_model (doc), query_model (search), is_default (пресет для новых scopes)
Scopeembedding_provider_id, embedding_dim (immutable после create)

Правила:

  • при создании scope выбирается embed (или берётся is_default);
  • embedding_dim копируется с провайдера и не меняется;
  • сменить провайдера на scope можно только с той же dim → авто-reindex;
  • ingest использует default_model; search — query_model || default_model;
  • поиск по нескольким scopes группирует их по провайдеру и сливает через RRF с FTS.

POST /rag/scopes/:scope/reindex — пересчитать векторы текущим провайдером scope.

Yandex embeddings

Отдельного vendor нет: vendor=openai. base_url можно оставить OpenAI-совместимым (https://ai.api.cloud.yandex.net/v1) — для эмбеддингов ai-router всё равно ходит в нативный https://llm.api.cloud.yandex.net/foundationModels/v1/textEmbedding (modelUri + text, заголовок Api-Key для ключей AQVN…).

ПолеЗначение
embedding_dim256
default_modelemb://<folder_id>/text-search-doc/latest
query_modelemb://<folder_id>/text-search-query/latest

Preview /models у Yandex часто отдаёт 404 — UI позволяет ввести URI вручную. Без query_model поиск деградирует (query и documents в разных пространствах Yandex).

Проверьте, что скоуп (например tt) привязан именно к этому embed-провайдеру, а не к чатовому. Ingest берёт провайдера из настроек скоупа.

Space scopes и ACL

Конвенция имени: space-{spaceUUID}.

  • GET /rag/scopes возвращает:
    • все space-* для spaces, доступных пользователю (owner или shared);
    • админам — все существующие scopes (общие и space);
    • общие (не space-*) scopes, пошаренные пользователю (shared_user_ids);
    • legacy scopes, в которых у пользователя есть собственные документы.
  • Space scopes auto-provision при list (alias = имя space).
  • Общую базу можно открыть выбранным пользователям через shared_user_ids на PUT /rag/scopes/:scope. Доступ к space-базам по-прежнему через ACL пространства.
  • SearchKnowledgeBase / tool search_knowledge_base принимают user_id на бэкенде и отбрасывают недоступные space scopes. Пошаренные общие scopes ищутся целиком (без фильтра owner_id). Legacy scopes без шаринга — только среди документов с owner_id = user.
  • AI-чат в Notes подставляет defaultScopes = ["space-{currentSpaceId}"].

Синхронизация заметок

Сервис documents публикует NATS-события:

SubjectКогда
documents.createdсоздание / duplicate
documents.updatedpatch
documents.deletedsoft/hard delete

ai-router подписан и делает trusted ingest / delete по (scope=space-{spaceID}, source_id=documentID).

Retrieval

Поиск вызывается tool'ом search_knowledge_base (чат с scopes) или через usecase SearchKnowledgeBase(userID, …).

ШагОписаниеКонфиг
ACL filterпересечение запрошенных scopes с доступными spaces / шарингом / admin
Query rewriteLLM исправляет опечатки / раскрывает аббревиатурыRAG_QUERY_REWRITE_ENABLED
FTSwebsearch_to_tsquery('simple') + ts_rank_cd
Vectorcosine distance (<=>), отсев по similarityRAG_VECTOR_MIN_SCORE
RRFscore = Σ 1/(k + rank)RAG_RRF_CONSTANT, RAG_CANDIDATE_LIMIT
RerankLLM listwise ranking top-кандидатов → top-NRAG_RERANK_ENABLED
Multi-hopповторный поиск с другим rewrite при слабом recallRAG_MULTI_HOP_ENABLED

Формат результата для LLM:

Source: «FAQ» [category=support, author=team]
текст чанка…

Scopes

МетодПутьОписание
GET/rag/scopesСписок scopes, доступных текущему пользователю (админ — все)
GET/rag/scopes/:scopeНастройки одного scope (403 без доступа)
PUT/rag/scopes/:scopeUpsert alias / description / system_prompt / default_origin / embedding_provider_id / shared_user_ids
POST/rag/scopes/:scope/reindexПересчитать embeddings scope
DELETE/rag/scopes/:scopeУдалить документы и чанки scope
DELETE/rag/scopes/:scope/settingsУдалить только настройки scope

system_prompt scope дополняет дефолтный anti-hallucination prompt, а не заменяет его.

Конфигурация

ПеременнаяПо умолчаниюОписание
PORT8101Порт HTTP-сервера
DB_*PostgreSQL (DB_NAME=ai_router_db)
JWT_SECRETВалидация JWT
SERVICE_TOKENМежсервисный токен (ACL через spaces, sync)
AI_DEFAULT_PROVIDERopenaiПровайдер по умолчанию
AI_REQUEST_TIMEOUT120sTimeout запросов к LLM
AI_MAX_RETRIES3Число повторов при ошибке
RAG_CANDIDATE_LIMIT20Сколько кандидатов брать из FTS и из vector до RRF
RAG_DEFAULT_LIMIT5Лимит результатов по умолчанию
RAG_VECTOR_MIN_SCORE0.2Мин. cosine similarity (0 = без порога)
RAG_RRF_CONSTANT60Константа k в формуле RRF
RAG_RERANK_ENABLEDtrueLLM-rerank кандидатов после RRF
RAG_QUERY_REWRITE_ENABLEDtrueПереформулировка запроса перед поиском
RAG_MULTI_HOP_ENABLEDtrueПовторный поиск при слабом recall
RAG_CHUNK_MAX_SIZE2000Размер чанка при ingest (символы)
RAG_CHUNK_OVERLAP200Overlap чанков (символы)
MESSAGING_URLnats://localhost:4222NATS URL
SERVER_WRITE_TIMEOUT300sWrite timeout (длинный под стриминг)

Для снижения latency на retrieval можно отключить тяжёлые шаги:

bash
RAG_RERANK_ENABLED=false
RAG_QUERY_REWRITE_ENABLED=false
RAG_MULTI_HOP_ENABLED=false

API (кратко)

RAG (/api/v1/rag)

МетодПутьОписание
POST/rag/documentsIngest документа
GET/rag/scopesСписок scopes
GET/rag/scopes/:scopeНастройки scope
PUT/rag/scopes/:scopeUpsert настроек scope
POST/rag/scopes/:scope/reindexReindex embeddings scope
DELETE/rag/scopes/:scopeУдалить документы scope
DELETE/rag/scopes/:scope/settingsУдалить настройки scope

Полный список эндпоинтов (провайдеры, чаты, completions) — в Swagger UI: http://localhost:8101/swagger/index.html.

Function calling / Tools

Инструменты регистрируются в internal/infrastructure/tools и подключаются через ToolRegistry в container. Клиент передаёт имена в tools[] на send message; при наличии scopes автоматически добавляется search_knowledge_base.

ToolSide effectsНазначение
run_commandcommand-manager: create jobЗапуск команды по имени/UUID
search_commandsread-onlyПоиск команд перед запуском
get_job_status / get_job_logs / stop_jobread / command-managerСтатус, логи, стоп job
run_flowflow-manager: test pipelineЗапуск/тест процесса
write_starlark_code / test_starlark_codenone / TryCodeNodeScaffold и прогон Starlark
create_task / update_task / search_tasks / get_task / change_task_statustask-trackerЗадачи в space
create_note / update_note / search_notes / get_notedocumentsЗаметки
create_drawio / update_drawio / read_drawiodocuments (draw.io)Диаграммы; capability diagram
create_bpmn / update_bpmn / read_bpmndocuments (BPMN)Процессные схемы
search_media / get_media_filespark-gridМедиатека пространства
search_tables / get_table / create_table / *_table_rowsdata-tablesТаблицы в space
search_env / create_env / update_envenv-managerСекреты и конфиги
web_search / fetch_urlSearXNG / HTTPПоиск в вебе и загрузка URL
update_command_draft / update_note_draft / update_flow_draft / update_skill_draftnone (UI patch)Черновики в открытый редактор
search_knowledge_baseRAG readПоиск по scopes (авто при scopes)
ask_userUI clarifyУточняющий вопрос пользователю

UI-patch tools (update_*_draft) возвращают JSON; фронт слушает NATS ai.chat.tool.call / ai.chat.tool.result и применяет поля в открытый редактор.

Sidecar GGUF (ush platform enable llama-planner|llama-diagram|llama-fill) поднимает OpenAI-compatible /v1; ai-router при старте пишет провайдеров unishift-llama-planner / -diagram / -fill (capability = тип sidecar). Подробнее: Локальный LLM.

Starlark

См. starlark-api.md — модуль up.ai_router (провайдеры, чаты, ingest_document с metadata, scopes).