zeroclaw 是一個 Rust-first 的自主 Agent 執行時(autonomous agent runtime),設計目標是高效能、高穩定性、高擴充性與高安全性。它的架構以 trait 為核心,包含 Provider(LLM)、Channel(Telegram/Discord/Slack)、Tool、Memory、Security、Peripheral(STM32/RPi GPIO)等模組。這篇聚焦在它如何設計 LLM Provider 的抽象層,統一了 OpenAI、Anthropic、Gemini 等各種不同 API 的介面。
一個 Trait,一個必填方法
整個抽象的核心是 Provider trait,但它聰明的地方在於:只有一個方法是必須實作的:
#[async_trait]
pub trait Provider: Send + Sync {
async fn chat_with_system(
&self,
system_prompt: Option<&str>,
message: &str,
model: &str,
temperature: f64,
) -> anyhow::Result<String>;
// 其他方法都有預設實作,最終都會呼叫 chat_with_system
async fn simple_chat(&self, message: &str, model: &str, temperature: f64) -> anyhow::Result<String> { ... }
async fn chat_with_history(&self, messages: &[ChatMessage], ...) -> anyhow::Result<String> { ... }
async fn chat(&self, request: ChatRequest<'_>, ...) -> anyhow::Result<ChatResponse> { ... }
// ...
}這意味著加一個新 provider 最少只需要 ~30 行程式碼。其他的 simple_chat、chat_with_history 這些便利方法,都有預設實作會幫你委派過去。
這裡用了 async_trait crate,因為 trait 裡的 async fn 需要特別處理才能支援 dyn Provider。
統一的訊息型別
各家 LLM API 的訊息格式其實大同小異,zeroclaw 定義了一套統一的型別:
pub struct ChatMessage {
pub role: String, // "system", "user", "assistant", "tool"
pub content: String,
}
pub struct ChatResponse {
pub text: Option<String>,
pub tool_calls: Vec<ToolCall>,
}
pub struct ToolCall {
pub id: String,
pub name: String,
pub arguments: String, // JSON 字串
}ChatMessage 還提供了工廠方法:ChatMessage::user("hello")、ChatMessage::assistant("hi") 這樣,寫起來很順。
多輪對話(包含 tool call 結果)則用一個 enum 來表達:
pub enum ConversationMessage {
Chat(ChatMessage),
AssistantToolCalls { text: Option<String>, tool_calls: Vec<ToolCall> },
ToolResults(Vec<ToolResultMessage>),
}Decorator Pattern:功能疊加
Provider trait 自己也可以被包起來再實作 Provider,這讓功能可以自由組合。
ReliableProvider:自動重試 + API key 輪換
pub struct ReliableProvider {
providers: Vec<(String, Box<dyn Provider>)>,
max_retries: u32,
base_backoff_ms: u64,
api_keys: Vec<String>,
key_index: AtomicUsize, // 用 AtomicUsize 做 round-robin
model_fallbacks: HashMap<String, Vec<String>>,
}遇到 429 Rate Limit 就輪換 API key、指數退避重試;遇到 4xx 非 429 的錯誤就直接放棄;甚至還能解析 Retry-After header 來決定等多久。
RouterProvider:按 model 名稱路由
// 用 "hint:" 前綴來指定路由
fn resolve(&self, model: &str) -> (usize, String) {
if let Some(hint) = model.strip_prefix("hint:") {
if let Some((idx, resolved_model)) = self.routes.get(hint) {
return (*idx, resolved_model.clone());
}
}
(self.default_index, model.to_string())
}傳入 "hint:reasoning" 就會路由到你設定好對應「reasoning 任務」的 provider + model 組合。很適合多 provider 環境下的靈活調度。
Tool Calling 的三種模式
各家 API 對 function calling 的格式差異很大,zeroclaw 用一個 enum 來表達:
pub enum ToolsPayload {
Gemini { function_declarations: Vec<serde_json::Value> },
Anthropic { tools: Vec<serde_json::Value> },
OpenAI { tools: Vec<serde_json::Value> },
PromptGuided { instructions: String }, // 給不支援原生 tool call 的 provider 用
}實際上支援三種策略:
- 原生 Tool Calling:Anthropic、OpenAI、Gemini 都有各自的格式,provider 各自轉換。
- Prompt 注入(PromptGuided):把 tool 的說明文字注入到 system prompt,讓 LLM 用
<tool_call>JSON</tool_call>這樣的 XML tag 來回應。 - 自動降級:
OpenAiCompatibleProvider會先嘗試原生格式,如果收到"unknown parameter: tools"這類錯誤,就自動切換到 prompt 注入模式。這樣就算某個 OpenAI 相容的 endpoint 不支援 tool call,也能無縫 fallback。
各家 Provider 的奇特之處
實作各家 provider 的過程中,可以看到各家 API 設計的一些有趣差異:
Anthropic:支援 Prompt Caching,會自動對大型 system prompt(>3072 bytes)或長對話(>4 則)加上 cache_control: ephemeral,降低費用。
Gemini:是唯一把 API key 放在 query string(?key=xxx)而不是 header 裡的。而且 OAuth 使用者(Gemini CLI)必須打完全不同的 endpoint(cloudcode-pa.googleapis.com)。
OpenAiCompatibleProvider:一個實作打遍幾乎所有 OpenAI 相容的 API——Venice、Cloudflare、Groq、Mistral、xAI、DeepSeek、Perplexity……超過 15 個 provider 都是它的變體。靠著 AuthStyle enum(Bearer、XApiKey、Custom)和可設定的 base URL 來區分。
工廠函式
最後,使用者不需要直接 new 這些 struct,而是透過三個工廠函式:
// 簡單建立單一 provider
create_provider(name, api_key)
// 加上重試 + fallback 功能
create_resilient_provider(..., reliability)
// 加上多 provider 路由
create_routed_provider(..., model_routes)API key 解析的優先順序是:明確傳入的參數 → provider 專屬的環境變數(如 ANTHROPIC_API_KEY)→ 通用的 ZEROCLAW_API_KEY → API_KEY。
小結
zeroclaw 的設計讓我印象深刻的幾個點:
- 最小介面原則:只強制一個方法,其他都是預設實作,加新 provider 成本很低。
- Decorator 而非繼承:retry、routing 都是包裝器,跟具體 provider 完全解耦。
- 漸進式降級:tool calling 從原生到 prompt 注入的自動 fallback,使用者完全感知不到。
- 務實:面對各家 API 的奇葩設計(Gemini 的 query param auth、各種 reasoning_content 欄位)直接在具體實作裡處理,不讓這些噪音污染核心抽象。