流式传输与进度事件
流式传输与进度事件
学习目标
理解 Nanobot 如何实现内容、推理和文件编辑三层流式输出,以及 WebSocket 分段输出机制。
前置知识
- 流式传输架构 — Token/块/预览三层流式模型
- Think 过滤流式 — 推理内容的流式传输
下文假设你已理解上述概念,直接聚焦 Nanobot 的具体实现。
项目实践
三种流式模式
Nanobot 的 AgentRunner._request_model() 支持三种流式模式:
| 模式 | 触发条件 | 特点 |
|---|---|---|
| 完整流式 | hook.wants_streaming() == True | 直接调用提供商的 chat_stream_with_retry |
| 进度流式 | 提供商支持 progress_deltas 但 hook 不需要完整流式 | 通过 IncrementalThinkExtractor 过滤推理内容 |
| 非流式 | 无流式需求 | 直接调用 chat_with_retry |
文件编辑进度事件
Nanobot 实现了实时的文件编辑进度流:
live_file_edits = StreamingFileEditTracker( workspace=spec.workspace, tools=spec.tools, emit=_emit_live_file_edits,)当 on_progress 回调接受文件编辑事件时:
- 在工具调用 delta 流中追踪文件修改
- 实时推送编辑进度到前端(WebUI 显示进度条)
- 工具调用完成后应用最终 call_id
- 未匹配的工具调用报告错误
WebSocket 分段输出
WebUI 使用分段流式(stream segment),每个工具调用阶段为一个独立段:
stream_base_id = f"{msg.session_key}:{time.time_ns()}"stream_segment = 0
def _current_stream_id() -> str: return f"{stream_base_id}:{stream_segment}"每次 on_stream_end(resuming) 调用时,stream_segment 递增。resuming=True 表示工具调用继续(spinner 应重新启动),resuming=False 表示最终回复。
外超时管理
Nanobot 对不同类型请求应用不同的超时策略:
| 请求类型 | 超时控制 |
|---|---|
| 非流式请求 | NANOBOT_LLM_TIMEOUT_S(默认 300s) |
| 流式请求 | 不使用外超时,依赖提供商级别的 NANOBOT_STREAM_IDLE_TIMEOUT_S |
| 进度流式 | 不使用外超时 |
原因:流式请求如果应用外超时,长推理会被错误杀掉,因为总耗时可能远超单次延迟阈值。
问题与规避
流式内容已发送但请求失败
问题:如果流式输出已经开始(内容 delta 已发送),但 LLM 返回错误,不应重试(否则用户看到重复内容)。
规避:should_retry_guard=lambda: not has_streamed_content 确保一旦有流式内容输出,不再重试。
推理内容泄漏到正文
问题:某些提供商的推理内容混在普通内容中发送。
规避:IncrementalThinkExtractor 监控流式内容,自动识别和提取推理块,通过 emit_reasoning() 分离推送。
设计取舍
为什么区分完整流式与进度流式
原因:不同渠道对流式的需求不同。WebSocket 频道需要完整流式(每段 delta 单独推送),而某些渠道只需要进度提示(如工具调用提示)。进度流式模式允许在不支持完整流式的提供商上仍提供部分用户体验。