fix(stage2): устранена обрезка ответа (max_tokens 2048 + повтор) и проверка заголовков для кириллицы
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
'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); });
|
||||
Reference in New Issue
Block a user