# Пошаговый план реализации AI-ассистента документации Дата: 2026-10-07 Кому: субагент DeepSeek Flash (исполнитель механических шагов) Кто отвечает за интеграцию/проверку: ассистент (я) Контекст: см. `BRIEF_docs_ai_assistant.md` (решения), `reviews/` (рецензия + диалог). Рабочий каталог кода: `~/TF/tf_docs` (Node.js, без внешних зависимостей, node >= 18). > КАЖДЫЙ шаг ниже — отдельная порция для субагента Flash. > Flash делает механику, я (ассистент) принимаю/интегрирую/проверяю. > После каждого шага — коммит осмысленным сообщением, push только по команде владельца. --- ## Шаг 0. Блокеры и рекомендации (утвердить у владельца) **Не код. Нужны решения владельца до начала.** - Блокеры: 7.1 (путь `//_api/assist`), 7.2 (политика отправки данных в ЛЛМ), 7.3 (`X-Forwarded-For`/`ASSIST_TRUSTED_PROXY_CIDR`), 7.5 (не снят — уточнить), ротация ключа `api.aillm.ru`. - Рекомендации одним списком: 7.4, 7.6, 7.7, 7.8, 7.9, 7.11. - Приёмник: `llm_project/DECISIONS_v0.md` (фиксируем принятое). --- ## Шаг 1. Live-проверка `api.aillm.ru` **Задача Flash:** написать одноразовый скрипт-пробник (НЕ часть `assist.js`) и вернуть факты. Файл: `llm_project/probe/probe_aillm.js` (запуск вручную, результат — в `llm_project/probe/RESULTS.md`). Проверить и зафиксировать: 1. `response_format: {type: "json_object"}` — поддерживается? (даёт ли валидный JSON) 2. `stream: true` — какой формат чанков (`data:` SSE)? нужен ли нам вообще (v1 — нет). 3. `reasoning_effort: "low"` — принимается? есть ли `reasoning_content` в ответе? 4. Ответ на 429 — какие заголовки (`Retry-After`? `X-RateLimit-*`?), тело. 5. Размер контекста — до скольки токенов принимает запрос (увеличивая промпт, найти предел/ошибку). 6. Формат ошибки при пустом `content` / обрыве. 7. Сколько длится этап 1-типа запрос (засечь 3 образца) — для дедлайна 40 с. **Критерий готово:** `RESULTS.md` содержит таблицу ответов по всем 7 пунктам + пример сырого ответа (JSON) для этапов 1 и 2. **После шага 1:** финализируем `llmChat` (структуру разбора ответа). --- ## Шаг 2. Каркас `assist.js` **Задача Flash:** создать `assist.js` (новый модуль), НЕ трогая `server.js`. Модули по брифу: - `validateInput(req)` — Content-Type `application/json`, Content-Length > 2048 → 413, чтение потока со счётчиком байт (> 2 КБ → 413), разбор JSON, поле `question` (непустая строка). - rate-limit — локальная таблица ключ=IP (или IP/stand), периодическая чистка; лимит делится на число реплик (`ASSIST_REPLICAS`). - семафор `ASSIST_MAX_CONCURRENT` = 4; сверх — 503 + `Retry-After: 5`, тело `{"error":"busy","message":"..."}`. - объединение одинаковых запросов (ключ = `stand` + нормализованный запрос): два счётчика («ожидающие клиенты» / «держащие вызов»); обрыв клиента не отменяет общий вызов, пока есть держащие; отмена ЛЛМ только когда отключились все. - `llmChat(messages, {max_tokens, controller})` — POST на `api.aillm.ru`, `AbortController`, разбор только `content`. - общий дедлайн `ASSIST_TOTAL_DEADLINE_MS` = 40000; повтор этапа 1 только если запас > 15 с; `req.on('close')` → abort. - кэш ответов (ключ = `stand` + нормализованный запрос), TTL. - логирование: длина вопроса, `stand`, время этапов, статус, кэш, токены. Текст запросов — НЕ логировать. **Критерий готово:** модуль экспортирует обработчик, собирается без ошибок (`node -c assist.js`), покрыт беглыми юнит-пробами в `test/` (validateInput, семафор, дедуп-ключ). --- ## Шаг 3. Эталонный набор вопросов **Задача Flash:** создать `llm_project/golden/golden_questions.json` — 20–30 вопросов. Формат каждой записи: ```json {"question": "...", "expected_pages": ["path/на_страницу"], "resource_type": "nubes_..."} ``` Вопросы: по разным ресурсам (`nubes_*`), включая подресурсы (`_user`, `_database`, `_topic`), негативные (вопрос не про ресурс, вопрос с выдуманным ресурсом вроде `nubes_instance`). **Критерий готово:** файл валиден, вопросы реалистичны (взяты из опубликованной документации стенда `nubes-test`). --- ## Шаг 4. Индекс и поиск **Задача Flash:** реализовать загрузку и лексический поиск по `search_index.json`. - Источник: `VM_DOCS_BASE_URL//search/search_index.json` (только этот путь). - Снять формат индекса с реального стенда (см. шаг 7/8 верификации): структура записи, поля `location` (вид `page/#anchor`), `title`, `text` (HTML → удалить). - Лекс. поиск: токенизация, BM25-подобный скор, top-K разделов, группировка по страницам. - Собрать список реальных `nubes_*` из имён страниц (для этапа 1). - Порог «не найдено» = «совпало >= N уникальных ключевых слов», N подобрать по эталону (шаг 3). **Критерий готово:** на эталонном наборе поиск выдаёт ожидаемые страницы с приемлемой точностью (записать метрику). --- ## Шаг 5. Этап 1 (извлечение `resource_type`) **Задача Flash:** реализовать этап 1. - Системный промпт (рус.) со списком реальных `nubes_*` из индекса. - Ответ: `resource_type`; если не в списке — отбрасывается (нет ресурса). - Падение/таймаут этапа 1 — продолжить без него (только токен-поиск). **Критерий готово:** на эталоне корректно извлекает/отбрасывает `resource_type`; выдуманные (`nubes_instance`) — отбрасываются. --- ## Шаг 6. Объединение + этап 2 **Задача Flash:** реализовать объединение (П.8) и этап 2. Объединение: - Поиск по токенам исходного запроса — наравне с поиском после этапа 1. - Страница ресурса — всегда первой в контекст (если `resource_type` в списке). - Кандидаты объединяются, повторы по `location`, оценка = max из двух; контекст до K без повторов; страница ресурса входит в K. - Падение этапа 1 — только токен-поиск, без обязательной страницы. Этап 2: - Промпт со строгим шаблоном «Роль/Задача/Контекст/Требования/Ограничения». - Ответ без заголовков → 502 `bad_llm_output`. - Жёсткий `max_tokens`. - `sources` = `//` + `location`; отбрасывать `//`, `:`, `..`. **Критерий готово:** полный сквозной ответ на эталоне, корректные `sources`. --- ## Шаг 7. Маршрут в `server.js` + Dockerfile + README **Задача Flash (под контролем):** в `server.js` добавить POST-ветку `//_api/assist` **до** проверки 405 (строки 72–75). - `stand` через `STAND_RE`. - `assist.js` грузить в try/catch; ошибки → 500 JSON. - Поведение статики и падение без `VM_DOCS_BASE_URL` — НЕ менять. - Dockerfile: скопировать `assist.js` (+ при необходимости `lib/`). - README: описать эндпоинт и переменные окружения (`ASSIST_*`, ключ). **Критерий готово:** `node -c`, статика работает как раньше, POST отвечает JSON (200/4xx/5xx). --- ## Шаг 8. Виджет (фронтенд) **Задача Flash:** написать виджет (встраиваемый JS/CSS/HTML или шаблон). - `stand` из первого сегмента `location.pathname`; нет сегмента — не показывать. - Плавающая кнопка на всех страницах. - Под полем ввода: «не вводите ключи и пароли». - Обращение к `//_api/assist`. - Обработка 503 (`Retry-After`) — показ «занято, повторите». **Критерий готово:** виджет открывается, шлёт запрос, показывает ответ и `sources`-ссылки. --- ## Шаг 9. Итоговая оценка качества **Задача Flash:** прогнать эталонный набор (шаг 3) через полный пайплайн, собрать метрики. - Точность `resource_type`, точность страниц, корректность `sources`, % 502/ошибок, задержки. **Критерий готово:** отчёт `llm_project/golden/EVAL_*.md` с таблицей. --- ## Шаг 10. Деплой — ТОЛЬКО по команде владельца. Ассистент сам НЕ деплоит. ## Шаг 11. Документация (README финал, переменные, эксплуатация). --- ## Порядок выполнения и зависимости 1. Шаг 0 (утверждение) → блокер, ждать владельца. 2. Шаг 1 и Шаг 2 — параллельно (каркас не зависит от результата шага 1; `llmChat` финализируется после шага 1). 3. Шаг 3 → 4 → 5 → 6 (зависимы). 4. Шаг 7 → 8 (после 6). 5. Шаг 9 (оценка), Шаг 10 (деплой, по команде), Шаг 11 (документация). ## Непроверенные допущения (снять на стенде) - `proxy-read-timeout` nginx-ingress (шаг 1/верификация). - Формат `search_index.json` (шаг 4). - Пропускает ли ingress POST (`curl -X POST` на стенде) — отдельная верификация. - Поддержка `reasoning_effort`/`response_format`/`stream`/429 у `api.aillm.ru` (шаг 1).