Создание инфраструктуры LLM или ИИ в Rust может дать несколько преимуществ, несмотря на доминирование Python в пространстве ИИ.

  1. Производительность: Rust известен своей высокой производительностью и низкоуровневым контролем, что может иметь решающее значение для создания крупномасштабных систем ИИ. Языковые модели, особенно модели глубокого обучения, могут потребовать больших вычислительных ресурсов. Производительность Rust может привести к значительному повышению скорости по сравнению с Python, что делает его более подходящим для эффективной обработки ресурсоемких вычислительных задач.
  2. Безопасность памяти: строгие правила компиляции Rust и модель владения обеспечивают безопасность памяти, предотвращая распространенные ошибки, такие как разыменование нулевого указателя и гонки данных. Это может сделать системы ИИ на основе Rust более надежными и менее подверженными сбоям, что особенно важно для долго работающих языковых моделей или критических приложений ИИ.
  3. Параллелизм: встроенная в Rust поддержка параллелизма и облегченных потоков может привести к эффективному использованию многоядерных процессоров. Это может быть полезно при реализации параллельной обработки для обучения больших языковых моделей или одновременной обработки нескольких запросов на вывод.

Да, стоит помнить, что обширная экосистема Python, хорошо зарекомендовавшие себя библиотеки (например, TensorFlow, PyTorch) и простота использования сделали его предпочтительным выбором для многих проектов ИИ. Но если у вас есть чувство исследования и вы определили свои конкретные требования, а Rust находится наверху, это может быть актуальным чтением для вас.

Попробуем ответить на вопрос: есть ли в Rust универсальная библиотека, как в Python есть langchain для работы с LLM?

P.S. Возможно, стоит поэкспериментировать с некоторыми строительными блоками langchain в Python. Для этого я создал langchain-llm-katas, попробуйте.

Строительные блоки цепной библиотеки

Если мы на минуту уменьшим масштаб и проигнорируем высокоуровневые рабочие процессы, такие как агенты, наборы инструментов и запеченные варианты использования, мы увидим общую инфраструктуру, лежащую в основе всех тех, которые позволяют существовать этим высокоуровневым конструкциям:

  • LLM — загрузка, вызов LLM и поддержка различных моделей, для которых необходимо:
  • Встраивание и токенизация — превращение текста во встраивание, где текст загружается с помощью:
  • Загрузчики — превращают различные форматы документов в удобоваримый для LLM плоский текст, который необходимо постобработать:
  • Разделители — для создания разборчивых, полезных фрагментов исходного текста, для работы с лимитами токенов LLM и для хранения в:
  • Базы данных векторов, которые используются для формулирования запросов, отправляемых LLM, а также для питания:
  • Память — это делается для поддержки контекста и сеансов с LLM. Но также у нас есть инфраструктура для:
  • Шаблоны — структурируют запрос от LLM, который может содержать вызов:
  • Инструменты — это набор реальных инструментов, таких как калькулятор или браузер, которые LLM может автоматизировать, прочитав структурированную подсказку.

Так как же все это выглядит сейчас в Rust?

Инфраструктура и строительные блоки

LLM и Трансформеры

лм

llm — это экосистема библиотек Rust для работы с большими языковыми моделями. Он построен на основе быстрой и эффективной библиотеки GGML для машинного обучения. Это надежная, обширная и стабильная библиотека для работы с моделями, до такой степени, что это единственная библиотека, которую всегда следует выбирать, и хорошо, что альтернатив не так много.

Вот как выполнить вывод, используя только ящик llm:

let model = llm::load_dynamic(
    Some(model_architecture),
    &model_path,
    tokenizer_source,
    Default::default(),
    llm::load_progress_callback_stdout,
)
.unwrap_or_else(|err| {
    panic!("Failed to load {model_architecture} model from {model_path:?}: {err}")
});
let mut session = model.start_session(Default::default());
let res = session.infer::<Infallible>(
    model.as_ref(),
    &mut rand::thread_rng(),
    &llm::InferenceRequest {
        prompt: prompt.into(),
        parameters: &llm::InferenceParameters::default(),
        play_back_previous_tokens: false,
        maximum_token_count: None,
    },
    // OutputRequest
    &mut Default::default(),
    |r| match r {
        llm::InferenceResponse::PromptToken(t) | llm::InferenceResponse::InferredToken(t) => {
            print!("{t}");
            std::io::stdout().flush().unwrap();
            Ok(llm::InferenceFeedback::Continue)
        }
        _ => Ok(llm::InferenceFeedback::Continue),
    },
);

