Курс Alibaba Cloud ACP, урок 2.2: расширяем знания бота (RAG)
Полный перевод урока 2.2 курса Alibaba Cloud ACP: как устроен RAG (индексация и поиск с генерацией), приложение на LlamaIndex по документам компании, сохранение и загрузка индекса, многоходовый RAG с переписыванием вопроса, задачи с разбором.
🚄 Введение
Заголовок раздела «🚄 Введение»Вы уже знаете, что RAG — действенный способ расширить знания большой модели. В этом уроке вы изучите, как работает RAG, и создадите RAG-приложение, которое отвечает на вопросы по внутренним документам компании.
🍁 Цели урока
Заголовок раздела «🍁 Цели урока»После урока вы сможете:
- объяснить, как работает RAG;
- создать RAG-приложение.
1. Как работает RAG
Заголовок раздела «1. Как работает RAG»На экзамене можно потерять баллы, если забыл какое-то понятие или формулу. Но если экзамен с открытой книгой, достаточно найти самые подходящие к вопросу сведения и добавить к ним своё понимание — и можно отвечать.
С большой моделью то же самое. Если при обучении она не видела каких-то знаний (например, регламентов вашей компании), то на прямой вопрос о них ответит неточно. А если при генерации дать ей нужные знания для справки — как на экзамене с открытой книгой, — качество ответа заметно вырастет.
Отсюда ключевая идея, которую мы уже упоминали, — контекстная инженерия (Context Engineeringконтекст-инжиниринг (context engineering)EN context engineeringРешение, какие токены и в каком порядке попадают в окно на каждом вызове — инструкции, примеры, документы, история, результаты инструментов.): наполнять «контекстное окно» модели ровно той информацией, которая нужна для задачи. Информации мало — модель «не знает»; слишком много или не по делу — качество падает, а стоимость растёт.
генерация с дополнением извлечёнными даннымигенерация с дополнением извлечёнными данными (RAG)EN retrieval-augmented generation (RAG) (Retrieval Augmented Generation, генерация с дополнением найденным), которую мы сейчас изучим, — один из самых важных и действенных приёмов контекстной инженерии. Он решает именно проблему «модели не хватает знаний». RAG-приложение обычно состоит из двух частей: построение индекса и поиск с генерацией.
1.1 Построение индекса
Заголовок раздела «1.1 Построение индекса»Перед экзаменом вы, возможно, делаете пометки в справочных материалах, чтобы потом быстрее найти нужное. Так же и RAG-приложение заранее «размечает» материалы до того, как отвечать. Этот процесс называется построением индекса, и в нём четыре шага.
- Разбор документов. Вы превращаете увиденное в книге в понятный вам текст; так же и RAG-приложение сначала загружает документы базы знаний и разбирает их в текст, понятный модели.
- Нарезка текста. Решая задачу, вы не листаете всю книгу, а ищете несколько самых подходящих абзацев — поэтому заранее грубо делите материал на части. Так же RAG-приложение после разбора режет текст на фрагменты (нарезкачанкинг (нарезка документов)EN chunkingРазбиение документов на фрагменты (чанки) для индексации. Каждый чанк получает свой эмбеддинг.), чтобы потом быстро находить самое подходящее к вопросу.
- Векторизация текста. На экзамене вы сначала ищете в материалах самые подходящие абзацы, а потом отвечаете.
В RAG-приложении для этого обычно используют модель-эмбеддерэмбеддинг-модельEN embedding modelМодель, которая превращает текст целиком в один вектор так, что близкие по смыслу тексты дают близкие векторы (BGE, multilingual-e5, Qwen3-Embedding).: она превращает
и фрагменты, и вопрос в числа (векторы), а сравнение их сходства находит самые подходящие фрагменты. Превращение
текста в числа и называется векторизацией.
Если вам интересны детали этого процесса, загляните в раздел «Дополнительное чтение» в конце урока.
- Хранение индекса. Векторы фрагментов сохраняют в векторную базу данныхвекторная база данныхEN vector database, чтобы не повторять все шаги при каждом ответе — это ускоряет отклик.
flowchart LR
D["Документы<br/>PDF, DOCX, MD"] --> P["1. Разбор<br/>в текст"] --> C["2. Нарезка<br/>на фрагменты"] --> E["3. Векторизация<br/>моделью-эмбеддером"] --> S[("4. Векторная<br/>база")]Построив индекс, RAG-приложение может находить фрагменты текста, относящиеся к вопросу пользователя.
1.2 Поиск и генерация
Заголовок раздела «1.2 Поиск и генерация»Поиск и генерация — это два этапа из названия RAG: Retrieval и Generation. Поиск — как поиск сведений в
материалах на открытом экзамене, генерация — как ответ на вопрос по найденным материалам.
- Поиск. На этом этапе находятся фрагменты, самые близкие к вопросу. Модель-эмбеддер превращает вопрос в вектор и сравнивает его по смысловой близости с векторами фрагментов в базе. Поиск — самое важное звено RAG: представьте, что на экзамене вы нашли не те материалы — ответ точно будет неверным. Этот шаг лучше всего показывает суть контекстной инженерии: «точно выбрать нужное» из огромного объёма знаний, чтобы наполнить контекст. Найти самое подходящее — первый шаг к качественной генерации. Для точности поиска, кроме сильной модели-эмбеддера, применяют переранжированиереранкерEN rerankerВторая ступень поиска — точная, но медленная модель (обычно кросс-энкодер) пересортировывает десятки кандидатов от быстрого ретривера. (rerank), поиск с окном предложений и другие приёмы — их вы изучите в следующих уроках.
- Генерация. Найдя фрагменты, RAG-приложение вставляет вопрос и фрагменты в шаблон промпта, и модель пишет ответ.
Здесь важнее умение модели обобщать прочитанное, а не её собственные знания. Устройство шаблона — ещё одно
ключевое звено контекстной инженерии: мало дать модели найденные «материалы», надо ещё ясно «объяснить», как ими
пользоваться.
Типичный шаблон (в переводе):
Ответь на вопрос пользователя по следующим сведениям: {найденные фрагменты}. Вопрос пользователя: {question}.
flowchart LR
Q["Вопрос"] --> V["Вектор вопроса"] --> R{"Поиск близких<br/>фрагментов"}
DB[("Векторная база")] --> R
R --> T["Шаблон промпта:<br/>фрагменты + вопрос"] --> L["LLM"] --> A["Ответ"]2. Создаём RAG-приложение
Заголовок раздела «2. Создаём RAG-приложение»Реализовать всё это вручную непросто. Но с LlamaIndex для этого хватит совсем немного кода.
2.1 Проверьте текущее окружение Python
Заголовок раздела «2.1 Проверьте текущее окружение Python»Прежде чем запускать код урока, убедитесь, что переключились на созданное ранее окружение Python — например,
Python (llm_learn)из прошлых уроков.Внимание: в каждом следующем уроке проверяйте, не нужно ли вручную переключить окружение ноутбука.
2.2 Простое RAG-приложение
Заголовок раздела «2.2 Простое RAG-приложение»Как и в прошлом уроке, сначала выполните код, который загружает ключ API Bailian в окружение.
# Загружаем ключ API Bailian для вызова моделей Qwenimport os, sysos.chdir(os.path.join(os.path.dirname(os.path.abspath('')), 'course_core'))sys.path.insert(0, os.getcwd())
from config.load_key import load_key, load_nltkload_key()# В продакшене не выводите ключ API в логи, чтобы он не утёкprint(f'''Ваш ключ API: {os.environ["DASHSCOPE_API_KEY"][:5]+"*"*5}''')
load_nltk()В папке docs мы подготовили несколько вымышленных документов компании (регламенты, описание должностей и т. п.).
Сейчас вы создадите по ним RAG-приложение.
# Импортируем зависимостиfrom llama_index.embeddings.dashscope import DashScopeEmbedding,DashScopeTextEmbeddingModelsfrom llama_index.core import SimpleDirectoryReader,VectorStoreIndexfrom llama_index.llms.openai_like import OpenAILike
# Пропускаем загрузку NLTK: LlamaIndex по умолчанию использует NLTK для предобработки текста, но в некоторых окружениях# загрузка падает из-за сети. Переменная окружения LLAMA_INDEX_DISABLE_NLTK=1 отключает NLTK.import osos.environ["LLAMA_INDEX_DISABLE_NLTK"] = "1"
# Эти две строки убирают предупреждения (WARNING), чтобы не мешали читать; в продакшене настраивайте уровень логов по необходимостиimport logginglogging.basicConfig(level=logging.ERROR)
print("Разбираем файлы...")# В LlamaIndex есть SimpleDirectoryReader: он загружает файлы из папки как объекты document — это шаг разбора.# Здесь читаем только нужные курсу форматы и пропускаем ноутбуки, чтобы не запускать медленный разбор через nbconvert.documents = SimpleDirectoryReader('./docs', required_exts=['.pdf', '.docx', '.md']).load_data()
print("Создаём индекс...")# from_documents включает шаги нарезки и построения индексаindex = VectorStoreIndex.from_documents( documents, # указываем модель-эмбеддер embed_model=DashScopeEmbedding( # можно взять и другие модели эмбеддингов Alibaba Cloud: https://help.aliyun.com/zh/model-studio/getting-started/models#3383780daf8hw model_name=DashScopeTextEmbeddingModels.TEXT_EMBEDDING_V2 ))print("Создаём движок вопросов...")query_engine = index.as_query_engine( # потоковый вывод streaming=True, # здесь модель qwen-plus; можно взять и другие текстовые модели Qwen в Alibaba Cloud: https://help.aliyun.com/zh/model-studio/getting-started/models#9f8890ce29g5u llm=OpenAILike( model="qwen-plus", api_base="https://dashscope.aliyuncs.com/compatible-mode/v1", api_key=os.getenv("DASHSCOPE_API_KEY"), is_chat_model=True ))print("Генерируем ответ...")streaming_response = query_engine.query('Каким инструментом управления проектами пользоваться в нашей компании?')print("Ответ:")# потоковый выводstreaming_response.print_response_stream()2.3 Сохранение и загрузка индекса
Заголовок раздела «2.3 Сохранение и загрузка индекса»Вы, наверное, заметили, что индекс строится довольно долго. Если сохранить его на диск и загружать при необходимости, а не строить заново, ответ будет гораздо быстрее. В LlamaIndex для этого есть простые методы.
# Сохраняем индекс в локальные файлыindex.storage_context.persist("knowledge_base/test")print("Файлы индекса сохранены в knowledge_base/test")# Загружаем индекс из локальных файловfrom llama_index.core import StorageContext,load_index_from_storagestorage_context = StorageContext.from_defaults(persist_dir="knowledge_base/test")index = load_index_from_storage(storage_context,embed_model=DashScopeEmbedding( model_name=DashScopeTextEmbeddingModels.TEXT_EMBEDDING_V2 ))print("Индекс успешно загружен из knowledge_base/test")Загрузив индекс с диска, снова задайте вопрос и убедитесь, что всё работает.
print("Создаём движок вопросов...")query_engine = index.as_query_engine( # потоковый вывод streaming=True, # здесь модель qwen-plus; можно взять и другие текстовые модели Qwen в Alibaba Cloud llm=OpenAILike( model="qwen-plus", api_base="https://dashscope.aliyuncs.com/compatible-mode/v1", api_key=os.getenv("DASHSCOPE_API_KEY"), is_chat_model=True ))print("Генерируем ответ...")streaming_response = query_engine.query('Каким инструментом управления проектами пользоваться в нашей компании?')print("Ответ:")streaming_response.print_response_stream()Этот код можно обернуть в функции, чтобы быстро пользоваться им дальше по ходу курса.
from chatbot import rag
# Индекс уже построен на предыдущих шагах, поэтому его можно просто загрузить. Если нужно перестроить индекс,# добавьте строку: rag.indexing()index = rag.load_index(persist_path='./knowledge_base/test')query_engine = rag.create_query_engine(index=index)
rag.ask('Каким инструментом управления проектами пользоваться в нашей компании?', query_engine=query_engine)2.4 Многоходовый диалог в RAG
Заголовок раздела «2.4 Многоходовый диалог в RAG»В прошлых уроках вы научились вести многоходовый диалог с моделью, храня список messages. Но в RAG-приложении
многоходовый диалог сталкивается с особой трудностью.
Трудность многоходового RAG
Заголовок раздела «Трудность многоходового RAG»Вспомните этап поиска: система сравнивает вопрос пользователя по смыслу с фрагментами базы знаний и находит самые близкие. А что, если вопрос опирается на контекст из истории диалога?
Пример:
- Первый ход: пользователь спрашивает «Где рабочее место Чжан Саня?»
- Второй ход: пользователь продолжает: «А кто его руководитель?»
Если искать только по вопросу второго хода «А кто его руководитель?», система не знает, кто такой «он», и, скорее всего, найдёт не те фрагменты — ответ будет неточным.
Решение: переписать вопрос
Заголовок раздела «Решение: переписать вопрос»Если подать в поиск всю историю диалога вместе с вопросом, текст получится слишком длинным, и качество эмбеддингов упадёт. Общепринятое в индустрии решение:
- Переписать вопрос: модель по истории диалога переписывает текущий вопрос, вкладывая в него нужный контекст;
- Искать как обычно: поиск и генерация идут по переписанному вопросу.
В примере выше модель перепишет «А кто его руководитель?» в «Кто руководитель Чжан Саня?» — и поиск найдёт нужное.
В LlamaIndex есть удобные средства для многоходового диалога в RAG-приложении.
from llama_index.core import PromptTemplatefrom llama_index.core.llms import ChatMessage, MessageRolefrom llama_index.core.chat_engine import CondenseQuestionChatEngine
# Шаблон промпта для переписывания вопроса (переведён с китайского)custom_prompt = PromptTemplate( """ Дана история диалога (между человеком и помощником) и следующий вопрос человека. Перепиши этот вопрос в самостоятельный, включив в него весь нужный контекст из диалога.
<История диалога> {chat_history}
<Следующий вопрос> {question}
<Переписанный самостоятельный вопрос>""")
# Имитируем историю диалогаcustom_chat_history = [ ChatMessage(role=MessageRole.USER, content="Какие бывают специализации у инженеров по разработке контента?"), ChatMessage(role=MessageRole.ASSISTANT, content="Инженер по разработке контента — комплексная техническая должность."),]
# Создаём движок вопросовquery_engine = index.as_query_engine( streaming=True, llm=OpenAILike( model="qwen-plus", api_base="https://dashscope.aliyuncs.com/compatible-mode/v1", api_key=os.getenv("DASHSCOPE_API_KEY"), is_chat_model=True ))
# Создаём чат-движок с поддержкой многоходового диалогаchat_engine = CondenseQuestionChatEngine.from_defaults( query_engine=query_engine, condense_question_prompt=custom_prompt, chat_history=custom_chat_history, llm=OpenAILike( model="qwen-plus", api_base="https://dashscope.aliyuncs.com/compatible-mode/v1", api_key=os.getenv("DASHSCOPE_API_KEY"), is_chat_model=True ), verbose=True # подробный вывод: видно переписанный вопрос)
# Спрашиваем только «Каковы основные обязанности?», не упоминая «инженера по разработке контента»streaming_response = chat_engine.stream_chat("Каковы основные обязанности?")for token in streaming_response.response_gen: print(token, end="")Запустив этот код, вы увидите: хотя в последнем вопросе нет слов «инженер по разработке контента», модель по истории диалога переписала его в «Каковы основные обязанности инженера по разработке контента?» — нашла нужные сведения и ответила точно.
💡 Подсказки:
CondenseQuestionChatEngineсам ведёт историю диалога; текущую историю можно посмотреть черезchat_engine.chat_history.verbose=Trueпоказывает переписанный вопрос — удобно для отладки и понимания работы системы.- Качество переписывания прямо влияет на поиск; настраивайте шаблон переписывания под свою задачу.
📝 3. Итоги урока
Заголовок раздела «📝 3. Итоги урока»В этом уроке вы изучили, как работает RAG, и своими руками построили полноценное RAG-приложение. Повторим:
- Как работает RAG. Полное RAG-приложение обычно состоит из двух этапов: построение индекса и поиск с генерацией. Построение индекса — это четыре шага: разбор документов, нарезка текста, векторизация и хранение индекса. Понимая принцип работы, вы сможете лучше улучшать и совершенствовать RAG-приложения.
- Создание RAG-приложения. С помощью высокоуровневых средств LlamaIndex вы быстро создали RAG-приложение и научились сохранять и загружать индекс.
- Многоходовый диалог в RAG. Вы узнали об особой трудности многоходового RAG (при поиске теряется контекст) и
о стандартном решении (переписать вопрос). С
CondenseQuestionChatEngineиз LlamaIndex вы сделали RAG-приложение, понимающее контекст диалога.
RAG-приложение уже неплохо отвечает на вопросы вроде «каким инструментом управления проектами пользоваться в компании», но пока его возможности базовые. В следующих уроках мы разберём, как их расширить. Следующий урок — о том, как улучшить ответы RAG-приложения с помощью промптов.
Дополнительное чтение
Заголовок раздела «Дополнительное чтение»Векторизация текста
Заголовок раздела «Векторизация текста»Компьютер не понимает напрямую, насколько похожи фразы «я люблю есть яблоки» и «обожаю есть яблоки». Зато он умеет измерять сходство двух векторов одной размерности (обычно через косинусное сходствокосинусное сходствоEN cosine similarityКосинус угла между векторами — скалярное произведение, делённое на произведение длин. Для нормированных векторов равно скалярному произведению.). Векторизация с помощью модели-эмбеддера переводит естественный язык в понятную компьютеру числовую форму.
Обучение моделей-эмбеддеров обычно включает этап контрастивного обученияконтрастивное обучениеEN contrastive learningсближаем представления «правильных пар» и раздвигаем «неправильные»: на вход подают множество пар текстов с пометкой «связаны / не связаны», и цель обучения — чтобы у связанных пар сходство векторов росло, а у несвязанных — падало.
На этапе построения индекса, если после нарезки получилось фрагментов , модель-эмбеддер превращает каждый в вектор , и они сохраняются в векторную базу.
На этапе поиска вопрос пользователя модель превращает в вектор и находит в базе самых похожих на векторов (это число вы задаёте сами); по связи «вектор — фрагмент» получаются фрагменты текста — это и есть результат поиска.
🔥 Проверьте себя
Заголовок раздела «🔥 Проверьте себя»Задачи из оригинала (одиночный выбор) с разбором. Ответ отмечайте — после ответа появится объяснение авторов курса.
Локально, без облака Alibaba (от переводчика)
Заголовок раздела «Локально, без облака Alibaba (от переводчика)»Урок можно пройти на своей машине. Нужны две замены:
# pip install llama-index llama-index-llms-openai-like llama-index-embeddings-huggingfacefrom llama_index.embeddings.huggingface import HuggingFaceEmbeddingfrom llama_index.llms.openai_like import OpenAILike
# 1) Эмбеддер — локальная модель с Hugging Face или ModelScope (многоязычная, понимает китайский и русский)embed_model = HuggingFaceEmbedding(model_name="BAAI/bge-m3")
# 2) LLM — ваш vLLM или Ollama с OpenAI-совместимым API (урок 8.1.1)llm = OpenAILike(model="Qwen/Qwen3-8B", api_base="http://localhost:8000/v1", api_key="EMPTY", is_chat_model=True)Дальше передавайте embed_model=embed_model и llm=llm вместо DashScopeEmbedding(...) и OpenAILike(... qwen-plus ...).
Документы курса лежат в репозитории (course_core/docs). С Qwen3 добавьте к запросам /no_think или выключите
размышление на сервере, иначе ответы будут медленнее (см. документацию Qwen, часть 2).