ИИ-поддержка клиентов: Как собрать RAG-бота с бесшовной передачей менеджеру

Внедрение больших языковых моделей (LLM) вроде ChatGPT для ответов на вопросы клиентов — один из самых популярных способов снижения нагрузки на техподдержку. Однако стандартные ИИ-модели имеют два критических недостатка для корпоративного использования: у них нет доступа к вашим закрытым данным (инструкции, цены, API) и они склонны придумывать недостоверные факты («галлюцинировать»).
Для решения этих проблем в продакшене используется архитектура RAG (Retrieval-Augmented Generation). Сначала бот ищет ответ в локальной базе знаний компании, извлекает релевантные текстовые фрагменты и передает их в качестве контекста в LLM. Модель формулирует ответ строго на основе предоставленных документов.
Для обеспечения премиального клиентского сервиса система должна содержать протокол эскалации на оператора. Если ИИ не находит ответа в базе, диалог мгновенно переключается на человека.
В этой статье мы соберем полноценный RAG-цикл на Node.js, настроим поиск в PostgreSQL с расширением pgvector и опишем логику переключения диалога.
Архитектура работы RAG-бота поддержки
[Пользователь спрашивает: "Как настроить Stripe?"]
│
v
(Генерация векторного эмбеддинга)
[text-embedding-3-small] (1536 dim)
│
v
(Векторный поиск в PostgreSQL)
SELECT * FROM match_kb_docs(vector, threshold = 0.75)
│
+-----------------------+
| |
(Контекст НАЙДЕН) (Контекст НЕ НАЙДЕН / Низкий скор)
│ │
v v
[Формируем промпт с текстом KB] [Ставим сессии статус human_handling]
│ │
v v
[Отправляем запрос в gpt-4o-mini] [Оповещаем менеджеров в Slack/CRM]
│ │
v v
[Отправляем ИИ-ответ клиенту] [Отправляем: "Перевожу на менеджера"]
1. Настройка базы данных Pgvector
Включите расширение vector в вашей БД PostgreSQL и создайте таблицу для хранения фрагментов документации. Количество измерений (dimensions) вектора должно совпадать с выбранной моделью эмбеддингов (1536 для OpenAI text-embedding-3-small).
-- Включаем поддержку векторного расширения
CREATE EXTENSION IF NOT EXISTS vector;
-- Создаем таблицу Базы Знаний
CREATE TABLE kb_documents (
id SERIAL PRIMARY KEY,
content TEXT NOT NULL,
embedding VECTOR(1536) NOT NULL,
category VARCHAR(50) DEFAULT 'general',
created_at TIMESTAMP DEFAULT NOW()
);
-- Индексируем векторную колонку (используем HNSW для высокой скорости на масштабе)
CREATE INDEX kb_hnsw_idx ON kb_documents USING hnsw (embedding vector_cosine_ops);
Создадим SQL-функцию поиска документов по косинусному расстоянию:
CREATE OR REPLACE FUNCTION match_kb_docs(
query_embedding VECTOR(1536),
match_threshold FLOAT,
match_count INT
)
RETURNS TABLE (id INT, content TEXT, similarity FLOAT)
LANGUAGE plpgsql AS $$
BEGIN
RETURN QUERY
SELECT kb.id, kb.content, 1 - (kb.embedding <=> query_embedding) AS similarity
FROM kb_documents kb
WHERE 1 - (kb.embedding <=> query_embedding) > match_threshold
ORDER BY kb.embedding <=> query_embedding
LIMIT match_count;
END;
$$;
2. Реализация векторного поиска на Node.js
Напишем сервис, который будет обращаться к API OpenAI для генерации вектора из вопроса пользователя и осуществлять поиск по базе PostgreSQL:
import { OpenAI } from 'openai';
import pg from 'pg';
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL });
export async function getEmbedding(text: string): Promise<number[]> {
const response = await openai.embeddings.create({
model: 'text-embedding-3-small',
input: text.replace(/\n/g, ' '),
});
return response.data[0].embedding;
}
export async function findRelevantContext(queryText: string): Promise<string> {
const queryVector = await getEmbedding(queryText);
const sql = `SELECT * FROM match_kb_docs($1::vector, 0.75, 3)`;
const client = await pool.connect();
try {
const result = await client.query(sql, [JSON.stringify(queryVector)]);
if (result.rows.length === 0) {
return '';
}
// Объединяем найденные статьи в один текстовый блок
return result.rows.map(row => row.content).join('\n---\n');
} finally {
client.release();
}
}
3. Системный промпт против галлюцинаций ИИ
Чтобы модель не придумывала несуществующие тарифы и правила, промпт должен жестко запретить генерацию ответов при отсутствии информации в контексте:
export async function generateAIResponse(userQuestion: string, context: string): Promise<string> {
if (!context) {
return 'ERROR_UNRESOLVED';
}
const systemPrompt = `
Вы — оператор техподдержки TeleGo.io.
Отвечайте на вопросы пользователя, используя ТОЛЬКО предоставленный контекст.
Правила:
1. Опирайтесь исключительно на предоставленные факты. Не придумывайте ничего от себя.
2. Если в контексте нет ответа на вопрос или он не связан с темой, ответьте строго одним словом: "ERROR_UNRESOLVED".
3. Пишите вежливо, в деловом стиле.
Контекст:
---
${context}
---
`;
const response = await openai.chat.completions.create({
model: 'gpt-4o-mini',
messages: [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: userQuestion }
],
temperature: 0.0, // Нулевая температура гарантирует стабильный результат
});
return response.choices[0].message.content || 'ERROR_UNRESOLVED';
}
4. Логика маршрутизации и передача человеку
При получении сообщения в Telegram проверяем, не общается ли пользователь с живым оператором. Если нет — запускаем RAG-цикл. Если ИИ вернул ERROR_UNRESOLVED — переключаем статус чата.
import TelegramBot from 'node-telegram-bot-api';
const bot = new TelegramBot(process.env.TELEGRAM_BOT_TOKEN!, { polling: true });
bot.on('message', async (msg) => {
const chatId = msg.chat.id;
const userText = msg.text;
if (!userText) return;
// 1. Получаем статус сессии из БД
const session = await db.getChatSession(chatId);
// Если чат переключен на человека, пересылаем сообщения в Slack/панель менеджеров
if (session?.status === 'human_handling') {
await forwardToManagers(chatId, msg);
return;
}
// 2. Выполняем RAG-поиск
try {
const context = await findRelevantContext(userText);
const aiResponse = await generateAIResponse(userText, context);
// 3. Проверка на эскалацию
if (aiResponse.trim() === 'ERROR_UNRESOLVED') {
// Обновляем статус сессии в базе данных
await db.updateChatSession(chatId, { status: 'human_handling' });
// Оповещаем менеджеров через вебхук
await notifySupportStaff(chatId, userText);
await bot.sendMessage(
chatId,
"К сожалению, я не нашел ответ в документации. Я переключил этот чат на службу поддержки. Специалист ответит вам в ближайшее время."
);
return;
}
// 4. Отправляем ИИ-ответ пользователю
await bot.sendMessage(chatId, aiResponse);
} catch (error) {
console.error('Ошибка в цикле RAG:', error);
await bot.sendMessage(chatId, "Произошла ошибка. Перевожу диалог на оператора.");
await db.updateChatSession(chatId, { status: 'human_handling' });
}
});
В моем проекте TeleGo.io мы создали аналогичную RAG-систему для обработки частых вопросов пользователей по настройкам ботов и платежам. Если пользователь задает сложный технический вопрос, на который нет готового ответа, сессия автоматически переводится на панель оператора.
Этот процесс эскалации становится еще эффективнее, если связать его с Интеграцией Telegram-ботов с CRM для автоматического логирования событий. А саму панель общения оператора с клиентами можно удобно разместить прямо внутри Telegram Web App.
Источники и документация
- OpenAI Embeddings — генерация векторов, модель text-embedding-3-small из примера
- pgvector — расширение PostgreSQL для векторного поиска
- Telegram Bot API — приём сообщений и long polling
Внедрение RAG-архитектуры с автоматическим переключением на оператора кратно снижает затраты на поддержку: типовые вопросы закрывает ИИ, а люди подключаются только к сложным диалогам — без потери качества сервиса.
Если вы хотите разработать умного ИИ-бота для техподдержки, настроить векторный поиск Pgvector или интегрировать чаты с AmoCRM/Bitrix24, ознакомьтесь с моей услугой Создания Telegram-ботов под ключ или запишитесь на Консультацию.
Частые вопросы
Чем RAG-бот отличается от «просто ChatGPT в боте»?
RAG-бот отвечает строго по вашей базе знаний: сначала находит релевантные фрагменты документации через векторный поиск, затем передаёт их модели как контекст. Это устраняет главные проблемы «голой» LLM — галлюцинации и незнание ваших цен, инструкций и API.
Что происходит, если бот не знает ответа?
Срабатывает протокол эскалации: если векторный поиск не нашёл релевантного контекста или модель вернула маркер неуверенности, сессия помечается как human_handling, менеджеры получают уведомление, а все следующие сообщения клиента пересылаются живому оператору.
Сколько стоит эксплуатация такого бота?
Две статьи расходов: генерация эмбеддингов при индексации базы знаний (разовая и копеечная) и вызовы LLM на каждый вопрос — оплата по фактическому использованию API. На типовом потоке вопросов это заметно дешевле ставки оператора поддержки.