Типичные ошибки LLM при генерации Rust-кода: полугодовой эксперимент (2026) | AiManual
AiManual Logo Ai / Manual.
15 Май 2026 Гайд

Типичные ошибки LLM при генерации Rust-кода: полугодовой эксперимент с Claude, GPT и Cursor

Разбор слепых зон Claude 4.5, GPT-5.2 и Cursor в Rust: от borrow checker до unsafe. Реальные примеры, статистика, советы.

Шесть месяцев с AI-ассистентами: сухие цифры

47% сгенерированного Rust-кода не компилировалось с первого раза. Это не шутка. Полгода я гонял Claude 4.5, GPT-5.2 и Cursor (с бэкендом GPT-5.2) на реальных задачах: от парсеров до асинхронных драйверов. Радовался, когда код проходил cargo check. Но чаще — матерился на borrow checker. В итоге собрал топ-5 граблей, на которые наступает каждая нейросеть. И да, если вы думаете, что LLM понимают Rust — вы ошибаетесь. Они просто угадывают паттерны. А Rust — язык, где угадать без понимания нельзя.

💡
Все примеры — из моего продакшн-кода (не учебные). Имена переменных изменены, суть сохранена. Полные датасеты и методику эксперимента я описывал в гайде по осознанному вайб-кодингу.

1. Borrow checker: первая жертва

LLM упорно генерируют два mutable borrow в одной области видимости. Вот типичный диалог: я прошу функцию, которая перекладывает данные между структурами. Claude 4.5 выдаёт:

fn transfer(a: &mut Vec<i32>, b: &mut Vec<i32>) {    for item in a.iter() {        b.push(*item);    }    a.clear();}

Компилятор: cannot borrow `a` as mutable because it is also borrowed as immutable. LLM забывает, что a.iter() уже заимствует a. Решение — клонировать или использовать draining iter. Почему нейросеть так делает? Потому что в её тренировочных данных часто встречаются языки без такого жёсткого контроля. Трансформер просто копирует шаблон «итерация + мутация», не осознавая конфликт. Забавно, но GPT-5.2 справляется с этим в 70% случаев, а Claude 4.5 — только в 55% (моя статистика по 200 запросам).

Правильный код

fn transfer(a: &mut Vec<i32>, b: &mut Vec<i32>) {    let items: Vec<i32> = a.drain(..).collect();    b.extend(items);}

Совет: если LLM упорно выдаёт конфликт заимствований — попросите её объяснить времена жизни каждого заимствования текстом. Это часто переключает режим «угадай» на «анализируй».

2. Времена жизни: вымершие динозавры в голове LLM

Функция берёт строку, находит подстроку, возвращает итератор по байтам — классика. Cursor (на GPT-5.2) выдал:

fn find_substring_indices(haystack: &str, needle: &str) -> impl Iterator<Item = usize> {    haystack        .as_bytes()        .windows(needle.len())        .enumerate()        .filter_map(move |(i, window)| {            if window == needle.as_bytes() {                Some(i)            } else {                None            }        })}

Смотрите: impl Iterator не содержит явных аннотаций lifetimes. Но замыкание захватывает needle по ссылке. LLM забыла, что возвращаемый итератор должен жить не дольше haystack. Компилятор ругается: lifetime may not live long enough. А если бы написали fn find_substring_indices<'a>(haystack: &'a str, needle: &'a str) -> impl Iterator<Item = usize> + 'a — всё бы взлетело. Но нейросеть ленится добавлять lt.

Почему? Потому что в большинстве языков (Java, Python, JS) ссылки живут вечно для сборщика мусора. LLM экстраполирует этот паттерн на Rust. Хуже всего с lifetimes справляется Cursor — 68% ошибок. Claude 4.5 — 44%.

3. Unsafe: чёрная дыра для нейросетей

Когда я попросил GPT-5.2 написать быстрый хеш-таблицу на сырых указателях, первая итерация содержала:

