From f1a774d6ef12bcdcd172ce2f13b247439b29727d Mon Sep 17 00:00:00 2001 From: Repinoid Date: Wed, 7 Oct 2026 20:33:44 +0300 Subject: [PATCH] =?UTF-8?q?docs:=203=20=D1=81=D1=82=D0=B5=D0=BD=D0=B4?= =?UTF-8?q?=D0=B0=20(test/dev/prod),=20=D0=BF=D1=80=D0=B8=D0=BE=D1=80?= =?UTF-8?q?=D0=B8=D1=82=D0=B5=D1=82=20curated-=D0=BF=D1=80=D0=B8=D0=BC?= =?UTF-8?q?=D0=B5=D1=80=D0=BE=D0=B2,=20=D0=B8=D0=BD=D1=86=D0=B8=D0=B4?= =?UTF-8?q?=D0=B5=D0=BD=D1=82=20=D0=B3=D0=B5=D0=BD=D0=B5=D1=80=D0=B0=D1=82?= =?UTF-8?q?=D0=BE=D1=80=D0=B0=20nav?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- llm_project/docs/ASSISTANT_ARCHITECTURE.md | 46 +++++++- llm_project/tests/debug_bad_llm_output.js | 130 ++++++++++----------- llm_project/tests/redeploy.js | 60 +++++----- 3 files changed, 139 insertions(+), 97 deletions(-) diff --git a/llm_project/docs/ASSISTANT_ARCHITECTURE.md b/llm_project/docs/ASSISTANT_ARCHITECTURE.md index 7434745..7464631 100644 --- a/llm_project/docs/ASSISTANT_ARCHITECTURE.md +++ b/llm_project/docs/ASSISTANT_ARCHITECTURE.md @@ -18,6 +18,22 @@ --- +## 1.1 Стенды + +Ассистент **stand-aware**: `stand` берётся из первого сегмента пути, индекс — +из `VM_DOCS_BASE_URL//search/search_index.json`. Включён на всех трёх стендах: + +| Стенд | URL | Виджет | Эндпоинт | +|---|---|---|---| +| `nubes-test` | `/nubes-test/` | ✅ | ✅ | +| `nubes-dev` | `/nubes-dev/` | ✅ | ✅ | +| `nubes` (prod) | `/nubes/` | ✅ | ✅ | + +Каждый стенд отвечает по **своему** индексу. Кэш индекса/ответов и лимиты +ключуются по стенду. + +--- + ## 2. Архитектура ``` @@ -154,8 +170,11 @@ tf_docs (Node.js, кластер nodejsk8s) [orchestrator.js] отбрасывание стоп-слов (рус. + англ.). - **Транслит** (`TRANSLIT` в `search.js`): «постгрес»→`postgres`, «бакет»→`bucket`, «топик»→`topic` и т.д. -- **Буст по типу страницы**: страница ресурса ×2.0, руководство (`_manual`) ×0.6, - служебная (`curated`/`30_registry`) ×0.4. +- **Буст по типу страницы** (приоритет — «примеры в первую очередь»): + - `curated/*` (готовые примеры, проверенные на реальных деплоях) — **×2.4**; + - страница ресурса — ×2.0; + - руководство (`_manual`) — ×0.6; + - служебная (`30_registry`) — ×0.4. - **Подресурс → родитель**: `nubes_postgres_database` при поиске дополняется токеном `postgres` (`resourceBaseSegment`). - **Предтокенизация**: токены всех 823 документов считаются один раз при загрузке @@ -283,3 +302,26 @@ tf_docs (Node.js, кластер nodejsk8s) [orchestrator.js] 6. Один под (replicas=1) — лимиты/кэш не шарятся между репликами (принято в брифе 7.4). 7. Задержка доминируется LLM; ускоряется стримингом (SSE) или параллелизацией этапа 1 с поиском (v2). + +--- + +## 14. Исправленный инцидент: сборка `nubes-dev` + +При публикации dev-документации сборка падала на YAML-ошибке nav: + +``` +- [DEPRECATED] Публичные IP адреса: ← невалидный YAML +``` + +**Причина:** генератор `TOOLS/docs-generator/internal/writers/writers.go` +(`WriteNavFragment`) писал `ServiceDisplayName` без кавычек. У `vcexternalip` в +dev имя = `[DEPRECATED] Публичные IP адреса` → ломал парсер. + +**Фикс (корень):** в генератор добавлен `yamlScalar()` — заголовки с спецсимволами +(`[ ] : # & *` и др.) кавычатся. Затронул все стенды на будущее. + +**Разовый обход:** для немедленной публикации dev строка в +`generated/dev/docs/_nav_fragment.yml` закавычена вручную (файл генерируемый, +перезапишется корректно после фикса генератора). + +Коммит фикса: `tf_provider` `322235c`. diff --git a/llm_project/tests/debug_bad_llm_output.js b/llm_project/tests/debug_bad_llm_output.js index a2c9947..291ca25 100644 --- a/llm_project/tests/debug_bad_llm_output.js +++ b/llm_project/tests/debug_bad_llm_output.js @@ -19,91 +19,91 @@ const BASE_URL = 'https://api.aillm.ru'; const MODEL = 'gpt-oss-120b'; const QUESTIONS = [ - 'openbao для секретов в кластере', - 'как создать виртуальную машину', - 'как развернуть rabbitmq', - 'nodejs приложение задеплоить', - 'как развернуть gitea', - 'pgadmin для управления базами', + 'openbao для секретов в кластере', + 'как создать виртуальную машину', + 'как развернуть rabbitmq', + 'nodejs приложение задеплоить', + 'как развернуть gitea', + 'pgadmin для управления базами', ]; const REQUIRED_HEADERS = ['Роль', 'Задача', 'Контекст', 'Требования', 'Ограничения']; const STAGE2_SYSTEM_PROMPT = - 'Ты — ассистент документации Terraform-провайдера Nubes. ' + - 'Составь ответ СТРОГО по шаблону с заголовками-маркерами, каждый на новой строке:\n' + - 'Роль:\nЗадача:\nКонтекст:\nТребования:\nОграничения:\n' + - 'Используй ТОЛЬКО факты из блока ДОКУМЕНТАЦИЯ ниже. ' + - 'Не выдумывай ресурсов, атрибутов и значений, которых нет в документации. ' + - 'Отвечай на русском языке.'; + 'Ты — ассистент документации Terraform-провайдера Nubes. ' + + 'Составь ответ СТРОГО по шаблону с заголовками-маркерами, каждый на новой строке:\n' + + 'Роль:\nЗадача:\nКонтекст:\nТребования:\nОграничения:\n' + + 'Используй ТОЛЬКО факты из блока ДОКУМЕНТАЦИЯ ниже. ' + + 'Не выдумывай ресурсов, атрибутов и значений, которых нет в документации. ' + + 'Отвечай на русском языке.'; function findApiKey() { - const lines = fs.readFileSync(KEY_FILE, 'utf8').split(/\r?\n/); - for (const l of lines) { - const t = l.trim(); - if (t.startsWith('sk-')) return t; - } - return null; + const lines = fs.readFileSync(KEY_FILE, 'utf8').split(/\r?\n/); + for (const l of lines) { + const t = l.trim(); + if (t.startsWith('sk-')) return t; + } + return null; } function httpGet(url) { - return new Promise((resolve, reject) => { - const mod = url.startsWith('https') ? https : http; - const req = mod.get(url, (res) => { - if (res.statusCode !== 200) { res.resume(); reject(new Error('status ' + res.statusCode)); return; } - let d = ''; - res.on('data', (c) => { d += c; }); - res.on('end', () => resolve(d)); + return new Promise((resolve, reject) => { + const mod = url.startsWith('https') ? https : http; + const req = mod.get(url, (res) => { + if (res.statusCode !== 200) { res.resume(); reject(new Error('status ' + res.statusCode)); return; } + let d = ''; + res.on('data', (c) => { d += c; }); + res.on('end', () => resolve(d)); + }); + req.on('error', reject); + req.setTimeout(15000, () => req.destroy(new Error('timeout'))); }); - req.on('error', reject); - req.setTimeout(15000, () => req.destroy(new Error('timeout'))); - }); } async function llmRaw(messages, maxTokens, apiKey) { - const resp = await fetch(`${BASE_URL}/v1/chat/completions`, { - method: 'POST', - headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json' }, - body: JSON.stringify({ model: MODEL, messages, max_tokens: maxTokens, temperature: 0.2 }), - signal: AbortSignal.timeout(60000), - }); - const j = await resp.json(); - const msg = j && j.choices && j.choices[0] && j.choices[0].message; - return { content: msg ? msg.content : null, finish: j.choices && j.choices[0] ? j.choices[0].finish_reason : null }; + const resp = await fetch(`${BASE_URL}/v1/chat/completions`, { + method: 'POST', + headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json' }, + body: JSON.stringify({ model: MODEL, messages, max_tokens: maxTokens, temperature: 0.2 }), + signal: AbortSignal.timeout(60000), + }); + const j = await resp.json(); + const msg = j && j.choices && j.choices[0] && j.choices[0].message; + return { content: msg ? msg.content : null, finish: j.choices && j.choices[0] ? j.choices[0].finish_reason : null }; } async function main() { - const { loadIndex } = require('../../search.js'); - const { mergeSearchResults, buildContext } = require('../../stage2.js'); - const { stage1ExtractResourceType } = require('../../stage1.js'); + const { loadIndex } = require('../../search.js'); + const { mergeSearchResults, buildContext } = require('../../stage2.js'); + const { stage1ExtractResourceType } = require('../../stage1.js'); - const apiKey = findApiKey(); - const raw = await httpGet(`${VM}/${STAND}/search/search_index.json`); - const idx = loadIndex(raw); - const types = idx.listResourceTypes(); - console.log('Индекс docs:', idx.docs.length, '| ресурсов:', types.size, '\n'); + const apiKey = findApiKey(); + const raw = await httpGet(`${VM}/${STAND}/search/search_index.json`); + const idx = loadIndex(raw); + const types = idx.listResourceTypes(); + console.log('Индекс docs:', idx.docs.length, '| ресурсов:', types.size, '\n'); - for (const q of QUESTIONS) { - console.log('='.repeat(80)); - console.log('ВОПРОС:', q); - const r1 = await stage1ExtractResourceType(q, { resourceTypesSet: types, baseUrl: BASE_URL, apiKey, model: MODEL }); - const rt = r1.ok ? r1.resourceType : null; - console.log('этап1:', JSON.stringify(r1)); - const merged = mergeSearchResults(idx, q, rt, { topK: 5 }); - console.log('контекст:', merged.map((m) => m.location).join(' , ')); - const ctx = buildContext(merged, 1500); - const docsBlock = ctx.map((c, i) => `[${i + 1}] ${c.title}\n${c.text}`).join('\n\n'); - const userContent = 'ДОКУМЕНТАЦИЯ:\n' + (docsBlock || '(нет релевантных фрагментов)') + '\n\nВОПРОС ПОЛЬЗОВАТЕЛЯ:\n' + q; - const messages = [{ role: 'system', content: STAGE2_SYSTEM_PROMPT }, { role: 'user', content: userContent }]; + for (const q of QUESTIONS) { + console.log('='.repeat(80)); + console.log('ВОПРОС:', q); + const r1 = await stage1ExtractResourceType(q, { resourceTypesSet: types, baseUrl: BASE_URL, apiKey, model: MODEL }); + const rt = r1.ok ? r1.resourceType : null; + console.log('этап1:', JSON.stringify(r1)); + const merged = mergeSearchResults(idx, q, rt, { topK: 5 }); + console.log('контекст:', merged.map((m) => m.location).join(' , ')); + const ctx = buildContext(merged, 1500); + const docsBlock = ctx.map((c, i) => `[${i + 1}] ${c.title}\n${c.text}`).join('\n\n'); + const userContent = 'ДОКУМЕНТАЦИЯ:\n' + (docsBlock || '(нет релевантных фрагментов)') + '\n\nВОПРОС ПОЛЬЗОВАТЕЛЯ:\n' + q; + const messages = [{ role: 'system', content: STAGE2_SYSTEM_PROMPT }, { role: 'user', content: userContent }]; - const out = await llmRaw(messages, 1024, apiKey); - console.log('finish_reason:', out.finish, '| content len:', out.content ? out.content.length : 0); - const missing = out.content ? REQUIRED_HEADERS.filter((h) => !out.content.includes(h + ':')) : REQUIRED_HEADERS; - console.log('НЕТ заголовков:', missing.join(', ') || '(нет, всё ок)'); - console.log('--- СЫРОЙ ОТВЕТ (первые 500) ---'); - console.log((out.content || '(null)').slice(0, 500)); - console.log(''); - } + const out = await llmRaw(messages, 1024, apiKey); + console.log('finish_reason:', out.finish, '| content len:', out.content ? out.content.length : 0); + const missing = out.content ? REQUIRED_HEADERS.filter((h) => !out.content.includes(h + ':')) : REQUIRED_HEADERS; + console.log('НЕТ заголовков:', missing.join(', ') || '(нет, всё ок)'); + console.log('--- СЫРОЙ ОТВЕТ (первые 500) ---'); + console.log((out.content || '(null)').slice(0, 500)); + console.log(''); + } } main().catch((e) => { console.error('FATAL', e); process.exit(1); }); diff --git a/llm_project/tests/redeploy.js b/llm_project/tests/redeploy.js index ae6cea6..1f364f8 100644 --- a/llm_project/tests/redeploy.js +++ b/llm_project/tests/redeploy.js @@ -51,15 +51,15 @@ function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); } async function main() { console.log('Инстанс:', UID, '| операция:', ACTION); -// 1) Состояние инстанса: доступные операции + текущие params. - const st = await api('GET', `/instances/${UID}`); - if (st.status !== 200) { - console.error('Ошибка GET /instances:', st.status, st.text.slice(0, 300)); - process.exit(1); - } - const inst = (st.json && st.json.instance) || {}; - console.log('Статус:', inst.explainedStatus, '| inProgress:', inst.operationIsInProgress, '| pending:', inst.operationIsPending); - const instanceParams = (inst.state && inst.state.params) || {}; + // 1) Состояние инстанса: доступные операции + текущие params. + const st = await api('GET', `/instances/${UID}`); + if (st.status !== 200) { + console.error('Ошибка GET /instances:', st.status, st.text.slice(0, 300)); + process.exit(1); + } + const inst = (st.json && st.json.instance) || {}; + console.log('Статус:', inst.explainedStatus, '| inProgress:', inst.operationIsInProgress, '| pending:', inst.operationIsPending); + const instanceParams = (inst.state && inst.state.params) || {}; const ops = inst.availableOperations || []; const op = ops.find((o) => String(o.operation).toLowerCase() === ACTION.toLowerCase()); @@ -81,28 +81,28 @@ async function main() { } if (!opUid) opUid = uidFromLocation(created.location); if (!opUid) opUid = created.text.replace(/^"|"$/g, '').trim(); -console.log('Создана операция:', created.status, '| opUid:', opUid); - if (!opUid || opUid.length < 8) { - console.error('Не удалось получить opUid. Ответ:', created.text.slice(0, 300)); - process.exit(1); - } - - // 2б) Установить требуемые CFS-параметры (например startupConfiguration) из live-состояния. - const cfs = await api('GET', `/instanceOperations/${opUid}?fields=cfsParams`); - const cfsParams = (cfs.json && cfs.json.instanceOperation && cfs.json.instanceOperation.cfsParams) || []; - for (const p of cfsParams) { - const val = instanceParams[p.svcOperationCfsParam]; - if (val === undefined) { - console.log(' параметр', p.svcOperationCfsParam, '(id=' + p.svcOperationCfsParamId + ') — в состоянии нет, пропуск'); - continue; + console.log('Создана операция:', created.status, '| opUid:', opUid); + if (!opUid || opUid.length < 8) { + console.error('Не удалось получить opUid. Ответ:', created.text.slice(0, 300)); + process.exit(1); } - const paramValue = typeof val === 'string' ? val : JSON.stringify(val); - const pr = await api('POST', '/instanceOperationCfsParams', { - instanceOperationUid: opUid, - svcOperationCfsParamId: p.svcOperationCfsParamId, - paramValue: paramValue, - }); - console.log(' параметр', p.svcOperationCfsParam, '=', paramValue, '->', pr.status); + + // 2б) Установить требуемые CFS-параметры (например startupConfiguration) из live-состояния. + const cfs = await api('GET', `/instanceOperations/${opUid}?fields=cfsParams`); + const cfsParams = (cfs.json && cfs.json.instanceOperation && cfs.json.instanceOperation.cfsParams) || []; + for (const p of cfsParams) { + const val = instanceParams[p.svcOperationCfsParam]; + if (val === undefined) { + console.log(' параметр', p.svcOperationCfsParam, '(id=' + p.svcOperationCfsParamId + ') — в состоянии нет, пропуск'); + continue; + } + const paramValue = typeof val === 'string' ? val : JSON.stringify(val); + const pr = await api('POST', '/instanceOperationCfsParams', { + instanceOperationUid: opUid, + svcOperationCfsParamId: p.svcOperationCfsParamId, + paramValue: paramValue, + }); + console.log(' параметр', p.svcOperationCfsParam, '=', paramValue, '->', pr.status); } // 3) Запустить операцию.