раст-берт

Вероятно, самая универсальная и производительная библиотека для rust для использования моделей трансформаторов, вдохновленная библиотекой huggingfaces. Это универсальный магазин локальных моделей трансформеров для разных задач, от перевода до встраивания.

  • Множество вариантов использования и примеров
  • Очень широкая, как и оригинальная библиотека трансформеров
  • Вы можете многое сделать, просто используя эту библиотеку, и вам может не понадобиться ничего другого.

Что касается вариантов использования, вот пример анализа настроений:

let sentiment_classifier = SentimentModel::new(Default::default())?;
                                                        
let input = [
    "Probably my all-time favorite movie, a story of selflessness, sacrifice and dedication to a noble cause, but it's not preachy or boring.",
    "This film tried to be too many things all at once: stinging political satire, Hollywood blockbuster, sappy romantic comedy, family values promo...",
    "If you like original gut wrenching laughter you will like this movie. If you are young or old then you will love this movie, hell even my mom liked it.",
];
let output = sentiment_classifier.predict(&input);

И вложения:

let model = SentenceEmbeddingsBuilder::remote(
        SentenceEmbeddingsModelType::AllMiniLmL12V2
    ).create_model()?;
let sentences = [
    "this is an example sentence",
    "each sentence is converted"
];
let output = model.encode(&sentences)?;

И еще множество вариантов использования.

Вложения и токенизация

Вы можете делать вложения со следующим:

  • rust-bert
  • llm

тиктокен

Эта библиотека построена на основе библиотеки OpenAI Rust tiktoken и немного расширяет ее. Это должен быть хороший универсальный магазин для ваших потребностей в токенизации.

use tiktoken_rs::p50k_base;
let bpe = p50k_base().unwrap();
let tokens = bpe.encode_with_special_tokens(
  "This is a sentence   with spaces"
);
println!("Token count: {}", tokens.len());

Погрузчики

На момент написания не существует единого загрузчика, такого как unstructured, который мог бы загружать и конвертировать документы, не заботясь о реализации конкретного провайдера. Некоторым из них может потребоваться некоторая смазка локтя, чтобы преобразовать содержимое в плоский формат, подобный LLM-документу (обычный текст, страницы), например, зацикливая и извлекая рабочие листы из файлов Excel.

Однако вот разумное сопоставление формата файла с соответствующей библиотекой Rust:

Разветвители

https://github.com/benbrandt/text-splitter

Единственная библиотека, достаточно практичная для разделения. Похоже, используется подход «правильное разделение», при котором вам не нужно выбирать, следует ли разбивать на новые строки, символы, рекурсивно или токены. Он спустится и использует соответствующий метод, чтобы максимизировать размеры фрагментов.

Подсказки

На момент написания статьи нет общего консенсуса и нет библиотеки, которая объединяет концепцию шаблона, как это имеет langchain. Тем не менее, использование библиотеки шаблонов и построение на ее основе — отличный способ реализации подсказок.

В Rust есть несколько замечательных библиотек шаблонов, которые подходят для работы с шаблонами:

  • Handlebars — для стандартных, привычных, минималистичных шаблонов логики.
  • Тера — для тех, кто знаком с jinja2
  • Жидкость — для тех, кто знаком с жидкостью

В большинстве случаев руль — лучший выбор, который будет знаком широкой публике.

Векторные базы данных

В настоящее время не существует единого интерфейса для векторных баз данных, который обеспечивает общую оболочку для многих поставщиков. Изучите несколько проектов с открытым исходным кодом, связанных с LLM. Вы будете находить повторные реализации одних и тех же интерфейсов снова и снова, что очень жаль, и тем не менее ни одна библиотека не предоставляет общий интерфейс.

Учитывая такой упрощенный интерфейс, создание такой библиотеки обеспечивает хорошую рентабельность инвестиций. В основном это должно быть:

  • add_documents
  • similarity_search

К вашему сведению: на Rust реализовано множество отдельных векторных баз данных, как коммерческих, так и с открытым исходным кодом. Qdrant, пожалуй, самый популярный open source.

Память

Есть несколько проектов, реализующих память или историю. Большинство из них реализуют службу, которая хранит для вас индексы и истории поиска, и их также можно использовать в качестве справочника по тому, как выполнять ваш рабочий процесс в Rust при работе с ИИ.

Некоторые интересные:

  • мемекс — часть подзорной трубы, хорошо читает код, выполняет хранение документов и семантический поиск для проектов LLM.
  • indexify — долговременная память LLM
  • motorhead — память/восстановление информации

