Разработка AI-ассистента для документации продукта
AI-ассистент для продуктовой документации позволяет пользователям получать ответы на вопросы о продукте вместо поиска в разрозненных docs-сайтах. Ключевые требования: точные цитаты из документации (не галлюцинации), версионирование (ответ должен соответствовать версии продукта пользователя), возможность эскалации к поддержке.
Специфика документационного ассистента
from anthropic import Anthropic
from langchain_openai import OpenAIEmbeddings
from langchain_community.vectorstores import Chroma
import json
from typing import Optional
client = Anthropic()
embeddings_model = OpenAIEmbeddings(model="text-embedding-3-small")
class DocAssistant:
def __init__(self, product_name: str, db_path: str):
self.product_name = product_name
self.vectorstore = Chroma(
collection_name=f"docs_{product_name}",
embedding_function=embeddings_model,
persist_directory=db_path,
)
def answer(
self,
question: str,
product_version: Optional[str] = None,
conversation_id: Optional[str] = None,
) -> dict:
"""Отвечает на вопрос по документации"""
# Фильтрация по версии если указана
where_filter = {"version": product_version} if product_version else None
results = self.vectorstore.similarity_search_with_score(
question, k=5, filter=where_filter
)
if not results:
return {
"answer": f"По вашему вопросу ничего не найдено в документации {self.product_name}.",
"sources": [],
"confidence": "low",
"suggest_support": True,
}
context = "\n\n".join([
f"[{doc.metadata.get('title', 'Документ')}, {doc.metadata.get('section', '')}]:\n{doc.page_content}"
for doc, _ in results[:4]
])
response = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=2048,
system=f"""Ты — специалист по поддержке продукта {self.product_name}.
СТРОГИЕ ПРАВИЛА:
1. Отвечай ТОЛЬКО на основе предоставленной документации
2. Цитируй конкретные разделы при необходимости
3. Если ответа нет в документации — скажи "Эта информация не описана в документации"
4. Не придумывай функциональность
5. Для шагов — используй нумерованные списки
6. В конце всегда предлагай: "Нужна дополнительная помощь? Обратитесь в поддержку: [email protected]" """,
messages=[{
"role": "user",
"content": f"""Вопрос: {question}
{f"Версия продукта: {product_version}" if product_version else ""}
Документация:
{context}"""
}]
)
answer_text = response.content[0].text
# Определяем уверенность по наличию конкретных цитат
confidence = "high" if any(
r[1] < 0.3 for r in results[:2] # Низкое расстояние = высокое сходство
) else "medium"
return {
"answer": answer_text,
"sources": [
{
"title": doc.metadata.get("title"),
"section": doc.metadata.get("section"),
"url": doc.metadata.get("url"),
"version": doc.metadata.get("version"),
}
for doc, _ in results[:3]
],
"confidence": confidence,
"suggest_support": confidence == "low",
}
Индексирование документации из разных источников
import aiohttp
from bs4 import BeautifulSoup
from langchain.text_splitter import MarkdownHeaderTextSplitter
class DocIndexer:
def __init__(self, vectorstore: Chroma):
self.vectorstore = vectorstore
self.md_splitter = MarkdownHeaderTextSplitter(
headers_to_split_on=[("##", "section"), ("###", "subsection")]
)
async def index_gitbook(self, base_url: str, version: str = "latest"):
"""Индексирует документацию GitBook"""
async with aiohttp.ClientSession() as session:
# Получаем sitemap
async with session.get(f"{base_url}/sitemap.xml") as resp:
sitemap = await resp.text()
import re
urls = re.findall(r'<loc>(.*?)</loc>', sitemap)
for url in urls[:100]: # Ограничиваем
async with session.get(url) as page_resp:
html = await page_resp.text()
soup = BeautifulSoup(html, "html.parser")
title = soup.find("h1")
content = soup.find("article") or soup.find("main")
if not content:
continue
text = content.get_text(separator="\n", strip=True)
chunks = self.md_splitter.split_text(text)
self.vectorstore.add_texts(
texts=[c.page_content for c in chunks],
metadatas=[{
"title": title.get_text() if title else "Unknown",
"url": url,
"version": version,
"section": c.metadata.get("section", ""),
} for c in chunks]
)
def index_markdown_files(self, docs_dir: str, version: str = "latest"):
"""Индексирует локальные .md файлы документации"""
for md_file in Path(docs_dir).rglob("*.md"):
content = md_file.read_text()
chunks = self.md_splitter.split_text(content)
# Извлекаем заголовок из первой строки H1
title = md_file.stem.replace("-", " ").title()
for line in content.splitlines():
if line.startswith("# "):
title = line[2:].strip()
break
self.vectorstore.add_texts(
texts=[c.page_content for c in chunks],
metadatas=[{
"title": title,
"file": str(md_file.relative_to(docs_dir)),
"version": version,
"section": c.metadata.get("section", ""),
} for c in chunks]
)
Виджет для docs-сайта
// docs-chat-widget.js
class DocsChatWidget {
constructor(config) {
this.apiUrl = config.apiUrl;
this.productVersion = config.version || 'latest';
this.container = this.createWidget();
document.body.appendChild(this.container);
}
createWidget() {
const container = document.createElement('div');
container.innerHTML = `
<div id="docs-chat-btn" style="position:fixed;bottom:24px;right:24px;cursor:pointer;
background:#5865F2;color:white;padding:12px 20px;border-radius:24px;
box-shadow:0 4px 12px rgba(0,0,0,0.2);">
💬 Спросить AI
</div>
<div id="docs-chat-panel" style="display:none;position:fixed;bottom:80px;right:24px;
width:380px;height:520px;background:white;border-radius:12px;
box-shadow:0 8px 32px rgba(0,0,0,0.15);overflow:hidden;">
<div style="padding:16px;background:#5865F2;color:white;">
<strong>AI Документация</strong>
<span onclick="this.closest('#docs-chat-panel').style.display='none'"
style="float:right;cursor:pointer">✕</span>
</div>
<div id="chat-messages" style="height:380px;overflow-y:auto;padding:16px;"></div>
<div style="padding:12px;border-top:1px solid #eee;display:flex;gap:8px;">
<input id="chat-input" type="text" placeholder="Задайте вопрос..."
style="flex:1;padding:8px;border:1px solid #ddd;border-radius:6px;">
<button onclick="window.docsChat.send()" style="padding:8px 16px;
background:#5865F2;color:white;border:none;border-radius:6px;cursor:pointer;">→</button>
</div>
</div>
`;
return container;
}
async send() {
const input = document.getElementById('chat-input');
const question = input.value.trim();
if (!question) return;
input.value = '';
this.addMessage('user', question);
const response = await fetch(this.apiUrl + '/ask', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ question, version: this.productVersion })
});
const data = await response.json();
this.addMessage('assistant', data.answer, data.sources);
}
}
window.docsChat = new DocsChatWidget({
apiUrl: 'https://api.myproduct.com/docs-ai',
version: document.querySelector('meta[name="docs-version"]')?.content
});
Практический кейс: SaaS-продукт с 8000 пользователей
Документация: 320 страниц GitBook, 5 версий продукта.
Результат:
- Support tickets типа "как настроить X": -58%
- TTFR (time to first response): мгновенно vs 4 часа
- Удовлетворённость документацией (CSAT): 3.2 → 4.4
Сроки
- Базовый RAG + индексирование docs: 3–5 дней
- Виджет для docs-сайта: 2–3 дня
- Версионирование + мультиязычность: 1 неделя
- Интеграция с helpdesk: 1 неделя







