AI-ассистент для технической поддержки сайта

Техническая поддержка — это боль любого растущего проекта. Пользователи задают одни и те же вопросы, тикеты копятся, а время специалистов уходит на рутину. AI-ассистент решает эту проблему: он отвечает мгновенно, круглосуточно и не устаёт. Но чтобы он был полезен, а не раздражал, его нужно правильно построить.

В этой статье мы подробно разберём:

  • Зачем нужен AI-ассистент и какие задачи он решает.
  • Архитектуру: RAG, векторный поиск, LLM.
  • Подготовку базы знаний: FAQ, документация, статьи.
  • Векторизацию контента и создание векторной базы.
  • Подключение локальной LLM через Ollama.
  • Создание чат-виджета для сайта.
  • Обработку диалога: контекст, история, уточнения.
  • Интеграцию с существующей системой поддержки.
  • Мониторинг качества и улучшение базы знаний.
  • Практические примеры на Python, PHP и JavaScript.
  • Частые ошибки и лучшие практики.

1. Зачем нужен AI-ассистент

Представьте: у вас интернет-магазин на WordPress с 5000 товаров и 200 статьями в блоге. Пользователи постоянно спрашивают:

  • «Как оформить возврат?»
  • «Есть ли доставка в мой город?»
  • «Чем отличается эта модель от той?»

Вы можете:

  • Нанять больше операторов (дорого).
  • Написать огромный FAQ (никто не читает).
  • Поставить AI-ассистента (быстро, дёшево, эффективно).

Что умеет хороший AI-ассистент:

  • Отвечать на вопросы на основе вашего контента.
  • Ссылаться на источник (статью, страницу товара).
  • Вести диалог, уточнять детали.
  • Передавать сложные случаи живому оператору.
  • Работать 24/7 без выходных.

Что он не должен делать:

  • Выдумывать ответы (галлюцинировать).
  • Грубить или спорить.
  • Давать юридические или медицинские советы.
  • Раскрывать конфиденциальные данные.

2. Архитектура AI-ассистента

Система состоит из нескольких компонентов:

Пользователь → Чат-виджет → Бэкенд → RAG-поиск → LLM → Ответ
                                      ↑
                                 Векторная база
                                      ↑
                                 База знаний

Этапы работы:

  1. Индексация (один раз или при обновлении):
    • Собрать весь контент (FAQ, статьи, описания).
    • Разбить на чанки.
    • Сгенерировать эмбеддинги.
    • Сохранить в векторную базу.
  2. Обработка запроса:
    • Пользователь задаёт вопрос.
    • Вопрос преобразуется в эмбеддинг.
    • В векторной базе находятся топ-N релевантных чанков.
    • Чанки + вопрос отправляются в LLM.
    • LLM генерирует ответ на основе контекста.
    • Ответ возвращается пользователю с указанием источников.

Ключевой принцип: LLM не должна отвечать «из головы». Она отвечает только на основе найденных документов. Если в базе нет ответа, ассистент должен честно сказать: «Я не знаю, передаю вопрос оператору».

3. Подготовка базы знаний

Качество ассистента на 80% зависит от качества базы знаний.

3.1 Что включить в базу

  • FAQ — частые вопросы и ответы.
  • Документация — инструкции, руководства.
  • Статьи блога — полезный контент.
  • Описания товаров — если ассистент помогает с выбором.
  • Политики — возврат, доставка, оплата.
  • Тикеты поддержки — исторические вопросы и ответы (если есть).

3.2 Формат данных

Лучше всего использовать структурированный формат:

JSON
[
  {
    "id": "faq-001",
    "title": "Как оформить возврат?",
    "content": "Для возврата товара свяжитесь с поддержкой в течение 14 дней...",
    "url": "https://example.com/return",
    "category": "Возврат"
  },
  {
    "id": "article-042",
    "title": "Настройка SSL на Nginx",
    "content": "Для установки SSL-сертификата выполните...",
    "url": "https://example.com/ssl-nginx",
    "category": "Технические"
  }
]

3.3 Чанкинг

Разбивайте длинные тексты на фрагменты по 500–1000 символов с перекрытием 100–200 символов. Сохраняйте метаданные (заголовок, URL, категорию).

Пример на Python:

Python
def split_into_chunks(text, chunk_size=800, overlap=100):
    chunks = []
    start = 0
    while start < len(text):
        end = start + chunk_size
        chunk = text[start:end]
        chunks.append(chunk)
        start = end - overlap
    return chunks

4. Векторизация и векторная база

4.1 Выбор модели эмбеддингов

Для русского языка рекомендуются многоязычные модели:

  • intfloat/multilingual-e5-large
  • nomic-embed-text (через Ollama)

Для английского:

  • all-MiniLM-L6-v2
  • text-embedding-3-small (OpenAI)

4.2 Создание векторной базы

Используем ChromaDB — простую локальную векторную базу на Python.

Установка:

Bash
pip install chromadb sentence-transformers

Индексация:

Python
import chromadb
from chromadb.utils import embedding_functions
import json

