Практика: чат-бот по своим документам
Урок собирает изученное в один работающий Q&A-бот по вашим документам.
Q&A over docs — приложение, которое отвечает на вопросы пользователя, опираясь на загруженную базу собственных документов через RAG.
План сборки
Бот по документам — это конвейер из всех изученных блоков. Соберём его по шагам:
1. Загрузить документы => DocumentLoader
2. Нарезать на чанки => TextSplitter
3. Превратить в векторы => Embeddings
4. Сложить в индекс => VectorStore
5. Сделать retriever => store.as_retriever()
6. Собрать RAG-цепочку => context + question => prompt => model
7. (опц.) добавить память => RunnableWithMessageHistoryИндексация (один раз)
from langchain_community.document_loaders import TextLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_openai import OpenAIEmbeddings
from langchain_community.vectorstores import FAISS
docs = TextLoader("handbook.txt").load()
chunks = RecursiveCharacterTextSplitter(
chunk_size=500, chunk_overlap=50).split_documents(docs)
store = FAISS.from_documents(chunks, OpenAIEmbeddings(
model="text-embedding-3-small"))
store.save_local("faiss_index") # чтобы не индексировать зановоЦепочка ответа (на каждый вопрос)
from langchain_core.runnables import RunnableParallel, RunnablePassthrough
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_openai import ChatOpenAI
retriever = store.as_retriever(search_kwargs={"k": 4})
prompt = ChatPromptTemplate.from_template(
"Ты помощник по внутренней документации. Отвечай только по контексту, "
"если ответа нет — скажи 'не знаю'.\nКонтекст:\n{context}\n\nВопрос: {question}"
)
qa = (RunnableParallel(context=retriever, question=RunnablePassthrough())
| prompt | ChatOpenAI(model="gpt-4o-mini") | StrOutputParser())
print(qa.invoke("Какой регламент отпусков?"))Как работает под капотом
Ключевая идея — разделить дорогую индексацию и дешёвый ответ. Индексацию (шаги 1–4) делают один раз и сохраняют индекс на диск: повторно гонять эмбеддинги по всем документам на каждый вопрос — это лишние деньги и время. На запросе работает только лёгкая часть: retriever достаёт несколько чанков, промпт заземляет на них модель, парсер отдаёт строку. Память (если нужна диалоговость) оборачивает эту цепочку и подкладывает историю по session_id — как в разделе про память.
Частые ошибки
- Переиндексировать на каждый запрос. Сохраняйте индекс (
save_local) и грузите готовый. - Нет ветки «не знаю». Без неё бот фантазирует, когда в документах нет ответа.
- Не показывать источники. Для доверия выводите, из каких документов взят ответ (метаданные чанков).
Итог
- Q&A-бот = загрузка → чанки → эмбеддинги → индекс → retriever → RAG-цепочка (+ память).
- Индексация делается один раз и сохраняется; ответ использует лёгкую часть.
- Обязательны заземление «только по контексту» и ветка «не знаю».
- Показ источников повышает доверие к ответам.
Проверьте себя
1. Почему индексацию документов выносят в отдельный одноразовый шаг?
AТак требует LangChain
BЭмбеддинги по всем документам дороги и медленны — индекс считают раз и сохраняют
CИначе retriever не работает
DЧтобы спрятать данные от модели
2. Зачем в Q&A-боте предусматривать ответ «не знаю»?
AЧтобы сэкономить токены всегда
BЧтобы бот не фантазировал, когда в документах нет ответа
CЧтобы ускорить эмбеддинги
DЭто требование лицензии