5.4 · Проект 2: версия на LlamaIndex, сравнение
Неделя 5 · RAG · ⏱ ~5 ч · Проект
Зачем это AI-инженеру
Заголовок раздела «Зачем это AI-инженеру»В вакансиях часто встречаются LlamaIndex и LangChain, и на собеседовании спросят: «вы пользовались фреймворками? когда бы вы их не взяли?». Хороший ответ — не «да, знаю», а «я собрал один и тот же RAG руками и на LlamaIndex и вот что увидел в цифрах». Фреймворк ускоряет старт и даёт готовые интеграции, но прячет детали и усложняет отладку. Цель главы — получить собственное, обоснованное мнение.
Что такое LlamaIndex
Заголовок раздела «Что такое LlamaIndex»LlamaIndex — Python-фреймворк для приложений «LLM + свои данные». Он даёт готовые блоки для каждого этапа той самой цепочки из гл. 4.2 (загрузка → chunking → эмбеддинги → хранилище → поиск → ответ) и общие интерфейсы, чтобы блоки менять местами (другая векторная БД, другая модель эмбеддингов, другая LLM — замена одной строки).
Всё это мы уже написали руками за недели 4–5, поэтому проще всего знакомиться с фреймворком через соответствие нашей версии и его абстракций:
| Наша ручная версия | LlamaIndex |
|---|---|
Страница kb/ + метаданные из front matter |
Document (text + metadata, стабильный id_) |
| Чанк | Node / TextNode (с метаданными и связями с документом и соседями) |
chunk.py (по заголовкам) |
Node parser: MarkdownNodeParser, SentenceSplitter и др. |
ingest.py с дедупликацией по хэшу |
IngestionPipeline (transformations + docstore + vector store, кэш) |
embed.py |
Embedding model (Settings.embed_model), интеграции OpenAI/HuggingFace/… |
store.py (sqlite-vec) |
Vector store (встроенный в памяти, Qdrant, pgvector, Chroma, …) |
search() с фильтром по категории и без архива |
Retriever + MetadataFilters |
| BM25 + RRF | BM25Retriever + QueryFusionRetriever (режим reciprocal rank) |
rerank() |
Node postprocessor (например, SentenceTransformerRerank) |
plan_query (multi-query) |
QueryFusionRetriever с генерацией запросов; HyDE — HyDEQueryTransform |
answer() с [n] |
Response synthesizer / Query engine; CitationQueryEngine |
ask() |
index.as_query_engine(...) / RetrieverQueryEngine |
Многие интеграции живут в отдельных пакетах (llama-index-embeddings-huggingface, llama-index-llms-anthropic, llama-index-retrievers-bm25, llama-index-vector-stores-qdrant …) — ставим только нужные. API фреймворка меняется быстрее, чем API провайдеров: все примеры ниже иллюстративны, импорты и имена классов сверяем с текущей документацией.
Минимальный RAG на LlamaIndex
Заголовок раздела «Минимальный RAG на LlamaIndex»# Illustrative: verify imports and class names against docs.llamaindex.aiimport osfrom llama_index.core import Document, Settings, VectorStoreIndexfrom llama_index.core.node_parser import MarkdownNodeParserfrom llama_index.embeddings.huggingface import HuggingFaceEmbeddingfrom llama_index.llms.anthropic import Anthropic
Settings.embed_model = HuggingFaceEmbedding(model_name=os.environ["EMBED_MODEL"])Settings.llm = Anthropic(model=os.environ["ANSWER_MODEL"])
docs = [ Document(text=page.text, id_=page.doc, # doc = "kb/returns.md", stable metadata={"category": page.category, "archived": page.archived, "doc": page.doc}) for page in load_pages("kb") # reuse rag/load.py from AIL-20]nodes = MarkdownNodeParser().get_nodes_from_documents(docs)index = VectorStoreIndex(nodes)
engine = index.as_query_engine(similarity_top_k=5)resp = engine.query("Можно ли вернуть вскрытый чай?")print(resp) # answer textfor sn in resp.source_nodes: # retrieved chunks with scores print(sn.score, sn.node.metadata["doc"])Десяток строк — и рабочий RAG. Это и есть главный аргумент фреймворков. Обратим внимание: мы переиспользуем свой load.py из гл. 4.4 (front matter → метаданные) — честное сравнение касается chunking/индекса/поиска/ответа, а не чтения файлов. И сразу видна первая разница: MarkdownNodeParser ничего не знает о наших явных якорях {#opened-tea-returns}, поэтому ссылки kb/returns.md#... для цитат придётся достраивать самим.
Ingestion pipeline и дедупликация
Заголовок раздела «Ingestion pipeline и дедупликация»VectorStoreIndex(nodes) в минимальном примере строит индекс заново при каждом запуске. Для базы знаний, которую правят и переиндексируют, нужен аналог нашего ingest.py. IngestionPipeline применяет цепочку преобразований (парсер → извлечение метаданных → эмбеддинги) и, если подключить docstore, при повторной загрузке пропускает документы с тем же id_ и тем же хэшем содержимого, а изменённые — обновляет. Это аналог нашей логики doc_hash из гл. 4.4. Ключевое условие — стабильный id_ документа (путь страницы kb/returns.md), иначе дедупликация не сработает — та же ошибка, что uuid4() в ключе чанка из гл. 4.4.
Гибрид, фильтры, реранкинг
Заголовок раздела «Гибрид, фильтры, реранкинг»Минимальный пример ищет только по векторам. Соберём из блоков LlamaIndex ту же конфигурацию поиска, что победила в гл. 5.2:
# Illustrative: check the current names in docs.llamaindex.aifrom llama_index.core.retrievers import QueryFusionRetrieverfrom llama_index.core.postprocessor import SentenceTransformerRerankfrom llama_index.core.query_engine import RetrieverQueryEnginefrom llama_index.core.vector_stores import MetadataFilters, ExactMatchFilterfrom llama_index.retrievers.bm25 import BM25Retriever
filters = MetadataFilters(filters=[ ExactMatchFilter(key="category", value="returns"), ExactMatchFilter(key="archived", value=False), # skip returns-2025.md])returns_nodes = [n for n in nodes if n.metadata["category"] == "returns" and not n.metadata["archived"]]vector_r = index.as_retriever(similarity_top_k=20, filters=filters)bm25_r = BM25Retriever.from_defaults(nodes=returns_nodes, similarity_top_k=20)
fusion = QueryFusionRetriever( [vector_r, bm25_r], mode="reciprocal_rerank", # RRF, as in AIL-22 num_queries=3, # LLM generates extra queries, as in AIL-60 similarity_top_k=20,)rerank = SentenceTransformerRerank(model=os.environ["RERANK_MODEL"], top_n=5)engine = RetrieverQueryEngine.from_args(fusion, node_postprocessors=[rerank])Каждая строка соответствует главе плана: фильтр по категории и архиву (гл. 4.4/60), BM25 + RRF (гл. 5.2), генерация запросов (гл. 5.1), реранкер (гл. 5.2). Мы понимаем, что происходит внутри, — потому что делали это руками. Тот, кто начал с фреймворка, видит здесь только магические параметры. Заметим и мелочь: BM25-retriever не знает о фильтрах векторного индекса, поэтому returns_nodes фильтруем сами — забудем, и архивные правила возврата вернутся в выдачу через BM25.
Поиск собран из готовых блоков почти без потерь. С ответом сложнее. CitationQueryEngine нарезает найденные куски на пронумерованные источники и просит модель ставить ссылки [n] — похоже на наш гл. 5.3. Но промпт внутри — чужой. Чтобы добиться нашего поведения (фиксированная фраза отказа и передача вопроса человеку, правило про расхождение страниц — например, порог бесплатной доставки в delivery.md и promo.md, ответ на языке вопроса), придётся найти и переопределить шаблон промпта. Это типичный момент, где фреймворк начинает мешать: нужно знать, какой шаблон за что отвечает и как его подменить.
Отладка и наблюдаемость
Заголовок раздела «Отладка и наблюдаемость»Чтобы подменить чужой шаблон, сначала нужно его увидеть. Это частный случай главного вопроса при отладке RAG: «что именно ушло в модель?». В ручной версии это переменная, которую можно напечатать. В LlamaIndex — внутренности query engine. Инструменты:
resp.source_nodes— что было найдено и с какими скорами;- подробное логирование (
loggingуровня DEBUG) — видны шаги и промпты; - встроенная система callbacks/instrumentation и интеграции с внешними сервисами трассировки (подробнее — в главах про наблюдаемость и production);
engine.get_prompts()— посмотреть промпт-шаблоны, которые использует движок, иupdate_prompts()— подменить.
Как сравнивать честно
Заголовок раздела «Как сравнивать честно»Теперь у нас два пайплайна для одной задачи. Чтобы вывод «какой оставить» опирался на цифры, а не на впечатление, сравнение планируем заранее. Критерии и как их измерить:
| Критерий | Как измерить |
|---|---|
| Качество поиска | hit@3, hit@5, MRR на тех же 20 вопросах (resp.source_nodes → та же функция must_contain) |
| Качество ответа | та же ручная таблица из гл. 5.3 на 13 вопросах (корректность, цитаты, отказ) |
| Объём кода | строки своего кода без тестов (wc -l или cloc) для эквивалентной функциональности |
| Скорость | латентность p50 на вопрос; время индексации |
| Понятность при отладке | берём 2 неудачных вопроса и засекаем, сколько минут ушло, чтобы понять причину в каждой версии |
| Зависимости | число пакетов в окружении (`pip list |
Правила честного сравнения:
- Одни и те же входы: те же страницы
kb/с теми же метаданными, та же модель эмбеддингов, та же LLM, тот же реранкер. Иначе сравниваются модели, а не код. - Сопоставимый chunking.
MarkdownNodeParserрежет не так, как нашchunk.py(breadcrumbs вродеТовары > Лампа «Свет» > Гарантия, якоря, целые таблицы тарифов). Либо добавляем breadcrumbs иurlв метаданные/текст узлов, либо явно отмечаем разницу в выводах. Лучше — оба варианта: «из коробки» и «настроенный». - Одинаковые k на каждом этапе.
- Одинаковый golden set и функция оценки.
Когда фреймворк, когда руками
Заголовок раздела «Когда фреймворк, когда руками»| Фреймворк хорош | Ручная версия лучше |
|---|---|
| Прототип за день, нужно показать идею | Небольшой пайплайн, который надо понимать до строки |
| Много источников данных (готовые loaders) | Нестандартный chunking, особые метаданные (archived, якоря) |
| Нужно быстро перебрать векторные БД / модели | Жёсткие требования к латентности, контролю промптов |
| Команда уже на нём | Нужна простая отладка и минимум зависимостей |
| Нужны готовые продвинутые режимы (агенты, роутинг) | Обучение: понять, как всё устроено |
Частый зрелый ответ: «прототип на фреймворке, а критичный путь — на тонком собственном коде поверх SDK провайдера» — или гибрид, где из фреймворка берут только отдельные полезные куски (loaders, parsers). Вывод должен опираться на собственные цифры, а не на эту таблицу.
DeepSeek и Ollama: как это сделать у нас
Заголовок раздела «DeepSeek и Ollama: как это сделать у нас»Смена провайдера в LlamaIndex — это замена Settings.embed_model и Settings.llm. Для честного сравнения обе версии должны использовать те же модели, что и ручная: эмбеддинги из Ollama (или Sentence Transformers), генерацию на DeepSeek. База знаний публичная, поэтому отправлять её куски в DeepSeek можно; настоящие сообщения клиентов в облако не уходят ни в одной версии — тестовые вопросы выдуманы.
Эмбеддинги: Ollama или Hugging Face
Заголовок раздела «Эмбеддинги: Ollama или Hugging Face»# Illustrative: uv add llama-index-embeddings-ollama (pin the version)from llama_index.embeddings.ollama import OllamaEmbedding
Settings.embed_model = OllamaEmbedding( model_name=os.environ["EMBED_MODEL"], # e.g. bge-m3, same as the manual version base_url="http://localhost:11434",)Если ручная версия эмбеддит через Sentence Transformers, оставляем HuggingFaceEmbedding из «Теории» с той же моделью. Бэкенд должен совпадать с ручной версией, иначе сравниваются модели, а не фреймворк (глава 4.1: ollama:bge-m3 и st:BAAI/bge-m3 — не одно и то же).
LLM: DeepSeek — два пути
Заголовок раздела «LLM: DeepSeek — два пути»- Отдельная интеграция
llama-index-llms-deepseek(from llama_index.llms.deepseek import DeepSeek; ключ изDEEPSEEK_API_KEY). Осторожно: на ноябрь 2026 её страница в документации LlamaIndex показывает старые имена моделей (deepseek-chat,deepseek-reasoner), а в API DeepSeek сейчасdeepseek-flashиdeepseek-v4-pro. Проверяем, что интеграция принимает текущее имя модели и умеет передаватьthinking(если нет — переходим ко второму пути). OpenAILikeиз пакетаllama-index-llms-openai-like— тонкая обёртка для любых OpenAI-совместимых API:model,api_base="https://api.deepseek.com",api_key,is_chat_model=True,context_window. Она не проверяет имя модели по списку OpenAI, поэтому с новыми именами DeepSeek проблем меньше. Обычный классOpenAIиз LlamaIndex сapi_baseDeepSeek может падать на незнакомом имени модели.
Как выключить рассуждение DeepSeek через обёртку LlamaIndex, зависит от версии интеграции: ищем в ней параметр для дополнительного тела запроса (extra_body или аналог) и проверяем фактический запрос в DEBUG-логах. Это хороший пример пункта «чем мешает фреймворк» для таблицы сравнения.
LLM: Ollama
Заголовок раздела «LLM: Ollama»# Illustrative: uv add llama-index-llms-ollama (pin the version)from llama_index.llms.ollama import Ollama
Settings.llm = Ollama( model=os.environ["ANSWER_MODEL_LOCAL"], # e.g. qwen3:4b on an 8 GB machine request_timeout=120.0, # default is 30 s; local generation can be slower context_window=8000, # limit memory use; must fit 5 chunks + prompt)Память: на 8 GB RAM — модель класса 3–4B (по умолчанию qwen3:4b, ~2.5 GB), на 16 GB — 7–8B, на 32 GB и больше — 14B и крупнее. На 8 GB 7–8B — только когда браузер, IDE и всё тяжёлое закрыто. context_window здесь влияет не только на качество, но и на память: чем больше окно, тем больше RAM. LlamaIndex-версия на Ollama-LLM держит в памяти сразу эмбеддинг-модель, генератор и реранкер. Смотрим ollama ps и системный монитор, прежде чем запускать eval на 20 вопросах.
По странице интеграции у Ollama есть ещё thinking (рассуждение thinking-моделей) и json_mode.
Скрытые вызовы LLM стоят по-разному
Заголовок раздела «Скрытые вызовы LLM стоят по-разному»QueryFusionRetriever(num_queries=3) и некоторые синтезаторы ответа зовут LLM неявно. На DeepSeek это деньги и сетевые задержки (и рассуждение по умолчанию, если его не выключить), на Ollama — секунды нашей машины. Логируем число вызовов LLM на вопрос в обеих версиях — это строка в таблице сравнения.
Сравнение провайдеров для этой главы
Заголовок раздела «Сравнение провайдеров для этой главы»| Что нужно | Claude | OpenAI | DeepSeek | Ollama |
|---|---|---|---|---|
| LLM-интеграция LlamaIndex | llama-index-llms-anthropic |
llama-index-llms-openai |
llama-index-llms-deepseek (проверить имена моделей) или OpenAILike |
llama-index-llms-ollama |
| Эмбеддинги в LlamaIndex | Нет своих | llama-index-embeddings-openai |
Нет → Ollama / Hugging Face | llama-index-embeddings-ollama |
| Особые параметры через обёртку | — | — | thinking — проверить, как передать |
thinking, json_mode, request_timeout, context_window |
| Дефолт фреймворка, если ничего не задать | — | LlamaIndex по умолчанию тянется к OpenAI и ищет его ключ | — | — |
Что использовать в практике
Заголовок раздела «Что использовать в практике»Основной вариант — ручная и LlamaIndex-версии на одинаковых Ollama-эмбеддингах и DeepSeek-генерации. LlamaIndex на Ollama-LLM — полностью офлайн-вариант, по желанию. Claude через llama-index-llms-anthropic из «Теории» — «для сравнения». Settings.llm и Settings.embed_model задаём явно всегда: иначе фреймворк может пойти к дефолтному провайдеру.
Маршрут изучения
Заголовок раздела «Маршрут изучения»- Открываем LlamaIndex docs → Getting Started: установка и starter tutorial (≈40 мин). Делаем starter на 3 страницах своей
kb/, не на их примере. Обратим внимание на структуру пакетов (core + интеграции) иSettings. - Там же → раздел High-Level Concepts / Understanding: Loading, Indexing, Querying (≈30 мин). Сопоставляем каждый концепт со строкой таблицы соответствий выше.
- Там же → Ingestion Pipeline: transformations, docstore, кэш, управление документами (дедупликация по
id_) (≈30 мин). Выносим, как добиться, чтобы повторная загрузка не дублировала узлы. - Там же → Query Engine и Retrievers:
as_query_engine,RetrieverQueryEngine, metadata filters, BM25 retriever, Query Fusion (reciprocal rerank), node postprocessors / rerankers, Citation Query Engine (≈45 мин). Находим раздел про кастомизацию промптов (get_prompts/update_prompts). - Там же → раздел про observability / logging (≈15 мин): как увидеть промпт, который ушёл в модель.
- Там же → интеграции: Ollama Embeddings, Ollama LLM, DeepSeek LLM и справка по
OpenAILikeв API reference (≈25 мин). Выносим имена пакетов, классов и параметров. Сверяем имена моделей в примерах с текущей страницей моделей DeepSeek.
На что обратить внимание
Заголовок раздела «На что обратить внимание»- Версии и импорты. Половина примеров в интернете — со старыми путями импорта (до разделения на
llama_index.coreи пакеты интеграций). Доверяем только текущей документации; версии фиксируем вrequirements-llamaindex.txt. - Скрытые вызовы LLM. Некоторые компоненты (генерация запросов в fusion, извлечение метаданных, некоторые синтезаторы ответа) зовут LLM неявно — это стоимость и латентность. Смотрим логи.
- Дефолтная LLM / эмбеддинги. Если не задать
Settings.llmиSettings.embed_model, фреймворк может попытаться использовать дефолтного провайдера и искать соответствующий ключ в окружении. Задаём явно. - Режим синтеза ответа. Некоторые режимы делают несколько вызовов LLM на один вопрос (например, уточняют ответ кусок за куском). Выбираем тот, что соответствует ручной версии (один вызов со всеми кусками), иначе сравнение латентности нечестное.
- Чужие промпты. Поведение отказа и формат цитат по умолчанию не наши — без подмены шаблонов eval гл. 5.3 может упасть не из-за поиска.
- Архив в выдаче. Фильтр
archivedнужно повторить везде, где узлы попадают в поиск: вMetadataFiltersвекторного retriever’а и в списке узлов для BM25. Иначе отменённые правила изreturns-2025.mdвсплывут в одной из версий, и сравнение покажет разницу, которой нет. - Индекс в памяти по умолчанию — при перезапуске всё пересчитывается. Для честного замера скорости индексации и запроса используем persist или внешний vector store.
- Ключи не в коде и не в логах: при DEBUG-логах полные запросы с ключами не сохраняем.
Практика
Заголовок раздела «Практика»Папка projects/02-support-kb-rag/llamaindex_version/, отдельный requirements-llamaindex.txt (чтобы не засорять основное окружение — или отдельный venv).
-
Starter.
li_starter.py: 3 страницыkb/,VectorStoreIndex,as_query_engine, один вопрос («можно ли вернуть вскрытый чай?»). Печатаемsource_nodes. -
Провайдеры. В
li_settings.pyявно задаёмSettings.embed_model(OllamaOllamaEmbeddingилиHuggingFaceEmbedding— тот же бэкенд и модель, что в ручной версии) иSettings.llm(DeepSeek черезOpenAILikeилиllama-index-llms-deepseek). Версии пакетов — зафиксированы вrequirements-llamaindex.txt. В DEBUG-логе проверяем, что запросы уходят наapi.deepseek.comиlocalhost:11434, а не в OpenAI. -
Ingestion.
li_ingest.py: те же страницыkb/черезload.py,Documentсid_=docи метаданнымиcategory,archived,doc; у узлов —url(kb/<file>.md#<anchor>) иsection;IngestionPipelineс парсером и эмбеддингами той же модели, что в ручной версии; persist на диск. Проверяем: второй запуск не добавляет узлов. -
Поиск.
li_search.py: retriever с фильтром по категории и без архива, BM25 retriever, fusion (RRF) и реранкер — та же конфигурация, что победила в гл. 5.2. Подключаем свои правила определения категории из гл. 5.1 (переиспользуем код). -
Ответ.
li_answer.py:CitationQueryEngineилиRetrieverQueryEngineс подменённым промпт-шаблоном (наш системный промпт из гл. 5.3, фраза отказа). Черезget_prompts()убеждаемся, что шаблон действительно подменён. -
Eval тем же скриптом. Адаптер
li_adapter.py: функцииretrieve(question) -> list[str]иask(question) -> Answerс той же сигнатурой, что в ручной версии, — чтобыeval/run_retrieval_eval.pyи таблица гл. 5.3 запускались без изменений. -
Замеры. Строки кода (
cloc rag/vscloc llamaindex_version/), латентность p50, время полной индексации, число пакетов в окружении. -
Отладочный эксперимент. Выбираем 2 вопроса, где хотя бы одна версия ошиблась (кандидаты: стоимость доставки 3 кг в другой город — таблица тарифов; гарантия на лампу «Свет» против «Свет мини»). В каждой версии находим причину и засекаем время. Записываем, какие инструменты помогли.
-
Раздел README «Ручная версия vs LlamaIndex»:
Критерий Ручная LlamaIndex (из коробки) LlamaIndex (настроенный) hit@3 / hit@5 / MRR Ответы: корректно / с цитатами / отказы (из 13) Строк кода p50 латентность Время индексации Время на отладку 2 ошибок Зависимостей Вызовов LLM на вопрос И 5–10 предложений вывода: какую версию оставляем основной и почему, что из фреймворка понравилось и что мешало (включая то, насколько легко было подключить DeepSeek и Ollama и передать специфичные параметры вроде выключения рассуждения).
-
(Для сравнения, по желанию) Заменяем
Settings.llmна Claude (llama-index-llms-anthropic) и прогоняем 13 вопросов: сколько строк кода поменялось и как изменились ответы?
Готово, когда
Заголовок раздела «Готово, когда»- LlamaIndex-версия работает на той же базе знаний
kb/, с фильтром по категории, исключением архива, гибридом и реранкером, отвечает со ссылками на разделы и отказывает на вопросах вне базы знаний. - Обе версии прогнаны тем же eval-скриптом на тех же 20 (поиск) и 13 (ответы) вопросах.
- В README есть раздел «Ручная версия vs LlamaIndex» с таблицей и обоснованным выводом, какую версию оставить и почему.
Тест для самопроверки
Заголовок раздела «Тест для самопроверки»Вопрос 1
Как в LlamaIndex называются аналоги наших «страницы» и «чанка»?
Document (страница с метаданными и id_) и Node/TextNode (чанк).
Вопрос 2
Что делает IngestionPipeline с docstore при повторной загрузке, и какое условие для этого обязательно?
Пропускает документы с тем же id_ и неизменённым содержимым, обновляет изменённые. Обязательно — стабильный id_ у документа (например, путь страницы kb/returns.md).
Вопрос 3
Какой компонент LlamaIndex соответствует нашему BM25 + RRF из гл. 5.2?
QueryFusionRetriever в режиме reciprocal rerank поверх векторного retriever’а и BM25Retriever.
Вопрос 4
Почему для честного сравнения нужно использовать одну и ту же модель эмбеддингов и LLM в обеих версиях?
Иначе разница в качестве и скорости объясняется моделями, а не кодом/фреймворком — сравнение теряет смысл.
Вопрос 5
Что не так в этом коде?
docs = [Document(text=p.text, metadata={"category": p.category}) for p in pages]pipeline = IngestionPipeline(transformations=[parser, embed_model], docstore=docstore)pipeline.run(documents=docs) # runs every nightУ Document нет стабильного id_ (он генерируется случайно), поэтому docstore не узнает документ при повторном запуске, и каждую ночь узлы будут дублироваться; также в метаданных нет archived (архивные правила возврата попадут в поиск) и doc/url, нужных для цитат.
Вопрос 6
Где LlamaIndex чаще всего «мешает» в задаче RAG с цитатами?
b. Поведение по умолчанию задают встроенные шаблоны промптов; чтобы получить свой отказ и цитаты, их надо найти и подменить.
Вопрос 7
Как увидеть, какой именно промпт LlamaIndex отправил в модель?
Через engine.get_prompts() (шаблоны), подробное логирование/инструментацию (фактические вызовы) и source_nodes (что было в контексте).
Вопрос 8
Почему LlamaIndex-версия может оказаться медленнее ручной при той же конфигурации поиска? Достаточно двух причин.
Скрытые дополнительные вызовы LLM (генерация запросов, многошаговый синтез ответа); индекс в памяти без persist, пересчитываемый при старте; накладные расходы абстракций и другая конфигурация по умолчанию.
Вопрос 9
В каких ситуациях стоит выбрать фреймворк, а в каких — ручную версию? По одному примеру.
Фреймворк — быстрый прототип с множеством источников данных или перебор векторных БД; ручная — небольшой критичный пайплайн, где нужны полный контроль промптов, простая отладка и минимум зависимостей.
Вопрос 10
Почему в интернете много неработающих примеров LlamaIndex и как от этого защититься?
API фреймворка быстро меняется (например, разделение на llama_index.core и пакеты интеграций), старые примеры используют устаревшие импорты. Защита — только текущая документация и фиксированные версии в requirements.
Вопрос 11
DeepSeek подключили через класс OpenAI из LlamaIndex, указав api_base="https://api.deepseek.com" и model="deepseek-flash", и получили ошибку про неизвестную модель. Что использовать вместо этого?
OpenAILike (llama-index-llms-openai-like) с api_base, api_key, is_chat_model=True — он не проверяет имя по списку моделей OpenAI. Или интеграцию llama-index-llms-deepseek, если она принимает текущие имена моделей.
Вопрос 12
Settings.embed_model не задан, а ключа OpenAI в окружении нет. Что, скорее всего, произойдёт, и почему для стека «DeepSeek + Ollama» это важно?
Фреймворк попробует дефолтного провайдера эмбеддингов (OpenAI) и упадёт без ключа, а с ключом молча отправит тексты в OpenAI. В нашем стеке эмбеддинги должны быть локальными (у DeepSeek их нет), поэтому Settings.embed_model задают явно.
Вопрос 13
Почему LlamaIndex-версия должна эмбеддить через тот же бэкенд (Ollama или Sentence Transformers), что и ручная, а не просто «ту же модель bge-m3»?
Сборки одной и той же модели в Ollama и в Sentence Transformers — разные рантаймы, и их векторы могут отличаться. Если бэкенды разные, разница в цифрах может объясняться ими, а не фреймворком.
Что дальше
Заголовок раздела «Что дальше»Остался финальный шаг проекта: 5.5 · Проект 2: README и GitHub — чтобы проект можно было запустить с нуля и показать на собеседовании.