sampler 桥接:run_turn_via_sampler


文档摘要

sampler 桥接:runturnviasampler 本节摘要:在前几节拆解的循环里,有一个关键步骤反复出现—— 。它是 shell 层与 sampler 层之间的桥梁:把组装好的请求交给 sampler,收集 sampler 吐回的流式事件,翻译成循环能理解的三种结果(Response、CompactAndResubmit、RefreshAuthAndResubmit)。本节会拆解这座桥的内部结构,看清流式 token 如何被收集、工具调用增量如何累积、完成与失败如何被翻译、以及桥为何要把「语义错误」上浮给循环决策。理解这一节,shell 与 sampler 的边界就彻底清晰了,也为第 4 章深入 sampler 内部做好准备。

sampler 桥接:run_turn_via_sampler

本节摘要:在前几节拆解的循环里,有一个关键步骤反复出现——run_turn_via_sampler(request)。它是 shell 层与 sampler 层之间的桥梁:把组装好的请求交给 sampler,收集 sampler 吐回的流式事件,翻译成循环能理解的三种结果(Response、CompactAndResubmit、RefreshAuthAndResubmit)。本节会拆解这座桥的内部结构,看清流式 token 如何被收集、工具调用增量如何累积、完成与失败如何被翻译、以及桥为何要把「语义错误」上浮给循环决策。理解这一节,shell 与 sampler 的边界就彻底清晰了,也为第 4 章深入 sampler 内部做好准备。

一、为什么需要一座「桥」

回顾架构,shell 与 sampler 是两个独立的层,各自有自己的 Actor。它们之间不是直接函数调用,而是 Actor 消息通信。但 process_conversation_turn 这个循环需要的是一个同步感的体验:「我给你一个请求,你给我一个结果」。

这座桥(run_turn_via_sampler)的作用,就是把异步的、流式的、事件驱动的 sampler 交互,封装成循环可以直接消费的形式。它做三件事:

  1. 把请求提交给 sampler
  2. 收集 sampler 吐出的事件流,边收边转发给 UI(让用户看到流式输出)
  3. 流结束时,把结果翻译成循环能决策的三种 outcome 之一

没有这座桥,循环要直接处理 sampler 的复杂事件流,代码会乱;有了桥,循环只关心「调一次,得一个结果」,简洁清晰。

二、桥的整体结构

源码在 xai-grok-shell/src/session/acp_session_impl/sampler_turn.rs。简化伪代码:

async fn run_turn_via_sampler(request) -> SamplerTurnOutcome { # ① 提交请求,获得事件流与完成回调 let (event_rx, completion_rx) = sampler_handle.submit(request, config); # ② 边收事件边处理 let mut accumulated = AccumulatedResponse::new(); while let Some(event) = event_rx.recv().await { match event { ChannelToken { channel, text } => { # 流式 token:转发给 UI 实时显示 发 ACP 通知给 pager(让 UI 渲染这个 token) accumulated.append_token(channel, text); } ToolCallDelta { tool_index, name, args_delta } => { # 工具调用增量:累积起来 accumulated.append_tool_delta(tool_index, name, args_delta); } ModelMetadata { metadata } => { accumulated.record_metadata(metadata); } Retrying { attempt, kind, ... } => { # 重试中:通知 UI,继续等 发 ACP 通知:"正在重试(第 N 次)..." # 不结束循环,继续 recv } Completed { response, metrics } => { # 完成:用最终响应填充 accumulated accumulated.finalize(response, metrics); break; } Failed { error } => { # 失败:翻译成循环能处理的 outcome return translate_failure(error); } } } # ③ 把累积结果转成 outcome return SamplerTurnOutcome::Response(accumulated.into_response()); }

这个结构揭示了桥的三个核心动作:提交收集与转发翻译。下面逐一展开。

三、动作一:提交请求

let (event_rx, completion_rx) = sampler_handle.submit(request, config);

桥通过 sampler_handle(指向 SamplerActor 的句柄)提交一个 Submit 命令。这个命令带:

  • request:完整的对话请求(历史、系统提示、工具定义、采样参数)
  • config:SamplerConfig(model、temperature、top_p 等)

sampler 收到 Submit 后,注册一个 ActiveRequest,派生一个独立的异步 task 处理这次流式调用(第 4 章详谈),然后给桥返回两个通道:

  • event_rx:事件流接收端,桥从这里收 SamplingEvent
  • completion_rx:完成回调,最终响应会从这里来

提交后,桥进入「收事件」循环,不阻塞等待——它边收边处理,实现流式体验。

四、动作二:收集与转发事件

桥的核心是一个 while let Some(event) = event_rx.recv() 循环,逐个处理 sampler 吐来的事件。几类关键事件:

ChannelToken:流式 token

ChannelToken { channel, text }

模型生成了一个 token。channel 标明这是正文还是推理过程(reasoning)。桥做两件事:

  1. 转发给 UI:通过 ACP 通知 pager,让 UI 实时把这个 token 渲染出来。这就是用户看到的「边想边说」效果。
  2. 累积到本地:把 token 存进 accumulated,流结束后用来构造完整响应。

注意「转发」与「累积」是并行的——转发让用户立刻看到,累积是为了最终持有完整文本。这种「边显示边存」的设计,是流式体验的基础。

ToolCallDelta:工具调用增量

ToolCallDelta { tool_index, name, arguments_delta }

模型在生成一个工具调用,但它不是一次性给出完整参数,而是增量地吐出。比如要调用 bash 工具,模型会先吐 "comma,再吐 nd": "ls,再吐 "},桥要把这些片段拼起来。

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

