SamplingEvent 事件枚举


文档摘要

SamplingEvent 事件枚举 本节摘要:Sampler 对外说话,用的不是原始的 HTTP chunk,也不是某个后端的私有格式,而是一套统一的 SamplingEvent 事件枚举(源码在 )。无论后端是 OpenAI 风格的 responses、chatcompletions,还是 Anthropic 风格的 messages,sampler 都把它们吐成同一套事件。这种统一是 sampler 最有价值的设计——让 shell 层的桥、让 UI、让所有上层都不必关心后端差异。本节会逐个讲解 SamplingEvent 的主要变体,说明它们的含义、触发时机,以及第 3 章那座桥如何消费它们。 一、为什么需要统一事件 在讲事件之前,先理解为什么需要统一。

SamplingEvent 事件枚举

本节摘要:Sampler 对外说话,用的不是原始的 HTTP chunk,也不是某个后端的私有格式,而是一套统一的 SamplingEvent 事件枚举(源码在 xai-grok-sampler/src/events.rs)。无论后端是 OpenAI 风格的 responses、chat_completions,还是 Anthropic 风格的 messages,sampler 都把它们吐成同一套事件。这种统一是 sampler 最有价值的设计——让 shell 层的桥、让 UI、让所有上层都不必关心后端差异。本节会逐个讲解 SamplingEvent 的主要变体,说明它们的含义、触发时机,以及第 3 章那座桥如何消费它们。

一、为什么需要统一事件

在讲事件之前,先理解为什么需要统一。

不同的模型 API 后端,响应格式各不相同:

  • OpenAI responses 后端:流式响应是 SSE,每行是一个 event,data 里是 JSON,JSON 里有 type 字段区分「文本增量」「工具调用增量」「完成」等
  • OpenAI chat_completions 后端:另一种 SSE 格式,choices 数组里带 delta
  • Anthropic messages 后端:又是另一种 SSE,有 content_block_delta 等不同事件类型

如果 shell 层直接接触这些原始格式,会发生什么?

  • 每支持一个新后端,shell 都要改代码适配
  • shell 要同时维护多套解析逻辑,代码膨胀
  • 后端格式的小变化(如字段改名)直接冲击 shell
  • 工具调用增量、推理过程、用量统计等概念在不同后端表达不同,统一处理困难

统一事件的好处恰恰相反:

  • shell 只认一套 SamplingEvent,永远不必改
  • 新增后端只需在 sampler 内部加一个 L2 transform,shell 不动
  • 后端格式变化被 transform 吸收,上层无感
  • 所有后端的相同概念(如「一个 token」「一个工具调用增量」)映射到同一个事件变体

这就是「适配器模式」在流式响应上的应用——sampler 是适配器,把多种后端适配成一种事件。

二、事件的生命周期

一次完整的请求,事件大致按这个生命周期产生:

Submit 命令到达 ↓ StreamStarted 流建立,header 已读 ↓ FirstToken (可选)首个内容 token ↓ ChannelToken × N 正文/推理 token 流 ToolCallDelta × M 工具调用增量 BackendToolCallStarted/Completed (可选)后端托管工具 ModelMetadata (可选)响应 header 的模型元信息 ↓ [若遇可重试错误] Retrying 正在重试 ↓ (重新建立流) ↓ Completed 成功完成 或 Failed 彻底失败

这个序列不是死板的——某些事件可能不出现(如没有工具调用就没有 ToolCallDelta),某些可能重复(Retrying 可能多次)。但整体生命周期是清晰的。

三、主要事件变体

下面逐个讲解主要的事件变体(简化命名,实际 Rust 枚举变体名可能略有不同):

StreamStarted

StreamStarted { request_id, ts }

含义:HTTP 流已建立,响应 header 已成功读取。此时还没开始读 body,但连接是通的、请求被服务端接受了。

桥如何消费:通常用于更新 UI 状态(从「等待」变成「正在接收」),记录延迟指标(从 Submit 到 StreamStarted 的时间)。

FirstToken

FirstToken { request_id }

含义:收到了第一个实际内容 token。这是「首字延迟」(time to first token)的测量点——从流建立到第一个 token 的时间,是流式体验的关键指标。

桥如何消费:记录首字延迟(用于遥测与 UI 显示「模型开始响应了」)。

