Qwen Code/stats会话生成时序指标(TTFT / Generation Time / TPS)设计与实现解析
【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code
/stats是 Qwen Code 终端内置的会话统计入口,它同时支持交互式 Session 面板与非交互式文本输出。本文围绕设计文档 2026-07-24-generation-stats.md 展开,讲解 TTFT(首字延迟)、Generation Time(生成耗时)与 TPS(吞吐)三类指标的语义定义、从底层流式生成器到/stats界面的完整数据链路,并结合仓库源码给出可验证的实现依据。读完本文,你将掌握 Qwen Code 中生成时序指标的计算口径、排除规则与兼容性边界,并能在实际使用中正确解读/stats的 Generation Metrics 输出。
背景:为什么需要独立的生成时序指标
Issue #4252 提出一个明确诉求:/stats需要把“生成时序”与两类既有指标区分展示:
- 会话墙钟时间(session wall time):会话从开始到结束的整体耗时,受用户思考、工具执行等环节影响;
- 端到端 API 延迟(end-to-end API latency):一次模型请求从发起到完成的总时长,包含网络排队、首字等待与后续生成。
在这项改动之前,底层时序数据其实已经存在,只是没有通向/stats的“最后一公里”:
| 已有能力 | 位置 | 说明 |
|---|---|---|
ttftMs首字时序 | loggingContentGenerator.ts | 从请求派发到首个用户可见流式 chunk 的耗时 |
sampling_ms/output_tokens_per_second | session-tracing.ts | endLLMRequestSpan推导出的生成阶段耗时与吞吐 |
ApiResponseEvent请求级事件 | types.ts | 已携带请求时长、模型、prompt id 与输出 token 数,进入UiTelemetryService |
本次改动补上的正是缺失环节:把已有的 TTFT 值接入/stats所使用的“无内容会话指标”(content-free session metrics),让生成时序在交互与非交互两种模式下都能呈现。
指标语义与计算公式
对于每一个成功且带 TTFT 的流式响应,定义三个指标:
- TTFT(Time To First Token):即已有的
ttftMs测量值,表示从请求派发到首个用户可见流式内容出现的时间。源码中ttftMs只在首个包含用户可见内容的 chunk 上捕获(见下文),排除了仅含 role / usageMetadata 的中间 chunk。 - Generation Time(生成耗时):
max(0, duration_ms - ttft_ms),从首个用户可见流式内容出现到流结束。 - TPS(tokens per second):
output_token_count / generation_time_seconds,仅在生成耗时为 0 时不可用。
会话级聚合口径
SessionMetrics.generation(见 uiTelemetry.ts)按需惰性创建(lazily created),包含两类数据:
- 最近一次已完成的请求样本(
last):模型名、TTFT、生成耗时、输出 token 数; - 聚合计数:
timedRequests:已计时的请求总数;totalTtftMs:所有计时请求的 TTFT 之和;totalGenerationDurationMs:吞吐合格(throughput-eligible)请求的生成耗时之和;totalThroughputOutputTokens:吞吐合格请求的输出 token 之和。
对应的会话级派生指标为:
- 会话平均 TTFT=
totalTtftMs / timedRequests(所有计时请求的算术平均); - 会话 TPS=
totalThroughputOutputTokens / (totalGenerationDurationMs / 1000)(加权吞吐,而非各请求 TPS 的均值)。
零生成耗时(generationDurationMs === 0)的请求只计入 TTFT 统计,不进入 TPS 计算的两侧(分子分母都不含),以此避免除零并防止短请求被过度加权。这一口径在 uiTelemetry.ts 的聚合逻辑中完整实现:先校验ttft_ms存在、有限且非负,再计算generationDurationMs = Math.max(0, event.duration_ms - event.ttft_ms),仅当generationDurationMs > 0时才累加生成耗时与输出 token。
与端到端延迟、采样耗时的关系
在 session-tracing.ts 的 span 属性推导中,存在同源的sampling_ms = Math.max(0, duration - ttftMs),与本文的 Generation Time 公式一致;当sampling_ms > 0且有输出 token 数时还会写入output_tokens_per_second。值得注意的是,该处代码注释记录了一次 Phase 4a 的缺陷修复:旧公式duration - ttft - setup会重复扣除requestSetupMs(Phase 4b 起开始填充),导致重试请求的采样耗时被错误钳制为 0——这印证了“生成时序 = 总时长 − 首字耗时”这一口径在工程上需要小心维护。
数据流:从流式生成器到/stats
设计文档给出了端到端数据流:
LoggingContentGenerator.loggingStreamWrapper -> ApiResponseEvent(ttft_ms) -> logApiResponse -> UiTelemetryService -> SessionMetrics.generation -> SessionContext -> /stats第一跳:loggingStreamWrapper捕获 TTFT
LoggingContentGenerator是包裹底层ContentGenerator的装饰器(loggingContentGenerator.ts)。在loggingStreamWrapper的迭代循环中:
// Capture TTFT on the first stream chunk that contains user-visible // content. hasUserVisibleContent skips role-only / usageMetadata-only // chunks, so TTFT reflects "model produced something the operator can // attribute to user-perceived latency." if (ttftMs === undefined && hasUserVisibleContent(response)) { ttftMs = Date.now() - startTime; }关键实现细节:
ttftMs是方法内闭包局部变量而非实例字段,因为LoggingContentGenerator被多个并发的generateContentStream调用共享(每个ContentGenerator一个实例),实例字段会造成并发串扰;- 判定函数
hasUserVisibleContent跳过仅含 role / usageMetadata 的 chunk,保证 TTFT 反映“模型产出可感知内容”的时刻; - 流结束后,
ttftMs通过safelyLogApiResponse传给ApiResponseEvent构造函数(loggingContentGenerator.ts),事件定义在 types.ts,其中ttft_ms字段注释为 "Time from stream dispatch to first user-visible content."。
第二跳:endLLMRequestSpan的 Span 属性
同一份ttftMs还会在 span 收尾时写入 trace 属性:endLLMRequestSpan设置ttft_ms、sampling_ms与output_tokens_per_second(session-tracing.ts),供可观测性链路使用。设计文档同时说明,后续的 GenAI 对齐补充了标准化的gen_ai.response.time_to_first_chunk属性(见 loggingContentGenerator.ts 中performance.now() - requestIssuedAtMs的首 chunk 计时),它独立于私有的ttft_ms,/stats并不消费该标准属性,ApiResponseEvent.ttft_ms的数据流与“首个用户可见输出”语义保持不变。
第三跳:UiTelemetryService聚合进SessionMetrics
UiTelemetryService收到ApiResponseEvent后执行聚合(uiTelemetry.ts):
if ( event.ttft_ms === undefined || !Number.isFinite(event.ttft_ms) || event.ttft_ms < 0 || ... ) { /* 跳过:无 TTFT 的请求不产生生成样本 */ } const generation = metrics.generation ?? (metrics.generation = createInitialGenerationMetrics()); const generationDurationMs = Math.max(0, event.duration_ms - event.ttft_ms); generation.timedRequests++; generation.totalTtftMs += event.ttft_ms; if (generationDurationMs > 0) { generation.totalGenerationDurationMs += generationDurationMs; generation.totalThroughputOutputTokens += event.output_token_count; } generation.last = { model, ttftMs, generationDurationMs, outputTokens };GenerationMetrics与GenerationTimingSample的类型定义位于 uiTelemetry.ts,是SessionMetrics上的可选字段(generation?: GenerationMetrics)。随后SessionContext的 clone/equality 逻辑会复制并比较该可选对象,确保交互式面板在每个完成计时的请求后都能刷新(对应设计文档 Compatibility 一节的描述)。
排除规则:内部辅助提示词
生成指标会排除内部辅助提示词(internal helper prompts)对应的请求,原因有二:
- 这些请求不会写入可恢复的会话记录(resumable transcript);
- 若计入,既会让用户困惑,也会造成“实时会话”与“恢复后会话”的数值不一致。
主对话与子代理(subagent)请求保持计入,与既有会话级模型统计口径一致。源码侧,LoggingContentGenerator全程用isInternalPromptId(userPromptId)判别内部请求(loggingContentGenerator.ts),内部请求不采集响应文本,也不产生api_response日志,自然不进入生成样本。
兼容性边界
本次改动刻意保持最小侵入:
ApiResponseEvent.ttft_ms与SessionMetrics.generation均为增量且可选字段,已有记录事件与调用方保持有效;- 不新增第二个计时器,不把时序持久化到日/月 token 用量文件,不改变导出内容,也不改动 daemon / Web Shell 的 stats schema;
- 既有日/月记录继续只含 token 与 API 时长数据,保持 issue-4479-token-usage-stats-coordination.md 中记录的归属边界;
- 非流式响应、以及流式但从未产出用户可见内容的响应,保持原行为,不创建生成样本。
输出呈现:交互式与非交互式
非交互式文本输出
statsCommand.ts的formatGenerationMetrics(statsCommand.ts)按“最新请求 + 会话聚合”两个区块渲染:
Generation Metrics (Latest Request) Model: <model> TTFT: <duration> Generation Time: <duration> Output Tokens: <count> TPS: <x.x> tok/s Requests: <count> Average TTFT: <duration> Session TPS: <x.x> tok/s其中:
- 最新请求 TPS:
last.outputTokens / (last.generationDurationMs / 1000),生成耗时为 0 时显示—; - 平均 TTFT:
totalTtftMs / timedRequests; - 会话 TPS:
totalThroughputOutputTokens / (totalGenerationDurationMs / 1000)。
交互式 Session 面板
交互式/stats的 Session 标签页(StatsSessionTab)渲染同一份SessionContext中的SessionMetrics.generation数据,并在每次完成计时的请求后随 context 更新而刷新。新引入的高可见性标签(如 “Generation Time”“Average TTFT”“Session TPS” 等)覆盖了所有内置语言环境(i18n)。
验证与测试
设计文档列出的验证策略在仓库中均有对应测试支撑:
| 验证目标 | 测试位置 |
|---|---|
| 聚合、内部提示词排除、零生成耗时处理、会话隔离与重置 | uiTelemetry.test.ts、SessionContext.test.tsx |
TTFT 到达ApiResponseEvent且非可见流不携带 TTFT | loggingContentGenerator.test.ts |
| 非交互输出与交互 Session 标签渲染 | statsCommand.test.ts、StatsSessionTab.test.tsx |
| 新标签的 i18n 覆盖 | stats 相关测试中的多语言断言(如'生成耗时(TTFT/TPS)归属于生成指标') |
例如 statsCommand.test.ts 验证了“存在流式响应时包含实时生成时序”:构造ttftMs: 250、generationDurationMs: 1250、输出 50 token 的样本后,断言输出包含TPS: 40.0 tok/s与Session TPS: 40.0 tok/s,与公式50 / 1.25 = 40完全吻合。
小结
/stats的生成时序指标是一套“复用既有底层测量、补齐汇聚链路”的增量设计:TTFT 在LoggingContentGenerator流式包装层捕获,经ApiResponseEvent进入UiTelemetryService,惰性聚合为SessionMetrics.generation,最终由SessionContext驱动交互式 Session 面板与非交互式文本输出。理解max(0, duration_ms - ttft_ms)的生成耗时口径、加权吞吐 = 总输出 token / 总生成耗时的会话 TPS 口径、以及内部提示词排除规则,即可准确解读/stats中每个数字背后的真实含义。
【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考