Инструменты

На момент написания этой статьи ни одна библиотека инструментов/плагинов не предназначалась только для этой цели. То есть библиотеки, которые обеспечивают надежный интерфейс для:

  • Объявление инструмента
  • Возможности
  • Запуск инструмента (безопасно или нет)
  • Структурирование запроса и ответа инструмента

Тем не менее, эти реализации можно найти в нескольких реализациях цепочек, которые в настоящее время есть в Rust, которые обсуждаются ниже.

Цепи

llmchain-rs

Цепная библиотека небольшого объема. Я бы сказал, 30% того, что есть у langchain-py.

  • Ранние времена для этого проекта
  • Хорошее количество примеров
  • Ориентированные на Databend, но универсальные интерфейсы
  • Хорошее тестовое покрытие (в любом случае это сложная задача тестирования инфраструктуры LLM)

Давайте посмотрим на некоторые абстракции:

Встраивание

pub trait Embedding: Send + Sync {
    async fn embed_query(&self, input: &str) -> Result<Vec<f32>>;
    async fn embed_documents(&self, inputs: &Documents) -> Result<Vec<Vec<f32>>>;
}

магистр права

pub trait LLM: Send + Sync {
    async fn embedding(&self, inputs: Vec<String>) -> Result<EmbeddingResult>;
    async fn generate(&self, input: &str) -> Result<GenerateResult>;
    async fn chat(&self, _input: Vec<String>) -> Result<Vec<ChatResult>> {
        unimplemented!("")
    }
}
  • Интеграции в настоящее время: OpenAI, Azure-OpenAI и Databend, поэтому, вероятно, смешивание данных из Databend с AI было спусковым крючком для всего этого.

Документ

#[derive(Debug, Clone, Eq, PartialEq)]
pub struct Document {
    pub path: String,
    pub content: String,
    pub content_md5: String,
}
impl Document {
    pub fn create(path: &str, content: &str) -> Self {
        Document {
            path: path.to_string(),
            content: content.to_string(),
            content_md5: format!("{:x}", md5::compute(content)),
        }
    }
    pub fn tokens(&self) -> usize {
        chat_tokens(&self.content).unwrap().len()
    }
    pub fn size(&self) -> usize {
        self.content.len()
    }
}
#[derive(Debug)]
pub struct Documents {
    documents: RwLock<Vec<Document>>,
}

Быстрый

pub trait Prompt: Send + Sync {
    fn template(&self) -> String;
    fn variables(&self) -> Vec<String>;
    fn format(&self, input_variables: HashMap<&str, &str>) -> Result<String>;
}
  • Простая текстовая замена только переменных
  • Одноуровневый, без расширенных зависимых или частичных подсказок

Векторный магазин

#[async_trait::async_trait]
pub trait VectorStore: Send + Sync {
    async fn init(&self) -> Result<()>;
    async fn add_documents(&self, inputs: &Documents) -> Result<Vec<String>>;
    async fn similarity_search(&self, query: &str, k: usize) -> Result<Vec<Document>>;
}
  • Поддержка databend только в качестве поставщика
  • Отсутствует MMR для поиска (как и во всех интерфейсах, которые я видел до сих пор)
  • Отсутствует абстракция сохранения/загрузки (но принадлежит ли она трейту?)

Общий

  • Относительно солидная кодовая база и хорошие абстракции
  • Разделители (нет абстракции или стратегии на выбор, но это относится ко всем реализациям цепочек в Rust, которые я видел до сих пор)
  • Память (просто ранние времена там, кажется)

lm-chain

Более популярная цепочка библиотек, разделенная на ящики Rust, хорошие идиомы Rust и структуру. Не полный охват того, что есть у langchain-py. Может быть, 30%, и здесь еще рано, как и во всех других библиотеках.

Давайте посмотрим на абстракции здесь:

Встраивание

#[async_trait]
pub trait Embeddings {
    type Error: Send + Debug + Error + EmbeddingsError;
    async fn embed_texts(&self, texts: Vec<String>) -> Result<Vec<Vec<f32>>, Self::Error>;
    async fn embed_query(&self, query: String) -> Result<Vec<f32>, Self::Error>;
}

ВекторМагазин