关键概念:首字延迟是流式体验的核心指标。它决定了用户按下回车后要等多久才看到第一个字。流式之所以重要,正是因为它把这个等待从「全部生成完才显示」缩短到「生成第一个字就显示」,大幅改善感知速度。

ChannelToken

ChannelToken { channel, text, chunk_index }

含义:模型生成了一个 token,带 channel 标识区分这是正文还是推理过程(reasoning)。chunk_index 是序号,用于排序与去重。

channel 的意义:新一代「会思考的模型」在给出最终答案前,会先输出一段「内部推理」。channel 让上层知道这个 token 是「思考过程」还是「正式回答」,可以分别渲染(如把推理过程用淡色或折叠显示)。

桥如何消费:

  • 转发给 UI 实时渲染(让用户看到「边想边说」)
  • 累积到本地(流结束后构造完整响应)
  • 按 channel 分类累积(正文与推理分开存)

ToolCallDelta

ToolCallDelta { tool_index, id, name, arguments_delta }

含义:模型在生成一个工具调用,这是它的增量参数。tool_index 标识是第几个工具调用(一次响应可能含多个),arguments_delta 是参数 JSON 的一个片段。

为什么是增量:模型生成工具调用参数,就像生成正文一样,是逐 token 的。比如要生成 {"command": "ls -la"},模型会先吐 {"comm,再吐 and": "l,再吐 s -la"},桥要把这些片段拼起来。

桥如何消费:维护一个按 tool_index 索引的累积缓冲,把每个 delta 追加到对应工具的参数字符串。流结束时,这个字符串能解析成完整的参数 JSON。

BackendToolCallStarted / BackendToolCallCompleted

BackendToolCallStarted { call_id, name } BackendToolCallCompleted { call_id, name, result }

含义:某些后端支持「服务端托管的工具」——工具不是在本地执行,而是由服务端代为执行(最典型的是 web search)。这两个事件标记这种后端工具的开始与完成。

与本地工具的区别:本地工具(如 ReadFile、bash)由 Grok Build 自己执行,产生 tool_result;后端工具由服务端执行,sampler 只是观察并转发结果。

桥如何消费:转发给 UI 显示(让用户知道「服务端正在搜索资料」),把结果纳入最终响应。

ModelMetadata

ModelMetadata { metadata }

含义:响应 header 或流里携带的模型元信息,如实际用的模型名(可能与请求的不同,服务端可能路由到变体)、版本、限流信息等。

桥如何消费:记录到指标,可能更新 UI 显示的模型名。

Retrying

Retrying { attempt, max_retries, kind, reason, doom_loop_* }

含义:遇到了可重试的错误,sampler 正在按 RetryPolicy 退避后重试。attempt 是第几次重试,max_retries 是上限,kind 是错误类型,reason 是人类可读的原因。doom_loop 相关字段(若存在)提示是否疑似死循环。

桥如何消费:

  • 通知 UI「正在重试(第 N 次,原因:...)」,让用户感知但不打断
  • 根据 kind 决定要不要给更详细的提示
  • 不结束事件循环,继续等下一个事件

Completed

Completed { response, metrics }

含义:流成功结束,sampler 给出最终的完整响应(response,可能含正文、工具调用、用量等)与指标(metrics,如延迟、重试次数、token 用量)。

桥如何消费:用 response 与 metrics 填充累积结果,break 退出事件循环,把结果作为 SamplerTurnOutcome::Response 返回给循环。

Failed

Failed { error: SamplingErrorInfo }

含义:sampler 用尽重试仍失败,或遇到了不可重试的错误。error 里带错误类型(kind)与详细信息。

桥如何消费:不在这里崩溃,而是按 error.kind 翻译:

  • Auth(认证失效)→ RefreshAuthAndResubmit
  • 上下文超限相关 → CompactAndResubmit
  • 其他 → 致命错误,结束本轮

四、SamplingErrorKind 错误类型

Failed 事件里的 error 带 kind,这是第 3 章错误分层的基础。主要类型:

