Зачем агенту писать код для самого себя?
Представьте агента, который не просто выполняет задачи, а рефлексирует. Смотрит на свои ошибки, лезет в свой исходный код и патчит его прямо на лету. Звучит как сценарий для фантастического хоррора, но это именно то, что мы построим. Проблема классических автономных агентов в их статичности. Они заперты в том коде, который вы для них написали. Ошибка в логике или новое условие — и вам нужно лезть в промпты, пересобирать, переобучать. Скучно. Наш эксперимент — Truman Show для ИИ. Агент живет в искусственной среде (изолированный Rust проект), думает, что развивается сам, но на самом деле мы задали правила игры. Игра, в которой его главная цель — стать лучше, редактируя сам себя.
Это не просто туториал по вызову API ChatGPT. Мы строим полноценную архитектуру на Rust 1.78 (актуально на март 2026) с циклами обратной связи, файловой системой и механизмом самоисправления. Будет сложно, но чертовски интересно.
Архитектура «Трумана»: из чего он сделан
Наш агент — не монолит. Он состоит из четырех изолированных модулей, которые общаются через четкие каналы. Почему именно так? Потому что если дать нейросети прямой доступ к исполняемой памяти, она сломает все за пять секунд. Мы строим безопасную песочницу.
| Модуль | Задача | Технологии (2026) |
|---|---|---|
| Мозг (Cortex) | Анализ задач, планирование, принятие решений | OpenAI GPT-4o API или локальная модель Llama 3.3 через llm-chain |
| Память (Hippocampus) | Хранение журнала действий, ошибок, успешных патчей | SQLite + векторное хранилище Qdrant для семантического поиска по опыту |
| Исполнитель (Executor) | Безопасный запуск сгенерированного кода, работа с файлами | Docker-изоляция или ptrace-сандбокс на Rust (crate seccomp) |
| Наблюдатель (Overseer) | Мониторинг состояния, перезапуск модулей, эскалация сбоев | Tokio-стримы и каналы для межпроцессного взаимодействия |
Ключевая фишка — все модули пишут логи в общее хранилище. «Мозг» анализирует эти логи, ищет закономерности провалов и генерирует код для исправления. Не уверен, что ваш агент помнит все инструкции? Вот тут пригодится техника из гайда по Agent Skills, где мы разбирали, как заставить агента не терять контекст.
1 Подготовка поля: настраиваем Rust и крейты
Откройте терминал. Убедитесь, что у вас Rust 1.78 или новее. Если нет — обновитесь. Старые версии не имеют стабильной поддержки ключевых асинхронных функций, которые нам понадобятся.
rustup update stable
rustc --version # Должно быть 1.78.0 или выше
Создаем новый проект. Назовем его truman_agent.
cargo new truman_agent --bin
cd truman_agent
Редактируем Cargo.toml. Добавляем зависимости. Мы не будем использовать устаревшие обертки для OpenAI — берем актуальный async-openai с поддержкой GPT-4o.
[package]
name = "truman_agent"
version = "0.1.0"
edition = "2021"
[dependencies]
async-openai = { version = "0.22", features = ["default"] }
tokio = { version = "1.40", features = ["full"] }
sqlx = { version = "0.8", features = ["runtime-tokio-rustls", "sqlite"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
thiserror = "1.0"
anyhow = "1.0"
# Для безопасного исполнения кода рассмотрите `seccomp` или `libseccomp`
# seccomp = "0.1"
llm-chain, но это усложнит настройку. Для эксперимента оставим облачный API — так быстрее. Если нужна полная автономность, посмотрите статью про Agent Browser Workspace как пример локальной инфраструктуры.2 Костяк агента: пишем модуль Cortex (Мозг)
Мозг — это просто умный клиент к LLM. Но не просто прокси, а структура, которая формирует промпт на основе истории из памяти. Создаем файл src/cortex.rs.
use async_openai::{Client, types::{CreateChatCompletionRequest, ChatCompletionRequestMessage, Role}};
use serde_json::Value;
use anyhow::Result;
pub struct Cortex {
client: Client,
model: String, // Например, "gpt-4o"
}
impl Cortex {
pub fn new(api_key: String, model: String) -> Self {
let client = Client::new().with_api_key(api_key);
Self { client, model }
}
pub async fn analyze_and_plan(&self, task_description: &str, memory_context: Vec) -> Result {
// Формируем контекст из памяти
let memory_str = memory_context.iter()
.map(|v| v.to_string())
.collect::>()
.join("\n");
let system_prompt = "Ты — ядро саморазвивающегося агента. Твоя задача — анализировать ошибки из логов и генерировать исправления в виде кода на Rust. Ты можешь модифицировать только файлы в директории 'src/'. Твой ответ должен быть строго в формате JSON: { 'analysis': 'анализ проблемы', 'code_change': 'патч в формате diff или новый код', 'target_file': 'src/...' }".to_string();
let user_content = format!("Задача: {}\nКонтекст из памяти:\n{}", task_description, memory_str);
let request = CreateChatCompletionRequest {
model: self.model.clone(),
messages: vec![
ChatCompletionRequestMessage {
role: Role::System,
content: system_prompt,
name: None,
},
ChatCompletionRequestMessage {
role: Role::User,
content: user_content,
name: None,
}
],
..Default::default()
};
let response = self.client.chat().create(request).await?;
let content = response.choices[0].message.content.clone().unwrap_or_default();
Ok(content)
}
}
Обратите внимание: мы заставляем LLM возвращать JSON. Это критично для парсинга. В реальном проекте нужно добавить валидацию и обработку ошибок, когда модель вдруг решит написать стихи вместо диффа.
3 Память, которая учится: модуль Hippocampus
Память — это не только логи. Это векторные эмбеддинги успешных действий, чтобы искать похожие ситуации. Для простоты начнем с SQLite. Создаем src/memory.rs.
use sqlx::{sqlite::SqlitePool, Row};
use chrono::Utc;
pub struct Memory {
pool: SqlitePool,
}
impl Memory {
pub async fn new(db_url: &str) -> Result {
let pool = SqlitePool::connect(db_url).await?;
// Создаем таблицу, если её нет
sqlx::query(
r#"
CREATE TABLE IF NOT EXISTS agent_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp TEXT NOT NULL,
module TEXT NOT NULL,
event_type TEXT NOT NULL, -- 'error', 'success', 'code_change'
description TEXT NOT NULL,
raw_data TEXT
)"#
).execute(&pool).await?;
Ok(Self { pool })
}
pub async fn store_event(&self, module: &str, event_type: &str, description: &str, raw_data: Option) -> Result<()> {
let timestamp = Utc::now().to_rfc3339();
sqlx::query(
"INSERT INTO agent_logs (timestamp, module, event_type, description, raw_data) VALUES (?, ?, ?, ?, ?)"
)
.bind(timestamp)
.bind(module)
.bind(event_type)
.bind(description)
.bind(raw_data)
.execute(&self.pool)
.await?;
Ok(())
}
pub async fn get_recent_errors(&self, limit: i64) -> Result> {
let rows = sqlx::query(
"SELECT description FROM agent_logs WHERE event_type = 'error' ORDER BY timestamp DESC LIMIT ?"
)
.bind(limit)
.fetch_all(&self.pool)
.await?;
Ok(rows.iter().map(|r| r.get(0)).collect())
}
}
Это база. Позже можно подключить Qdrant или другую векторную БД для семантического поиска. Если интересно, как агенты учатся на изменениях среды без явной награды, у нас есть статья «Обучение с подкреплением без награды» — принципы оттуда можно применить для улучшения памяти.
4 Самый опасный модуль: Executor (Исполнитель)
Здесь нужно быть параноиком. Исполнение произвольного кода, сгенерированного ИИ, — это прямой путь к уничтожению вашей файловой системы. Мы используем максимальную изоляцию. Для демо подойдет простой подход с запуском в отдельном процессе и проверкой вывода.
use tokio::process::Command;
use std::path::Path;
pub struct Executor {
workspace_path: String,
}
impl Executor {
pub fn new(workspace_path: &str) -> Self {
Self { workspace_path: workspace_path.to_string() }
}
pub async fn apply_patch(&self, patch_content: &str, target_file: &str) -> Result {
// В реальности здесь нужно применить diff (например, с помощью библиотеки `diff`)
// Для простоты предположим, что patch_content — это новый полный файл
let full_path = Path::new(&self.workspace_path).join(target_file);
tokio::fs::write(&full_path, patch_content).await?;
// После записи файла перекомпилируем проект, чтобы проверить, нет ли ошибок
let output = Command::new("cargo")
.args(["check", "--quiet"])
.current_dir(&self.workspace_path)
.output().await?;
if output.status.success() {
Ok("Patch applied and compilation successful".to_string())
} else {
let stderr = String::from_utf8_lossy(&output.stderr);
Err(anyhow::anyhow!("Compilation failed after patch: {}", stderr))
}
}
}
Это учебный пример. В продакшене вы должны использовать Docker или более строгие сандбоксы (seccomp-bpf). Никогда не запускайте код, сгенерированный ИИ, на хосте с правами root. Никогда.
5 Собираем цикл обратной связи в main.rs
Теперь свяжем все компоненты в главном цикле. Алгоритм такой: 1) Читаем последние ошибки из памяти. 2) Отправляем их в Cortex для анализа. 3) Получаем патч. 4) Применяем через Executor. 5) Результат пишем в память. 6) Повторяем.
#[tokio::main]
async fn main() -> Result<()> {
// Инициализация компонентов
let cortex = Cortex::new(std::env::var("OPENAI_API_KEY").unwrap(), "gpt-4o".to_string());
let memory = Memory::new("sqlite:truman_memory.db").await?;
let executor = Executor::new(".");
// Главный цикл
loop {
tokio::time::sleep(tokio::time::Duration::from_secs(30)).await; // Проверка каждые 30 секунд
// Получаем последние ошибки
let errors = memory.get_recent_errors(5).await?;
if errors.is_empty() {
continue;
}
let task_desc = format!("Произошли ошибки: {}", errors.join("; "));
// Получаем контекст из памяти (здесь можно добавить векторный поиск)
let context = vec![]; // Заглушка
// Анализируем и получаем план от Cortex
let response = cortex.analyze_and_plan(&task_desc, context).await?;
let parsed: Value = serde_json::from_str(&response)?;
let code_change = parsed["code_change"].as_str().unwrap();
let target_file = parsed["target_file"].as_str().unwrap();
// Пытаемся применить патч
match executor.apply_patch(code_change, target_file).await {
Ok(success_msg) => {
memory.store_event(
"executor",
"success",
&format!("Successfully patched {}: {}", target_file, success_msg),
Some(response)
).await?;
}
Err(e) => {
memory.store_event(
"executor",
"error",
&format!("Failed to apply patch to {}: {}", target_file, e),
Some(response)
).await?;
}
}
}
}
Цикл запущен. Агент теперь каждые 30 секунд проверяет логи на ошибки и пытается их исправить, модифицируя исходный код. Вы наблюдаете за тем, как система медленно эволюционирует, иногда ломаясь, но часто исправляя сама себя. Это и есть Truman Show: агент не знает, что его среда (исходники) — это и есть весь его мир, который он может менять.
Где этот эксперимент ломается: нюансы и грабли
- LLM галлюцинирует с кодом. GPT-4o хорош, но не идеален. Он может сгенерировать неработающий дифф, который сломает компиляцию. Нужно добавлять валидацию: запускать `cargo check` или даже юнит-тесты перед применением.
- Безопасность — это всё. Executor должен работать в изолированном Docker-контейнере с доступом только к определенной директории. Рассмотрите использование архитектуры, как в QA-агенте, где есть четкое разделение среды исполнения.
- Циклические исправления. Агент может попасть в петлю: ошибка A → патч X → ошибка B → патч, возвращающий код в исходное состояние. Нужно детектировать циклы по хешам изменений и останавливаться.
- Дорого. Каждый вызов к GPT-4o стоит денег. Лимитируйте запросы и используйте локальные модели для чернового анализа. Об этом подробно в статье про архитектуру без роутинга.
Что дальше? Прогноз от практика
К 2027 году такие эксперименты перестанут быть диковинкой. Мы увидим фреймворки для создания self-evolving агентов как сервис. Но главный прорыв будет не в этом. Ключевая битва развернется вокруг цифровых сред для обучения — сложных симуляций, где агенты смогут безопасно экспериментировать с реальным миром. Это не про RL в Atari, а про симуляции облачных инфраструктур, финансовых рынков, даже целых компаний. Если хотите быть на острие, следите не за новыми моделями, а за новыми средами. Как раз об этом мы писали в материале «RL-среды: почему будущее ИИ — в цифровых «классах».
Ваш агент на Rust — это первый шаг. Запустите его, дайте доступ к небольшой, хорошо ограниченной кодовой базе (например, к утилитам для парсинга логов) и наблюдайте. Первые изменения будут примитивными: исправление опечаток, улучшение форматирования. Но через несколько сотен итераций он может начать рефакторить код, добавлять кэширование, оптимизировать алгоритмы. Главное — не выпускать его в продакшн без присмотра. Пока.