采样器 · 用 actor 模型管住并发的模型请求
grok 和模型对话这一侧叫「采样器」。它用一个 actor 把「并发、取消、重试」收在一处,无锁地管理多个同时进行的模型请求。这一章看它的三层设计与 actor 骨架。
采样器 · 用 actor 模型管住并发的模型请求
上一章看了主循环怎么在「模型 ↔ 工具」之间转。转向模型的那一侧,grok 叫它采样器(sampler)——crates/codegen/xai-grok-sampler。别被名字骗了,它不只是「发个 HTTP 请求」:一个交互式 agent 会同时有多个模型请求在飞(主对话、子任务、后台压缩……),每个都可能要取消、要重试,还不能互相踩。采样器就是把这些收在一处的组件。
三层设计
采样器把自己劈成三层,各管一件事(lib.rs 开头就自述了这个结构):
| 层 | 类型 | 职责 |
|---|---|---|
| L1 客户端 | SamplingClient | 发请求、解 SSE,吐出「原始类型化 chunk 流」 |
| L2 流变换 | stream::* | 把三种后端的原始 chunk 统一成同一套 SamplingEvent |
| L3 actor | SamplerActor / 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:
SamplerHandle::cancel(id)发Cancel命令;- actor 在
active_requests里找到那个请求的 token,.cancel(); - 正在跑的 task 在流循环里用
biasedselect 优先监听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 流怎么统一