fix(stage2): устранена обрезка ответа (max_tokens 2048 + повтор) и проверка заголовков для кириллицы

This commit is contained in:
Repinoid
2026-10-07 15:43:23 +03:00
parent 3ae8089cb5
commit b99344eed5
7 changed files with 2878 additions and 22 deletions
+43 -21
View File
@@ -198,8 +198,23 @@ const STAGE2_SYSTEM_PROMPT =
'Роль:\nЗадача:\nКонтекст:\nТребования:\nОграничения:\n' +
'Используй ТОЛЬКО факты из блока ДОКУМЕНТАЦИЯ ниже. ' +
'Не выдумывай ресурсов, атрибутов и значений, которых нет в документации. ' +
'Пиши сжато, без повторов и лишних примеров. ' +
'Отвечай на русском языке.';
/**
* Проверяет наличие заголовка-маркера в ответе: устойчиво к markdown (#, **),
* регистру и пробелам. Заголовок ищется в начале строки.
*
* @param {string} text
* @param {string} header
* @returns {boolean}
*/
function hasHeader(text, header) {
const esc = header.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const re = new RegExp('(^|\\n)\\s*(?:#{1,6}\\s*)?(?:\\*\\*|\\*)?\\s*' + esc + '\\s*(?:\\*\\*|\\*)?\\s*:', 'i');
return re.test(text);
}
/**
* Этап 2: генерация итогового ответа по строгому шаблону.
*
@@ -219,7 +234,7 @@ async function stage2Generate({
baseUrl = process.env.LLM_BASE_URL || '',
apiKey = process.env.LLM_API_KEY || '',
model = process.env.LLM_MODEL || DEFAULT_MODEL,
maxTokens = 1024,
maxTokens = 2048,
controller,
}) {
const docsBlock = context
@@ -237,28 +252,35 @@ async function stage2Generate({
{ role: 'user', content: userContent },
];
let content;
try {
content = await llmChat({
messages,
maxTokens,
temperature: 0.2,
controller,
baseUrl,
apiKey,
model,
});
} catch (err) {
return { ok: false, error: err && err.code ? err.code : 'stage2_failed' };
// До двух попыток: при обрезке ответа (нет заголовков) повторяем с бо́льшим бюджетом.
let budget = maxTokens;
for (let attempt = 0; attempt < 2; attempt++) {
let content;
try {
content = await llmChat({
messages,
maxTokens: budget,
temperature: 0.2,
controller,
baseUrl,
apiKey,
model,
});
} catch (err) {
return { ok: false, error: err && err.code ? err.code : 'stage2_failed' };
}
// Проверяем наличие обязательных заголовков (П.12).
const missing = REQUIRED_HEADERS.filter((h) => !hasHeader(content, h));
if (missing.length === 0) {
return { ok: true, answer: content };
}
// Не все заголовки на месте — вероятна обрезка по max_tokens. Повтор с x2.
budget = Math.min(budget * 2, 4096);
}
// Проверяем наличие обязательных заголовков (П.12).
const missing = REQUIRED_HEADERS.filter((h) => !content.includes(h + ':'));
if (missing.length > 0) {
return { ok: false, error: 'bad_llm_output' };
}
return { ok: true, answer: content };
return { ok: false, error: 'bad_llm_output' };
}
module.exports = {