Техническая поддержка — это боль любого растущего проекта. Пользователи задают одни и те же вопросы, тикеты копятся, а время специалистов уходит на рутину. AI-ассистент решает эту проблему: он отвечает мгновенно, круглосуточно и не устаёт. Но чтобы он был полезен, а не раздражал, его нужно правильно построить.
В этой статье мы подробно разберём:
- Зачем нужен AI-ассистент и какие задачи он решает.
- Архитектуру: RAG, векторный поиск, LLM.
- Подготовку базы знаний: FAQ, документация, статьи.
- Векторизацию контента и создание векторной базы.
- Подключение локальной LLM через Ollama.
- Создание чат-виджета для сайта.
- Обработку диалога: контекст, история, уточнения.
- Интеграцию с существующей системой поддержки.
- Мониторинг качества и улучшение базы знаний.
- Практические примеры на Python, PHP и JavaScript.
- Частые ошибки и лучшие практики.
1. Зачем нужен AI-ассистент
Представьте: у вас интернет-магазин на WordPress с 5000 товаров и 200 статьями в блоге. Пользователи постоянно спрашивают:
- «Как оформить возврат?»
- «Есть ли доставка в мой город?»
- «Чем отличается эта модель от той?»
Вы можете:
- Нанять больше операторов (дорого).
- Написать огромный FAQ (никто не читает).
- Поставить AI-ассистента (быстро, дёшево, эффективно).
Что умеет хороший AI-ассистент:
- Отвечать на вопросы на основе вашего контента.
- Ссылаться на источник (статью, страницу товара).
- Вести диалог, уточнять детали.
- Передавать сложные случаи живому оператору.
- Работать 24/7 без выходных.
Что он не должен делать:
- Выдумывать ответы (галлюцинировать).
- Грубить или спорить.
- Давать юридические или медицинские советы.
- Раскрывать конфиденциальные данные.
2. Архитектура AI-ассистента
Система состоит из нескольких компонентов:
Пользователь → Чат-виджет → Бэкенд → RAG-поиск → LLM → Ответ
↑
Векторная база
↑
База знаний
Этапы работы:
- Индексация (один раз или при обновлении):
- Собрать весь контент (FAQ, статьи, описания).
- Разбить на чанки.
- Сгенерировать эмбеддинги.
- Сохранить в векторную базу.
- Обработка запроса:
- Пользователь задаёт вопрос.
- Вопрос преобразуется в эмбеддинг.
- В векторной базе находятся топ-N релевантных чанков.
- Чанки + вопрос отправляются в LLM.
- LLM генерирует ответ на основе контекста.
- Ответ возвращается пользователю с указанием источников.
Ключевой принцип: LLM не должна отвечать «из головы». Она отвечает только на основе найденных документов. Если в базе нет ответа, ассистент должен честно сказать: «Я не знаю, передаю вопрос оператору».
3. Подготовка базы знаний
Качество ассистента на 80% зависит от качества базы знаний.
3.1 Что включить в базу
- FAQ — частые вопросы и ответы.
- Документация — инструкции, руководства.
- Статьи блога — полезный контент.
- Описания товаров — если ассистент помогает с выбором.
- Политики — возврат, доставка, оплата.
- Тикеты поддержки — исторические вопросы и ответы (если есть).
3.2 Формат данных
Лучше всего использовать структурированный формат:
[
{
"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:
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 chunks4. Векторизация и векторная база
4.1 Выбор модели эмбеддингов
Для русского языка рекомендуются многоязычные модели:
intfloat/multilingual-e5-largenomic-embed-text(через Ollama)
Для английского:
all-MiniLM-L6-v2text-embedding-3-small(OpenAI)
4.2 Создание векторной базы
Используем ChromaDB — простую локальную векторную базу на Python.
Установка:
pip install chromadb sentence-transformersИндексация:
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 запущена:
ollama serve
ollama pull mistralФункция генерации ответа:
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-поиск и генерация ответа
Полный цикл:
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
<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 для отправки запросов
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
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. Обработка диалога и контекста
Чтобы ассистент вёл диалог, нужно хранить историю сообщений.
На бэкенде:
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 answer9. Интеграция с существующей поддержкой
Если ассистент не может ответить, он должен передать вопрос оператору.
Пример:
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. |
| Нет истории диалога | Ассистент не понимает уточнений | Храните историю в сессии. |
| Нет источников | Пользователь не доверяет | Всегда показывайте ссылки на статьи. |
| Игнорирование обратной связи | Качество не растёт | Собирайте оценки и анализируйте. |
| Нет передачи оператору | Пользователь остаётся без помощи | Настройте создание тикетов. |
Лучшие практики:
- Ассистент должен отвечать только на основе базы знаний.
- Если ответа нет — честно сказать и передать оператору.
- Всегда указывать источники.
- Регулярно обновлять базу знаний.
- Тестировать на реальных вопросах.
👤 Зарегистрируйтесь, чтобы добавить материал в закладки.
