ZeroClaw — Trait 시스템
Layer 1 학습. 실제 소스 코드 기반. 파일:
src/tools/traits.rs,src/providers/traits.rs
왜 Trait인가
ZeroClaw의 핵심 설계 원칙은 하나야.
"컴포넌트 교체는 설정 파일만 바꾸면 된다. 코드는 건드리지 않는다."
이걸 가능하게 하는 게 Rust trait 시스템이야. 모든 주요 컴포넌트(LLM, 도구, 메모리, 채널)가 trait으로 정의되어 있어서, 코드는 구체적인 타입이 아닌 trait에만 의존한다.
Tool trait — 가장 단순한 예
// src/tools/traits.rs (실제 코드)
#[async_trait]
pub trait Tool: Send + Sync {
fn name(&self) -> &str;
fn description(&self) -> &str;
fn parameters_schema(&self) -> serde_json::Value;
async fn execute(&self, args: serde_json::Value) -> anyhow::Result<ToolResult>;
// spec()은 위 메서드들로 자동 구현됨 (default method)
fn spec(&self) -> ToolSpec {
ToolSpec {
name: self.name().to_string(),
description: self.description().to_string(),
parameters: self.parameters_schema(),
}
}
}
agent loop는 Box<dyn Tool>의 Vec을 갖고 있어. 어떤 타입인지 모르고, 알 필요도 없어. .execute(args) 호출만 하면 돼.
커스텀 도구 추가 예시:
struct FetchAssignmentTool { db: Arc<Db> }
#[async_trait]
impl Tool for FetchAssignmentTool {
fn name(&self) -> &str { "fetch_assignments" }
fn description(&self) -> &str { "학교 LMS에서 과제 목록 가져오기" }
fn parameters_schema(&self) -> serde_json::Value {
serde_json::json!({ "type": "object", "properties": {} })
}
async fn execute(&self, _args: serde_json::Value) -> anyhow::Result<ToolResult> {
let assignments = self.db.get_pending().await?;
Ok(ToolResult { success: true, output: format!("{:?}", assignments), error: None })
}
}
이것만 구현하면 ZeroClaw agent가 LLM에게 자동으로 이 도구를 노출해.
Provider trait — 핵심 설계 패턴
// src/providers/traits.rs (실제 코드 요약)
#[async_trait]
pub trait Provider: Send + Sync {
// 1. 이 provider가 뭘 지원하는지 선언
fn capabilities(&self) -> ProviderCapabilities {
ProviderCapabilities::default() // 기본: 아무것도 없음
}
// 2. 반드시 구현해야 하는 것 (단 하나)
async fn chat_with_system(
&self,
system_prompt: Option<&str>,
message: &str,
model: &str,
temperature: f64,
) -> anyhow::Result<String>;
// 3. 나머지는 전부 default 구현 제공
// chat_with_history, chat, chat_with_tools, stream_chat... 전부 위 메서드 기반으로 동작
}
핵심 패턴: chat_with_system 하나만 구현하면 나머지 모든 API가 자동으로 동작한다.
ProviderCapabilities — 능력 선언
pub struct ProviderCapabilities {
pub native_tool_calling: bool, // API 레벨 tool calling 지원?
pub vision: bool, // 이미지 입력 지원?
pub prompt_caching: bool, // 프롬프트 캐싱 지원?
}
provider가 native_tool_calling: false면 ZeroClaw가 자동으로 도구 목록을 시스템 프롬프트 텍스트로 주입한다.
native_tool_calling: true → API에 tools 필드로 전달 (Anthropic, OpenAI 방식)
native_tool_calling: false → system prompt 안에 XML 형식으로 도구 설명 주입
로컬 LLM(llama.cpp, ollama)은 대부분 후자야.
ToolsPayload — provider별 형식 차이 추상화
pub enum ToolsPayload {
Gemini { function_declarations: Vec<Value> },
Anthropic { tools: Vec<Value> },
OpenAI { tools: Vec<Value> },
PromptGuided { instructions: String }, // 로컬 LLM 폴백
}
각 provider가 convert_tools()를 구현해서 자신에 맞는 형식으로 변환한다. 기본 구현은 PromptGuided (텍스트 주입).
전체 그림 — Trait이 어떻게 연결되는가
agent loop
│
├── Box<dyn Provider> ← Claude, Ollama, 로컬 llama.cpp 전부 동일하게 호출
│ └── .chat(messages, tools)
│
├── Vec<Box<dyn Tool>> ← Shell, FileRead, 커스텀 도구 전부 동일하게 호출
│ └── .execute(args)
│
├── Box<dyn MemoryStore> ← SQLite, Markdown, ephemeral 전부 동일하게 호출
│ └── .recall(query)
│
└── Box<dyn Channel> ← Telegram, Discord, CLI 전부 동일하게 호출
└── .send(message)
agent loop는 구체적인 타입을 전혀 모른다. 전부 trait object (dyn Trait)로 다룬다.
Send + Sync가 붙는 이유
pub trait Tool: Send + Sync { ... }
pub trait Provider: Send + Sync { ... }
Tokio async 런타임에서 여러 스레드에 걸쳐 공유되기 때문. Arc<dyn Tool>로 감싸서 여러 async task가 동시에 같은 도구를 안전하게 사용할 수 있어야 해.
async_trait이 필요한 이유
Rust trait의 기술적 제약 때문이야. Rust는 기본적으로 trait 메서드에서 async fn을 허용하지 않아 (반환 타입이 impl Future인데 크기를 컴파일 타임에 알 수 없음). #[async_trait] 매크로가 이걸 자동으로 Box<dyn Future>로 변환해줘.
ToolResult — 도구 실행 결과
pub struct ToolResult {
pub success: bool,
pub output: String, // LLM에게 돌려줄 텍스트
pub error: Option<String>,
}
성공/실패 구분, 출력 텍스트, 에러 메시지. LLM이 이 결과를 받아서 다음 판단을 내려.
핵심 요약
| 개념 | 역할 | 필수 구현 |
|------|------|---------|
| Tool trait | 도구 추상화 | name, description, parameters_schema, execute |
| Provider trait | LLM 추상화 | chat_with_system (단 하나) |
| ProviderCapabilities | 능력 선언 | capabilities() 오버라이드 |
| ToolsPayload | 도구 형식 변환 | convert_tools() 오버라이드 |
| Send + Sync | 스레드 안전 | trait 정의에 포함 |
커스텀 도구 적용 패턴
// 학교 크롤러 도구 추가 예시
struct SchoolCrawlerTool;
#[async_trait]
impl Tool for SchoolCrawlerTool {
fn name(&self) -> &str { "check_assignments" }
fn description(&self) -> &str { "학교 LMS에서 미제출 과제 목록 조회" }
fn parameters_schema(&self) -> serde_json::Value {
serde_json::json!({ "type": "object", "properties": {} })
}
async fn execute(&self, _args: serde_json::Value) -> anyhow::Result<ToolResult> {
let assignments = crawl_lms().await?;
Ok(ToolResult {
success: true,
output: serde_json::to_string(&assignments)?,
error: None
})
}
}
ZeroClaw agent가 알아서 이 도구를 LLM에 노출하고, LLM이 필요할 때 호출해.
다음: Provider trait 구현체
src/providers/compatible.rs— OpenAI-compatible API (llama.cpp 연결)src/providers/reliable.rs— ReliableProvider (failover + retry 래퍼)
관련
- overview — ZeroClaw 학습 지도
- research/pi-sdk/extension-api — pi SDK Extension과 비교