一条请求样本:从协议输入到回包证据#
这章解决什么问题#
前面的请求生命周期章节已经把入口、输入规范化、session 分叉、多 worker、batch fan-out、多模态和元数据传播分别讲开了,但它们仍然更像一组稳定专题,而不是一本书里常见的“整合样章”。读者如果只逐章吸收,很容易分别记住:
- 请求会进入
TokenizerManager ReqState和Req不一样Scheduler会形成 batchDetokenizerManager会收尾RequestStage能给出证据
却不一定能把这些点重新压回同一条具体请求上。
这一章的任务,就是用一条最小但真实的请求样本,把前面这些分散机制重新串起来。它不再引入新的大主题,而是回答一个更像好技术书的问题:
- 当一条具体请求来到系统时,前面讲过的对象、队列、输出和证据字段,到底会以什么顺序出现?
先给这条样本一个明确形状#
为了让案例尽量简单,又能覆盖足够多的 runtime 现实,这里选一条很典型的请求形状:
- 外部表面:OpenAI-compatible chat request
- 输入:一段 messages,带
max_tokens、temperature - 运行方式:非 batch、非多 worker、非多模态
- 输出方式:streaming
- 目标:观察一条请求怎样从协议表面进入 runtime,再怎样带着证据返回
这条样本故意不带 tool call、schema、session 或多 worker。不是因为这些不重要,而是因为案例章的职责是先把最小闭环讲完整,再把复杂人格留给各自专章。优秀技术书里的样章通常也遵守这个原则:用足够真实的最小样本组织复杂知识,而不是在一个案例里塞进所有分支。
一张图:把这条样本真正压成一条线#
下面这张图解决的理解障碍是:前面章节虽然分别讲清了入口、对象、回包和证据,但读者不一定知道它们到底怎样首尾相接。这里要看的不是“有多少组件”,而是“一条请求怎样连续变形”。
flowchart LR
A["OpenAI-compatible chat request"] --> B["http_server / serving_chat"]
B --> C["GenerateReqInput"]
C --> D["TokenizerManager.generate_request()"]
D --> E["ReqState + tokenized request"]
E --> F["Scheduler / waiting queue / batch"]
F --> G["BatchTokenIDOutput"]
G --> H["DetokenizerManager"]
H --> I["BatchStrOutput"]
I --> J["_wait_one_response() / streaming chunk"]
J --> K["response_sent_to_client_time / finished_time / request log"]和前面那些分层图相比,这张图更像一本书里的案例图:它关心的不是某一层内部多复杂,而是这条样本怎样从“协议请求”连续变成“输出与证据”。
第一段:协议请求先被翻成内部请求对象#
这条样本进入系统时,首先表现成一个 OpenAI-compatible chat request。它不会直接进入 scheduler,而会先经过 http_server.py 的路由层和 serving_chat.py 这样的协议适配层。这里最重要的变化,不是“函数被调用了几次”,而是对象开始变形:
- 外部看到的是 chat request
- 运行时真正接住的,是
GenerateReqInput
也就是说,生命周期在一开始就已经完成了第一层翻译:从协议对象翻到 runtime 请求对象。前面 2.1 一次请求如何穿过 SGLang 和 2.4 输入如何被规范化、分词并送入运行时 讲的,就是这一步的边界。
从读代码的角度,这一步最值得抓的入口仍然是:
python/sglang/srt/entrypoints/http_server.pypython/sglang/srt/entrypoints/openai/serving_chat.pypython/sglang/srt/managers/io_struct.py
这里的重点不是把它们全部展开,而是建立一个稳定判断:协议层负责把外部调用表面折成统一请求对象,而不是自己维护调度状态。
第二段:TokenizerManager 把协议对象变成可调度对象#
请求到了 TokenizerManager.generate_request(...) 之后,又会经历第二层关键变形。对这条样本来说,至少会发生三件事:
- 请求参数被归一化,例如 sampling 相关字段、priority 默认值、部分 tracing 相关元数据。
TokenizerManager为这条请求建立ReqState,让后面的 streaming 回包有地方收敛。- 文本输入被 tokenize,形成真正可以发给 scheduler 的 tokenized request。
这一段最值得技术书强调的点,不是“tokenize 发生了”,而是同一条请求在这里被一分为二:
- 一份是入口/回包侧仍然持有的
ReqState - 一份是会继续送往 scheduler 的 tokenized request
这就是为什么前面反复强调 ReqState 和 Req 不是同一个对象。样章的价值,就在于让这种抽象差异不再停留在术语表里,而能真正挂回一条具体请求。
第三段:scheduler 看到的已经不是原始请求,而是等待中的调度单元#
请求进入 scheduler 之后,系统看到的就不再是 chat request,也不再是 ReqState,而是 waiting queue 里的调度单元。这里对这条样本来说最重要的变化有两层:
- 它从“单请求输入”变成了“等待加入某一轮 batch 的候选”
- 它的推进速度开始不只由输入内容决定,还由 waiting queue、预算和当前 batch 状态决定
也就是说,请求生命周期在这里开始受到第 4 节会讲的调度现实约束。即使这条样本非常简单,它也仍然要经过:
- waiting queue
- batch 成形
ScheduleBatch
然后才会进入执行层。样章的好处,是能帮助读者真正接受一个事实:请求在系统里从来不是“一直保持原样往前走”,而是不断被改写成更适合下游层次的对象。
第四段:输出先以 token 形式回来,再被重新组装成文本#
一旦执行层产出结果,这条样本首先回来的不是用户直接看到的文本,而是更低层的输出对象,例如 BatchTokenIDOutput。这意味着“请求完成”其实有两次不同的完成边界:
- 一次是执行层已经产出 token 结果
- 一次是调用方真正收到了 streaming chunk 或最终文本
这中间隔着 DetokenizerManager。它负责把 token 级结果重新翻成文本级结果,并通过回包链把这些结果送回 TokenizerManager。于是 TokenizerManager._wait_one_response(...) 才能继续做两件事:
- 持续向调用方流式发送 chunk
- 维护
ReqState,直到真正 finished
很多读者第一次读这部分时会下意识地以为“模型已经生成完,就等于请求完成了”。这条案例故意把两次完成边界拆开,就是为了让读者在很早的地方就建立更准确的运行时感觉。
第五段:证据字段是在收尾时真正闭环的#
如果这条样本是 streaming 请求,那么它不会只留下一个“结果字符串”,还会留下至少一条最小证据链。对这条样本最值得抓的证据字段通常包括:
RequestStage切片queue_time- TTFT
response_sent_to_client_timefinished_time- request logger / exporter 在 finished 时落下的记录
这里最重要的不是记住字段名,而是理解字段出现的顺序。对这条请求来说,response_sent_to_client_time 只会在对外发送真正完成之后才有意义,而 finished_time 则是系统内部收尾更靠后的边界。也正因为如此,前面第 8 节才需要单独讲“这些时间字段为什么看起来会打架”。
换句话说,证据链不是附属品,而是这条样本在系统里真正结束时留下的第二份产物。
把这条样本重新翻回五个阅读锚点#
如果你想顺着这条样本回源码,最稳的不是同时开十个文件,而是只抓五个锚点:
1. http_server / serving_chat:请求怎样从协议层落成内部对象
2. TokenizerManager.generate_request(...):入口侧如何建立 ReqState 并发出请求
3. Scheduler 主循环:请求何时真正进入 batch
4. DetokenizerManager.event_loop():token 结果怎样被翻回文本
5. _wait_one_response(...):chunk 怎样回到调用方,并在这里补齐证据这五个锚点的价值,不在于它们覆盖了所有细节,而在于它们正好对应这条案例的五次对象变化。只要这五个锚点稳住,后面再去看 batch fan-out、多模态、session 或 tool workflow,也更不容易迷路。
这条样本没有覆盖什么#
为了避免这章看起来像“万能总图”,这里也明确说清它没覆盖的东西:
- 没覆盖多 worker 入口放大
- 没覆盖 batch fan-out / parallel sampling
- 没覆盖 session / abort / health 分叉
- 没覆盖多模态输入
- 没覆盖结构化约束与 tool workflow
这不是缺点,而是案例章应该有的边界。它的任务是把最小主链压成一本书里真正可拿来反复回看的“样章”。复杂人格应该回到它们自己的章节,而不是在这里把案例写成故障树。
小结#
请求生命周期真正像一本书,而不是几篇并列文章,往往就差这样一章样章。它让前面分别讲过的协议层、内部对象、scheduler、回包链和证据字段重新站回同一条具体请求上。
如果读完这一章之后,你能稳定说出:
- 请求先在哪一层从协议对象变成内部对象
- 哪一层同时持有
ReqState和 tokenized request - 哪一层把请求改写成 batch 现实
- 哪一层把 token 结果重新翻回文本
- 哪些证据字段是在请求真正收尾时才闭环
那么第 2 节就不再只是“请求链若干专题”,而开始更像一本完整技术书里的一个成熟 part。
叶王 © 2013-2026 版权所有。如果本文档对你有所帮助,可以请作者喝饮料。