rig для Rust: полное руководство с примерами

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

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 оптимизирует запросы.
  • Выбирайте провайдера под задачу: для простых задач (суммаризация) используйте

Рекомендаций