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