xllm 扩展:把大模型接进脚本
一次调用、一套 API、三种服务商方言。xllm 负责请求序列化、SSE 流解码与重试;网络与 TLS 由宿主 xrt 提供,脚本里只有业务。
本章导读
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 系列档案,指针借用、不可变:
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:
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_GLM | Chat Completions(GLM 子方言) | open.bigmodel.cn/api/paas/v4 |
XLLM_PROVIDER_OPENAI_COMPAT | Chat Completions(兼容端点) | 任意兼容网关 |
XLLM_PROVIDER_OPENAI_RESPONSES | OpenAI Responses | api.openai.com/v5 |
XLLM_PROVIDER_ANTHROPIC | Anthropic Messages | api.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——密钥只存在于进程内存。
- 症状
- RequestProc 里直接 Complete,慢模型把 worker 卡住数十秒,整个服务吞吐塌陷。
- 原因
- Complete 是阻塞调用(含重试退避),跑在引擎 worker 线程等于占住协议驱动。正确姿势:回调立即返回,调用移到后台线程(ch41 的线程边界),或用 Start/Future 异步化,结果经应用侧通道写回。
编程练习
不联网跑通 34.2/34.3 的探针:档案 → 客户端 → 请求 → BuildRequestJson。
环境变量取 key,Complete 发一次真实请求,打印回复文本、finish 原因与用量;再用坏 key 复演错误路径。
xllmErrorRetryable 为假;密钥不出现在源码。封装 POST /ai/ask:body 的问题进 request,回复组 JSON 返回;key 走 xs.json Custom;调用在后台线程。
小结
| 要点 | 结论 |
|---|---|
| 定位 | 调用库:序列化/方言/流式/重试;不执行工具、不记历史 |
| 档案 | 非机密能力契约:窗口/上限/能力位,创建即校验 |
| 调用 | Complete 一步式;Start/Future 异步 + 双路取消 |
| 所有权 | 库分配的 Destroy/xllmFree;config 字符串全是借用 |
| 纪律 | 密钥走环境/Custom;阻塞调用离开引擎 worker |
- xrt 参考 · future — 异步调用背后的 future/cancel 模型