rig — библиотека для интеграции LLM в Rust
В мире Rust-разработки работа с большими языковыми моделями (LLM) долгое время оставалась сложной задачей. Большинство SDK были написаны на Python или JavaScript, а Rust-экосистема предлагала лишь разрозненные HTTP-клиенты. Библиотека rig (Rust Integration with GGUF/LLM) решает эту проблему, предоставляя единый, эргономичный и безопасный интерфейс для взаимодействия с десятками провайдеров LLM: OpenAI, Anthropic, Cohere, Google Gemini, Llama.cpp, Ollama и другими.
Зачем нужна rig? Во-первых, она предлагает типобезопасный API: все запросы и ответы строго типизированы, что исключает целый класс ошибок времени выполнения. Во-вторых, rig реализует пайплайны (pipelines) — композируемые цепочки вызовов LLM с поддержкой стриминга, контекстных окон и инструментов (tool calling). В-третьих, библиотека спроектирована с учётом производительности: асинхронность из коробки, минимальные аллокации и поддержка batching.
Эта статья — максимально подробное руководство по rig. Мы разберём установку, основные методы, напишем реальный пример и дадим советы по оптимизации. К концу вы сможете создавать production-ready приложения на Rust, использующие LLM.
Установка
Добавьте rig в ваш проект Cargo:
cargo add rig-core --features openai,anthropic,cohere,gemini,ollama,llama-cpp
Если вам нужен только один провайдер, например OpenAI:
cargo add rig-core --features openai
Для работы с локальными моделями через llama.cpp потребуется установить сам llama.cpp (скачайте с GitHub) и скомпилировать с поддержкой CUDA/Metal. Затем добавьте feature llama-cpp.
Минимальная версия Rust: 1.75.0. Проверьте:
rustc --version
Быстрый старт
Создайте файл src/main.rs и напишите простейший запрос к OpenAI:
use rig::providers::openai;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Инициализация клиента через переменную окружения OPENAI_API_KEY
let client = openai::Client::from_env();
// Создание модели
let model = client.model("gpt-4o-mini").build();
// Отправка запроса
let response = model.prompt("Расскажи шутку про программистов").await?;
println!("{}", response);
Ok(())
}
Запустите:
export OPENAI_API_KEY="sk-..."
cargo run
Вы увидите сгенерированную шутку. Поздравляю — вы только что интегрировали LLM в Rust!
Основные методы и функции
Рассмотрим ключевые методы rig. Для каждого приведём сигнатуру, описание и рабочий пример.
1. Client::from_env()
Сигнатура: pub fn from_env() -> Self
Что делает: Создаёт клиент для конкретного провайдера, читая API-ключ из переменной окружения (например, OPENAI_API_KEY).
use rig::providers::openai;
let client = openai::Client::from_env();
2. Client::model(name)
Сигнатура: pub fn model(&self, model_name: &str) -> ModelBuilder
Что делает: Возвращает билдер для конфигурации модели. Позволяет задать температуру, max_tokens, стоп-слова и другие параметры.
let model = client.model("gpt-4o")
.temperature(0.7)
.max_tokens(200)
.build();
3. Model::prompt(text)
Сигнатура: pub async fn prompt(&self, prompt: impl Into<String>) -> Result<String, ModelError>
Что делает: Отправляет текстовый запрос и возвращает полный ответ модели (без стриминга).
let answer = model.prompt("Какая столица Франции?").await?;
println!("{}", answer); // Париж
4. Model::stream_prompt(text)
Сигнатура: pub async fn stream_prompt(&self, prompt: impl Into<String>) -> Result<impl Stream<Item = Result<String, ModelError>>, ModelError>
Что делает: Отправляет запрос и возвращает асинхронный стрим токенов ответа. Полезно для UI с real-time выводом.
use futures::StreamExt;
let mut stream = model.stream_prompt("Напиши эссе о Rust").await?;
while let Some(token) = stream.next().await {
print!("{}", token?);
}
5. Model::chat(messages)
Сигнатура: pub async fn chat(&self, messages: Vec<ChatMessage>) -> Result<ChatResponse, ModelError>
Что делает: Отправляет историю диалога (системное сообщение, пользователь, ассистент). Поддерживает мульти-турные беседы.
use rig::completion::{ChatMessage, MessageRole};
let messages = vec![
ChatMessage::new(MessageRole::System, "Ты — эксперт по Rust"),
ChatMessage::new(MessageRole::User, "Что такое ownership?"),
];
let response = model.chat(messages).await?;
println!("{}", response.content);
6. Model::tool_call(tool, input)
Сигнатура: pub async fn tool_call(&self, tool: &dyn Tool, input: &str) -> Result<ToolResult, ModelError>
Что делает: Позволяет модели вызывать внешние инструменты (функции). rig автоматически сериализует аргументы и десериализует результат.
use rig::tool::Tool;
struct WeatherTool;
#[async_trait]
impl Tool for WeatherTool {
fn name(&self) -> &str { "get_weather" }
fn description(&self) -> &str { "Получить погоду для города" }
async fn call(&self, args: serde_json::Value) -> Result<serde_json::Value, ToolError> {
// Логика вызова API погоды
Ok(serde_json::json!({"temp": 22, "condition": "sunny"}))
}
}
let result = model.tool_call(&WeatherTool, "Москва").await?;
println!("{:?}", result);
7. Pipeline::chain(model1, model2)
Сигнатура: pub fn chain<A, B>(first: A, second: B) -> ChainPipeline<A, B>
Что делает: Создаёт пайплайн из двух моделей, где выход первой подаётся на вход второй. Полезно для фильтрации, суммаризации или перевода.
use rig::pipeline::Pipeline;
let translator = client.model("gpt-4o-mini").build();
let summarizer = client.model("gpt-4o-mini").build();
let pipeline = Pipeline::chain(translator, summarizer);
let result = pipeline.run("Long text in English...").await?;
8. Context::with_max_tokens(n)
Сигнатура: pub fn with_max_tokens(self, max_tokens: usize) -> Self
Что делает: Ограничивает размер контекстного окна (в токенах). rig автоматически обрезает историю диалога при превышении лимита.
use rig::context::Context;
let ctx = Context::new()
.with_max_tokens(4096)
.with_system_prompt("Ты — помощник");
let model = client.model("gpt-4o").with_context(ctx).build();
9. EmbeddingsModel::embed(texts)
Сигнатура: pub async fn embed(&self, texts: Vec<String>) -> Result<Vec<Vec<f32>>, ModelError>
Что делает: Генерирует векторные эмбеддинги для списка текстов. Используется для RAG (Retrieval-Augmented Generation) и семантического поиска.
let embedder = client.embed_model("text-embedding-3-small").build();
let embeddings = embedder.embed(vec!["Rust is fast".into(), "Python is easy".into()]).await?;
println!("{:?}", embeddings[0].len()); // 1536
10. ClientBuilder::with_api_key(key)
Сигнатура: pub fn with_api_key(self, api_key: &str) -> Self
Что делает: Позволяет явно указать API-ключ вместо использования переменной окружения. Полезно для тестов или конфигураций.
use rig::providers::openai;
let client = openai::Client::builder()
.with_api_key("sk-my-key")
.build();
Реальный практический пример: RAG-агент для анализа кода
Создадим агента, который принимает фрагмент Rust-кода, находит похожие примеры в локальной базе (через эмбеддинги) и даёт рекомендации по улучшению. Полный код — около 150 строк.
use rig::providers::openai;
use rig::completion::{ChatMessage, MessageRole};
use rig::embed::EmbeddingsModel;
use std::collections::HashMap;
use tokio::fs;
// Структура для хранения документации
struct DocStore {
chunks: Vec<String>,
embeddings: Vec<Vec<f32>>,
}
impl DocStore {
async fn new(embedder: &EmbeddingsModel) -> Self {
let content = fs::read_to_string("rust_docs.md").await.unwrap();
let chunks: Vec<String> = content
.split("\n\n")
.map(|s| s.to_string())
.collect();
let embeddings = embedder.embed(chunks.clone()).await.unwrap();
Self { chunks, embeddings }
}
fn search(&self, query_embed: &[f32], top_k: usize) -> Vec<String> {
let mut scores: Vec<(usize, f32)> = self.embeddings
.iter()
.enumerate()
.map(|(i, emb)| {
let dot: f32 = emb.iter().zip(query_embed).map(|(a, b)| a * b).sum();
(i, dot)
})
.collect();
scores.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
scores.into_iter()
.take(top_k)
.map(|(i, _)| self.chunks[i].clone())
.collect()
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Инициализация клиента
let client = openai::Client::from_env();
let embedder = client.embed_model("text-embedding-3-small").build();
let llm = client.model("gpt-4o-mini")
.temperature(0.3)
.build();
// Загрузка документации
let doc_store = DocStore::new(&embedder).await;
// Входной код для анализа
let user_code = r#"
fn calculate(a: i32, b: i32) -> i32 {
if a > b {
return a - b;
} else {
return b - a;
}
}
"#;
// Поиск релевантных чанков
let query_embed = embedder.embed(vec![user_code.to_string()]).await?;
let relevant_docs = doc_store.search(&query_embed[0], 3);
let context = relevant_docs.join("\n---\n");
// Формируем промпт с контекстом
let messages = vec![
ChatMessage::new(MessageRole::System, &format!(
"Ты — эксперт по Rust. Используй следующий контекст документации для ответа:\n{}",
context
)),
ChatMessage::new(MessageRole::User, &format!(
"Проанализируй этот код и предложи улучшения:\n```rust\n{}\n```",
user_code
)),
];
let response = llm.chat(messages).await?;
println!("Рекомендации:\n{}", response.content);
Ok(())
}
Этот пример демонстрирует: создание эмбеддингов, семантический поиск, контекстный промптинг и мульти-турный диалог. Вы можете адаптировать его под свою базу знаний.
Советы по производительности и типичные ошибки
Производительность
- Используйте стриминг:
stream_promptвместоpromptдля long-running запросов — это снижает задержку первого токена. - Кешируйте эмбеддинги: если ваша база документов статична, сохраняйте эмбеддинги в файл или БД (например, через
bincode). - Балансируйте max_tokens: не ставьте слишком большие лимиты — это увеличивает стоимость и время ответа.
- Используйте batching: для эмбеддингов передавайте сразу много текстов — rig оптимизирует запросы.
- Выбирайте провайдера под задачу: для простых задач (суммаризация) используйте