1. 模块概述
Provider 系统(nanobot/providers/)通过抽象基类 LLMProvider 统一了不同 AI 服务商的调用接口,提供智能重试、流式输出、错误分类和 fallback 回退等能力。
📍 核心源码:
nanobot/nanobot/providers/base.py#L1-L965
1.1 支持的 Provider
| Provider | 模块 | 特点 |
|---|---|---|
| Anthropic | anthropic_provider.py |
扩展思考、提示缓存、工具使用 |
| OpenAI Compatible | openai_compat_provider.py |
通用 OpenAI API 兼容 |
| OpenAI Responses | openai_responses/ |
新的 Responses API |
| Azure OpenAI | azure_openai_provider.py |
Azure 托管 |
| AWS Bedrock | bedrock_provider.py |
AWS 原生集成 |
| GitHub Copilot | github_copilot_provider.py |
Copilot API |
| OpenAI Codex | openai_codex_provider.py |
Codex CLI 集成 |
1.2 在系统中的位置
graph TB
AR[AgentRunner] -->|chat_stream_with_retry| LP[LLMProvider]
LP -->|_run_with_retry| LP
LP -->|chat/chat_stream| AN[AnthropicProvider]
LP -->|chat/chat_stream| OC[OpenAICompatProvider]
LP -->|chat/chat_stream| AZ[AzureOpenAIProvider]
LP -->|chat/chat_stream| BD[BedrockProvider]
FB[FallbackProvider] -->|try primary| LP
FB -->|fallback| LP2[Fallback Model]
FAC[ProviderFactory] -->|make_provider| LP
FAC -->|make_fallback| FB
2. 命名体系与易混淆函数对比
2.1 命名规律拆解
| 前缀 | 操作对象 | 操作 | 含义 |
|---|---|---|---|
chat_ |
— | with_retry |
带重试的非流式聊天调用 |
chat_ |
stream_ |
with_retry |
带重试的流式聊天调用 |
_safe_ |
— | chat |
包装 chat(),将异常转为错误响应 |
_safe_ |
— | chat_stream |
包装 chat_stream(),将异常转为错误响应 |
_run_ |
with_ |
retry |
重试策略的核心实现 |
_is_ |
transient_ |
response |
判断错误是否为瞬时(可重试) |
_is_ |
transient_ |
error |
基于文本标记判断错误是否为瞬时 |
_is_ |
retryable_ |
429_response |
判断 429 错误是否可重试 |
_extract_ |
retry_ |
after |
从错误文本中提取重试等待时间 |
_extract_ |
retry_after_ |
from_headers |
从 HTTP 头部提取重试等待时间 |
_extract_ |
retry_after_ |
from_response |
从 LLMResponse 提取重试等待时间 |
_extract_ |
error_ |
type_code |
从错误载荷中提取 type/code |
_sanitize_ |
empty_ |
content |
清理消息中的空内容块 |
_sanitize_ |
request_ |
messages |
只保留 Provider 允许的消息键 |
_strip_ |
image_ |
content |
移除消息中的图像内容(原地) |
_enforce_ |
role_ |
alternation |
确保消息角色交替(Provider 兼容性) |
2.2 易混淆函数对比表
| 对比维度 | chat_with_retry |
chat_stream_with_retry |
|---|---|---|
| 底层调用 | _safe_chat → chat() |
_safe_chat_stream → chat_stream() |
| 流式回调 | ❌ 不支持 | ✅ on_content_delta / on_thinking_delta |
| 超时策略 | 受 LLM 超时保护 | 不受外层超时保护(有 stream idle timeout) |
| 已流式内容后的重试 | N/A | 抑制 delta 回调后重试 |
| 一句话区分 | 非流式调用,适合一次性请求 | 流式调用,适合需要实时展示的场景 |
| 对比维度 | _is_transient_response |
is_arrearage_response |
|---|---|---|
| 目的 | 判断是否应该重试 | 判断是否为欠费/配额错误 |
| 决策逻辑 | 结构化字段 → 状态码 → 文本标记 | 402 状态码 → type/code token → 文本标记 |
| 使用场景 | _run_with_retry 中决定是否继续重试 |
_run_core 中生成用户友好的错误消息 |
| 一句话区分 | 决定”要不要重试” | 决定”是不是没钱了” |
3. API Signatures
graph TB
subgraph "公共 API"
chat["chat() [abstract]"]
chat_stream["chat_stream()"]
chat_with_retry["chat_with_retry()"]
chat_stream_with_retry["chat_stream_with_retry()"]
get_default_model["get_default_model() [abstract]"]
end
subgraph "内部方法"
safe_chat["_safe_chat()"]
safe_chat_stream["_safe_chat_stream()"]
run_with_retry["_run_with_retry()"]
is_transient["_is_transient_response()"]
is_arrearage["is_arrearage_response() [classmethod]"]
enforce_role["_enforce_role_alternation()"]
end
chat_with_retry --> safe_chat
safe_chat --> chat
chat_stream_with_retry --> safe_chat_stream
safe_chat_stream --> chat_stream
chat_with_retry --> run_with_retry
chat_stream_with_retry --> run_with_retry
1 | class LLMProvider(ABC): |
4. 数据结构深度解析
4.1 LLMResponse
4.a 结构体存在的理由
LLMResponse 统一了所有 Provider 的返回格式。不同 Provider 的原生响应格式差异很大(Anthropic 有
thinking_blocks,OpenAI 用tool_calls数组,DeepSeek 有reasoning_content),LLMResponse 将这些差异归一化为统一结构,使 AgentRunner 无需关心底层 Provider。
4.b 字段三层分析表
| 字段 | 设计动机 | 反事实 | 替代方案 |
|---|---|---|---|
error_status_code |
区分 429(限流/可重试)和 402(欠费/不可重试) | 只能通过文本匹配判断错误类型,不可靠 | 可以用异常类型,但跨 Provider 的异常类型不统一 |
error_kind |
粗粒度错误分类(timeout/connection) | 无法区分超时和连接错误,重试策略不精准 | 可以用多个布尔字段,但扩展性差 |
error_should_retry |
Provider 显式声明是否可重试,避免启发式判断 | 依赖文本匹配的启发式可能误判 | 可以只用文本匹配,但 error_should_retry 提供确定性 |
thinking_blocks |
Anthropic 扩展思考的增量块 | Anthropic 的思考内容只能通过 reasoning_content 字符串传递 | 可以用 reasoning_content 存储序列化 JSON,但增加解析成本 |
4.2 GenerationSettings
4.a 结构体存在的理由
GenerationSettings 提供 Provider 级别的默认生成参数。AgentRunner 可以通过 AgentRunSpec 覆盖这些默认值,但如果不覆盖,就使用 Provider 预设的合理默认值。这避免了每个调用点都需要显式传递 temperature/max_tokens。
5. 关键算法剖析
5.1 重试策略(_run_with_retry)
重试策略是 Provider 系统最复杂的部分,处理两种模式:
flowchart TD
A[调用 LLM] --> B{finish_reason == error?}
B -->|No| C[返回响应]
B -->|Yes| D{已流式输出内容?}
D -->|Yes, timeout| E[恢复流 + 重试]
D -->|Yes, 其他| F[不重试, 返回错误]
D -->|No| G{是瞬时错误?}
G -->|No| H{有图片内容?}
H -->|Yes| I[去图片重试一次]
H -->|No| J[返回错误]
G -->|Yes| K{persistent 模式?}
K -->|Yes| L{相同错误超限?}
L -->|Yes| M[停止重试]
L -->|No| N[等待 retry_after 延迟]
K -->|No| O{重试次数超限?}
O -->|Yes| M
O -->|No| N
N --> A
关键参数:
_CHAT_RETRY_DELAYS = (1, 2, 4):标准模式 3 次重试,指数递增_PERSISTENT_MAX_DELAY = 60:持久模式最大等待 60 秒_PERSISTENT_IDENTICAL_ERROR_LIMIT = 10:相同错误连续 10 次后放弃
5.2 429 错误智能分类
1 | # 不可重试的 429(欠费/配额) |
决策优先级:
error_should_retry字段(Provider 显式声明)- 结构化 error_type / error_code token 匹配
- 错误文本中的标记匹配
- 未知 429 默认 重试(保守策略)
6. 设计决策分析
6.1 为什么使用 SENTINEL 模式而不是 None 表示”使用默认值”?
决策:chat_with_retry 的参数默认值使用 _SENTINEL = object() 而不是 None。
权衡:
- ✅ 调用者可以显式传递
None表示”不使用此参数” - ✅ 区分”未提供”(使用 generation 默认值)和”显式设为 None”(禁用)
- ❌ 增加了一层 sentinel 检查逻辑
6.2 为什么流式请求不受外层 LLM 超时保护?
决策:在 _request_model 中,流式请求的 outer_timeout_s = None。
权衡:
- ✅ 长时间推理流不会被外层超时杀死(如 5 分钟的深度思考)
- ✅ 流式请求有自己的
NANOBOT_STREAM_IDLE_TIMEOUT_S保护(默认 90 秒无数据则超时) - ❌ 如果流永不空闲但也不产生有用内容,可能永远不超时
6.3 为什么默认参数使用 _SENTINEL 并在方法体内解析?
决策:不在方法签名中使用 = None 然后 = self.generation.xxx,而是在方法体内检查 _SENTINEL。
权衡:
- ✅
None是有效的用户输入(表示”不限制”),不能用于表示”使用默认值” - ✅ 调用链中每一层都可以独立决定是否覆盖
- ❌ 每个方法都需要 sentinel 检查样板代码
7. 学习检查点
📝 本章小结
- LLMProvider 是抽象基类,所有 Provider 必须实现
chat()和get_default_model() - 重试策略分两种模式:standard(3次指数退避)和 persistent(无限重试至相同错误超限)
- 429 错误智能分类:区分欠费/配额(不可重试)和限流(可重试)
- LLMResponse 统一所有 Provider 的返回格式,通过结构化错误元数据支持精准重试判断
- 流式请求有独立的空闲超时保护,不阻塞长时间推理
🤔 思考题
为什么
_run_with_retry在重试前要检查should_retry_guard?什么情况下流式请求在输出内容后还需要重试?参考答案
should_retry_guard在流式场景中检查”是否已经向用户展示了部分内容”。如果已展示内容,大多数错误不应重试(用户已经看到部分输出,重试会产生重复内容)。唯一的例外是 timeout(nanobot/nanobot/providers/base.py#L870-L871):流停滞但尚未完成,此时调用on_stream_recover启动新的流段继续输出。_enforce_role_alternation中为什么要将连续的 assistant 消息中的 tool_calls 合并?直接用后面的覆盖前面的不行吗?参考答案
在
nanobot/nanobot/providers/base.py#L508-L516,合并策略是:如果当前消息有 tool_calls,则替换前一条(新 tool_calls 比旧的更有价值);如果前一条有 tool_calls 而当前没有,则跳过当前(保留 tool_calls 比纯文本回复更重要)。这确保了 tool_calls 信息不丢失——如果简单地用后一条覆盖前一条,可能丢失重要的工具调用。