采样器 · 用 actor 模型管住并发的模型请求

grok 和模型对话这一侧叫「采样器」。它用一个 actor 把「并发、取消、重试」收在一处,无锁地管理多个同时进行的模型请求。这一章看它的三层设计与 actor 骨架。

作者 David更新于 第 8 篇(共 60 篇)

采样器 · 用 actor 模型管住并发的模型请求

上一章看了主循环怎么在「模型 ↔ 工具」之间转。转向模型的那一侧,grok 叫它采样器(sampler)——crates/codegen/xai-grok-sampler。别被名字骗了,它不只是「发个 HTTP 请求」:一个交互式 agent 会同时有多个模型请求在飞(主对话、子任务、后台压缩……),每个都可能要取消、要重试,还不能互相踩。采样器就是把这些收在一处的组件。

三层设计

采样器把自己劈成三层,各管一件事(lib.rs 开头就自述了这个结构):

层类型职责
L1 客户端SamplingClient发请求、解 SSE,吐出「原始类型化 chunk 流」
L2 流变换stream::*把三种后端的原始 chunk 统一成同一套 SamplingEvent
L3 actorSamplerActor / SamplerHandle并发、重试、取消、事件协调

这一章看 L3(actor),下一章看 L2(三种流怎么统一),再下一章看 L1 的 SSE 解析与事件。

actor:一个命令循环 + 每请求一个 task

SamplerActor(actor/mod.rs)的骨架非常经典:

  • 它自己是单个 tokio task,跑一个 run() 循环,从一条 mpsc 命令通道收命令(Submit / Cancel / UpdateConfig / IsActive / ActiveCount)。
  • 每来一个 Submit,它不自己去跑请求,而是 tokio::spawn 一个 task 丢进 JoinSet<RequestId>,让流式工作并发进行。
  • 因为所有状态(active_requests: HashMap)只在这一个 actor task 里读写,不需要任何锁——这就是 actor 模式最舒服的地方:把「共享状态」变成「单线程状态 + 消息传递」。
图表加载中…

循环里有个细节值得学:tokio::select! 用了 biased,优先处理 JoinSet 里已完成的 task(把它从 active_requests 清掉)再收新命令。顺序颠倒的话,刚结束的请求可能还挂在表里,active_count() 就不准了。

handle:外面怎么用它

其它 crate 不直接碰 actor,只拿一个 SamplerHandle(handle.rs)。它 #[derive(Clone)]、内部只包一个 cmd_tx,所以克隆极便宜——谁想发请求就克隆一份。actor 在所有 handle 都 drop 后自动退出(命令通道收到 None)。

两种用法:

  • 发了不管:submit(id, request)、cancel(id)——事件走共享的 event_tx 通道异步流出。
  • 发了等结果:submit_and_collect(id, request) 返回 CompletionResult(即 Result<(ConversationResponse, InferenceLatencyStats), SamplingError>),源码注释点名的用户是压缩、摘要、/btw 旁路提问这类「不需要流式 UI、只要最终结果」的场景。它的加强版 submit_and_collect_with_metadata 还会带回这次请求的 doom-loop 信号与恢复记录——主循环每次采样走的就是它,事件照样实时流向 UI。

取消:一条 token 串起来

取消是 agent 的刚需(你按 Ctrl+C 取消当前 turn、或子任务被叫停)。grok 用 tokio_util::sync::CancellationToken:

  1. SamplerHandle::cancel(id) 发 Cancel 命令;
  2. actor 在 active_requests 里找到那个请求的 token,.cancel();
  3. 正在跑的 task 在流循环里用 biased select 优先监听 cancel_token.cancelled(),一触发就停,产出 Cancelled。

还有两个巧思:同 RequestId 重复 Submit 会取消旧的(后来者胜);submit_and_collect 系列(实现在 submit_and_collect_with_metadata 里)额外挂一个 RAII 守卫 CancelOnDrop——future 被 drop(调用方放弃等待)时自动取消在途请求,不留孤儿。

用法照进来:你加 --log-sampling(隐藏参数,也可以设 GROK_LOG_SAMPLING=1)时,每个请求的生命周期(重试、耗时、token)会写进 ~/.grok/logs/sampling.jsonl——事件由这一层的 sampling_log.rs 以 tracing 形式打出,再由 xai-grok-telemetry 里的专用 layer 落盘。想亲眼看 actor 在忙什么,开它。

和《从 LLM 到 Coding Agent》对照

那本的 裸 LLM API 章讲了「一次模型调用要处理哪些事」。grok 把这些事——并发、取消、重试、流式——从会话逻辑里抽出来,做成一个可复用的 actor(它甚至复用了 xai-hunk-tracker 的 actor 套路)。这就是「教学最小实现只需一个 await,生产实现需要一个 actor」的典型例子。


上一篇:Agent loop 主循环 · 下一篇:三种 API 流怎么统一

本页目录