AI-поиск по внутренней документации и базе знаний
Внутренняя база знаний компании — это часто несколько сотен статей в Confluence или Notion, которые устарели на 30–40%, написаны разными авторами без единого стиля и плохо связаны между собой. Keyword-поиск по ним работает посредственно: сотрудник пишет «как настроить VPN», а нужная статья называется «Инструкция по подключению к корпоративной сети».
AI-поиск решает проблему через семантическое понимание, автоматически находит связанные материалы и генерирует ответ с цитатами — не «вот 10 статей, разбирайся», а «вот ответ на твой вопрос из документа №3, см. также раздел 4.2 в документе №7».
Стек и архитектура для документационного поиска
from llama_index.core import VectorStoreIndex, SimpleDirectoryReader
from llama_index.core.node_parser import SentenceSplitter
from llama_index.core.retrievers import VectorIndexRetriever
from llama_index.core.query_engine import RetrieverQueryEngine
from llama_index.core.postprocessor import SentenceTransformerRerank
from llama_index.vector_stores.qdrant import QdrantVectorStore
from llama_index.embeddings.huggingface import HuggingFaceEmbedding
import qdrant_client
# Модель эмбеддингов — мультиязычная для RU/EN документации
embed_model = HuggingFaceEmbedding(
model_name="intfloat/multilingual-e5-large",
embed_batch_size=32
)
# Чанкинг с перекрытием — важен для документации
splitter = SentenceSplitter(
chunk_size=512,
chunk_overlap=64,
paragraph_separator="\n\n"
)
# Reranker для финального ранжирования
reranker = SentenceTransformerRerank(
model="cross-encoder/ms-marco-MiniLM-L-6-v2",
top_n=5
)
# Построение индекса
client = qdrant_client.QdrantClient(url="http://localhost:6333")
vector_store = QdrantVectorStore(client=client, collection_name="kb_docs")
index = VectorStoreIndex.from_vector_store(
vector_store=vector_store,
embed_model=embed_model
)
query_engine = RetrieverQueryEngine(
retriever=VectorIndexRetriever(index=index, similarity_top_k=15),
node_postprocessors=[reranker],
)
Главная проблема: чанкинг документации
Документация устроена иерархически — раздел, подраздел, параграф. Наивный чанкинг по 512 токенов разрезает контекст в неудачных местах: ответ на вопрос «что делать если X» может находиться в заголовке раздела и трёх параграфах под ним, а чанк содержит только один параграф без заголовка.
Решение — parent-child chunking:
from llama_index.core.node_parser import HierarchicalNodeParser
from llama_index.core.retrievers import AutoMergingRetriever
from llama_index.core.storage.docstore import SimpleDocumentStore
# Иерархическое разбиение: крупные чанки для контекста, мелкие для поиска
hier_parser = HierarchicalNodeParser.from_defaults(
chunk_sizes=[2048, 512, 128] # большой → средний → малый
)
nodes = hier_parser.get_nodes_from_documents(documents)
docstore = SimpleDocumentStore()
docstore.add_documents(nodes)
# При поиске находим мелкий чанк, но отдаём родительский контекст
retriever = AutoMergingRetriever(
vector_retriever,
storage_context,
verbose=True,
simple_ratio_thresh=0.3 # если 30% мелких чанков из родителя → берём родителя
)
На реальных тестах: AutoMerging Retriever даёт faithfulness +12% и relevance +18% по сравнению с flat chunking на корпусе технической документации (тест на 300 вопросов, оценка через RAGAS).
Автоматическое обновление индекса
class DocumentationIndexer:
def __init__(self, confluence_client, vector_store):
self.confluence = confluence_client
self.index = vector_store
self.last_indexed = {} # page_id → last_modified timestamp
async def incremental_update(self):
"""Обновляет только изменённые документы"""
all_pages = self.confluence.get_all_pages(space_key="KB")
for page in all_pages:
page_id = page["id"]
modified = page["version"]["when"]
if self.last_indexed.get(page_id) == modified:
continue # не изменился
# Удаляем старые чанки этой страницы
self.index.delete(filter={"page_id": page_id})
# Парсим и индексируем заново
content = self.confluence.get_page_body(page_id)
nodes = self._parse_and_chunk(content, page)
self.index.add(nodes)
self.last_indexed[page_id] = modified
return {"updated": len([p for p in all_pages
if self.last_indexed.get(p["id"]) != p["version"]["when"]])}
Slack-бот и web-интерфейс
В большинстве проектов основная точка входа — Slack-бот, не отдельный портал:
from slack_bolt import App
from slack_bolt.adapter.socket_mode import SocketModeHandler
app = App(token=SLACK_BOT_TOKEN)
@app.message(re.compile(r"^/kb (.+)"))
def handle_kb_query(message, say, context):
query = context["matches"][0]
result = query_engine.query(query)
blocks = [
{"type": "section", "text": {"type": "mrkdwn",
"text": f"*Ответ:*\n{result.response}"}},
{"type": "section", "text": {"type": "mrkdwn",
"text": f"*Источники:*\n" + "\n".join([
f"• <{n.metadata['url']}|{n.metadata['title']}>"
for n in result.source_nodes[:3]
])}}
]
say(blocks=blocks)
Кейс: IT-компания, 200 человек, 1200 статей в Confluence. Среднее время ответа на типичный вопрос через Slack-бот — 1,4 сек. Accuracy (верный ответ в топ-1 по оценке команды, выборка 200 запросов) — 82%. Количество повторных вопросов в #general упало на 43% за первый месяц работы бота.
Сроки
- Базовый RAG-поиск по Confluence/Notion: 2–3 недели
- С Slack-интеграцией и обновлением: 3–5 недель
- Полноценная система с аналитикой запросов и выявлением пробелов в документации: 6–8 недель







