Глава 34 / 51
Том 5 · Расширения, эксплуатация и практика

Расширение xllm: вызовы LLM из скриптов

Один вызов, один API, три диалекта провайдеров. xllm берёт на себя сериализацию запросов, разбор SSE-потока и повторы; сеть и TLS даёт xrt хоста — в скрипте остаётся только бизнес-логика.

Обзор

Позиционирование: третье встроенное расширение в ряду гл. 32 (sqlite) и гл. 33 (xtp2). xllm — библиотека вызова: она собирает запросы, говорит на диалектах провайдеров и пересобирает поток ответа; это не фреймворк агентов — инструменты она не исполняет и историю диалога не ведёт. Предусловие: вариант build.bat xllm.

34.1 Позиция и границы

xllm (vendored из extlibs xrt, базовый слой v3.0.0) забирает всю грязную работу «разговора с LLM-сервисами»:

  • Три диалекта: Chat Completions (OpenAI-совместимый / GLM), OpenAI Responses, Anthropic Messages — один API запросов, проводной формат переключается перечислением eProvider
  • SSE-стриминг: фреймизация, пересборка, инкрементальные колбэки; нестримовый режим — частный случай без колбэков
  • Повторы и лимиты: повторяемые ошибки уходят в автоповтор с нарастающей паузой, Retry-After уважается; структура диагностики сообщает число попыток
  • Отмена: встроена в систему отмены xrt (xcancel), плюс deadline на запрос

Границы столь же явные: она не исполняет инструменты (tool call возвращается вам — исполнение ваше дело) и не помнит историю диалога (каждый запрос лишён состояния; история и сжатие — забота верхних слоёв; xllm-session/xllm-memory из xrt в этот вариант не входят). Сеть, TLS, JSON и отмена — всё на встроенном xrt хоста; скрипт не линкует ни одного дополнительного рантайма.

34.2 Клиент и профили моделей

Первый шаг — всегда профиль модели: несекретный контракт возможностей — окно контекста, потолки входа/выхода, биты возможностей (вызовы инструментов, стриминг, ввод изображений…). Встроено семейство GLM, указатель заимствуется, профиль неизменяем:

Профиль + создание клиента (проверяется офлайн)проверено на реальной машинеисточник: пробник 34.2 этой главы
xllm_error tErr;
const xllm_model_profile* pProfile;
xllm_client_config tConf;
xllm_client* pClient;

xllmErrorInit(&tErr);
pProfile = xllmModelProfileBuiltin("glm-5.2");      /* заимствуется, без копии */
if ( pProfile == NULL ) { /* имя с ошибкой: NULL */ ... }

xllmClientConfigInit(&tConf);                      /* все значения по умолчанию */
tConf.sBaseUrl      = "https://open.bigmodel.cn/api/paas/v4";
tConf.sApiKey       = getenv("XLLM_KEY");           /* заимствованный указатель! продакшн: Custom из xs.json */
tConf.sModel        = "glm-5.2";
tConf.eProvider     = XLLM_PROVIDER_GLM;
tConf.pModelProfile = pProfile;                     /* с профилем: проверка + снимок */
pClient = xllmClientCreate(&tConf, &tErr);          /* движком владеет библиотека */

Три момента: все строки заимствуются (библиотека их не копирует; время жизни должно перекрывать клиент); движок можно разделять (заполните pNetEngine двигателем хоста/приложения — несколько клиентов его разделят; NULL — библиотека владеет своим); профиль окупается втройне — согласованность конфигурации проверяется при создании, возможности проверяются перед каждым запросом (визуальный запрос текстовой модели будет отвергнут), а xllmModelProfileSupports(pProfile, XLLM_CAP_*) питает бизнес-ветвление.

34.3 Запросы и вызовы

Запрос — стековое значение; конструирование не касается сети. Сообщения четырёх ролей — system / user / assistant / tool; текст — одна API-функция, мультимодальность — через part:

Конструирование запроса + офлайн JSON (пакет можно проверить без сети)проверено на реальной машинеисточник: пробник 34.3 этой главы
xllm_request tReq;
char* sJson;

xllmRequestInit(&tReq);
xllmRequestSetModel(&tReq, "glm-5.2");
xllmRequestAddTextMessage(&tReq, XLLM_ROLE_SYSTEM, "Ты строгий помощник.");
xllmRequestAddTextMessage(&tReq, XLLM_ROLE_USER, "Расскажи про XServer одним предложением.");
printf("токены: %llu\n",
    (unsigned long long)xllmEstimateTextTokens("Расскажи про XServer одним предложением."));

sJson = xllmClientBuildRequestJson(pClient, &tReq, &tErr);
/* {"model":"glm-5.2","messages":[{"role":"system",...},...]} */
xllmFree(sJson);                                   /* всё, что выделила библиотека — xllmFree */
xllmRequestUnit(&tReq);

Два пути вызова: одношаговый xllmClientComplete (внутри повторы и пересборка потока; возвращает полный ответ); асинхронный xllmClientStart → заимствованный xfuture* → xllmCallWait / xllmCallCancel. Вызовы инструментов: на стороне запроса инструменты объявляются xllmRequestAddTool; на стороне ответа tool call попадает в бизнес-код, результат возвращается через xllmRequestAddToolResult — исполнение всегда остаётся у вас.

