系列:《0.8MB 跑通 Qwen:从零实现 ARM 零依赖纯 C 推理引擎》(30 天 × 90 篇) | 适配模型:Qwen3-VL-8B-Instruct(千问3_VL_8B_Instruct)· Qwen3-VL-2B-Instruct · Qwen3-30B-A3B | 测试设备:RK3588(4×Cortex-A76 + 4×Cortex-A55,aarch64)
系列总纲:《0.8MB 跑通 Qwen》30 天 90 篇 · 总纲(阿里云社区)
上一篇:20-1《极简 HTTP:socket/bind/select 轮询》| 下一篇:20-3《SSE 流式:响应怎么写一半就发给客户端》
真机实测通过:本文实验已在 RK3588 板端实测完成(2026-09;方法学与原始记录见仓库 docs 与《实验脚本》目录)
一句话导读:推理引擎的路由与自研最小 JSON:strcmp 路由表加递归下降解析器,把"URL+JSON 体"变成行为;请求把 messages 拼成 msgs,被 400 精确挡在兼容性边界的板端实测。
关键词:手搓 Qwen 推理引擎、千问大模型推理、Qwen3-VL、零依赖纯 C、JSON、路由、自研解析器、HTTP、兼容性边界
导语:连接进来之后,URL 与请求体怎么变成一次行为?这套引擎用一张 strcmp 路由表加一个递归下降的自研最小 JSON 解析器,把字段名改错的请求精确挡在 400 上,展示兼容性边界该怎样用真实请求去戳。
HTTP 砖块就位后,vllm_server.c/vllm_admin.c 负责把"URL + JSON 体"变成行为。这篇讲两件事:路由表怎么组织,以及自研 JSON 解析器为什么"够用就行"——并用板端实测演示:请求里把 messages 拼成 msgs,服务端用自研解析器给出 400 'messages' array is required。兼容性断裂被精确地挡在边界上。
1. 知识点:路由的本质是"字符串分发",JSON 的本质是"结构约定"
- 路由:HTTP 请求的 method + path 决定行为。极简引擎不需要正则路由框架——一张
strcmp表(或少量 if 链)就够:GET /health、POST /v1/chat/completions、POST /admin/api/model/load……每个 handler 一个函数签名,拿解析好的VHttpRequest返回VHttpResponse。 - JSON:OpenAI 兼容层(Day 21)与 admin 面(Day 22)都依赖 JSON。引一个 JSON 库是"简单"的,但会带进一个完整解析器 + 依赖审计面。引擎的做法是把 VJson 嵌进
vllm_http.c(一个文件里 172–463 行):对象/数组/字符串/数值/布尔,外加一套极小的 set/get 与序列化。够用阈值:只要覆盖"请求体的合法形状 + 错误能精确报出",就比引库更可控。
为什么"错误能精确报出"重要?一个自研解析器可以做到:messages 缺失 → 明确说缺 messages;角色名写错 → 甚至能宽松放行(实测见 §3)。引第三方库反而要为了一个错误信息去读库代码。这是"零依赖"工程风格的收益点之一——但它同样有边界:VJson 只支持 OpenAI 请求用到的 JSON 子集(无注释、无 NaN 等),上框架的人未必能接受,取舍写明在文件头。
2. 对应代码:路由表与解析器锚点
路由层(vllm_server.c 主分发 + vllm_admin.c 1120–1170 的 admin 路由表):
/* vllm_admin.c:GET 集合 */
if (strcmp(req->method, "GET") == 0 &&
strcmp(req->path, "/admin/api/status") == 0) {
... }
else if (strcmp(req->method, "POST") == 0 &&
strcmp(req->path, "/admin/api/model/load") == 0) {
handle_admin_model_load(...); }
JSON 解析器(vllm_http.c):
static VJson *parse_value(VJsonParser *P); // 223:类型分发
static VJson *parse_object(VJsonParser *P); // 172:{"k":v,...}
static VJson *parse_array(VJsonParser *P); // 205
static char *parse_string_body(VJsonParser *P); // 100:转义 + UTF-8
void vjson_obj_set(VJson *obj, const char *key, VJson *val); // 291
读取顺序建议:先 parse_value(类型分发)→ parse_object/parse_array(递归结构)→ parse_string_body(转义最易错的部分)。它是递归下降解析器,几十行覆盖全部需求。
3. 改动后果:字段名改错,兼容性在哪一层断
实测口径:板端 serve(模型已加载)+ x86 客户端,POST
/v1/chat/completions,2026-09-07。
① 正确字段:
{"model":"x","messages":[{"role":"user","content":"你好"}],"max_tokens":8}
→ 200,正常生成
② 字段名拼错 messages → msgs:
→ 400 {"error":{"message":"'messages' array is required","type":"invalid_request_error","code":0}}
③ 角色名写错 role → userr:
→ 200(正常回答)——解析器/兼容层对角色做了"未知即忽略"的宽容处理
④ content 缺失({"role":"user"} 无内容字段):
→ 200,空回复 finish_reason=stop(宽松放行)
四个结果拼出兼容层的真实边界:
messages是必填且结构校验:缺它就 400,错误信息直接点名字段——这是解析器最严的一层;- 角色与内容字段是宽松的:
userr与缺content都放行。前者会被当未知角色忽略,后者产出一个空回复。这不是"bug",是兼容层"对 OpenAI 客户端各种不严谨请求的容忍度"设计——真正的下游(如越权/格式攻击)还有 admin 层与 attestation 兜底(Day 26/27); - 诚实提醒:宽松不是缺陷清单的免死金牌——如果你在生产要严格校验,应该在路由层自己加白名单,而不是依赖这个自研解析器"猜"。
这就是"改动后果"课的标准示范:协议边界的每一条缝,都该用一次真实请求去戳,而不是读代码猜。
4. 学员调试任务
- A 档(本地/板端动手):复刻 §3 的 ①②③④ 四个请求,记录状态码与错误体;再试:
messages传成对象{}、传成空数组[]、把max_tokens传成字符串"8"——各是什么结果?(第三个大概率暴露自研解析器的数值宽容度,记录下来)。 - B 档(纯读源码):读
vllm_http.c100–260 行的解析器,回答:①parse_string_body处理了哪些转义(\n、\uXXXX等),漏处理哪个会有什么后果?② 为什么vjson_obj_get找不到键返回 NULL 而不是报错,调用方怎么区分"键不存在"与"值为 null"?③ admin 路由为什么宁可写一串strcmp也不用switch(path[0])(提示:路径前缀冲突与可读性)。
预期输出:一张"篡改字段 × 状态码 × 错误体"对照表,附一个你额外发明的"宽松/严格"边界用例。
收尾
- 本篇源码点名:vllm_http.c(VJson 解析器 100–463)、vllm_server.c(主路由)、vllm_admin.c(admin 路由 1120–1170)
- 开源仓库:Kestrel-LLM (Gitee)(AGPL-3.0-or-later 或商业许可,二选一)
- 下篇预告:请求解析对了,但对话要"边生成边看到字"——20-3 进 SSE:响应头先发、事件流逐 token 推,
curl -N能看见什么。