#[async_trait]
pub trait VectorStore<E, M = EmptyMetadata>
where
    E: Embeddings,
    M: serde::Serialize + serde::de::DeserializeOwned,
{
    type Error: Debug + Error + VectorStoreError;
    async fn add_texts(&self, texts: Vec<String>) -> Result<Vec<String>, Self::Error>;
    async fn add_documents(&self, documents: Vec<Document<M>>) -> Result<Vec<String>, Self::Error>;
    async fn similarity_search(
        &self,
        query: String,
        limit: u32,
    ) -> Result<Vec<Document<M>>, Self::Error>;
}

Разветвители

Похоже, сплиттер - это Tokenizer

pub trait Tokenizer {
    fn tokenize_str(&self, doc: &str) -> Result<TokenCollection, TokenizerError>;
		fn to_string(&self, tokens: TokenCollection) -> Result<String, TokenizerError>;
    fn split_text(
        &self,
        doc: &str,
        max_tokens_per_chunk: usize,
        chunk_overlap: usize,
    ) -> Result<Vec<String>, TokenizerError>;
}

Быстрый

Подсказка — это конструкция, которая позволяет работать с абстракцией Chat или Text сообщений. Шаблоны мощные и основаны на tera.

Документ

#[derive(Debug)]
pub struct Document<M = EmptyMetadata>
where
    M: serde::Serialize + serde::de::DeserializeOwned,
{
    pub page_content: String,
    pub metadata: Option<M>,
}

Больше похоже, чем отличается по сравнению с документом langchain-py.

DocumentStore (Загрузчик?)

#[async_trait]
pub trait DocumentStore<T, M>
where
    T: Send + Sync,
    M: Serialize + DeserializeOwned + Send + Sync,
{
    type Error: std::fmt::Debug + std::error::Error + DocumentStoreError;
async fn get(&self, id: &T) -> Result<Option<Document<M>>, Self::Error>;
    async fn next_id(&self) -> Result<T, Self::Error>;
    async fn insert(&mut self, documents: &HashMap<T, Document<M>>) -> Result<(), Self::Error>;
}

Общий

  • Хорошее разделение между функциями и ящиками
  • Чувствует себя ржавым
  • Не хватает разветвителей, загрузчиков
  • Отсутствие интеграций в целом и особенно векторных хранилищ

СмартГПТ

Это не библиотека и не инфраструктура, но внутренности построены хорошо и могут быть полезны для цепочки библиотек. Давайте посмотрим на абстракции:

магистр права

pub trait LLMModel : Send + Sync {
    async fn get_response(&self, messages: &[Message], max_tokens: Option<u16>, temperature: Option<f32>) -> Result<String, Box<dyn Error>>;
    async fn get_base_embed(&self, text: &str) -> Result<Vec<f32>, Box<dyn Error>>;
    fn get_token_count(&self, text: &[Message]) -> Result<usize, Box<dyn Error>>;
    fn get_token_limit(&self) -> usize;
    fn get_tokens_from_text(&self, text: &str) -> Result<Vec<String>, Box<dyn Error>>;
}

Некоторые методы были удалены, поскольку они были реализованы по умолчанию.

Сообщение

#[derive(Clone, Debug)]
pub enum Message {
    User(String),
    Assistant(String),
    System(String)
}

Моделирование сообщения по шаблону User-AI-System

Система Памяти (История?)

pub trait MemorySystem : Send + Sync {
    async fn store_memory(&mut self, llm: &LLM, memory: &str) -> Result<(), Box<dyn Error>>;
async fn get_memory_pool(&mut self, llm: &LLM, memory: &str, min_count: usize) -> Result<Vec<RelevantMemory>, Box<dyn Error>>;
    async fn get_memories(
    }
...

Хороший интерфейс памяти

Команда (Инструмент?)

pub trait CommandImpl : Send + Sync {
    async fn invoke(&self, ctx: &mut CommandContext, args: ScriptValue) -> Result<CommandResult, Box<dyn Error>>;
fn box_clone(&self) -> Box<dyn CommandImpl>;
}

Общий

  • Этот проект имеет относительно богатую коллекцию инструментов и может быть лучшим началом для универсальной библиотеки, реализующей инструменты.
  • Кодовая база очень прагматична, потому что почти все на самом деле используется.
  • Там, где функционал был не нужен — его нет (естественно), и тут не хватает кодовой базы, по сравнению с ленгчейном: сплиттеров, загрузчиков, шаблонов подсказок, векторных хранилищ и общего смысла множественных провайдеров
  • Использовать его в качестве библиотеки langchain общего назначения было бы неудобно, если только вы не сталкиваетесь с вариантами использования, которые уже решает этот проект. Которого я думаю довольно быстро не хватит.

Спасибо за прочтение.