第 34 章 / 共 51 章
卷五 · 扩展、运维与实战

xllm 扩展:把大模型接进脚本

一次调用、一套 API、三种服务商方言。xllm 负责请求序列化、SSE 流解码与重试;网络与 TLS 由宿主 xrt 提供,脚本里只有业务。

本章导读

站位:与 ch32(sqlite)、ch33(xtp2)并列的第三个内置扩展。xllm 是调用库——它替你组装请求、说服务商的方言、拆流式响应;它不做代理框架:不执行工具、不管理对话历史。前置:build.bat xllm 变体。

34.1 定位与边界

xllm(vendored 自 xrt extlibs,核心层 v3.0.0)解决的是"和 LLM 服务说话"的全部脏活:

  • 三方言:Chat Completions(OpenAI 兼容 / GLM)、OpenAI Responses、Anthropic Messages——同一套请求 API,eProvider 一个枚举切换
  • SSE 流式:分帧、重组、增量回调;非流式只是没有回调的特例
  • 重试与限速:可重试错误自动退避重试,尊重 Retry-After;诊断结构告诉你试了几次
  • 取消:接 xrt 取消体系(xcancel),请求级 deadline

边界同样明确:它不执行工具(tool call 返回给你,执行是你的事)、不记对话历史(每请求无状态,历史与压缩是上层的事——xrt 的 xllm-session/xllm-memory 分层未编入本变体)。网络、TLS、JSON、取消全部走宿主内建 xrt,脚本不需要链接任何额外运行时。

34.2 客户端与模型档案

第一步永远是模型档案(model profile)——一份非机密的能力契约:上下文窗口、输入/输出上限、能力位(工具调用、流式、视觉输入……)。内置 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");           /* 借用指针!生产走 xs.json Custom */
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("tokens: %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 路由回调里收请求 → 后台线程(ch41 的边界纪律)里 Complete → 结果写回。阻塞的 Complete 不要跑在引擎 worker 回调里。

34.4 方言、流式与重试

Provider方言典型 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 里的字符串、profile、引擎借用调用方管理,须活得比 client 久

线程边界:客户端可跨线程使用(内部有连接池与锁),但单个 xllm_call 不并发;future 是借用的——Wait/Destroy 之前有效。变体隔离与 sqlite/xtp2 同律:default 变体没有 <xllm.h>(编译期报缺头文件),脚本用 #ifdef XS_USE_XLLM 探测,嵌套 TCC 环境同样可见。

常见错误

密钥进了源码或仓库
症状
API key 硬编码在 main.c,随代码入库/打包泄漏。
原因
sApiKey 是借用指针,写起来太顺手。正确姿势:key 放环境变量或 xs.json 的 Custom 旋钮(不进 git),ServiceInit 读取后填入 config——密钥只存在于进程内存。
在引擎回调里同步 Complete
症状
RequestProc 里直接 Complete,慢模型把 worker 卡住数十秒,整个服务吞吐塌陷。
原因
Complete 是阻塞调用(含重试退避),跑在引擎 worker 线程等于占住协议驱动。正确姿势:回调立即返回,调用移到后台线程(ch41 的线程边界),或用 Start/Future 异步化,结果经应用侧通道写回。

编程练习

基础离线全链路

不联网跑通 34.2/34.3 的探针:档案 → 客户端 → 请求 → BuildRequestJson。

验收:JSON 输出含 model 与两条 message;token 估算 > 0;default 变体下同样脚本编译失败(隔离生效)。
进阶真实调用

环境变量取 key,Complete 发一次真实请求,打印回复文本、finish 原因与用量;再用坏 key 复演错误路径。

验收:正常路径输出完整;坏 key 返回 XLLM_ERROR_AUTH 且 xllmErrorRetryable 为假;密钥不出现在源码。
挑战AI 问答路由

封装 POST /ai/ask:body 的问题进 request,回复组 JSON 返回;key 走 xs.json Custom;调用在后台线程。

验收:curl 全链路通;压测期间健康路由不受慢模型拖累;坏 key 返回结构化错误而非 500 裸崩。

小结

要点结论
定位调用库:序列化/方言/流式/重试;不执行工具、不记历史
档案非机密能力契约:窗口/上限/能力位,创建即校验
调用Complete 一步式;Start/Future 异步 + 双路取消
所有权库分配的 Destroy/xllmFree;config 字符串全是借用
纪律密钥走环境/Custom;阻塞调用离开引擎 worker
xrt 延伸阅读