
AIエージェントを本番運用する際、「LLMプロバイダーを切り替えたい」「チャットツールを変更したい」といった要求は頻繁に発生します。しかし、多くのフレームワークでは、こうした変更にコード修正が必要です。
ZeroClaw は、この問題を「trait駆動設計」で解決しています。設定ファイル1行の変更だけで、LLMプロバイダー、チャットツール、メモリバックエンド、実行環境を切り替えられます。本記事では、Rustの「trait」という仕組みを使った、この柔軟なアーキテクチャを解説します。
ZeroClawとは
ZeroClawは、Rust で書かれた超軽量AIエージェントインフラストラクチャです。メモリ使用量5MB未満、起動時間10ms以下という性能で、$10のハードウェアでも動作します。
主な特徴は以下の通りです:
- 超軽量: メモリ5MB未満(Node.js版OpenClawと比較して99%削減)
- 高速起動: 10ms以下(0.8GHzコアで)
- 完全スワップ可能: Provider/Channel/Tool/Memory全てが設定変更だけで切り替え可能
- セキュリティ: ペアリング認証、サンドボックス、allowlist標準装備
本記事では、この「完全スワップ可能」を実現する設計に焦点を当てます。
「trait」とは何か
Rustの「trait」は、他の言語でいう「インターフェース」に相当する機能です。簡単に言えば、「こういう機能を持っていますよ」という約束を定義するものです。
クラス図で理解する
「音を鳴らせるもの」というtraitを例に、クラス図で構造を見てみましょう:
classDiagram
class Soundable["Soundable (trait)"] {
+make_sound() String
}
class Dog {
+make_sound() String
}
class Cat {
+make_sound() String
}
class Car {
+make_sound() String
}
Soundable <|.. Dog : implements
Soundable <|.. Cat : implements
Soundable <|.. Car : implements
Soundableというtraitは「make_sound()メソッドを持つこと」を要求します。Dog、Cat、Carはそれぞれ独自の実装を提供しますが、使う側は「Soundableなもの」として統一的に扱えます。
Rustのコードで書くと以下のようになります:
| |
C++の純粋仮想関数との違い
C++経験者なら「純粋仮想関数と何が違うの?」と思うかもしれません。確かに、抽象基底クラスを使えば同様の構造は実現できます:
| |
しかし、Rustのtraitには重要な違いがあります:
| 観点 | C++ 純粋仮想関数 | Rust trait |
|---|---|---|
| 継承関係 | クラス定義時に決定(class Dog : public Soundable) | 後から追加可能(impl Soundable for Dog) |
| 多重継承 | ダイヤモンド継承問題が発生しうる | 複数traitを安全に実装可能 |
| 既存型への適用 | 不可能(元のクラス定義を変更する必要) | 可能(外部の型にもtraitを実装できる) |
| デフォルト実装 | 仮想関数で可能だが複雑 | シンプルに記述可能 |
| vtableコスト | 常に発生 | 静的ディスパッチならゼロコスト |
特に重要なのは「後から追加可能」という点です。例えば、標準ライブラリのString型に対して、自分で定義したtraitを実装できます:
| |
C++では、std::stringクラスを変更せずに新しい仮想関数を追加することは不可能です。この柔軟性が、ZeroClawのようなプラグイン可能なアーキテクチャを実現する鍵になっています。
AIエージェントでの応用
ZeroClawでは、この仕組みをAIエージェントの各コンポーネントに適用しています。「LLMと会話できるもの」「メッセージを送受信できるもの」「データを保存できるもの」といった抽象的な機能を定義し、具体的な実装を差し替え可能にしています。
ZeroClawのtrait駆動アーキテクチャ
ZeroClawでは、主要なコンポーネント全てがtraitとして定義されています。以下のクラス図は、Provider traitの構造を示しています:
classDiagram
class Provider["Provider (trait)"] {
+chat(messages) Result
+stream_chat(messages) Stream
}
class OpenAIProvider {
-api_key: String
-model: String
+chat(messages) Result
+stream_chat(messages) Stream
}
class AnthropicProvider {
-api_key: String
-model: String
+chat(messages) Result
+stream_chat(messages) Stream
}
class OllamaProvider {
-endpoint: String
-model: String
+chat(messages) Result
+stream_chat(messages) Stream
}
class CustomProvider {
-api_url: String
-api_key: String
+chat(messages) Result
+stream_chat(messages) Stream
}
Provider <|.. OpenAIProvider : implements
Provider <|.. AnthropicProvider : implements
Provider <|.. OllamaProvider : implements
Provider <|.. CustomProvider : implements
同様の構造が、他のコンポーネントにも適用されています:
| Subsystem | Trait | 役割 | 実装例 |
|---|---|---|---|
| AI Models | Provider | LLMとの会話 | OpenAI, Anthropic, Ollama, カスタムエンドポイント |
| Channels | Channel | メッセージ送受信 | CLI, Telegram, Discord, Slack, WhatsApp |
| Memory | Memory | データ永続化 | SQLite, PostgreSQL, Markdown, なし |
| Tools | Tool | エージェントの機能 | shell, file, git, browser, http_request |
| Runtime | RuntimeAdapter | コード実行環境 | Native, Docker |
| Tunnel | Tunnel | 外部公開 | Cloudflare, Tailscale, ngrok, なし |
この設計により、コードを1行も変更せずに、設定ファイルだけで実装を切り替えられます。
実例: LLMプロバイダーの切り替え
最も分かりやすい例として、LLMプロバイダーの切り替えを見てみましょう。
OpenAIからAnthropicへ
設定ファイル(~/.zeroclaw/config.toml)で、以下の2行を変更するだけです:
| |
これだけで、エージェントの会話相手がOpenAIからAnthropicに切り替わります。コードの再コンパイルも、アプリケーションの再起動も不要です(チャネルが起動中なら、次のメッセージから自動的に新しい設定が適用されます)。
ローカルLlamaへの切り替え
さらに、ローカルで動作するOllama に切り替えることもできます:
| |
カスタムエンドポイントの使用
自前のLLM APIサーバーを使う場合も、設定変更だけで対応できます:
| |
これらの切り替えが可能なのは、ZeroClawが「Providerというtraitを実装したもの」として各LLMを扱っているからです。
実例: チャネルの切り替え
メッセージの送受信方法(チャネル)も、同じ仕組みで切り替えられます。
CLIからTelegramへ
開発時はCLIで動作確認し、本番ではTelegramで運用する、といった使い分けができます:
| |
複数チャネルの同時利用
設定を追加するだけで、複数のチャネルを同時に有効化できます:
| |
これにより、Telegram、Discord、Slackのどこからメッセージを送っても、同じエージェントが応答します。
実例: メモリバックエンドの切り替え
データの保存方法(メモリバックエンド)も、設定変更だけで切り替えられます。
開発時はSQLite、本番はPostgreSQL
開発環境ではローカルのSQLiteを使い、本番環境では共有のPostgreSQLを使う、といった構成が簡単に実現できます:
| |
メモリを完全に無効化
テスト環境やステートレスな用途では、メモリを完全に無効化することもできます:
| |
この場合、Memory traitの「何もしない実装」(no-op backend)が使われます。
なぜtrait駆動設計が重要なのか
1. 依存関係の逆転
通常、アプリケーションは具体的な実装(例: OpenAI APIクライアント)に依存します。これでは、実装を変更するたびにアプリケーションコードを修正する必要があります。
trait駆動設計では、アプリケーションは抽象的なtrait(例: Provider)に依存し、具体的な実装は外部から注入されます。これにより、アプリケーションコードを変更せずに実装を差し替えられます。
通常の設計:
Application → OpenAI API Client
trait駆動設計:
Application → Provider trait ← OpenAI/Anthropic/Ollama
2. テスト容易性の向上
本番環境では実際のLLM APIを使い、テスト環境ではモックを使う、といった切り替えが簡単になります:
| |
3. 段階的な移行
新しいLLMプロバイダーやチャットツールに移行する際、一部のユーザーだけ新しい実装を試す、といった段階的な移行が可能になります。
4. ベンダーロックインの回避
特定のLLMプロバイダーやクラウドサービスに依存しない設計になるため、価格改定やサービス終了のリスクに柔軟に対応できます。
ZeroClawの実装を見てみる
実際のコードを見ると、trait駆動設計の仕組みがより明確になります。
Provider traitの定義
ZeroClawでは、Provider traitが以下のように定義されています(簡略化):
| |
このtraitを実装すれば、どんなLLMでもZeroClawで使えます。
具体的な実装例
OpenAI用の実装は、このtraitを満たすように書かれています:
| |
Anthropic用も同じtraitを実装します:
| |
実装の選択
設定ファイルの内容に基づいて、実行時に適切な実装が選択されます:
| |
このBox<dyn Provider>という型が重要です。これは「Providerというtraitを実装した何か」を表し、具体的な型(OpenAIかAnthropicか)を気にせず扱えます。
他のフレームワークとの比較
LangChain(Python)
LangChain もプロバイダーの抽象化を提供していますが、Pythonの動的型付けに依存しています:
| |
ZeroClawのtrait駆動設計では、コンパイル時に型安全性が保証されます。
LlamaIndex(Python)
LlamaIndex も同様に、Pythonの柔軟性を活かした設計ですが、型チェックは実行時になります。
ZeroClawの優位性
Rustのtrait駆動設計により、以下の利点があります:
- コンパイル時の型チェック: 実装の不整合を実行前に検出
- ゼロコストの抽象化: 実行時のオーバーヘッドがない
- 明示的な契約: traitが「何ができるか」を明確に定義
trait駆動設計を自分のプロジェクトに応用する
ZeroClawのアプローチは、Rust以外の言語でも応用できます。
TypeScriptでの応用
TypeScript のinterfaceを使えば、同様の設計が可能です:
| |
Goでの応用
Go のinterfaceも同じ考え方で使えます:
| |
設計のポイント
どの言語でも、以下のポイントを押さえることが重要です:
- 小さなインターフェース: 必要最小限のメソッドだけを定義
- 明確な責務: 1つのインターフェースは1つの責務に集中
- 設定駆動: 実装の選択を設定ファイルで制御
- 依存性の注入: 具体的な実装を外部から注入
ZeroClawを試してみる
ZeroClawはGitHub で公開されています。Rustの開発環境があれば、以下のコマンドでビルドできます:
| |
Homebrewを使う場合は、さらに簡単です:
| |
インストール後、以下のコマンドで対話的なセットアップが始まります:
| |
プロバイダー、モデル、チャネルを選択すると、設定ファイルが自動生成されます。
まとめ
ZeroClawのtrait駆動設計は、AIエージェントの柔軟性を大きく向上させます:
- 設定変更だけで実装を切り替え: コード修正不要
- 複数の実装を同時利用: Telegram/Discord/Slackを同時に有効化
- テストと本番で異なる実装: モックと実際のAPIを簡単に切り替え
- ベンダーロックインの回避: 特定のサービスに依存しない
この設計は、Rustの「trait」という仕組みを活用していますが、考え方自体は他の言語でも応用できます。TypeScriptのinterface、Goのinterface、Javaのinterfaceなど、多くの言語が同様の抽象化機能を提供しています。
AIエージェントを構築する際は、「具体的な実装に依存しない」設計を意識することで、長期的なメンテナンス性と柔軟性を確保できます。ZeroClawのアーキテクチャは、その優れた実例と言えるでしょう。