RustでLLMアプリケーションを構築する選択肢として、オープンソースライブラリの「Rig」が注目されています。Rigは、OpenAI、Anthropic、Geminiなど複数のLLMプロバイダーを共通のRustインターフェースで扱いながら、エージェント、ツール呼び出し、RAG、ストリーミング、ローカルモデルまでを一つの設計にまとめます。
本稿では、Ratatui製のターミナル型コーディングエージェント「Rat Code」を題材に、RigがRustエンジニアに提供する価値と、実務で検討すべきポイントを整理します。
Rigが解決する問題:LLMプロバイダー依存の分離
LLMアプリケーションでは、プロバイダーごとにAPIの形式やツール呼び出し、ストリーミングの扱いが異なります。複数のサービスがOpenAI互換APIを掲げていても、実際の挙動まで完全に同じとは限りません。
Rigは、アプリケーション側がプロバイダー固有の実装に直接依存しないよう、共通のインターフェースを提供します。基本的な構成は次のとおりです。
- プロバイダークライアント:LLMサービスとの接続を担当
- モデル:利用するモデルや補完機能を表現
- エージェント:システム指示、トークン制限、ツールなどを組み合わせる
- ツール:エージェントが実行できるRustの機能
この分離によって、アプリケーションの主要なロジックを維持したまま、利用するモデルや接続先を切り替えやすくなります。
OpenAI SDKなどとの違い
特定プロバイダーのSDKを直接利用する方法は、最初の実装が分かりやすく、最新機能へ早く追従できる場合があります。一方で、プロバイダー変更時にはリクエスト形式、エラー処理、ストリーミング処理などに修正が発生しやすくなります。
Rigの立ち位置は、特定サービスの機能を最短で呼び出すSDKというより、LLMを組み込んだアプリケーションの共通構造をRustで表現するためのライブラリです。
| 観点 | プロバイダー固有SDK | Rig |
|---|---|---|
| 導入の分かりやすさ | 特定サービスに集中しやすい | 共通抽象の理解が必要 |
| プロバイダー切り替え | アプリケーション側の修正が発生しやすい | 共通インターフェースで分離しやすい |
| 独自機能への追従 | サービス固有機能を利用しやすい | 共通化された範囲で扱う設計 |
| ツールやRAG | SDKや周辺実装に依存 | エージェント構成の一部として整理 |
| Rustとの親和性 | SDKの設計に依存 | 型、トレイト、非同期処理を活用 |
実際には、特定プロバイダーの機能を最大限に使いたいのか、将来の切り替えや構成の再利用性を重視するのかで選択が変わります。
エージェントは「モデル呼び出しの一段上」にある
単純なLLM呼び出しは、テキストを入力し、テキストを受け取る処理です。エージェントはその周囲に、アプリケーションとして必要な制約や能力を加えます。
Rigでは、エージェントに次のような要素を設定します。
- 毎回のリクエストに含める指示文、いわゆるシステムプロンプト
- 出力に使用できるトークン数
- モデルが呼び出せるツール
- 必要に応じたストリーミング処理
Rigでは、エージェントへの基本指示をpreambleとして表現します。例えばコーディングエージェントであれば、次のような役割を定義します。
let agent = model
.agent()
.preamble(
"あなたはRustプロジェクトを支援するコーディングエージェントです。\n\
ファイルを変更する前に内容を確認し、実行したコマンドと結果を説明してください。"
)
.build();
上記は構成の考え方を示す概念例です。実際のメソッド名やビルダー構成は利用するRigのバージョンに合わせて実装します。
重要なのは、プロバイダーへの接続処理と、エージェントの役割定義を分離できる点です。接続部分はRigに任せ、アプリケーション固有の振る舞いはpreambleやツールとして管理できます。
ツール呼び出しをRustの型で表現する
AIエージェントを実用化するうえで重要なのが、モデルに外部操作を許可する「ツール」です。ツールは、ファイル読み取り、データベース検索、計算、外部サービス呼び出しなどを、モデルから利用可能な機能として公開します。
Rigでは、ツールをRustのToolトレイトで表現します。ツールの定義には、少なくとも次の情報が含まれます。
- ツール名
- ツールの説明
- 入力の型
- 出力の型
- エラーの型
- 実行処理
概念的には、次のような構造になります。
struct ReadFile {
root: std::path::PathBuf,
}
struct ReadFileArgs {
path: String,
}
// RigのToolトレイトを実装するイメージ
// 入力型、出力型、エラー型、実行処理を定義する
impl ReadFile {
async fn call(&self, args: ReadFileArgs) -> Result<String, ToolError> {
let target = self.root.join(args.path);
let content = tokio::fs::read_to_string(target).await?;
Ok(content)
}
}
実際には、Rigが要求するトレイトの関連型やメソッドに合わせて実装します。ポイントは、ツールの引数と戻り値をRustの型として管理できることです。
ツールの入力はJSON Schemaとしてモデルに伝えられます。これにより、モデルは「どの引数を、どの形式で渡せるか」を構造化された情報として判断できます。文字列を手作業で解析する方式と比べ、入力形式の不一致を減らしやすい設計です。
ただし、型安全なのはあくまでRustアプリケーション内部です。モデルが生成する引数は外部入力として扱い、次のような防御が必要です。
- パスが許可されたディレクトリ内にあるか検証する
- シェルコマンドを無制限に実行しない
- ツールのタイムアウトを設定する
- 破壊的な操作にはユーザー確認を挟む
- エラー内容や秘密情報をモデルへ過剰に返さない
特にコーディングエージェントでは、ファイル書き込みやシェル実行を許可する範囲が、そのままセキュリティ境界になります。
Rat Codeに見るエージェントの最小構成
ニュースで紹介されたRat Codeは、RustのターミナルUIライブラリであるRatatuiとRigを組み合わせた小規模なコーディングエージェントです。
アプリケーションの役割は大きく次のように分かれています。
main.rsなどでプロバイダー、モデル、エージェントを初期化する- ファイル読み取り、ファイル書き込み、シェル実行の機能をツールとして定義する
- ツールをエージェントへ登録する
- RigのストリーミングAPIで生成途中の応答を受け取る
- Ratatuiで応答や実行状況をターミナルへ描画する
この構成の利点は、UIとAI実行の責務が分離されることです。Ratatuiは表示と入力を担当し、Rigはモデルとの通信やエージェントの実行を担当します。
ストリーミングを使えば、モデルの応答がすべて生成されるまで画面を待たせる必要がありません。生成されたチャンクを順に受け取り、ターミナルへ反映できます。CLIツールでは、体感的な待ち時間を短くするうえで特に有効です。
一方で、ストリーミング処理では次の点を設計しておく必要があります。
- 生成中とツール実行中の状態表示
- 通信切断時の再試行や中断
- 部分的に生成されたテキストの扱い
- ツール呼び出し結果を表示するかどうか
- ユーザー入力を受け付けるタイミング
エージェントをツールとして組み合わせる
Rigでは、エージェント自体を別のエージェントから呼び出せるツールとして扱う設計も可能です。これは、複数の専門エージェントを組み合わせる際に役立ちます。
例えば、次のような構成が考えられます。
- オーケストレーター:ユーザー要求を分解する
- コード分析エージェント:既存コードを調査する
- テストエージェント:テストケースを提案する
- ドキュメントエージェント:変更内容を説明する
各エージェントを独立した機能として実装し、上位エージェントからツール経由で呼び出せば、責務を分けた構成にできます。
ただし、エージェントを増やすほど、呼び出し回数、レイテンシ、トークン消費、失敗時の復旧処理が複雑になります。最初から多エージェント構成にするのではなく、単一エージェントと明確なツールで要件を満たせるかを検討するのが現実的です。
RAGとローカルLLMへの対応
Rigは、外部データを参照して回答を生成するRAGにも対応します。一般的なRAGでは、次の流れで情報をLLMへ渡します。
- 文書を分割する
- 文書とユーザーの質問をベクトル化する
- ベクトルストアから類似度の高い文書を検索する
- 検索結果をコンテキストとしてモデルへ渡す
- 参照情報に基づいて回答を生成する
Rigはデータベース連携やベクトルストアの抽象化を提供します。対応済みのデータベースを利用するほか、ベクトルを保存できる既存のデータストアに対して、Rigのベクトルストア用トレイトを実装するという拡張も可能です。
RAGを業務システムへ組み込む場合は、検索精度だけでなく、次の設計も重要になります。
- 文書の更新をどのタイミングで埋め込み直すか
- アクセス権限を検索結果へ反映できるか
- 参照元を回答に表示するか
- コンテキストの長さをどう制限するか
- 検索できなかった場合にどう回答するか
また、Rigはローカルモデルの利用にも対応しています。Ollamaやllama.cppのようにローカルで動作するサーバーへ接続する方法に加え、Candleを通じてRustアプリケーション内で推論する構成もあります。
ローカル推論には、次のような利点があります。
- 機密データを外部APIへ送信せずに済む可能性がある
- ネットワーク接続に依存しにくい
- 推論環境をアプリケーションの近くに配置できる
- WebAssemblyを含む特定の実行環境へ組み込みやすい場合がある
一方で、モデルサイズ、メモリ使用量、推論速度、対応モデルなどの制約もあります。クラウドモデルとローカルモデルは単純な優劣ではなく、データ保護、コスト、レイテンシ、運用環境を基準に使い分けるべきです。
LLM統合テストの現実的なアプローチ
LLMアプリケーションのテストには、通常のAPIクライアントとは異なる難しさがあります。モデルの出力は完全には決定的でなく、プロバイダー側のモデル更新によって同じ入力への応答が変わる可能性もあります。
Rigが採用している重要な仕組みが、HTTP通信を記録して再生するカセット方式です。ライブのプロバイダーに対するリクエストとレスポンスを記録し、CIではモックサーバーを使って同じ通信を再生します。
この方法で検証できるのは、主に次のような項目です。
- リクエストの形式が正しいか
- 認証やヘッダーの扱いが壊れていないか
- レスポンスのデシリアライズが動作するか
- ストリーミングやツール呼び出しの処理が維持されているか
- プロバイダーアダプターの変更で既存挙動が壊れていないか
Rigでは、多数のプロバイダー通信を記録し、プルリクエストごとのCIで再生しています。ライブAPIへ毎回接続しないため、テストを短時間かつ安定して実行できます。
ただし、カセットテストだけでは「回答の品質」は検証できません。品質評価には、別途ライブモデルを使った定期テストや評価用データセットが必要です。
つまり、LLMアプリケーションではテストを次の二層に分けると整理しやすくなります。
- 統合テスト:APIとの通信、変換、エラー処理をカセットで検証する
- 品質評価:ライブモデルの回答が業務要件を満たすか定期的に評価する
この分離は、Rustに限らずLLMを組み込むシステム全般に応用できる考え方です。
RustエンジニアがRigを選ぶときの判断基準
Rigは、次のようなプロジェクトと相性がよいでしょう。
- 将来的にLLMプロバイダーを切り替える可能性がある
- ツールやRAGをエージェントの構成要素として管理したい
- 型によってツールの入力・出力を明確にしたい
- 非同期処理やストリーミングをRustアプリケーションへ統合したい
- ローカル推論や組み込み型の推論を検討している
- API統合をモック化し、CIで安定して検証したい
一方、特定プロバイダーの最新機能を最優先したい場合や、対象サービスのAPIを直接細かく制御したい場合は、プロバイダー固有SDKとの比較が必要です。Rigを採用する場合でも、抽象化によって隠れる機能と、アプリケーション側で追加実装が必要な部分を切り分けることが重要です。
まとめ:RustでAIを実装するための現実的な出発点
Rigの価値は、LLMを呼び出すための薄いラッパーにとどまりません。プロバイダー、モデル、エージェント、ツール、RAG、ストリーミング、ローカル推論を、Rustの型とトレイトを使って組み合わせるための設計基盤を提供します。
Rat Codeのような小さなターミナルエージェントは、その構造を理解するためのよい題材です。まずは単純な質問応答から始め、次の順で機能を追加すると、実装と検証の範囲を管理しやすくなります。
- プロバイダーとモデルを接続する
- エージェントに役割と制約を設定する
- 読み取り専用のツールを追加する
- ストリーミングでUIを更新する
- 書き込みやコマンド実行に承認フローを加える
- RAGやローカルモデルを要件に応じて導入する
- カセット方式の統合テストと品質評価を分離する
Rustの型安全性、非同期処理、実行バイナリとしての配布しやすさは、AIエージェントを実運用へ近づけるうえで有効です。Rigは、プロバイダー選択の自由度と、ツールを中心としたエージェント設計を重視するRustエンジニアにとって、実践的な検討候補になりそうです。
出典: JetBrains Blog