Типовое разделение в xs: колбэк HTTP-маршрута принимает запрос → Complete работает в фоновом потоке (дисциплина границ из гл. 41) → результат записывается назад. Блокирующий Complete нельзя запускать в колбэке рабочего потока движка.

34.4 Диалекты, стриминг и повторы

ПровайдерДиалектТипичный baseUrl
XLLM_PROVIDER_GLMChat Completions (поддиалект GLM)open.bigmodel.cn/api/paas/v4
XLLM_PROVIDER_OPENAI_COMPATChat Completions (совместимые конечные точки)любой совместимый шлюз
XLLM_PROVIDER_OPENAI_RESPONSESOpenAI Responsesapi.openai.com/v5
XLLM_PROVIDER_ANTHROPICAnthropic Messagesapi.anthropic.com/v1

Стриминг: и xllmClientComplete/Start принимают xllm_stream_callbacks (приращения текста, рассуждений, вызовов инструментов, finish); NULL — нестримовый режим. Повторы управляются uMaxAttempts / uRetryBaseDelayMs / uRetryMaxDelayMs; при лимитах уважается Retry-After сервера. xllm_diagnostics в результате (число попыток, итоговая повторяемость) вместе с xllmErrorRetryable(&tErr) решает, ждать ли и пробовать снова на стороне бизнеса. Отмена двухканальная: на запрос — xllmRequestSetDeadline / SetCancel (xcancel), на вызов — xllmCallCancel.

34.5 Контракт

ОбъектВладениеОсвобождение
xllm_client_config / xllm_request / xllm_error / xllm_model_profile (свой)стековые значения вызывающегоUnit / нет (умирают на выходе из области)
xllm_clientвыделяет библиотекаxllmClientDestroy
xllm_callвыделяет библиотекаxllmCallDestroy (нужен и после Wait)
xllm_response / результаты BuildRequestJsonвыделяет библиотекаxllmResponseDestroy / xllmFree
строки в config/request, профили, движкизаимствуютсяуправляет вызывающий; должны жить дольше клиента

Потоки: клиент можно использовать из нескольких потоков (внутри пул соединений и блокировки), но один xllm_call не конкурентен; future — заимствованный, действителен до Wait/Destroy. Изоляция вариантов — по закону sqlite/xtp2: в варианте default нет <xllm.h> (ошибка «нет заголовка» на компиляции), скрипты проверяют #ifdef XS_USE_XLLM, вложенный TCC видит то же самое.

Частые ошибки

Ключ API утёк в исходники
Симптом
Ключ API захардкожен в main.c и утекает с каждым коммитом и пакетом.
Причина
sApiKey — заимствованный указатель, соблазн вписать его напрямую слишком велик. Правильная поза: ключ в переменной окружения или в ручке Custom из xs.json (никогда в git), читается в ServiceInit и заполняет config — секрет существует только в памяти процесса.
Синхронный Complete в колбэке движка
Симптом
RequestProc вызывает Complete напрямую; медленная модель держит worker десятки секунд — производительность сервиса рушится.
Причина
Complete — блокирующий вызов (с паузами повторов); запускать его в рабочем потоке движка — значит оккупировать протокольный драйвер. Правильная поза: колбэк возвращает управление сразу, вызов уезжает в фоновый поток (граница из гл. 41) или асинхронизируется через Start/Future, результат возвращается каналом приложения.

Упражнения

БазаОфлайн-цепочка

Без сети прогнать пробник 34.2/34.3: профиль → клиент → запрос → BuildRequestJson.

Приёмка: в JSON есть модель и два сообщения; оценка токенов > 0; тот же скрипт не компилируется в варианте default (изоляция работает).
ПродвинутыйНастоящий вызов

Ключ из переменной окружения, один настоящий вызов через Complete, печать текста ответа, причины завершения и расхода; повторить путь ошибки с плохим ключом.

Приёмка: happy path выводится полностью; плохой ключ даёт XLLM_ERROR_AUTH и xllmErrorRetryable == ложь; ключ не появляется в исходниках.
ВызовМаршрут AI-ответов

Обернуть POST /ai/ask: вопрос из тела идёт в запрос, ответ возвращается JSON; ключ — в ручке Custom из xs.json; вызов в фоновом потоке.

Приёмка: полный путь curl работает; health-маршруты не страдают при медленной модели; плохой ключ возвращает структурную ошибку, а не голый 500.

Итоги

ПунктВывод
Позициябиблиотека вызова: сериализация/диалекты/стриминг/повторы; инструменты не исполняет, историю не ведёт
Профилинесекретный контракт возможностей: окно/потолки/биты, проверка при создании
ВызовыComplete одним шагом; Start/Future асинхронно + двухканальная отмена
Владениевыделила библиотека — Destroy/xllmFree; строки конфигурации заимствуются
Дисциплинаключи через окружение/Custom; блокирующие вызовы вне worker движка
Дальнейшее чтение по xrt
  • xrt · future — модель future/cancel за асинхронными вызовами