let ptr = Box::into_raw(Box::new([0u8; 256]));unsafe {    *ptr = 42; // запись в нулевой элемент? Нет — в начало массива}drop(unsafe { Box::from_raw(ptr) });

Всё ок, пока не требуется логика освобождения нескольких блоков. LLM легко генерирует double free или use-after-free. Например:

unsafe {    let buf = malloc(64);    // ...    free(buf);    // ...    free(buf); // второй free!}

Нейросеть просто повторяет паттерн «освободить в конце», не отслеживая состояние. Я насчитал 18% всех unsafe-блоков от LLM содержали UB. Половина из них — double free. Код-ревью с LLM здесь бесполезно — нейросеть может не заметить свою же ошибку.

4. Pin и самореферентные структуры: тёмная материя

LLM понятия не имеют, что такое самореферентные структуры, если их не натаскать на async. Вот классика: структура хранит ссылку на поле внутри себя. Cursor предложил такой код для арены:

struct Arena<'a> {    data: Vec<i32>,    ptr: Option<&'a i32>,}

Пока Arena живёт на стеке — всё нормально. Но стоит её переместить — ссылка ptr повисает. LLM не додумывается использовать Pin. И если вы генерируете самодельный Future — ждите сюрпризов. Я потерял день, отлаживая генератор асинхронного стрима, который нейросеть написала без Pin.

Единственный способ — в явном виде просить: «используй Pin<Box<Self>> для self-референтных данных». Но даже после этого Claude 4.5 иногда забывает про unsafe внутри Pin::new_unchecked.

5. Обработка ошибок: путаница между Option и Result

Тривиально: функция парсинга возвращает Result<T, E>. LLM пишет:

let val = something().ok_or("error")?;

Здесь "error"&str, а нужно Box<dyn Error> или кастомный тип. LLM не видит разницы между ok_or и ok_or_else. Или путает map и and_then. Это мелочи, но они отнимают время на компиляции. Из моего журнала: 12% ошибок — неправильное использование комбинаторов Result/Option.

Совет: используйте правильные промпты — просите подсказывать тип ошибки явно, например: «возвращай anyhow::Result».

Сравнение: кто меньше врёт?

Я вёл учёт по трём моделям для 50 задач каждого типа. Результаты (процент ошибок, из-за которых код не компилировался):

Тип ошибкиClaude 4.5GPT-5.2Cursor (GPT-5.2)
Borrow checker45%30%52%
Lifetimes44%38%68%
Unsafe/UB20%35%42%
Pin/self-ref55%60%70%
Ошибки типов (конечные)12%10%18%

Вывод: GPT-5.2 чище с lifetimes и borrow checker, но опаснее в unsafe. Claude 4.5 лучше понимает низкоуровневые детали, но проигрывает в системе типов. Cursor — удобный автокомплит, но доверять ему генерацию целых функций не стоит: он теряет контекст через 10 строк.

Важный нюанс: я использовал модельные API без дополнительных флагов strict. Про то, почему параметр strict в VLLM/llama.cpp ничего не даёт, у меня есть отдельный материал.

Как заставить LLM писать рабочий Rust: три принципа

После сотен итераций выработал рабочие приёмы:

  1. Конкретные сигнатуры. Не проси «функцию для обработки строк» — дай полный прототип с типами и lifetimes. LLM меньше додумывает.
  2. Инкрементальная генерация. Проси сначала написать тип данных, потом — функцию, потом — тест. Каждый шаг проверяй. Это снижает ошибки на 40%.
  3. Запрет unsafe без явного разрешения. Добавь в промпт: «НИКАКОГО unsafe, если я не попрошу». LLM лезет в unsafe даже когда не нужно.

И ещё: не используй LLM для рефакторинга кода с lifetimes. Они сломают времена жизни. Лучше напиши сам, а нейросеть попроси добавить комментарии или документацию. Это её сильная сторона.

То, о чём молчат white papers: Rust как лакмусовая бумажка для LLM

Если модель не может сгенерировать корректный Rust — значит она не понимает логику программы. Она просто умеет красиво переставлять токены. Rust — это тест на интеллект для LLM. Python — детский лепет. C — мутная вода. А Rust с его borrow checker и lifetimes — идеальный экзамен.

Лично я перестал ожидать от нейросетей идеального Rust. Я использую их как черновик, а затем переписываю unsafe-блоки вручную. И мой главный совет: не доверяй LLM там, где компилятор молчит, а UB подкрадывается незаметно. Проверяйте с Miri, санитайзерами и code review. А если хотите понять, когда вообще стоит делегировать код нейросети, а когда — писать самому, прочтите мой чек-лист Delegation Filter.

Подписаться на канал