流式输出、错误与停止
对应源码
| 主题 | 源码 |
|---|---|
| stream、timeout 和停止分支 | nanobot/agent/runner.py::AgentRunner._request_model、AgentRunner._run_core |
| Provider stream 与 retry | nanobot/providers/base.py::LLMProvider.chat_stream_with_retry、LLMProvider._run_with_retry |
| Provider fallback | nanobot/providers/fallback_provider.py::FallbackProvider |
| progress/reasoning hook | nanobot/agent/progress_hook.py、nanobot/agent/hook.py |
| bus stream segment | nanobot/agent/loop.py::AgentLoop._dispatch |
| 取消入口 | nanobot/agent/loop.py::AgentLoop.stop、nanobot/command/builtin.py |
学习目标
读完后,你应当能够:
- 区分内容 delta、reasoning、progress 和最终响应;
- 说明 LLM wall timeout 与 stream idle timeout;
- 理解 retry、fallback、空响应恢复和 max iteration;
- 根据 stop reason 找到相应 Runner 分支。
前置知识
- 理解 AgentRunner 主循环;
- 理解异步 callback 和取消;
- 理解 HTTP/Provider 错误的基本分类。
1. “流式”包含多种事件
nanobot 运行中可能出现:
| 类型 | 含义 |
|---|---|
| content delta | 模型最终文本的增量 |
| reasoning delta | Provider 返回的思考/推理内容 |
| tool hint | 即将调用工具的提示 |
| progress | 工具或 Agent 生命周期进度 |
| retry wait | 正在等待重试 |
| stream end | 当前 stream segment 结束 |
| final response | 完整 Turn 的最终文本 |
| runtime event | Turn/Goal/Model 状态变化 |
它们不是同一个 Channel metadata,也不应由一个 UI 标记混合表达。
2. 核心流式路径
sequenceDiagram participant P as Provider participant R as AgentRunner participant H as Hook/Callback participant E as CLI/API/Channel P-->>R: content delta R-->>H: on_content_delta H-->>E: 展示增量 P-->>R: tool call 或最终完成 R-->>H: on_stream_end R-->>E: 最终 AgentRunResult
入口适配:
- CLI:终端 renderer;
- API:SSE chunk;
- SDK:typed stream event;
- Channel:
send_delta(); - WebSocket:协议消息。
3. 一个 Turn 可以有多个 stream segment
当模型先输出内容、再调用工具、再继续回答时,stream 可能暂停和恢复。AgentLoop 为 Channel stream 生成 segment ID,避免不同阶段的 delta 被错误拼成同一个气泡。
因此不能把第一次 stream_end 直接理解为整个 Turn 已完成。最终完成仍以:
- Runner 返回;
- SAVE;
- RESPOND;
- TurnCompleted/runtime wire event
为准。
4. 两种主要 timeout
LLM wall timeout
限制一次模型请求允许占用的总时间。
- 环境变量:
NANOBOT_LLM_TIMEOUT_S - 默认值以
AgentRunner当前源码为准。
Stream idle timeout
限制流在没有任何事件的情况下可以空闲多久。
- 环境变量:
NANOBOT_STREAM_IDLE_TIMEOUT_S - 解析位置:
nanobot/providers/base.py - 不是整个请求的总时长。
wall timeout:从请求开始到结束的总墙钟边界
idle timeout:两次流事件之间的最大沉默时间AgentRunner._request_model() 还明确避免把两种 timeout 叠加到健康的长流上:
if wants_streaming:
coro = self.provider.chat_stream_with_retry(...)
else:
coro = self.provider.chat_with_retry(**kwargs)
# Streaming requests already have provider-level idle timeouts.
outer_timeout_s = None if (wants_streaming or wants_progress_streaming) else timeout_s
response = (
await coro
if outer_timeout_s is None
else await asyncio.wait_for(coro, timeout=outer_timeout_s)
)这是压缩后的源码节选。非流式请求可由外层 wall timeout 限制;流式请求依靠 Provider 的 idle timeout,避免一个持续正常产生事件、但总耗时较长的 stream 被错误终止。
5. Provider retry
LLMProvider 的 retry 逻辑根据:
- HTTP status;
- error kind;
- Provider retry-after;
- retry mode;
- 是否已产生 stream 内容;
- 调用方 callback
决定是否等待和重试。
不是所有错误都应重试。例如:
- 认证错误通常不应持续重试;
- 限流或临时服务错误可能重试;
- 已经向用户输出部分 stream 后,盲目重试可能产生重复文本。
6. Fallback model
providers/factory.py::make_provider() 可以根据配置创建 fallback chain。fallback 候选可以是:
- named preset;
- inline Provider/model 配置。
关键理解:
- fallback 发生在 Provider 层;
- primary 和 fallback 可以有不同 model、context window、token 和 reasoning 参数;
- 具体错误是否触发 fallback 由 Provider/fallback 实现决定;
- fallback 不等于 AgentRunner 再启动一个全新 Session。
7. 空响应恢复
模型可能成功返回但 content 为空。Runner 会:
- 进行有限次数空响应重试;
- 必要时尝试 finalization retry;
- 如果仍没有最终内容,产生统一空响应结果;
- AgentLoop SAVE 阶段确保需要回复的 Turn 不保存空 final content。
这与 Provider 抛异常不同。
8. finish_reason=length
如果模型输出因长度截断且已有非空内容,Runner 可以继续请求完成剩余内容,受恢复次数限制。
调试时要区分:
- 内容为空;
- 内容被截断;
- Tool Call;
- Provider error;
- max iterations。
9. Max iterations
AgentRunSpec.max_iterations 限制模型—工具循环次数,防止无限调用。
达到上限后可能:
- 进行 finalization;
- 返回 max-iteration 提示;
- 对活跃 Goal 采用 continuation 策略。
不要简单把上限设置得极大来掩盖循环不终止。先检查:
- 模型是否反复调用同一工具;
- 工具错误是否给了可行动信息;
- goal predicate 是否一直为真;
- injection 是否持续进入;
- prompt 是否缺少终止条件。
10. Tool error
普通 Tool 错误通常回到模型。以下情况更可能结束 Runner:
fail_on_tool_error;- 明确的 fatal error;
- 重复 workspace 违规;
- 无法恢复的安全错误;
- Runner 自身异常。
AgentRunResult.error 和 stop_reason 应一起观察。
11. 取消
取消可能来自:
/stop;- 进程 shutdown;
- 上层 Task cancel;
- timeout。
AgentLoop 在取消时需要:
- 停止对应活跃 Task;
- 恢复或保留 checkpoint/pending user turn;
- 不破坏 Session 合法边界;
- 关闭 MCP 等资源;
- 让真实 Task cancellation 正确传播。
取消后的“没有最终回复”不等于 Session 中什么都没保存,因为 user 消息可能已提前持久化。
调试矩阵
| 现象 | 观察 |
|---|---|
| UI 有 delta 但一直不结束 | on_stream_end、Runner 是否返回、TurnCompleted |
| 输出重复 | 是否在已输出 delta 后发生 retry |
| 长时间无内容 | stream idle timeout、reasoning stream、Provider 状态 |
| 工具无限重复 | iteration、tool event、tool result、终止提示 |
用户 /stop 后历史丢失 | pending user turn、checkpoint、取消恢复 |
| 内容突然截断 | finish reason、length recovery |
| primary 失败后模型变化 | fallback chain、runtime model event |
源码与测试
nanobot/agent/runner.py::_request_modelnanobot/agent/runner.py::_run_corenanobot/providers/base.pynanobot/providers/fallback_provider.pynanobot/agent/progress_hook.pytests/agent/test_runner_progress_deltas.pytests/agent/test_runner_reasoning.pytests/agent/test_runner_errors.pytests/agent/test_runner_fallback.pytests/agent/test_task_cancel.pytests/providers/test_stream_idle_timeout_config.py
自检
- stream end 为什么不一定等于 Turn end?
- idle timeout 与 wall timeout 有什么区别?
- 为什么已输出部分 delta 后重试需要更谨慎?
- 空响应与 Provider error 的恢复路径有什么不同?
- 达到 max iterations 时为什么不应只提高上限?