LlamaIndex: индексация данных на Claude и GPT
LlamaIndex разделяет LLM для синтеза ответов и модель для эмбеддингов — гейтвей закрывает только первую часть. Разбираем, как это настроить честно.
5 мин · · Обновлено · разработка
LlamaIndex использует две разные модели: одну — для превращения текста в эмбеддинги при построении индекса, другую — для синтеза ответа на этапе запроса. Гейтвей закрывает вторую часть полностью: и Claude, и GPT, и GLM доступны как Settings.llm. А вот эндпоинта для эмбеддингов в API нет — это стоит понять сразу, а не после часа отладки IndexError.
Разделение на Settings.llm и Settings.embed_model — это устройство самого LlamaIndex, а не особенность конкретного провайдера, но именно на нём спотыкаются те, кто первый раз подключает сторонний гейтвей: библиотека по умолчанию пытается получить оба сервиса из одного и того же OpenAI-подобного клиента, и если не задать embed_model явно, она тихо полезет за эмбеддингами напрямую в OpenAI со своим собственным ключом окружения.
LLM для запросов через OpenAI-совместимый класс
from llama_index.llms.openai_like import OpenAILike
from llama_index.core import Settings
Settings.llm = OpenAILike(
model="gpt-5.6-sol",
api_base="https://api.zukko.pro/v1",
api_key="fetch_ваш_ключ",
is_chat_model=True,
context_window=1000000,
)Класс OpenAI из llama_index.llms.openai сверяет имя модели со своей таблицей известных моделей OpenAI и на незнакомых именах спотыкается, поэтому для моделей гейтвея используйте OpenAILike (пакет llama-index-llms-openai-like). is_chat_model=True отправляет запросы в /chat/completions, а context_window задаёт лимит контекста явно (для glm-5.3 и glm-5.3-flash — 128000). api_base здесь — это полный путь с /v1, потому что класс передаёт его напрямую в конструктор openai.OpenAI(base_url=...), а тот уже сам достраивает /chat/completions. Без /v1 в api_base запрос уйдёт на несуществующий путь и вернётся 404.
LLM для запросов через Anthropic-класс
from llama_index.llms.anthropic import Anthropic
from llama_index.core import Settings
Settings.llm = Anthropic(
model="claude-opus-5-5",
base_url="https://api.zukko.pro",
api_key="fetch_ваш_ключ",
context_window=1_000_000,
)У Anthropic-класса параметр называется base_url, а не api_base, и адрес — без /v1: Anthropic-клиент сам добавляет /v1/messages. context_window стоит указать явно: LlamaIndex хранит внутреннюю таблицу известных моделей с их лимитами контекста, и для модели, которой в этой таблице ещё нет, библиотека подставляет консервативное значение по умолчанию — из-за этого движок запроса может обрезать историю диалога так, будто модель держит несколько тысяч токенов, хотя на самом деле лимит гейтвея — 1,000,000.
Почему эмбеддинги остаются отдельно
Среди эндпоинтов гейтвея — /v1/messages, /v1/chat/completions, /v1/responses, /v1/models, /v1/models/info, /v1/balance, /v1/usage, /v1/web-search — нет /v1/embeddings. Это значит, что Settings.embed_model нужно оставить на отдельном провайдере эмбеддингов, локальном или стороннем:
from llama_index.embeddings.huggingface import HuggingFaceEmbedding
Settings.embed_model = HuggingFaceEmbedding(model_name="BAAI/bge-small-en-v1.5")Гейтвей в этой связке отвечает только за LLM, который синтезирует финальный ответ (у моделей Claude и GPT — с поддержкой изображений и контекстом до 1,000,000 токенов; у GLM — 128K и без vision), — построение самого векторного индекса идёт мимо него.
Сборка индекса и запрос
Порядок здесь важен: Settings.llm и Settings.embed_model — глобальные настройки модуля, и если задать их после того, как индекс уже начал строиться в другом потоке, часть документов может проиндексироваться с настройками по умолчанию. Для скриптов пакетной индексации надёжнее выставлять оба значения в самом начале файла, до первого импорта SimpleDirectoryReader. Это особенно заметно в Jupyter-ноутбуках, где ячейки перезапускаются в произвольном порядке во время экспериментов.
from llama_index.core import VectorStoreIndex, SimpleDirectoryReader
documents = SimpleDirectoryReader("./docs").load_data()
index = VectorStoreIndex.from_documents(documents) # эмбеддинги — через Settings.embed_model
query_engine = index.as_query_engine(llm=Settings.llm)
response = query_engine.query("Что написано про лимиты API в документации?")
print(response)Веб-поиск поверх собственного индекса
Модели каталога не ходят в интернет сами — если запрос выходит за пределы проиндексированных документов, нужен отдельный POST /v1/web-search, обёрнутый в FunctionTool:
from llama_index.core.tools import FunctionTool
from llama_index.core.agent import ReActAgent
import requests
def web_search(query: str) -> str:
"""Ищет актуальную информацию в интернете, если её нет в индексе."""
resp = requests.post(
"https://api.zukko.pro/v1/web-search",
headers={"Authorization": "Bearer fetch_ваш_ключ"},
json={"query": query},
)
return resp.json()
search_tool = FunctionTool.from_defaults(fn=web_search)
query_engine_tool = FunctionTool.from_defaults(
fn=lambda q: str(query_engine.query(q)),
name="local_docs",
description="Отвечает по локальной базе документов",
)
agent = ReActAgent.from_tools([query_engine_tool, search_tool], llm=Settings.llm)
response = agent.chat("Сравни наш регламент с последними изменениями на сайте вендора")Веб-поиск и синтез ответа — два разных по тарификации вызова: первый списывается отдельно за сам факт поиска, второй — по обычным правилам чат-модели.
Параллельные запросы и список разрешённых моделей
VectorStoreIndex.from_documents может синтезировать ответы параллельно, если задать num_workers в IngestionPipeline, и это же касается запросов через query_engine с несколькими воркерами. Если ключ ограничен списком разрешённых моделей (настраивается на странице ключа в кабинете), а в пайплайне где-то по ошибке используется другая модель — например, дефолтная gpt-3.5-turbo из примера в README вместо явно заданной, — часть параллельных запросов будет падать с 403 model_not_allowed_for_key, а часть проходить, потому что Settings.llm для одних вызовов подставляется корректно, а для сторонних оберток типа SubQuestionQueryEngine — нет, если модель не передана в них явно на каждом уровне.
Модель не найдена
Примеры из старой документации LlamaIndex часто ссылаются на text-davinci-003 или gpt-4:
{"error": {"message": "The model 'text-davinci-003' does not exist", "code": "model_not_found"}}Актуальный список без ключа:
curl https://api.zukko.pro/v1/models429 при массовой индексации
as_query_engine с включённым response_mode="tree_summarize" может отправить десятки запросов подряд на один большой корпус документов — это ближе к нагрузочному тесту, чем к обычному чату. Если на ключе в кабинете настроен дневной или месячный лимит расхода, такой прогон может упереться в key_daily_limit_reached посреди индексации. Дневной лимит сбрасывается в 00:00 UTC. Паузы между батчами тут не помогут: лимит ключа считается в долларах, а своего ограничения скорости у шлюза нет (429 без кодов лимита ключа приходит от провайдера). Для батчевых задач стоит либо поднять или снять лимит с рабочего ключа, либо разбить индексацию на части под дневной бюджет.
Проверка перед запуском пайплайна
curl https://api.zukko.pro/v1/chat/completions \
-H "Authorization: Bearer fetch_ваш_ключ" \
-H "Content-Type: application/json" \
-d '{"model":"gpt-5.6-sol","messages":[{"role":"user","content":"ping"}]}'Если ответ пришёл, но query_engine.query() всё равно падает — проверьте, что Settings.embed_model вообще задан: без него LlamaIndex попытается по умолчанию обратиться к OpenAI напрямую за эмбеддингами и упадёт с ошибкой авторизации, которая на первый взгляд выглядит как проблема с ключом гейтвея, хотя ключ гейтвея тут ни при чём. Баланс и списания видно через GET /v1/balance и GET /v1/usage, ключи выпускаются в кабинете https://zukko.pro/app/api-keys. Если индексация идёт в CI и падает без внятной причины именно там, а не на локальной машине — проверьте, что переменная с ключом вообще передана в окружение раннера, а не только в .env, который CI не читает по умолчанию.
zukko
Платите в 25×
меньше
за Claude Code & ChatGPT
Один ключ fetch_* и base URL api.zukko.pro — в Cursor, Claude Code, Codex и любом SDK.
Похожие статьи
- Промпт-инъекция: что это и как защитить бота и агентаОбъясняем, как работает промпт-инъекция через пользовательский ввод и чужие документы, и какие меры снижают ущерб: изоляция данных, права инструментов, лимиты ключа.
- Свой прокси к нейросети на Node.js: не показывать API-ключ во фронтендеНебольшой сервер на Express между вашим фронтендом и API нейросети: ключ остаётся на сервере, а модели, длина запроса и частота ограничены.
- DeepSeek API на Python: запросы, стриминг, JSON и обработка ошибокГотовый код для DeepSeek V4.1 Flash и V4 Pro на OpenAI SDK: запрос, стриминг, разбор JSON и аккуратная обработка ошибок.
- Промпт-инжиниринг: примеры и 12 готовых шаблонов для работыДвенадцать шаблонов промптов для рабочих задач: что в них написано, почему именно так и сколько стоит один вызов по API.