# Загрузка базы знаний
with open('knowledge_base.json', 'r', encoding='utf-8') as f:
    articles = json.load(f)

# Инициализация ChromaDB
client = chromadb.PersistentClient(path="./chroma_db")
embedding_fn = embedding_functions.SentenceTransformerEmbeddingFunction(
    model_name="intfloat/multilingual-e5-large"
)
collection = client.get_or_create_collection(
    name="support",
    embedding_function=embedding_fn
)

# Добавление чанков
for article in articles:
    chunks = split_into_chunks(article['content'])
    for i, chunk in enumerate(chunks):
        collection.add(
            documents=[chunk],
            metadatas=[{
                'title': article['title'],
                'url': article['url'],
                'category': article['category']
            }],
            ids=[f"{article['id']}_{i}"]
        )

print(f"Проиндексировано {collection.count()} чанков")

5. Подключение LLM через Ollama

Используем локальную модель Mistral или Llama 3.

Проверка, что Ollama запущена:

Bash
ollama serve
ollama pull mistral

Функция генерации ответа:

Python
import requests

def ask_llm(prompt, context):
    full_prompt = f"""Ты — ассистент технической поддержки. Отвечай на вопрос пользователя, используя только предоставленный контекст. Если в контексте нет ответа, скажи "Я не знаю, передаю вопрос оператору".

Контекст:
{context}

Вопрос: {prompt}

Ответ:"""
    
    response = requests.post('http://localhost:11434/api/generate', json={
        'model': 'mistral',
        'prompt': full_prompt,
        'stream': False,
        'options': {
            'temperature': 0.3,
            'num_predict': 512
        }
    })
    return response.json()['response']

6. RAG-поиск и генерация ответа

Полный цикл:

Python
def get_answer(question, n_results=3):
    # 1. Поиск релевантных чанков
    results = collection.query(
        query_texts=[question],
        n_results=n_results
    )
    
    # 2. Формирование контекста
    context = "\n\n".join(results['documents'][0])
    sources = [meta['url'] for meta in results['metadatas'][0]]
    
    # 3. Генерация ответа
    answer = ask_llm(question, context)
    
    return {
        'answer': answer,
        'sources': sources
    }

# Пример
result = get_answer("Как оформить возврат?")
print("Ответ:", result['answer'])
print("Источники:", result['sources'])

Пример ответа:

Ответ: Для возврата товара свяжитесь с поддержкой в течение 14 дней с момента покупки. Подготовьте чек и упаковку. Мы оформим возврат и вернём деньги в течение 3–5 рабочих дней.

Источники: ['https://example.com/return', 'https://example.com/faq']

7. Создание чат-виджета на сайте

Теперь сделаем интерфейс, который пользователь увидит на сайте.

7.1 HTML + CSS + JavaScript

HTML
<div id="chat-widget">
    <div id="chat-header">Поддержка</div>
    <div id="chat-messages"></div>
    <div id="chat-input-container">
        <input type="text" id="chat-input" placeholder="Введите вопрос...">
        <button id="chat-send">Отправить</button>
    </div>
</div>

<style>
#chat-widget {
    position: fixed;
    bottom: 20px;
    right: 20px;
    width: 350px;
    height: 500px;
    border: 1px solid #ccc;
    border-radius: 10px;
    background: #fff;
    display: flex;
    flex-direction: column;
    box-shadow: 0 4px 12px rgba(0,0,0,0.15);
    font-family: Arial, sans-serif;
}
#chat-header {
    background: #007bff;
    color: #fff;
    padding: 15px;
    border-radius: 10px 10px 0 0;
    font-weight: bold;
}
#chat-messages {
    flex: 1;
    overflow-y: auto;
    padding: 10px;
}
.message {
    margin: 5px 0;
    padding: 8px 12px;
    border-radius: 10px;
    max-width: 80%;
}
.message.user {
    background: #e3f2fd;
    margin-left: auto;
}
.message.bot {
    background: #f1f1f1;
}
#chat-input-container {
    display: flex;
    border-top: 1px solid #ccc;
}
#chat-input {
    flex: 1;
    border: none;
    padding: 10px;
    outline: none;
}
#chat-send {
    border: none;
    background: #007bff;
    color: #fff;
    padding: 10px 20px;
    cursor: pointer;
}
</style>

7.2 JavaScript для отправки запросов

JavaScript
const chatMessages = document.getElementById('chat-messages');
const chatInput = document.getElementById('chat-input');
const chatSend = document.getElementById('chat-send');

function addMessage(text, sender) {
    const div = document.createElement('div');
    div.className = `message ${sender}`;
    div.textContent = text;
    chatMessages.appendChild(div);
    chatMessages.scrollTop = chatMessages.scrollHeight;
}

