第 3 章 一个请求的一生
MoE 大模型低成本部署框架第 3 章 一个请求的一生
CHAPTER 03

第 3 章 一个请求的一生

9 分钟阅读 · 2,486 字

本章是全书的脊柱:一次 /v1/chat/completions 调用从浏览器/SDK 出发,穿过中继、网关、
服务层、引擎内核,变成一个个 token 再回到客户端——每一跳写清它做了什么、
在哪个文件、可能怎么死
。读这一章不需要预先知道其他章的内容。

行号口径:截至 2026-10-19,rag-proxy/rag_proxy.py(工作树)、Strata/serve/server.py(已上线版本)、
SSEStraBed/src/program/generate.cpp。引擎行号会随注释整理漂移,关键处以「函数名 + 约 :行号」为准。


3.1 出发:客户端手里有什么

以 Claude Code 为例(这也是最苛刻的用户形态):它每次调用都携带完整会话历史 + 全部工具 JSON schema。
2026-10-19 的一次实测请求(书稿会话,留档 ~/logs/infer-segs/2026-10-19/seg-124459):
326 条消息、55 个工具、序列化后 1,075,447 字符,引擎实测 154,796 token。
这个数字后面每一跳都会再被"计算"一遍——而且三跳各算各的口径(3.7 详述)。

客户端侧协议是 Anthropic Messages(/v1/messages)或 OpenAI Chat(/v1/chat/completions)。
本服务的公网出口同时支持两者,但协议在哪一层转换是个关键设计(3.2)。

3.2 第一跳:公网 → frps 中继 → Pod :8080

客户端 → https://<PUBLIC_HOST>:<PUBLIC_PORT>(或 api.ssemarket.cn 中转)
       → frps 中继:TLS 在这里终结
       → Pod :8080:明文 HTTP(平台链路约定,容器侧不再自己起 TLS)

约束在这里生效:Pod 对外只有 8080 一个端口(02 章约束 5),所以引擎的 8091/8092 只监听回环,
外部不可达。所有能力——鉴权、分流、守卫、观测、并发监控——都必须挤进 :8080 这一个面。
这就是"网关模式"的由来,不是审美选择,是端口逼的。

3.3 第二跳:rag-proxy 网关(rag_proxy.py,:8080)

一个 POST 进来后按顺序过这些闸(do_POST,约 :1890 起):

① 鉴权(ssecho_auth)。非 /health 等公开路径先过 _gate:无登录态回 401
NOT_LOGGED_IN。引擎自身的 x-api-key 不下发给浏览器——网关持有 key,转发时替换
(:1643 注释:代理自身的内部调用才显式带 key)。

② 解析与模型名分流(route_upstream,:201)。读 model 字段查 config.json 的
model_routes:qwen3.8-flash-fast → http://127.0.0.1:8091(快道),
qwen3.8-flash/未登记 → 默认普道 :8092。网关不做并发限制——限流在中转/路由器按模型 ID 处理
(双通道报告 10-07 的分工,沿用至今)。

③ 文档预处理(doc_extract.find_documents,2026-10-19 加入)。请求 parts 里的
PDF/DOCX/XLSX/CSV/文本被抽成可读文本,扫描 PDF 渲染成页面图,按剩余窗口预算头 70% + 尾 30%
截断(预算参数在 config.json → documents)。这一步存在的原因:上游对文档部件的旧行为是
静默丢弃,用户以为发的是论文,引擎看到的是空白。

④ 思考预算守卫(thinking_guard,:286)。客户端没表达思考偏好时,给答复预留
answer_reserve_tokens=4096(至少 max_tokens/3),通过引擎的按请求参数
reasoning_budget_tokens 实现——思考到预算就走模型的收尾协议(REASONING_WRAP_UP),
不重读已进窗的前缀。它治的是"想完了却没有答案":2026-10-08 实测快道 173 次、普道 81 次。

⑤ 窗口预检(2026-10-19 新增,本报告 §落地记录)。当前生产 rag_enabled=false(纯透传模式),
这跳的逻辑:

est = 上界估算(按字符分类:CJK 1.05 tok/字、数字 1.05、其他 0.70)
est + margin ≤ 窗口 - slack        → 直通(零计数开销)
否则                                → 调 serve 的 count_tokens 精确计数(只 tokenize,不跑模型)
    实测 + margin ≥ 窗口            → 就地 400:"prompt is too long: N tokens > M maximum"
                                      (不进引擎、不浪费 GPU、0 次上游来回)
    装得下但输出超余量              → 按实测压 max_tokens(clamp_max_tokens,:265)
    计数接口不可用                  → 降级原样透传(由引擎 400 + 改写网兜底)

注意这里已经有第二个 token 计数了(客户端自己还有一个)。三个口径的互相打架见 3.7。

⑥ 转发(_proxy,:1600 区间)。逐块流式透传,不重新编码——上游 SSE 是什么字节就往下写什么
字节,顺带 tee 一份给 infer_store(留档)和并发登记(/concurrency 数据源)。
客户端断开 ≠ 上游停止:断流处理单独记账(disconnected 状态)。

⑦ 400 改写网。引擎如果回了超窗 400,网关解析错误里的实测数(overfit_info):
输出超余量 → 照报的余量压 max_tokens 重发一次(客户端看不见这个 400);
提示词本身超窗 → 不再重试,把 serve 写给运维看的内部文案(#545)改写成
Anthropic 口径的结构化错误回客户端(prompt_tokens/max_context/max_output_tokens 字段)。

3.4 第三跳:serve 服务层(Strata/serve/server.py,:8091/:8092)

网关后面的进程是 Python 服务层,每个 lane 一个实例,它自己持有 tokenizer 与 chat 模板,
引擎子进程(strata --serve --pack ...)只接收渲染后的 token 序列:

  1. 协议转换:/v1/chat/completions 走 openai_to_messages,/v1/messages 走
    anthropic_to_messages——两条方言汇成同一份内部 messages/tools 结构,再补共享思考档
    (with_shared)。
  2. 消毒:sanitize_prompt_inputs。特殊 token 字面量(<|endoftext|>/<|im_end|>)在这里
    被转义/处理——10-18 的"只有 thinking 没有正文"事故的修复点(18 章)。
  3. 渲染与计数:svc.template.render(messages, tools, **kw) → tok.encode(..., parse_special=True)。
    这产出的就是本书的权威 token 数:引擎侧的计费、窗口算术、count_tokens 端点全部用这一个口径。
  4. 窗口算术(prepare,约 :1750):room = max_context - slack - len(ids)。
    max_new > room 时行为由 fit_max_tokens 决定:开 → 压到 max(1, room)(“短答好过 400”);
    关 → 400(#545 文案)。10-19 晚起两条 lane 配置均为开。
  5. 排队与派发:self.fifo = threading.Lock()(:1268)——服务层的总闸,
    一次只放一个请求进引擎;忙时 ModelBusy 409、加载中 EngineStarting 503。
    这把锁是全书最大瓶颈"prefill 全局串行"的所在地之一(09 章)。
  6. 留档与登记:begin_request 建记录;响应流经 infer_store 记下
    prompt_tokens/completion_tokens/cached_tokens 与完整内容,分段 gzip 进本地
    ~/logs/infer-segs/ 并上传 MinIO。

3.5 第四跳:引擎内核(SSEStraBed,generate.cpp)

token 序列进入 C++ 引擎后,一个请求的一生变成两个阶段:

prefill(读提示词)。批量分段读入,每段更新 KV(int8,超出常驻 32K 窗口的部分放内存)。
实测吞吐 ~3,350 tok/s(可观测性实验报告,2026-10-14 前后,独跑):258K 提示词 ≈ 82 秒。
前缀缓存命中时跳过重读:64K 提示词 TTFT 从 13.4s 降到 0.79s(同报告,17×)。
环境变量 STRATA_PREFILL_YIELD_CHUNKS/STEPS(generate.cpp 约 :6681)控制读提示词时的
分段让出——让出窗口给解码,避免"预填充饿死解码"。

decode(逐 token)。每层:注意力(GDN/QSA 通路)→ 路由打分 → top-10 专家查三级存储
(VRAM 热缓存 → RAM arena → 磁盘)→ PLE 查表(页缓存)→ 残差。MTP 草稿头一次猜 4 个 token,
批步 run_slots(约 :5658)一次验证一批;接受率全程 ~60%(实测口径见 10 章)。
单路解码 76 tok/s;4 路批槽每路 20 tok/s、总吞吐 77——GPU 在 4 槽已饱和,加并发买到的只有排队
(可观测性实验报告 TL;DR,12 路总吞吐 86 封顶)。

token 一旦产出就沿原路返回:引擎 → serve(SSE 事件化:text/thinking/tool_start…)
→ 网关逐块透传 → 中继 → 客户端。finish_reason + usage 收尾,usage 里的 prompt/completion
计数由 serve 记账进 infer_store——这就是本 Pod 的计费口径(3.7)。

3.6 全程时刻表(一个 150K 提示词请求的真实剖面)

以 10-19 书稿会话留档为样本(seg-124459):

时刻 事件 耗时
t+0 客户端发出 1.07M 字符 POST —
t+~0.05s 网关:鉴权、路由、文档检查、思考预算注入 <50ms
t+~0.1s serve:sanitize + 模板渲染 + tokenize(154,796 tok) 数百 ms 级
t+? serve FIFO:如果前面有人在读长提示词,这里是分钟级排队 0 ~ 数百 s
t+prefill 引擎读提示词:~154,796/3,350 ≈ 46s(前缀全复用则 <1s) 秒~分钟
之后 解码 76 tok/s(独跑)/ ~20 tok/s(4 路) 输出长度决定
收尾 usage 回传、infer 留档、MinIO 分段 异步

TTFT 公式(实测口径):TTFT ≈ 0.4s + prompt_tokens/3350 + 排队时间——最后一项是
8 并发下能把 TTFT 拉到 4.7 分钟的那一项(问题 #1,09 章)。

3.7 一图流:同一个请求的四个 token 数

2026-10-19 的真实事故把这件事演示得淋漓尽致。同一次请求:

谁在数 数字 口径 用途
Claude Code /context 157.9k 字符启发式(~4 字符/token)+ 上次该渠道 usage 校准 显示与自动压缩决策
外部中继 ~398,890 自己的上界估算 准入(阈值 256,000,十进制)
网关上界估算 516,561(同请求) 字符分类上界(英文按 0.70,高估 ~3.3×) 决定是否精算/clamp
引擎实测 372,066 Qwen tokenizer 对渲染后提示词的真实编码 窗口判定与计费(权威)

结论:计费与窗口判定的唯一权威口径是引擎实测;其余都是"看情况用的估计"。
中文内容会让客户端口径系统性低估 ~2.4×,上界估算器对英文重的内容系统性高估 ~3.3×。
事故复盘与修复清单在 18 章;权威口径的落地(预检 + 改写网)在 04 章。
中继的 256,000 与引擎的 262,144(=256×1024)不对齐,是跨层协作问题,已移交中继维护方。

3.8 这条链可能在哪断

断点 症状 归属
中继/TLS 连接超时、5xx 平台侧
网关鉴权 401 NOT_LOGGED_IN ssecho_auth
窗口 400 prompt is too long(带实测数) 网关预检或引擎 400 改写
服务层忙 409 a request is running or queued FIFO
引擎启动中 503 “the engine is starting (a minute or two)” 模型重载
引擎死亡 流中 error 事件,“the next request restarts it” 自愈重启(16 章)
特殊 token 早停 只有 thinking 无正文 / 工具 JSON 截断 sanitize(18 章已修主因)
Pod 重建 网关内存态全丢(会话/嵌入缓存/组装块) 无持久化(约束 10)

3.9 本章小结

  • 一个请求要过三层计数、四道闸、两种协议方言、一把全局 FIFO;每一层的"聪明"都对应一条历史事故。
  • 权威 token 数只有一个:serve 渲染 + 引擎 tokenizer 实测。其余数字都有系统性偏差,方向还相反。
  • 延迟预算几乎全在 prefill 与 FIFO 排队;解码速度反而与上下文长度关系很小。
  • 想改行为先定位你在哪一跳:网关(Python,重启秒级)、服务层(Python,随 lane 重启)、
    引擎(C++,要重编 + 停服窗口)。
全部章节