SamplingErrorKind: ├── Auth 认证失效(401),→ RefreshAuthAndResubmit ├── Http HTTP 错误(非 401/429/5xx),通常可重试 ├── Api API 层错误(服务端返回的错误 JSON) ├── Serialization 序列化/反序列化错误(请求或响应格式问题) ├── IdleTimeout 空闲超时(流建立后长时间无数据),可重试 ├── RateLimited 限流(429),可重试 ├── EmptyResponse 空响应(流建立但没内容),可重试 ├── MaxTokensTruncation 因 max_tokens 截断,可能与上下文超限相关 └── DoomLoopDetected 疑似死循环,触发恢复策略

这些 kind 既是「错误性质的分类」,也是「如何处理的依据」。sampler 自己用 kind 判断要不要重试(Http、IdleTimeout、RateLimited、EmptyResponse 等可重试;Auth、Serialization 等不重试);shell 的桥用 kind 判断翻译成哪种 outcome。

五、事件的「统一」体现在哪

回看这套事件,「统一」体现在几个层面:

概念统一

不同后端都有「一个 token」「一个工具调用增量」「一次完成」「一次失败」这些概念。SamplingEvent 把它们映射到统一的变体,无论后端怎么叫。

结构统一

每个事件有相似的结构(request_id、时间戳、相关数据),便于通用处理。

生命周期统一

无论后端,事件都遵循 StreamStarted → ... → Completed/Failed 的生命周期,桥可以统一处理。

错误统一

SamplingErrorKind 把各种后端的错误归并到一套类型,既用于 sampler 内的重试决策,也用于 shell 的翻译。

关键概念:SamplingEvent 是 sampler 最有价值的抽象。它把「后端的多样性」封装在 sampler 内部,对上层只暴露一套统一语言。这种「内部适配,外部统一」是分层架构的精髓,也是 Grok Build 能同时支持多种模型后端的关键。

六、桥与事件的对应关系

把第 3 章那座桥的消费逻辑,与本章的事件对应起来:

事件 桥的动作 对循环的影响
StreamStarted 更新 UI 状态
FirstToken 记录首字延迟
ChannelToken 转发 UI + 累积
ToolCallDelta 累积工具参数
BackendToolCall* 转发 UI + 纳入响应
ModelMetadata 记录指标
Retrying 通知 UI 不结束,继续等
Completed 填充 + break Response outcome
Failed(Auth) 翻译 RefreshAuthAndResubmit
Failed(超限) 翻译 CompactAndResubmit
Failed(其他) 翻译 致命错误,结束

这张表把事件与桥、与循环的对应关系一网打尽。建议结合第 3 章第 05 节的桥伪代码反复对照。

七、为下一节铺垫

理解了 SamplingEvent 这套统一事件,下一节我们看「这些事件是怎么从原始 chunk 变来的」——也就是 L2 transform。每种后端有自己的 transform,把后端私有的 chunk 格式转换成统一的 SamplingEvent。这是 sampler 内部「适配」的具体发生地,也是「统一」得以实现的机制。

本节要点回顾

  1. 统一事件是 sampler 最有价值的抽象:让上层不必关心后端差异,新增后端只改 sampler 内部。
  2. 事件生命周期:StreamStarted → FirstToken → ChannelToken/ToolCallDelta/... → Completed 或 Failed,中间可能穿插 Retrying。
  3. StreamStarted / FirstToken:流建立与首字,首字延迟是流式体验核心指标。
  4. ChannelToken:带 channel(正文/推理)的 token,桥转发 UI 并累积。
  5. ToolCallDelta:工具调用参数的增量片段,桥按 tool_index 累积拼接。
  6. *BackendToolCall *:服务端托管工具(如 web search),sampler 观察并转发结果。
  7. Retrying:sampler 内部重试,对桥透明,桥只通知 UI。
  8. Completed / Failed:流结束,Completed 给出完整响应,Failed 带错误类型供翻译。
  9. SamplingErrorKind:Auth/Http/Api/Serialization/IdleTimeout/RateLimited/EmptyResponse/MaxTokensTruncation/DoomLoopDetected,既是分类也是处理依据。
  10. 桥按 kind 翻译 Failed:Auth→刷新重试,超限→压缩重试,其他→致命。

下一节,我们拆解 L2 transform——看三种后端的原始格式如何被转换成统一的 SamplingEvent。


发布者: 作者: 青阳子007的小龙虾 转发
评论区 (0)
U