关键概念:模型生成工具调用是「流式」的,就像它生成正文一样。桥要在流式增量与「完整工具调用」之间做缓冲与拼接,这是 ReAct 循环里一个容易被忽视但很重要的细节。

Retrying:重试中

Retrying { attempt, max_retries, kind, reason, ... }

sampler 遇到了可重试的错误(网络抖动、限流等),正在按重试策略退避重试。桥不结束循环,只是通知 UI「正在重试」,然后继续等下一个事件。这让重试对用户可见但不打断——用户看到「重试中」,但不必干预。

注意:Retrying 事件里的 kind 是错误类型(网络、限流、空响应等),桥可以据此决定要不要给 UI 更详细的提示。但桥自己不处理重试逻辑——重试是 sampler 的职责,桥只是旁观。

Completed:成功完成

Completed { response, metrics }

流成功结束,sampler 给出最终的完整响应(可能包含正文、工具调用、用量等)与指标(延迟、重试次数等)。桥用这些填充 accumulated,然后 break 退出循环。

Failed:彻底失败

Failed { error }

sampler 用尽重试仍失败,或遇到了不可重试的错误。桥不在这里崩溃,而是把错误翻译成循环能决策的 outcome(下一节详谈)。

五、动作三:翻译结果

桥最精妙的部分是「翻译」——把 sampler 的「成功/失败」二分,翻译成循环能决策的三种 outcome:

enum SamplerTurnOutcome { Response(完整响应), # 正常拿到响应 CompactAndResubmit, # 上下文超限,需压缩后重发 RefreshAuthAndResubmit, # 认证失效,需刷新后重试 }

Response:正常情况。流成功完成,响应里可能有正文、可能有工具调用。循环拿到后进入决策阶段(执行工具 or 结束)。

CompactAndResubmit:遇到「上下文超限」错误。这通常表现为服务端返回一个特定的错误码或消息,说明请求的 token 数超过模型窗口。桥识别出这类错误后,不当作普通失败,而是翻译成 CompactAndResubmit。循环收到后会触发上下文压缩,然后 continue 用更短的请求重试。

RefreshAuthAndResubmit:遇到「认证失效」错误(通常是 401)。这说明 access_token 过期或无效。桥翻译成这个 outcome,循环收到后会刷新认证(用 refresh_token 换新 access_token,或触发重新登录流程),backoff 一会儿,然后 continue 重试。

翻译的依据:桥怎么知道一个失败是「上下文超限」还是「认证失效」还是「普通失败」?依据是 SamplingErrorKind(第 4 章详谈)——sampler 在 Failed 事件里带上错误类型,桥按类型分流:

fn translate_failure(error) -> SamplerTurnOutcome { match error.kind { MaxTokensTruncation | 上下文超限相关 => CompactAndResubmit, Auth => RefreshAuthAndResubmit, 其他 => 真正失败,抛给循环作为致命错误 } }

关键概念:桥把「错误」分成了两类:可恢复的语义错误(上下文超限、认证失效)与不可恢复的致命错误(其他)。可恢复的翻译成 Resubmit outcome,让循环用 continue 处理;不可恢复的才真正中断。这种分层让 Agent 对常见的服务端波动具有韧性。

六、桥的几个设计要点

把桥的结构看清楚,有几个设计要点值得琢磨:

要点一:桥是「适配器」,不持有业务逻辑

桥本身不做业务决策——不决定要不要重试(那是 sampler)、不决定要不要压缩(那是循环)、不决定工具怎么执行(那是 tool_bridge)。它只做「翻译与转发」。这种纯粹的适配器角色,让桥的代码相对简单,也容易测试。

要点二:流式转发与累积并行

桥对每个 token / delta 都做两件事:转发给 UI + 累积到本地。这两个动作并行进行,既保证了用户体验(实时显示),又保证了最终持有完整数据(用于后续决策)。这是流式系统的常见模式。

要点三:重试对桥透明

sampler 的重试(Retrying 事件)对桥是「事件流的一部分」,桥不参与重试决策,只通知 UI。这让重试策略的调整(改退避时间、改最大次数)完全局限在 sampler 内,不影响桥与循环。

要点四:错误翻译是分层的

桥只翻译「语义错误」(上下文、认证),其他错误原样上浮。这避免了桥变成一个「什么错都处理」的大杂烩,保持了职责聚焦。更深层的错误处理(如 doom-loop 检测)在别处。

要点五:桥不取消,sampler 取消

如果用户中途取消(Cancel 命令),桥不会自己中断事件流——它会收到一个 Cancelled 事件(来自 sampler,因为 sampler 的 request_task 响应了取消令牌),然后正常退出循环。取消的实际动作(中断 HTTP 请求)在 sampler 里发生,桥只是感知结果。这种分工让取消逻辑集中在一处。

七、把桥放回循环

最后,把桥放回上一节的循环里,看它如何嵌入:

process_conversation_turn: loop { 准备工具、组装请求 match run_turn_via_sampler(request) { ← 本节的桥 Response(resp) => { if resp 含工具调用: 执行工具,塞回历史,continue else: 收尾,return Done } CompactAndResubmit => { 压缩上下文,continue } RefreshAuthAndResubmit => { 刷认证 + backoff,continue } } }

桥是循环里「调用模型」这一步的全部封装。循环不直接接触 sampler 的事件流,也不直接处理 HTTP 细节——它只看到三种干净的 outcome。这种分层让循环的代码简洁,也让 sampler 的演进(如支持新后端、改重试策略)不影响循环。

八、为第 4 章铺垫

本节讲的是「桥」,桥的另一端是 sampler。下一章我们会深入 sampler 内部,看清:

  • SamplerActor 如何用单线程命令循环 + 每请求并发 task 处理多个在飞请求
  • SamplingEvent 这个统一事件枚举的完整定义
  • 三种 API 后端的 L2 transform 如何把不同格式统一
  • RetryPolicy 与 CancellationToken 如何实现可靠的重试与取消
  • doom-loop 检测如何防止 Agent 陷入死循环

理解了本节的桥,第 4 章的 sampler 就有了清晰的「上游」——你知道 sampler 吐出的每个事件,是如何被桥消费、翻译、转发的。这种上下游的连贯,是理解整个系统的关键。

本节要点回顾

  1. 桥封装异步流式交互为同步感结果:让循环只看到三种 outcome,不必处理 sampler 的复杂事件流。
  2. 桥的三动作:提交请求、收集转发事件、翻译结果。
  3. 提交:通过 sampler_handle.submit,获得事件流与完成回调。
  4. 收集转发:ChannelToken(转发+累积)、ToolCallDelta(累积)、Retrying(通知)、Completed(填充+break)、Failed(翻译)。
  5. 翻译:Response(正常)、CompactAndResubmit(上下文超限)、RefreshAuthAndResubmit(认证失效),依据 SamplingErrorKind。
  6. 设计要点:桥是纯适配器、流式转发与累积并行、重试对桥透明、错误翻译分层、取消在 sampler。
  7. 桥让循环简洁:循环只接触三种 outcome,sampler 的演进不影响循环。
  8. 为第 4 章铺垫:理解桥,就有了 sampler 的清晰「上游」。

下一节,我们收束本章,讲清 shell 层如何处理错误——特别是 sampler 的传输错误与 shell 的语义错误的分层,以及上下文超限与认证失效这两种「可恢复错误」的完整处理流程。


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