第 3 章 一个请求的一生
本章是全书的脊柱:一次
/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 序列:
- 协议转换:
/v1/chat/completions走openai_to_messages,/v1/messages走
anthropic_to_messages——两条方言汇成同一份内部 messages/tools 结构,再补共享思考档
(with_shared)。 - 消毒:
sanitize_prompt_inputs。特殊 token 字面量(<|endoftext|>/<|im_end|>)在这里
被转义/处理——10-18 的"只有 thinking 没有正文"事故的修复点(18 章)。 - 渲染与计数:
svc.template.render(messages, tools, **kw)→tok.encode(..., parse_special=True)。
这产出的就是本书的权威 token 数:引擎侧的计费、窗口算术、count_tokens端点全部用这一个口径。 - 窗口算术(
prepare,约 :1750):room = max_context - slack - len(ids)。
max_new > room时行为由fit_max_tokens决定:开 → 压到max(1, room)(“短答好过 400”);
关 → 400(#545 文案)。10-19 晚起两条 lane 配置均为开。 - 排队与派发:
self.fifo = threading.Lock()(:1268)——服务层的总闸,
一次只放一个请求进引擎;忙时ModelBusy409、加载中EngineStarting503。
这把锁是全书最大瓶颈"prefill 全局串行"的所在地之一(09 章)。 - 留档与登记:
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++,要重编 + 停服窗口)。