Как AI CareerBot сохранял контекст диалога с GigaChat
Разбираем один кусок интеграции GigaChat API в HR-лендинг: как собирали историю диалога, чтобы follow-up по резюме не начинался с нуля.

«Он помнит, что уже советовал по резюме, или снова начинает с нуля?»
В AI CareerBot мы подключали GigaChat API к продуктовому HR-лендингу для соискателей. Сценарии были прикладные: пользователь приходит за советами по резюме, уточняет формулировки, потом переключается на подготовку к собеседованию и задаёт follow-up по предыдущему ответу.
На первых днях запуска через лендинг прошли тысячи пользователей, и слабое место стало видно быстро: если чат не удерживает контекст, он превращается в форму «задал вопрос — получил абзац». Для резюме это плохо. Пользователь спрашивает: «А если этот опыт описать короче?» — и ожидает, что AI помнит, о каком опыте шла речь.
Почему нельзя было отправлять весь диалог
У GigaChat есть ограниченное контекстное окно. Полный диалог растёт быстро: фрагменты резюме, ответы ассистента, уточнения, подготовка к вопросам на интервью. Если просто добавлять в запрос все сообщения подряд, рано или поздно важные куски перестают помещаться.
Самый спорный момент был не про анимации на лендинге, а про границу памяти: фронту нужен был непрерывный диалог, бекенду — предсказуемый размер запроса. В итоге историю стали хранить на стороне сервиса и перед вызовом GigaChat собирать короткий контекст: стабильная инструкция, компактная память диалога и последние реплики.
Ключевой кусок: сборка messages перед GigaChat
Упрощённо это выглядело так:
const SYSTEM_PROMPT = `
Ты AI-помощник для соискателя.
Помогай улучшать резюме и готовиться к собеседованиям.
Не выдумывай опыт за пользователя, сначала уточняй недостающие детали.
`;
export async function askCareerBot(sessionId: string, userText: string) {
const dialog = await dialogStore.load(sessionId);
const messages = [
{ role: 'system', content: SYSTEM_PROMPT },
...(dialog.summary
? [{ role: 'system', content: `Краткая память диалога:\n${dialog.summary}` }]
: []),
...dialog.messages.slice(-config.recentMessagesLimit),
{ role: 'user', content: userText.trim() },
];
const answer = await gigaChat.chat({ messages });
await dialogStore.append(sessionId,
{ role: 'user', content: userText },
{ role: 'assistant', content: answer.content },
);
await dialogStore.refreshSummary(sessionId);
return answer.content;
}
Здесь важны три слоя.
Первый — SYSTEM_PROMPT. Он задаёт рамку продукта: советы по резюме и подготовка к интервью. Отдельно фиксируем, что ассистент не должен придумывать опыт за пользователя. Для HR-сценария это критично: красиво сформулированная неправда в резюме пользователю не помогает.
Второй — dialog.summary. Это не полный лог, а короткая память: что пользователь уже рассказал, какие правки обсуждали, к какому типу вакансии готовится. Она нужна для вопросов вроде «сделай этот пункт увереннее» или «какие вопросы могут задать по этому проекту».
Третий — последние сообщения. Они сохраняют локальный контекст: местоимения, тон, конкретную формулировку, к которой пользователь вернулся. Без этого модель видит только сухую выжимку и может отвечать слишком общо.
Почему историю хранили не во фронте
Лендинг был анимированный: чат должен быстро реагировать, показывать состояние ожидания, вовлекать пользователя в диалог. Но память диалога нельзя было привязывать только к состоянию интерфейса. Обновление страницы, повторный заход или сбой на клиенте не должны обнулять консультацию.
Поэтому фронт отвечал за сценарий общения и анимацию, а сервер — за историю, сборку контекста и вызов GigaChat. Так проще контролировать размер messages и менять правила памяти без релиза клиентской части.
Ещё один практический плюс: можно отдельно смотреть, какие follow-up ломаются. Если пользователь уточняет «а этот абзац сделай короче», а в запрос к GigaChat не попал исходный абзац, проблема не в модели. Проблема в том, как мы собрали контекст.
Что проверяли на реальных диалогах
После запуска смотрели не на абстрактное «качество AI», а на конкретные переходы внутри диалога:
- пользователь прислал резюме и попросил переписать один пункт;
- потом попросил сделать формулировку короче;
- затем переключился на вопросы для собеседования по тому же опыту;
- после этого вернулся к резюме и попросил финальную версию.
Для такого сценария сохранённая история важнее красивого первого ответа. Первый ответ можно получить и без памяти. Нормальный follow-up появляется только тогда, когда в запросе к GigaChat есть то, к чему пользователь отсылает словами «это», «тот проект» и «предыдущий вариант».
В код-ревью таких интеграций теперь первым смотрим на сборку messages: что попадает в контекст, что сжимается в summary и где проходит граница последних реплик.