async function sendQuestion() {
    const question = chatInput.value.trim();
    if (!question) return;

    addMessage(question, 'user');
    chatInput.value = '';

    try {
        const response = await fetch('/api/support', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ question })
        });

        const data = await response.json();
        addMessage(data.answer, 'bot');

        if (data.sources && data.sources.length > 0) {
            const sourcesDiv = document.createElement('div');
            sourcesDiv.className = 'message bot';
            sourcesDiv.textContent = 'Источники: ';
data.sources.forEach(s => {
    const a = document.createElement('a');
    a.href = s; a.target = '_blank'; a.rel = 'noopener noreferrer'; a.textContent = s;
    sourcesDiv.appendChild(a);
    sourcesDiv.appendChild(document.createTextNode(', '));
});
            chatMessages.appendChild(sourcesDiv);
        }
    } catch (error) {
        addMessage('Ошибка: не удалось получить ответ', 'bot');
    }
}

chatSend.addEventListener('click', sendQuestion);
chatInput.addEventListener('keypress', (e) => {
    if (e.key === 'Enter') sendQuestion();
});

7.3 Бэкенд на PHP

PHP
<?php
header('Content-Type: application/json');

$input = json_decode(file_get_contents('php://input'), true);
$question = $input['question'] ?? '';

if (empty($question)) {
    echo json_encode(['error' => 'Вопрос не задан']);
    exit;
}

// Вызов Python-скрипта (или напрямую к Ollama + векторной базе)
$command = "python3 /path/to/rag.py " . escapeshellarg($question);
$output = shell_exec($command);
$result = json_decode($output, true);
if (!is_array($result) || !isset($result['answer'])) {
    echo json_encode(['error' => 'Ассистент временно недоступен. Попробуйте позже.']);
} else {
    echo json_encode($result);
}
?>

Альтернатива: использовать Qdrant (векторная база с HTTP API) и обращаться к ней напрямую из PHP.

8. Обработка диалога и контекста

Чтобы ассистент вёл диалог, нужно хранить историю сообщений.

На бэкенде:

Python
session_history = {}

def get_answer_with_history(session_id, question):
    if session_id not in session_history:
        session_history[session_id] = []
    
    history = session_history[session_id]
    
    # Поиск с учётом истории
    search_query = question
    if history:
    # ищем по последнему вопросу пользователя, а не по ответу ассистента
    last_user = next((m['text'] for m in reversed(history) if m['role'] == 'user'), None)
    search_query = (last_user + ' ' + question) if last_user else question
    
    results = collection.query(query_texts=[search_query], n_results=3)
    context = "\n\n".join(results['documents'][0])
    
    # Формирование промпта с историей
    prompt = "История диалога:\n"
    for msg in history[-3:]:  # последние 3 сообщения
        prompt += f"{msg['role']}: {msg['text']}\n"
    prompt += f"user: {question}\nassistant:"
    
    answer = ask_llm(prompt, context)
    
    history.append({'role': 'user', 'text': question})
    history.append({'role': 'assistant', 'text': answer})
    
    return answer

9. Интеграция с существующей поддержкой

Если ассистент не может ответить, он должен передать вопрос оператору.

Пример:

Python
def get_answer_or_ticket(question):
    result = get_answer(question)
    
    if "Я не знаю" in result['answer'] or "передаю вопрос оператору" in result['answer']:
        # Создание тикета
        ticket_id = create_ticket(question)
        return {
            'answer': f"Я не смог найти ответ. Ваш вопрос передан оператору. Номер тикета: {ticket_id}",
            'ticket': ticket_id
        }
    
    return result

def create_ticket(question):
    # Интеграция с вашей системой тикетов (например, через API)
    # ...
    return "TICKET-12345"

10. Мониторинг качества

Чтобы ассистент становился лучше, собирайте обратную связь.

Что логировать:

  • Вопрос пользователя.
  • Ответ ассистента.
  • Источники.
  • Оценку пользователя (полезно/не полезно).
  • Если пользователь ушёл без ответа.

Как улучшать:

  • Добавлять новые статьи в базу знаний на основе частых вопросов.
  • Корректировать промпты.
  • Обучать модель на реальных диалогах (fine-tuning).
  • Регулярно проверять, что ассистент не галлюцинирует.

11. Частые ошибки и лучшие практики

ОшибкаПоследствияРешение
Нет базы знанийАссистент выдумывает ответыСоберите FAQ, статьи, документацию.
Плохой чанкингПотеря контекстаИспользуйте перекрытие, разбивайте по абзацам.
Слабая модельПлохой русский, ошибкиИспользуйте Mistral, Llama 3, Qwen.
Нет истории диалогаАссистент не понимает уточненийХраните историю в сессии.
Нет источниковПользователь не доверяетВсегда показывайте ссылки на статьи.
Игнорирование обратной связиКачество не растётСобирайте оценки и анализируйте.
Нет передачи операторуПользователь остаётся без помощиНастройте создание тикетов.

Лучшие практики:

  • Ассистент должен отвечать только на основе базы знаний.
  • Если ответа нет — честно сказать и передать оператору.
  • Всегда указывать источники.
  • Регулярно обновлять базу знаний.
  • Тестировать на реальных вопросах.

👤 Зарегистрируйтесь, чтобы добавить материал в закладки.