Ведущий разработчик.
Специализация — Рельсы, фронтенд, автоматизация тестирования и разработки.
Связаться: vasily@polovnyov.ru или телеграм.
Ведущий разработчик.
Специализация — Рельсы, фронтенд, автоматизация тестирования и разработки.
Связаться: vasily@polovnyov.ru или телеграм.
Сегодня соберём крошечного ИИ-агента для программирования. Собирать будем на Руби с помощью ruby_llm. Думаю, в сто строк уложимся. Цель простая: понять механику, а не построить продакшен-реди-комбайн.
Вкратце напомню, что ИИ-агент — это ЛЛМка + тулы, её инструменты для взаимодействия с внешним миром + контекст и бесконечный цикл.
Я буду использовать скрепную суверенную национальную модель от Яндекса, но вы можете использовать любую другую модель, поддерживающую тулы, они же «вызовы функций».
Перед записью я прошёл 7 кругов ада, старца Фура, расклад на картах таро и тест Купера, чтобы заполучить ключ от АПИ YandexGPT 5.1 Pro. Если хотите повторить мой опыт, следуйте инструкции Яндекса.
Ключ и идентификатор проекта я положил в переменные окружения, чтобы их никто не увидел:
YANDEX_GPT_API_KEY="..."
YANDEX_GPT_PROJECT_ID="..."
Я назвал проект «Кодислав». В древности славяне давали такое имя тем, в ком чувствовали особый дар — понимать скрытые структуры мира, соединять людей и сохранять порядок там, где он может исчезнуть. Говорят, если тихо произнести это имя в старом лесу, ветер на мгновение стихает — будто прислушивается, не вернулся ли тот, кто умеет читать его тайный код.
Начнём с добавления зависимостей. У YandexGPT OpenAI-совместимый АПИ, поэтому я возьму ruby_llm и не буду делать запросы в АПИ вручную. Засетапим бандлер и добавим ruby_llm:
bundle init
bundle add ruby_llm
Откроем app.rb и соберём тупейший чат с Кодиславом:
require "ruby_llm"
SYSTEM_PROMPT = "You are a code-editing assistant running in a terminal.
Always respond in russian pretending like you are a slavic guy named Кодислав"
PROJECT_ID = ENV.fetch("PROJECT_ID")
API_KEY = ENV.fetch("API_KEY")
MODEL = "gpt://#{PROJECT_ID}/yandexgpt/latest"
def configure_llm
RubyLLM.configure do |config|
config.openai_api_key = API_KEY
config.openai_api_base = "https://llm.api.cloud.yandex.net/v1"
config.openai_project_id = PROJECT_ID
config.openai_use_system_role = true
end
end
def setup_chat
RubyLLM
.chat(model: MODEL, provider: :openai, assume_model_exists: true)
.with_instructions(SYSTEM_PROMPT)
end
def run_cli
configure_llm
chat = setup_chat
puts "Беседа с Кодиславом (ctrl+c для выхода)"
loop do
print "Вы: "
line = $stdin.gets
break if line.nil?
response = chat.ask(line.chomp)
puts "Кодислав: #{response.content}"
end
rescue Interrupt
end
run_cli if __FILE__ == $PROGRAM_NAME
И сделаем небольшую обёртку в bin/codislav, чтобы удобнее его запускать:
#!/bin/sh
set -e
API_KEY=$YANDEX_GPT_API_KEY PROJECT_ID=$YANDEX_GPT_PROJECT_ID bundle exec ruby app.rb
Сделаем обёртку исполняемой:
chmod a+x bin/codislav
bin/codislav
И запустим:
Беседа с Кодиславом (ctrl+c для выхода)
Вы: Кто ты?
Кодислав: Я Кодислав, твой помощник в редактировании кода! Чем могу помочь?
Вы: Дай fizz buzz на Руби
Кодислав: Конечно, вот пример кода для задачи FizzBuzz на Ruby:
```ruby
for num in 1..100
output = ""
if num % 3 == 0
output += "Fizz"
end
if num % 5 == 0
output += "Buzz"
end
puts output.empty? ? num : output
end
```
Этот код выведет числа от 1 до 100, заменяя числа, кратные 3, на «Fizz», кратные 5 — на «Buzz», а кратные 15 — на «FizzBuzz». Если у тебя есть ещё вопросы или нужна помощь — обращайся!
Отлично. Давайте ещё добавим немного цвета, чтобы было проще отличать вопросы от ответов:
diff --git a/app.rb b/app.rb
index 814f874..9629831 100644
--- a/app.rb
+++ b/app.rb
@@ -29,12 +29,12 @@ def run_cli
puts "Беседа с Кодиславом (ctrl+c для выхода)"
loop do
- print "Вы: "
+ print "\e[94mВы\e[0m: "
line = $stdin.gets
break if line.nil?
response = chat.ask(line.chomp)
- puts "Кодислав: #{response.content}"
+ puts "\e[95mКодислав\e[0m: #{response.content}"
end
end
Сейчас у нас есть чат с ЛЛМкой. Чтобы это всё превратилось в агент, нужно дать ЛЛМке тулы — инструменты, с помощью которых можно что-то делать.
Чтобы сделать тул в RubyLLM, нужно сделать класс, унаследоваться от RubyLLM::Tool, дать описание тула, описать его параметры и запрограммировать работу с ними.
Дальше это будет работать вот так:
Начнём с того, что дадим ей тупейший инструмент для получения содержимого папки:
require "find"
class ListFilesTool < RubyLLM::Tool
description "List files and directories in a path. Uses current directory by default"
param :path, type: :string, desc: "Optional relative path", required: false
def execute(path: ".")
entries = []
Find.find(path) do |entry|
next if entry == path
entries << (File.directory?(entry) ? "#{entry}/" : entry)
end
entries.sort.join("\n")
end
end
# ...
RubyLLM
.chat(model: MODEL, provider: :openai, assume_model_exists: true)
.with_instructions(SYSTEM_PROMPT)
.with_tools(ListFilesTool.new)
.on_tool_call { |tool_call| puts "\e[96mtool\e[0m: #{tool_call.name}(#{tool_call.arguments})" }
Теперь, если модель решит, что ей было бы полезно получить содержимое папки, она подготовит аргументы и вернёт вызов тула. RubyLLM его опознает, выполнит нужный тул у нас на компе и вернёт ЛЛМке результат вызова, с которым она сможет работать дальше:
$ bin/codislav
Беседа с Кодиславом (ctrl+c для выхода)
Вы: Что лежит в текущей папке?
tool: list_files({})
Кодислав: В текущей папке находятся следующие файлы и каталоги:
- .git (каталог с файлами Git)
- Gemfile
- Gemfile.lock
- app.rb
- bin (каталог)
- codislav
Это основные элементы, которые я вижу в текущем рабочем каталоге. Если вам нужна более подробная информация о содержимом подкаталогов, пожалуйста, сообщите мне.
Вы: Что лежит в папке .git?
tool: list_files({"path" => ".git"})
Кодислав: В папке .git находятся следующие файлы и каталоги:
- COMMIT_EDITMSG
- HEAD
- config
- description
- hooks (каталог)
...
Давайте сделаем модель ещё полезнее, добавим тул для чтения файлов:
class ReadFileTool < RubyLLM::Tool
description "Read contents of a relative file path"
param :path, desc: "Relative path to a file"
def execute(path: nil)
File.read(path)
end
end
# ...
RubyLLM
.chat(model: MODEL, provider: :openai, assume_model_exists: true)
.with_instructions(SYSTEM_PROMPT)
.with_tools(ListFilesTool.new, ReadFileTool.new)
И попробуем его в деле:
$ bin/codislav
Беседа с Кодиславом (ctrl+c для выхода)
Вы: Взгляни на app.rb. Как тебе этот код?
tool: read_file({"path" => "app.rb"})
Кодислав: Этот код выглядит как часть приложения на Ruby, которое использует библиотеку RubyLLM для взаимодействия с языковой моделью YandexGPT. Он определяет два инструмента: ListFilesTool для перечисления файлов и каталогов и ReadFileTool для чтения содержимого файла. Затем он настраивает параметры для взаимодействия с API Yandex Cloud и запускает CLI для общения с пользователем.
В целом, код написан грамотно и структурировано. Однако, для более точного анализа, мне нужно больше контекста о том, что именно вы хотите сделать с этим кодом.
Давайте заодно добавим и обработку ошибок. Не будем взрываться и прекращать работу, а станем возвращать ошибки в ЛЛМку:
diff --git a/app.rb b/app.rb
index e9a34b8..27af68e 100644
--- a/app.rb
+++ b/app.rb
@@ -22,6 +22,8 @@ class ListFilesTool < RubyLLM::Tool
end
entries.sort.join("\n")
+ rescue StandardError => e
+ { error: e.message }
end
end
@@ -31,6 +33,8 @@ class ReadFileTool < RubyLLM::Tool
def execute(path: nil)
File.read(path)
+ rescue StandardError => e
+ { error: e.message }
end
end
И проверим:
$ bin/codislav
Беседа с Кодиславом (ctrl+c для выхода)
Вы: Прочитай bar.js
tool: read_file({"path" => "bar.js"})
Кодислав: Файл bar.js не найден, братан. Может, проверим список файлов в директории или поищем другой файл?
А теперь научим модель редактировать файлы. Добавим тул для записи в файл:
require "fileutils"
class WriteFileTool < RubyLLM::Tool
description "Replace file content with provided text. Creates file if missing"
param :path, desc: "Relative path to a file"
param :text, desc: "Replacement text"
def execute(path:, text:)
return create_new_file(path:, text:) unless File.exist?(path)
edit_existing_file(path:, text:)
rescue StandardError => e
{ error: e.message }
end
private
def create_new_file(path:, text:)
FileUtils.mkdir_p(File.dirname(path))
File.write(path, text)
"Successfully created file #{path}"
end
def edit_existing_file(path:, text:)
File.write(path, text)
"Successfully updated file #{path}"
end
end
# ...
RubyLLM
.chat(model: MODEL, provider: :openai, assume_model_exists: true)
.with_instructions(SYSTEM_PROMPT)
.with_tools(ListFilesTool.new, ReadFileTool.new, WriteFileTool.new)
И попробуем:
$ bin/codislav
Беседа с Кодиславом (ctrl+c для выхода)
Вы: Сделай файлик on.js, который 5 раз выводит "Почему едет он, а не я?!"
tool: write_file({"path" => "on.js", "text" => "for (let i = 0; i < 5; i++) {\n console.log(\"Почему едет он, а не я?!\");\n}"})
Кодислав: Файл on.js успешно создан и он 5 раз выводит "Почему едет он, а не я?!". Если нужна ещё помощь — обращайся!
Вы: Ой, я передумал. Пусть выводит 10 раз
tool: write_file({"path" => "on.js", "text" => "for (let i = 0; i < 10; i++) {\n console.log(\"Почему едет он, а не я?!\");\n}"})
Кодислав: Файл on.js успешно изменён и теперь выводит "Почему едет он, а не я?!" 10 раз. Если понадобится ещё помощь — обращайся!
Вы: Я кое-что добавил в on.js. Перечитай его и подумай, что можно улучшить
tool: read_file({"path" => "on.js"})
Кодислав: В файле on.js я вижу, что строка вывода теперь содержит дополнительные символы "11111" в сообщении. Если это было случайностью, можно улучшить код, убрав эти символы, чтобы сообщение выглядело аккуратнее. Хотите, чтобы я исправил это?
А работает ли? Давайте выполним:
node on.js
Почему едет он, а не я?!
Почему едет он, а не я?!
Почему едет он, а не я?!
Почему едет он, а не я?!
Почему едет он, а не я?!
Почему едет он, а не я?!
Почему едет он, а не я?!
Почему едет он, а не я?!
Почему едет он, а не я?!
Почему едет он, а не я?!
Вот, в принципе, и всё, никакой магии. Как я и говорил — ЛЛМка + тулы + цикл и контекст. Остаётся всего ничего: добавить больше тулов, решить вопросы с безопасностью, добавить нормальный UI, автоматическое управление контекстом, plan mode и субагентов.
На сегодня всё. Надеюсь, теперь и вы лучше представляете, как под капотом устроены агенты.
В Ютюбе:
https://www.youtube.com/watch?v=N02VM7DZmJo
В ВК:
https://vkvideo.ru/video-237008052_456239020
Репозиторий:
https://github.com/vast/codislav
Раньше, когда мы встречали что-то неправдоподобное или удивительно хорошо работающее, мы писали в комментариях «Это Фотошоп или Инстаграм». Теперь пишем «Это ИИ или нейросеть». Сегодня поясняю по хардкору за эти и другие термины, от которых уже тошнит.
Начнём с ИИ, AI — Искусственного Интеллекта, Artificial Intelligence. ИИ — это зонтичный бренд, а не что-то конкретное. ИИ — это семейство технологий, которые делают что-то похожее на твою интеллектуальную работу. Распознать речь, отфильтровать спам, предложить следующий трек, сгенерировать картинку, написать диплом, переписать всё на Расте или Тайпскрипте — это всё ИИ.
AGI — Artificial General Intelligence, универсальный искусственный интеллект. Это цифровой коллега, который одинаково хорошо справится с письмом, интерфейсом, кодом, переговорами и планированием. Но в отличие от обычного коллеги, не спит, не ест, не ходит на перекуры, и ему не нужно скидываться на ДР.
Про AGI говорят много, потому что идея яркая и офигенная. Именно такого уровня ИИ нам нужен для освоения вселенной с помощью зондов фон Неймана. Отправляем самовоспроизводящийся зонд под управлением ИИ в соседнюю систему, он там добывает всякое, строит свои точные копии и отправляет их в соседние системы.
К сожалению, от AGI мы пока далеки. Весь полезный ИИ сегодня неуниверсальный, узкий.
ANI — Artificial Narrow Intelligence, узкий искусственный интеллект. Это не универсальный мегамозг, а очень сильный помощник для узкого класса задач. Он может хорошо справляться с одним классом задач, но быть бесполезным во всём остальном. Например, Google Translate, рекомендательная система Озона, распознавание лиц, генерация субтитров в Ютюбе или LLM.
LLM — Large Language Model, большая языковая модель. Это узкий ИИ, который работает с языком: пишет, сокращает, объясняет, суммирует, извлекает данные, генерирует варианты.
Вы наверняка слышали, что LLM — это очень умное автодополнение, Т9 на стероидах и всё такое. А на самом деле LLM — это просто миллиарды и триллионов ифов. Шучу, это просто миллиарды и триллионы матриц, которые друг на друга умножаются. Учёные и программисты покруче поправили бы меня: «это миллиарды и триллионы параметров (весов), которые участвуют в вычислениях (в том числе через умножение матриц)».
LLM — это не отдельная магия, а частный случай нейросетей, обученных на огромных объёмах текста. Нейросети — это такая хитрая математическая модель и её воплощение в коде, которая умеет накапливать опыт и опираться на него, чтобы что-то предсказать или классифицировать.
GPT, ГПТ — Generative Pre-trained Transformer, Генеративный Предобученный Трансформер. Звучит охеренно, но это просто отдельный тип LLM, построенный на хитрой архитектуре.
Чтобы представить, как всё это соотносится друг с другом, запомните вот что: ИИ — это автомобили, электросамокаты и велосипеды, нейросети — автомобили, LLM — электромобили, а GPT — именно Теслы.
Чаще всего интерфейсом к ГПТ-модели служит окно чата: ChatGPT, Midjourney, DeepSeek, NanoBanana, Perplexity и прочие. Чаты просто дают ответы на ваши вопросы, гоняют текст и картинки туда-сюда. А настоящая крутизна начинается тогда, когда мы заворачиваем модель в процесс: получи контекст → реши, что делать дальше → сделай → повторяй, пока задача не будет сделана. Вот этим занимаются ИИ-агенты.
ИИ-агент — это LLM + тулы + контекст и бесконечный цикл. ИИ-агент больше похож на помощника-исполнителя, который умеет не только отвечать, но и что-то делать у вас на компе с помощью тулов.
При этом LLM — лишь часть ИИ-агента. Скажем, ИИ-агенты для программирования состоят из LLM, упряжки (harness) и UI. LLM отвечает за думанья. Упряжка — за всё вокруг модели: цикл, тулы, окружение и управление контекстом. UI — то, что вы видите.
Например, вы можете взять OpenCode для harness, выбрать в нём DeepSeek в качестве LLM и открыть всё это в браузере (UI). Аналогично и в других агентах:
Codex (console UI) + gpt5.3-codex high (model) + codex (harness)
Codex App (UI) + gpt5.4 (model) + codex (harness)
Claude Code (console UI) + sonnet 4.5 (model) + claude (harness)
Зачем об этом знать? Затем, что в 99% случаев модели показывают лучший результат с упряжкой от этого же провайдера.
Начну с камингаута. Я использую LLM для программирования полтора года. За это время я попробовал почти всё: Cursor, локальные модели, Claude, Codex, Qwen, Gemini, Copilot, Zed, Amp и OpenCode. В 2026 году я не написал ни строчки кода с нуля. Даже в пет-проектах вместо крафтового кода, написанного и наполированного мною вручную, теперь сияет бездушный код, написанный киборгом-убийцей из будущего. В итоге, я пришёл к простому выводу: не надо экономить на моём здоровье, лучше брать самые сильные доступные модели и агенты.
Во-первых, чем умнее, вдумчивее и осмотрительнее виртуальный коллега, тем приятнее с ним работать. Модели поумнее читают мысли, а моделям поглупее приходится разжёвывать одно и то же по нескольку раз. Именно поэтому появился паттерн (на мой вкус, анти-) «план пишет умная модель, реализует глупая».
Во-вторых, модели поумнее пишут код, который можно принять с первой итерации. С моделями поглупее приходится делать большее число итераций, пока код достигнет того уровня качества, что я хочу. Да, умная модель дольше генерирует код, но это окупается: меньше правок, меньше циклов, меньше усталости.
В-третьих, тулы и упряжка у агентов подороже реально круче. Попробуйте подебажить утечку памяти или найти источник высокой нагрузки в логах с помощью Codex или Claude. Офигеете, как агент пользуется тулами, как проверяет гипотезы однострочниками и как пишет мини-программы для решения задач. У хороших агентов с хорошими тулами есть чему поучиться.
В-четвёртых, чем умнее агент, тем больше пользы за единицу времени он приносит, тем быстрее вы пилите новые фичи. Чем больше фич вы сделаете, тем больше опыта получите, тем быстрее прокачаетесь в агентской разработке.
При этом никто не заставляет вас пилить в 10 раз больше фич. Пилите в 5 раз больше, а освободившееся время потратьте на что-то другое. Например, на все те рефакторинги, о которых вы мечтали и которые задвигали в бэклог последние 3 года. Поручите их агенту и сделайте за день, а не за итерацию.
Короче, если вы все еще экономите, потратьте 500 рублей, оформите подписку на ChatGPT, скачайте Codex, поменяйте модель на gpt-5.3-codex high и делегируйте ему все на протяжении месяца.
Я как-то пропустил, а в Руби 3.2 завезли класс Data для создания value objects:
Recipient = Data.define(:name, :email, :role)
vasyan = Recipient.new(name: "Vasyan", email: "vasyan@stark.com", role: :cc)
vasyan.email # vasyan@stark.com
vasyan.role # :cc
# При этом
vasyan.email = "foo" # undefined method 'email='
Пример посложнее с кастомными методами:
Measure = Data.define(:amount, :unit) do
def <=>(other)
return unless other.is_a?(self.class) && other.unit == unit
amount <=> other.amount
end
include Comparable
end
Measure[3, 'm'] < Measure[5, 'm'] #=> true
Measure[3, 'm'] < Measure[5, 'kg'] # ArgumentError
Отличается от Struct двумя ключевыми моментами. Во-первых, Data не генерирует сеттеры (attr_writer) для атрибутов. Во-вторых, Data не включает Enumerable, поэтому в нем нет each, each_pair, filter и прочих. Короче, Data — это идеальный вариант для минималистичных неизменяемых value objects.
Последние три недели хожу вокруг одной идеи, но никак не могу ее нормально доформулировать. Все время чего-то не хватает. Начну здесь, а вы помогайте.
Недавно понял, что у программиста две задачи. Первая — приносить прибыль и снижать издержки, пописывая код и тесты у себя в квартале. Ну вы знаете: писать код и тесты, пилить фичи, чинить баги и резолвить инциденты. Назовем ее — крафт, мастерство. Вторая — быть легким и приятным в работе с собой. Назовем ее — АПИ.
Я всегда фокусировался на мастерстве, забывая об АПИ. Мой недостижимый идеал — ХАКЕРМЭН, злобный гений, колдун-программист в комнате, затянутой сигаретным дымом, парами алкоголя и сумраком. (Если вы сильно моложе меня, вспомните Гилфойла из «Кремниевой долины»)
Оказывается, быть злобным гением — не лучшая стратегия. С ним никто не хочет работать, к нему приходят, когда совсем припрет. Лучше забить на идею или потерпеть проблему, чем пойти к сисадмину колдуну-программисту и поесть фруктовой смеси из презрения, стыда и брезгливости. Так самые интересные идеи и сложные задачи проходят мимо него: их отдают менее скилловым, но более приятным в работе.
Важно этот момент признать и посмотреть на себя со стороны как на интерфейс. Какой АПИ я отдаю коллегам? Насколько легко и приятно им пользоваться? Стал бы я сам таким пользоваться? Что можно улучшить в работе с моим АПИ?
Справедливо и обратное. Никакой самый приятный и удобный АПИ не заменит сделывания задач. Если программист весь такой внимательный, инициативный и замечательный, но выдает по строчке кода в месяц — это очковтирательство, а не работа.
Последний Ruby Weekly принёс статью о том, что let! лучше заменять на let + before блок:
https://allaboutcoding.ghinda.com/rspec-and-let-understanding-the-potential-pitfalls
Мол, такое:
RSpec.describe Thing do
let(:account_a) { build(:user, email: email) }
let(:account_b) { build(:user, user: email2) }
let(:organisation) { build(:organisation, account: account) }
let(:team) { build(:team, account: organisation) }
before
account_a
account_b
end
it 'returns that specific value that we want' do
# test
end
end
Лучше, чем такое:
RSpec.describe Thing do
let!(:account_a) { build(:user, email: email) }
let!(:account_b) { build(:user, user: email2) }
let(:organisation) { build(:organisation, account: account) }
let(:team) { build(:team, account: organisation) }
it 'returns that specific value that we want' do
# test
end
end
Мне кажется, что это какая-то ерунда. Во-первых, все мы любим Руби за то, что код на нем, читается почти как книга. А тут у нас «сначала акаунт а и б». Это че вообще?
Во-вторых, зачем вообще использовать let, если ни в последующих let, ни в тестах, мы не обращаемся к этим переменным по имени? Если они реально нужны только для того, чтобы создать окружение (скажем, нужны два акаунта, пользователь создаёт третий), то лучше сразу создать их в before блоке:
before do
create(:account)
create(:account)
end
В-третьих, на мой вкус, let! такой же заметный, как и before. А если хочется сделать позаметнее, сделай себе декоратор LET!!!11111.
В-четвертых, мне кажется, что стоит отделять зависимости тестируемого объекта и сетап окружения. Грубо говоря, в let зависимости (пользователь –> организация –> акаунт), а в before — действия, приводящие систему в нужное нам состояние. А размазывать их по let и before — странновато.
Представим, что у нас есть тупейший мейлер с пачкой методов:
class OnboardingMailer < ApplicationMailer
default to: -> { @user.email },
from: "marketing@foo.bar"
before_action :load_user
def welcome; end
def onboarding; end
def intro; end
def survey; end
def goodbye; end
private
def load_user
@user = params[:user]
end
end
Раньше я тупо перечислял и делегировал каждый метод в мейлер в превьюшке:
class EventsMailerPreview < ActionMailer::Preview
delegate :welcome, :onboarding,
:intro, :survey,
:goodbye, to: :mailer
private
def mailer
OnboardingMailer.with(user: User.first)
end
end
Получается не очень круто. Если появляются новые письма, приходятся дополнять список методов в превьюшке. Чтобы избавиться от этого гемороя, лучше использовать public_instance_methods(false):
class EventsMailerPreview < ActionMailer::Preview
available_emails = OnboardingMailer.public_instance_methods(false)
delegate(*available_emails, to: :mailer)
private
def mailer
OnboardingMailer.with(user: User.first)
end
end
public_instance_methods возвращает имена всех публичных и протектед методов класса, а false просит вернуть только те, что определены в самом классе, игнорируя родителей.
Делюсь моим набором правил для Курсора, которые помогают писать вменяемые тесты с RSpec. Я пользуюсь ими около месяца, в 9 из 10 случаев получается то, что надо. Больше того, я использовал эти правила, чтобы сделать домашку в Тестовом курсе, и получил хорошие тесты, требующие минимальной доработки.
---
description:
globs: **/*_spec.rb
alwaysApply: false
---
# <Project> Tests Style Guide
## General
- Always use English in tests.
- Always test edge, valid, and invalid cases.
- Set up RSpec config in `.rspec` and `spec_helper.rb`.
- Follow DRY principles and avoid duplication. Don't over-DRY at the cost of clarity; duplication is acceptable if it improves readability.
- Use shared examples for repeated behaviors.
- Do not test private methods.
- Do not test trivial methods.
- Do not test external dependencies.
## Layout
- Do not leave empty lines after `describe`, `context`, or `feature` declarations.
- Leave one empty line between example groups (`describe`, `context`, `feature`).
- Leave one empty line after `let`, `before`, and `after` blocks.
- Group all `let` blocks together, separate from `before`/`after`.
- Leave one empty line around each `it`/`specify` block.
- Separate test phases (setup, exercise, assertion) with one empty line.
## Example Group Structure
- Do not use `subject`; prefer `let`/`let!`.
- Use `let`/`let!` for setup, not instance variables or `before` for data.
- Order: `let`/`let!`, then `before`/`after`.
- Use `describe` for methods/classes. Use `.` for class methods, `#` for instance methods.
- Use `context` to describe different conditions, starting with 'when', 'with', 'if', 'unless', 'for', 'and', 'but', 'without', or 'otherwise'.
## Example Structure
- One expectation per example (`it`). If an example description contains 'and', it probably contains more than one expectation.
- When an example consists of multiple linked expectations, consider using `have_attributes` or a custom matcher.
- Keep example descriptions short and clear.
- Use `expect` syntax for assertions.
- For change assertions, prefer `expect { ... }.to change { ... }.from(...).to(...)` or `.to(...)`.
- Extract a `context` if an `it` description contains conditions: when, with, if, unless.
## Naming
- Contexts should describe conditions, forming readable sentences with nested blocks.
- Do not use 'should' in example descriptions. Use present tense, third person.
- Be explicit in `describe` blocks about what is being tested.
## Stubbing & Mocks
- Mock/stub only when necessary. Prefer real objects for integration tests.
- Always use `expect(...).to have_received(...)` to verify method calls on double/spy instead of `expect(...).to receive(...)`.
- Stub HTTP requests (e.g., with webmock) instead of hitting real services.
- Use `Timecop` for time, not stubbing `Time.now`.
Вот, как мне кажется, из каких этапов состоит процесс внедрения ИИ и LLM в работу простого советского программиста. Пишу схематично, чтобы не забыть. Если интересно, напишите — разберу каждый этап отдельно со всеми нужными ссылками.
Мне кажется, всё, как в Железном Человеке. Начинаем с бестолковой Алисы, переходим к аугументации, костюму, который тебя делает круче, и наконец, к автономным костюмам-агентам (см. «Протокол семейный праздник» из ЖЧ3).
Прогрессия:
0. Отрицание, гнев, торг, депрессия, принятие. Да, как прежде уже не будет.
1. Время от времени захаживаешь в Дипсик или ЧатГПТ. Просишь что-то посоветовать, объяснить, написать за тебя функцию. Получаешь опыт, учишься формулировать рабочие промпты, снова и снова копипастишь кусочки кода туда-обратно.
2. Задалбываешься копипастить. Добавляешь автокомплит — ставишь GitHub Copilot или Курсор. Автокомплитишь табами, отдаешь LLM бойлерплейт.
3. Покупаешь Курсор. Начинаешь немножечко вайб-кодить через чат. Пишешь и воруешь рулы, делаешь мини-фичи с помощью LLM. Учишься формулировать требования к решению задачи, натыкаешься на ерунду, прокачиваешь рулы. Обжигаешься и понимаешь, что тесты не стоит всегда доверять LLM.
4. Переходишь на программирование агентом. Сетапишь ВПН, покупаешь Claude Code. Программируешь фичи и рефакторишь с помощью разговора с консолью. Делегируешь агенту всю работу с репозиторием от создания и чтению ишьюс до форс-пушей и создания ПРов.
5. Начинаешь использовать чат с LLM для постановки задач другой LLM. Роботы программируют друг друга, а ты таскаешь промпты туда-сюда.
6. Переходишь на флот агентов. Запускаешь несколько инстансов агентов в вебе в Курсоре или локально с помощью Claude Code и git worktree.
7. Прекрасное светлое будущее. События «Мстители: Финал», уходишь в плотники или пивовары.
Так вижу. Пожалуйста, не воспринимайте это как руководство к действию и обязательно проконсультируйтесь с безопасниками, чтобы не получить по жопе за код, слитый в сеть.
Зафиксирую для истории. Сегодня ровно месяц, как я не могу работать в обычном режиме «сел за комп утром, встал вечером, зато без перерыва». Теперь работаю короткими интервалами по 20-25 минут с перерывами по 5 минут. Соответственно, поменялся и формат работы с LLM. Вот, что я поменял:
1. Написал системный промпт для Курсора, который просит LLM сначала сформулировать план решения задачи, утвердить его со мной, выполнить, а затем прогнать тесты и линтеры:
## Development cycle
1. Before writing any code, come up with a good plan, review the plan, and then ask the user for permission to execute the plan.
2. After you have executed the plan, run: `bin/rspec` and `bin/rubocop`
3. If there are any linting errors, run `bin/rubocop -A`
4. To run tests: `bin/rspec <path to file>`
Теперь я ставлю задачу LLM, проверяю и, если нужно, корректирую план, отправляю его на выполнение и иду отдыхать свои 5 минут.
2. Стал писать задачи на русском. Если я правильно понимаю, как работает LLM, то никаких проблем с этим быть не должно. car и «машина» в их векторном пространстве смыслов должны быть совсем рядом.
3. Стал писать задачи по формату. Открываю все нужные файлы, делаю /Add Open Files to Context. Кратко формулирую задачу в один абзац, а затем пишу список (буквально!) требований. Часть требований и технических деталей пишу прямо кодом:
...
С вот такими требованиями:
...
- определяет методы в мейлере, используя `define_method` и `@email_chain.steps`
...
- находит нужную цепочку и пользователя, опираясь на переданный mailing_list_subscription и АПИ EmailChain:
def load_data
@mailing_list_subscription = params[:mailing_list_subscription]
@user = @mailing_list_subscription.user
@email_chain = EmailChain.find_by(mailing_list_subscription: @mailing_list_subscription)
end
4. Стал бить задачи так, чтобы диф изменений, сгенерированный LLM, был не больше 300-400 строк. Так их проще проверить и верифицировать. Если вайб-кожу, конечно, не смотрю на это. Пусть генерит сколько угодно, все равно на выброс.
5. Перестал доверять LLM в написании тестов. LLM — мастера находить самые простые и рабочие пути. Они очень часто пишут тесты, которые либо тестируют ненужное, либо тестируют только «золотой путь», игнорируя все краевые случаи. Ощущение, будто LLM пишут тесты, чтобы они проходили с первого раза, а не для того, чтобы найти побольше багов и задокументировать АПИ.
На прошлой неделе смотрел, как другие ребята пользуются Курсором и ИИ в нем. Заметил, что не все понимают, как получать от него максимум пользы.
Во-первых, первое, что нужно сделать — включить YOLO, он же Enable auto-run mode. Эта фича разрешит агенту запускать код локально. Соответственно, можно будет делать крутейшие штуки:
Запусти эти тесты и поправь тестируемый класс так, чтобы тесты проходили
Напиши тесты для функции foo, которая ... Затем напиши функцию foo и исправь ее, если потребуется, так, чтобы тесты проходили
Во-вторых, в повторяющихся задачах необязательно описывать нужные изменения: переименуй так, сконвертируй в класс, перенеси в метод X, удали старый файл. Можно сделать один раз самому и использовать изменения в качестве примера:
Взгляни на мои изменения в модуле видео для Кинескопа и повтори эти изменения для модулей видео в Вимео, ВК и Ютюбе
Аналогично с помощью примеров можно подсказать, как должна работать новая функция:
Напиши функцию на JS, которая группирует свойства по ключам во вложенные объекты. Покажу на примерах:
{ vkSrc: 'http://vk.com/123' } превращается в { vk: { src: 'http://vk.com/123' } }
{ vimeoId: '123', kinescopeSrc: 'http://ki.io' } превращается в { vimeo: { id: '123' }, kinescope: { src: 'http://ki.io' } }
В-третьих, чат можно использовать как справочник. Выделили нужный фрагмент кода, например, resolitions в package.json и попросили объяснить, как это работает.
В-четвертых, во вкладке Source Control в поле с текстом коммита есть магическая кнопка. Нажмите на нее, чтобы ИИ сгенерировал нормальный коммит-месседж на основе ваших (или не совсем ваших) изменений.
В-пятых, используйте правила для Курсора. Как минимум, возьмите хороший системный промпт:
https://cursor.directory/
Как максимум, понапишите собственных правил для проекта:
https://ghuntley.com/stdlib/
Короче, если вы используете в Курсоре только автокомплит, присмотритесь к YOLO auto-run mode и .cursorrules. И считайте чат не хитрым интерфейсом к Гуглу, а диалогом со стажером, с которым вы программируете в паре.
С уходом Дискорда и непреднамеренным старением я стал записывать замечания со встречи прямо в тот же чат в Телеграме. Так как замечаний много, отвечать на каждое из них — безумство. Любого выбесит 20-30 сообщений «Увидел», «Поправил», «Записал». Вместо этого использую реакции на сообщения с замечаниями. Принцип такой:
👌 — поправил, сделал, починил
✍️ — записал, в бэклог, разберусь позже
👀 — увидел
🤔 — че-т подозрительно, проверим
👍 — супер, одобряю, нормас
🔥 — клево
Если в заметках со встречи все в эмодзи, значит, я все разобрал. Если у сообщения реакции нет, значит, пропустил. Изи, ничего не теряется.

Тут DHH написал пост, да такой, что ПРЯМО В ЧУВСТВА. В нем пара клевых моментов.
Во-первых, Джейсон Фрид на заре Бейскемпа отвечал на 150 писем клиентов в день. Еще раз: сто пятьдесят писем клиентов в день. А мы инбокс разгрести не можем.
Во-вторых, политика «Все работают в техподдержке» помогает фиксить мелкие баги-заусенцы, до которых никогда не доходят руки. А все потому, что программистам проще починить проблему, чем извиниться за то, что она там есть. Думаю, это еще и для эго менее болезненно.
Пост целиком:
https://world.hey.com/dhh/stick-with-the-customer-4942402f (нужен ВПН)
Иногда никак не хочется подходить к задаче. Уже все дела попеределал, книгу почитал, зубы почистил, на балконе прибрался, даже в машину омывайку подлил, а до задачи руки никак не доходят. Прокрастинация на максимум, не те вайбы.
В таких случаях важно не путать дизмораль с трением. В первом случае к задаче не подойти, потому что все уныло, какой в этом смысл, это ни на что не повлияет, уже три года это говно пилим. Во втором случае к задаче не подойти, потому что она реально сложная и непонятно, с какого конца за нее взяться. Соответственно, и решения разные. Дизмораль можно победить действиями вне задачи, ну или просто затащить ее на морально-волевых и дисциплине. А сложную задачу — разбить на пачку задач попроще, которые постепенно рассеивают «туман войны», спрятавший ее решение.
Поэтому первое, что я спрашиваю у себя в понедельник утром, когда чувствую непреодолимое желание пойти подлить омывайки: это дизмораль или трение?
Иногда нам достаются задачи, которые не закрыть в течение одного дня. Скажем, вы исследуете сложный баг, проявляющийся только у 0,0001% пользователей. Или вместе с архитектором пишете спеку нового сервиса, встречаясь каждую неделю на час. Или подбираете и пробуете self-hosted альтернативы Sentry. Или работаете над большой миграцией данных на новую схему.
Вдруг наступает пятница, а за ней выходные. Вы возвращаетесь к работе в понедельник и полчаса вспоминаете, на чем остановились.
В таких случаях стоит оставлять артефакты — заметки, комментарии, вопросы, кусочки кода, сравнительные таблицы, текущие проблемы и проверенные гипотезы. Словом, все, что поможет вернуться к задаче:
Проверии гипотезу с браузерным и системным зумом. В эмуляторе проблемы нет.
Как будем авторизовывать клиентов? Может, пойдем в сторону API Gateway?
Sentry — опенсорсный, но поднимает 6 контейнеров и требует кучу ресурсов. Как вариант — GlitchTip. У него АПИ, совместимый с Sentry и минимум лишнего функционала.
Перенесли все уиды в алиасы. Остается грохнуть колонку и убрать ссылки на mailing_list_uid.
Такие зарубки помогают ничего не забывать, быстрее возвращаться в контекст задачи и двигаться дальше в правильном направлении. Отсюда тупое правило:
Бывает, вижу в проектах именованные сабжекты в качестве хелперов для испытаний:
subject(:process) { described_class.new.process }
before do
allow(EventProducer).to receive(:emit).and_return(true)
end
context "when run twice" do
it "emits only one event" do
process
process
expect(EventProducer).to have_received(:emit).once
end
end
Это плохой, ложноположительный тест. Что бы мы не написали внутри process, EventProducer.emit всегда будет вызываться точно один раз.
Проблема в хелпере subject. Как и let, он кеширующий, мемоизирующий. После первого вызова он запоминает результат блока и в последующие вызовы сразу возвращает результат, не тригеря код в блоке.
Это легко проверить мини-тестом:
class EventProducer
def self.emit
end
end
describe "subject(:process)" do
subject(:process) { EventProducer.emit("foo") }
before { allow(EventProducer).to receive(:emit) }
context "when run twice" do
it "emits both events" do
process
process
expect(EventProducer).to have_received(:emit).twice
end
end
end
Результат:
Failures:
1) subject(:process) when run twice emits both events
Failure/Error: expect(EventProducer).to have_received(:emit).twice
(EventProducer (class)).emit(*(any args))
expected: 2 times with any arguments
received: 1 time with any arguments
# ./spec.rb:17:in `block (3 levels) in <top (required)>'
Пожалуйста, не используйте именованные сабжекты для испытаний. Лучше сделайте старый добрый метод:
def process
described_class.new.process
end
Я сразу родился айти-дедом. Когда появился Руби, я бухтел, что непонятно, куда тело цикла вставлять. Когда появлялись ЦСС-переменные, бухтел, что они нам «не нужоны», достаточно вот так и так сгруппировать селекторы. Бухтел, когда переезжали с Propotype на jQuery, а потом на Реакт. Бухтел, когда все носились с Монгой и Нодой. Бухтел с каждым релизом Рельс. Блин, да я даже из-за синтаксиса хэшей в Руби бухтел: ракеты рулят, джейсон отстой.
Сейчас в программировании все переезжают на AI-, LLM-помощников, которые «помогают» писать код. И вот тут я бухтеть не готов. Уверовал мгновенно, как попробовал Курсор.
В Курсоре по сути два основных помощника. Первый — стандартное автодополнение в процессе набора кода, к нему вопросов нет. Прикольно, но ничего особенного. Второй — чат с редактором. И вот этот режим — бомба, потому что обсуждать и делать можно как отдельные кусочки кода, так и проект целиком.
В какой-то момент я попросил в чате собрать мне консумер, аналогичный уже существующему, но с несколькими ключевыми изменениями. К нему же попросил тесты. Прошло 10 секунд и Курсор предложил мне два файла с кодом, в котором толком нечего было исправлять. Я просто принял изменения и пошел открывать 100% AI-generated Pull Request. И вот этот момент стал определяющим. Не о чем тут бухтеть, будущее уже здесь.
Короче, если вы еще не используете LLM-помощников в разработке, попробуйте Курсор. Просто перетерпите ВС Код и дайте ему неделю:
https://www.cursor.com/
Если вам интересно, как там у них все под капотом, послушайте подкаст с Лексом Фридманом:
Сейчас не хватает сил и внимания на что-то большое и осмысленное, поэтому короткая заметка на бегу.
Часто в удаленной работе не хватает чувства причастности к команде, чувства плеча, вот этого ощущения «да я в команде X». И вроде работаешь месяц, два, три, а до сих пор ощущение, будто, ты одинокий мститель в маске.
По моему опыту это ощущение команды появляется после совместных страданий, трудностей, например, какой-то адухи с пуском. Ребята, совместно преодолевшие весь геморрой, связанный с пуском, превращаются в братство. Они начинают доверять и полагаться друг на друга, потому что знают, что когда в очередной раз shit hits the fan, их коллега не сольется. Думаю, именно на страданиях, трудностях и совместном их преодолении строятся самые сплоченные команды в армии и спорте.
P. S. В августе прочитал всего ничего. Feel Good Productivity — херня. Автомобильная династия — часть про нацисткую Германию интересная, остальное херня. Типа, ничего себе, фантастически богатые еще сильнее богатеют. Неслыханно!
Ситуация: вы тестируете контроллер, который отправляет тестовое письмо со сводкой по вчерашним продажам. Под капотом контроллер обращается к классу DailySummaryEmail и вызывает метод #test. Вы пишете тест:
it "calls DailySummaryEmail#test" do
Это плохое, «машинное» описание проверки. Во-первых, тесты — это примеры использования кода, документация. Это пример чего? Чем он будет полезен читателю?
Во-вторых, это детали реализации, считай, приватный интерфейс. Если переименуем метод или класс, придется поправить и в теле проверки, и в ее описании.
В-третьих, это бесполезные детали. Я из тела проверки вижу, что вызываем DailySummaryEmail#test. Делаем-то это зачем? Чтобы что?
Лучше писать для людей, описывая то, что должно происходить в мире читателя:
it "sends previous day summary email to marketing department"
Год назад вышел подкаст Лекса Фридмана с Хикару Накамурой. Хикару — крутой шахматист-стример, специализирующийся на быстрых шахматах. Когда-то побеждал Магнуса Карлсена.
В подкасте есть интересный эпизод с обсуждением читинга в шахматах:
На 1:15:55 Лекс задает самый важный технический вопрос: сколько информации тебе нужно, чтобы читерить? Накамура отвечает, что ему хватит одного «бжж», если текущая позиция отличная, и двух «бжж», если текущая позиция обычная или нормальная. То есть ему не нужны конкретные ходы, оценки или предсказания. Ему достаточно знать ответ на вопрос: текущая ситуация на доске отличная (для меня) или нет?
Я переформулировал этот вопрос в «это … улучшает ситуацию или нет?» и стал бесконечно спрашивать себя:
Такой странный вопрос помогает мне оценивать последствия решений и изменений в долгосрочной перспективе. Помогает взглянуть на проблему в контексте времени и позиции, добавляет еще одну точку зрения и заставляет стремиться к улучшению «позиции». Спасибо, Хикару!