110 lines
5.0 KiB
JavaScript
110 lines
5.0 KiB
JavaScript
'use strict';
|
|
|
|
/**
|
|
* debug_bad_llm_output.js — воспроизводит этап 2 локально для вопросов,
|
|
* на которых прод вернул 502 bad_llm_output, и печатает СЫРОЙ ответ LLM.
|
|
*
|
|
* Использует тот же промпт и тот же индекс (живой), что и прод.
|
|
* Запуск: node llm_project/tests/debug_bad_llm_output.js
|
|
*/
|
|
|
|
const fs = require('fs');
|
|
const https = require('https');
|
|
const http = require('http');
|
|
|
|
const STAND = 'nubes-test';
|
|
const VM = 'http://5.172.178.213';
|
|
const KEY_FILE = '/home/naeel/nubes/llm.key';
|
|
const BASE_URL = 'https://api.aillm.ru';
|
|
const MODEL = 'gpt-oss-120b';
|
|
|
|
const QUESTIONS = [
|
|
'openbao для секретов в кластере',
|
|
'как создать виртуальную машину',
|
|
'как развернуть rabbitmq',
|
|
'nodejs приложение задеплоить',
|
|
'как развернуть gitea',
|
|
'pgadmin для управления базами',
|
|
];
|
|
|
|
const REQUIRED_HEADERS = ['Роль', 'Задача', 'Контекст', 'Требования', 'Ограничения'];
|
|
|
|
const STAGE2_SYSTEM_PROMPT =
|
|
'Ты — ассистент документации 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;
|
|
}
|
|
|
|
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));
|
|
});
|
|
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 };
|
|
}
|
|
|
|
async function main() {
|
|
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');
|
|
|
|
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('');
|
|
}
|
|
}
|
|
|
|
main().catch((e) => { console.error('FATAL', e); process.exit(1); });
|