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-совместимый класс

bash
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-класс

bash
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 нужно оставить на отдельном провайдере эмбеддингов, локальном или стороннем:

bash
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-ноутбуках, где ячейки перезапускаются в произвольном порядке во время экспериментов.

bash
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:

bash
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:

bash
{"error": {"message": "The model 'text-davinci-003' does not exist", "code": "model_not_found"}}

Актуальный список без ключа:

bash
curl https://api.zukko.pro/v1/models

429 при массовой индексации

as_query_engine с включённым response_mode="tree_summarize" может отправить десятки запросов подряд на один большой корпус документов — это ближе к нагрузочному тесту, чем к обычному чату. Если на ключе в кабинете настроен дневной или месячный лимит расхода, такой прогон может упереться в key_daily_limit_reached посреди индексации. Дневной лимит сбрасывается в 00:00 UTC. Паузы между батчами тут не помогут: лимит ключа считается в долларах, а своего ограничения скорости у шлюза нет (429 без кодов лимита ключа приходит от провайдера). Для батчевых задач стоит либо поднять или снять лимит с рабочего ключа, либо разбить индексацию на части под дневной бюджет.

Проверка перед запуском пайплайна

bash
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.