AI-агент для кодинга своими руками: кратко о главном
🎯 О чём этот конспект: Детальный практический разбор внутренней архитектуры больших языковых моделей (LLM), инференс-движков и пошаговое создание собственного автономного CLI-агента для кодинга на чистом Node.js без сторонних библиотек. Разбираются принципы работы stateless-моделей, потоковой генерации токенов, сохранения сессионной памяти в JSONL и организация агентного цикла с вызовом инструментов (Tool Calling).
👤 Кому будет полезно: Вайбкодерам, fullstack-разработчикам и AI-инженерам, которые хотят перестать воспринимать AI-инструменты как «чёрный ящик» и научиться создавать кастомные агентские системы, обвязки (harnesses) и CLI-ассистентов под свои задачи.
✨ Что получите: Глубокое понимание механики работы LLM и полностью рабочий, расширяемый терминальный AI-агент, способный самостоятельно читать файлы проекта, модифицировать код, создавать новые модули и улучшать собственную кодовую базу.
1. Архитектура LLM и инференс: почему нейросеть — это не программа
Контекст: Большинство пользователей воспринимают ChatGPT или Claude как умные программы, способные мыслить и помнить контекст. На самом деле LLM (Large Language Model) — это статический файл с миллиардами числовых весов (матриц), полученных в результате обучения стоимостью в сотни миллионов долларов. Сама по себе модель не может выполнять код, обращаться к сети или сохранять данные. Для её работы необходим инференс-движок (Inference Engine / Runtime) — специальная программа (например, Ollama, llama.cpp, vLLM или закрытые серверные движки OpenAI/Anthropic), которая загружает матрицу весов в память, выполняет токенизацию входящего текста и рассчитывает вероятности появления следующего токена.
Тайминг: , ,
Выгода: Понимание математической природы LLM позволяет избавиться от ложных ожиданий («модель обиделась / не захотела»), правильно настраивать параметр температуры (temperature) и эффективно управлять расходом токенов в контекстном окне.
Как применить:
- Шаг 1: Понимание пошаговой генерации токенов — Инференс-движок разбивает входящую строку на токены (слова или части слов) с уникальными ID через токенизатор, прогоняет их через веса модели за один проход и выдает ровно один следующий токен на основе распределения вероятностей.
- Шаг 2: Механика стриминга — Сгенерированный токен добавляется обратно в конец контекста, и весь цикл повторяется заново. Пословный ответ в интерфейсе — это не имитация живой речи, а фундаментальное техническое ограничение авторегрессионного инференса.
- Шаг 3: Управление температурой — Параметр
temperatureопределяет выборку: при значении0движок строго выбирает токен с максимальной вероятностью (детерминированный ответ для кода), при повышении температуры возрастает вероятность выбора менее очевидных вариантов (творческий режим).
Цикл генерации инференс-движка:
1. [Входной промпт] -> Токенизатор -> Массив ID [12, 458, 90]
2. Массив ID -> Веса LLM -> Расчет вероятностей следующего токена
3. Выбор токена (на базе Temperature) -> Токен: "Hello"
4. Токен "Hello" отправляется клиенту (стриминг) И добавляется во входящий контекст
5. Повтор цикла до генерации стоп-последовательности (специальный байт конца генерации)Результат: Чёткое понимание того, почему контекст постоянно растёт в объёме, почему каждый новый ответ стоит дороже предыдущего и как инференс формирует посимвольный вывод.
2. Иллюзия памяти и роль продуктового каркаса (Harness)
Контекст: LLM абсолютно бессердечны и лишены долговременной памяти (stateless) — они не помнят ничего между отдельными HTTP-запросами. Иллюзия связного диалога в ChatGPT или Claude создается продуктовой обвязкой (harness): клиентское приложение перед каждым запросом извлекает всю историю сообщений из базы данных и передает её заново в модель как единый раздувающийся массив контекста. По мере увеличения истории фокус внимания модели размывается, что приводит к деградации логики и галлюцинациям.
Тайминг: , ,
Выгода: Предотвращение раздувания контекста позволяет экономить до 70% затрат на API-токены и удерживать высокую точность генерации за счет регулярной очистки сессий или суммаризации.
Как применить:
- Шаг 1: Организация клиент-серверного диалога — При каждом взаимодействии бэкенд сохраняет сообщение пользователя в БД, достает массив всех предыдущих реплик сессии и целиком отправляет его в API.
- Шаг 2: Управление длиной контекста — При достижении лимита токенов применяйте механизм
context compaction(сжатие истории через суммаризацию ключевых фактов) либо принудительно открывайте новую сессию под новую задачу. - Шаг 3: Формирование каркаса безопасности (Harness) — Разрабатывайте внешнюю программную обвязку вокруг LLM, которая перехватывает бесконечные циклы вызовов, валидирует входящие аргументы и запрещает деструктивные системные команды.
Результат: Архитектурно выверенная схема хранения состояния диалога на стороне приложения, исключающая зависимость от несуществующей «памяти» нейросети.
3. Принцип работы Tool Calling (Function Calling)
Контекст: Большая языковая модель сама по себе не имеет доступа в интернет, не может прочитать файл на жестком диске или выполнить команду в терминале. Взаимодействие с внешним миром строится исключительно через протокол вызова инструментов: разработчик передает в модель текстовое описание доступных функций (метаданные с JSON-схемой аргументов). Если модель на основе контекста решает, что для ответа нужно действие, она возвращает не текст для пользователя, а специальную структуру с именем функции и сгенерированными аргументами, требуя от бэкенда выполнить ее на своей стороне.
Тайминг: , ,
Выгода: Возможность превратить текстовую LLM в полноценного автономного AI-агента, способного компилировать программы, работать с базой данных, вызывать внешние REST API и управлять файловой системой.
Как применить:
- Шаг 1: Описание метаданных инструмента — Передавайте в параметре
toolsдекларативную JSON-схему с описанием того, что делает функция, какие параметры являются обязательными и какие типы данных ожидаются. - Шаг 2: Перехват намерения модели (Tool Call) — Если ответ модели содержит
tool_callsвместо текстового ответа, бэкенд приостанавливает диалог с пользователем и направляет вызов в локальный обработчик. - Шаг 3: Возврат результата работы инструмента — Бэкенд выполняет локальный код (например, чтение файла с диска), упаковывает результат в объект с ролью
tool(илиfunction_call_output) с указаниемcall_idи отправляет обновленный контекст обратно в модель.
Схема Tool Calling:
Пользователь: "Сколько жителей в Минске? Сохрани в PDF"
1. Запрос в LLM: [Сообщение пользователя] + [Инструменты: google_search, create_pdf]
2. Ответ LLM: Tool Call -> google_search(query: "население Минска")
3. Бэкенд: Выполняет поиск -> Получает: "2 млн человек"
4. Запрос в LLM: [История] + [Результат поиска: "2 млн"] + [Инструменты]
5. Ответ LLM: Tool Call -> create_pdf(filename: "minsk.pdf", content: "Население: 2 млн")
6. Бэкенд: Генерирует PDF -> Получает статус: "Файл minsk.pdf сохранен"
7. Запрос в LLM: [История] + [Результат генерации PDF]
8. Ответ LLM: Текстовый ответ -> "Население Минска составляет 2 млн человек. Файл minsk.pdf успешно создан."Результат: Полная автономность агентской системы, при которой LLM выступает в роли диспетчера решений, а исполняющая среда берет на себя выполнение реальных системных операций.
4. Инициализация проекта и построение CLI-интерфейса на Node.js
Контекст: Полноценный агент для кодинга в стиле Claude Code или Cursor CLI должен работать непосредственно в терминале разработчика. Для этого не требуются тяжелые фреймворки вроде LangChain или LlamaIndex — стандартная библиотека Node.js содержит все необходимые низкоуровневые инструменты: потоки ввода/вывода process.stdin, process.stdout и модуль readline для интерактивного взаимодействия, истории команд и навигации стрелками.
Тайминг: , ,
Выгода: Нулевое количество внешних npm-зависимостей (node_modules весит 0 байт), мгновенный запуск, полный контроль над потоками данных и возможность запуска в любых окружениях (включая легковесные Docker-контейнеры и CI/CD).
Как применить:
- Шаг 1: Инициализация окружения — Создайте проект с поддержкой ECMAScript Modules (ESM) и настройте базовые скрипты запуска с передачей переменных окружения.
mkdir in-engine && cd in-engine
git init
npm init -yВ файле package.json настройте тип модуля и скрипты:
{
"name": "in-engine",
"version": "1.0.0",
"type": "module",
"scripts": {
"start": "node --env-file=.env index.js",
"debug": "node --inspect --env-file=.env index.js"
}
}Создайте .gitignore и файл переменных .env:
echo "node_modules\n.env\nsessions\n" > .gitignore
echo "OPENAI_API_KEY=your_openai_api_key_here" > .env- Шаг 2: Реализация интерактивного терминального ввода через Readline — Создайте входной файл
index.js, используя встроенный модульnode:readlineи утилиту стилизации текстаnode:util:
// index.js
import readline from 'node:readline';
import { styleText } from 'node:util';
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
prompt: styleText('green', 'in-engine > ')
});
rl.prompt();
rl.on('line', async (line) => {
const input = line.trim();
if (!input) {
rl.prompt();
return;
}
if (input === 'exit' || input === 'quit') {
process.exit(0);
}
// Эхо-вывод для проверки терминального потока
process.stdout.write(`Вы ввели: ${input}\n`);
rl.prompt();
});Результат: Быстрый терминальный интерфейс с поддержкой истории предыдущих команд (стрелки вверх/вниз), редактирования строки и кастомного цветного промпта без сторонних библиотек.
5. Архитектурное разделение: изоляция AI-адаптера и интеграция с OpenAI API
Контекст: Чистая архитектура программного обеспечения требует четкого разделения ответственности (Separation of Concerns). Слой представления (CLI/Terminal) не должен знать о структуре HTTP-запросов к OpenAI, а бизнес-логика агента не должна быть жестко завязана на конкретного провайдера. Создание изолированного слоя доступа к данным (AI Adapter) позволяет инкапсулировать работу с сетью, обработку ошибок API, авторизацию и парсинг специфичных для провайдера структур данных.
Тайминг: , ,
Выгода: Возможность в любой момент заменить OpenAI на локальную Ollama, Anthropic Claude или DeepSeek, изменив только один файл адаптера и не трогая логику самого агента.
Как применить:
- Шаг 1: Создание модуля AI-адаптера — Создайте файл
ai-adapter.js, реализующий отправку HTTP-запросов через нативныйfetchбез SDK.
// ai-adapter.js
const API_URL = 'https://api.openai.com/v1/chat/completions';
const MODEL = 'gpt-4o-mini'; // или gpt-4o / o3-mini
export const aiAdapter = {
async ask(messages, tools = []) {
const apiKey = process.env.OPENAI_API_KEY;
if (!apiKey) {
throw new Error('OPENAI_API_KEY не задан в переменных окружения');
}
const payload = {
model: MODEL,
messages: messages
};
if (tools.length > 0) {
payload.tools = tools;
}
const response = await fetch(API_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`
},
body: JSON.stringify(payload)
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`OpenAI API error [${response.status}]: ${errorText}`);
}
const data = await response.json();
const choice = data.choices[0];
const message = choice.message;
return {
content: message.content || '',
toolCalls: message.tool_calls || [],
rawMessage: message
};
}
};- Шаг 2: Безопасное управление ключами API — При генерации API-токенов в кабинете OpenAI всегда задавайте ограничение срока действия (например, 1 день при стриминге/записи видео) и устанавливайте минимальные скоупы разрешений (
Restricted: Model capabilities only), чтобы предотвратить финансовые потери в случае утечки.
Результат: Изолированный сетевой шлюз, принимающий унифицированные массивы сообщений/инструментов и возвращающий нормализованный ответ с текстом и вызовами функций.
6. Реализация сессионной памяти в JSONL-хранилище
Контекст: Для сохранения контекста между запусками и ведения непрерывного диалога агенту необходимо персистентное хранилище. Использование классических массивов в едином JSON-файле неэффективно: для добавления одной реплики приходится считывать и перезаписывать весь многомегабайтный файл целиком. Промышленным стандартом для хранения истории переписки и стриминговых логов является формат JSONL (JSON Lines), где каждая новая запись представляет собой валидную JSON-строку, добавляемую в конец файла в режиме append.
Тайминг: , ,
Выгода: Мгновенная дозапись сообщений в файл с нулевыми накладными расходами памяти, устойчивость к падениям процесса и готовый фундамент для восстановления любых предыдущих сессий.
Как применить:
- Шаг 1: Создание репозитория памяти — Создайте файл
memory-repository.js, объединяющий работу с быстрым состоянием в оперативной памяти (RAM) и сохранением в файловую систему.
// memory-repository.js
import fs from 'node:fs';
import path from 'node:path';
export class MemoryRepository {
#memory = [];
#sessionFile = null;
constructor() {
const sessionsDir = path.resolve('sessions');
if (!fs.existsSync(sessionsDir)) {
fs.mkdirSync(sessionsDir, { recursive: true });
}
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
this.#sessionFile = path.join(sessionsDir, `session-${timestamp}.jsonl`);
}
save(messageOrMessages) {
const items = Array.isArray(messageOrMessages) ? messageOrMessages : [messageOrMessages];
for (const item of items) {
this.#memory.push(item);
const line = JSON.stringify(item) + '\n';
fs.appendFileSync(this.#sessionFile, line, 'utf-8');
}
}
getAll() {
return [...this.#memory];
}
}- Шаг 2: Структурирование сообщений в сессии — Каждая строчка в JSONL-файле хранит законченный объект реплики пользователя, ассистента или результат вызова системной функции:
{"role":"user","content":"Привет, меня зовут Дмитрий"}
{"role":"assistant","content":"Привет, Дмитрий! Рад знакомству."}
{"role":"user","content":"Создай файл readme.md"}Результат: Надежное хранилище диалогов, которое сохраняет историю сессии в папку sessions/ в режиме реального времени и предоставляет быстрый доступ к полному массиву контекста из оперативной памяти.
7. Создание реестра инструментов (Tools Registry) для работы с файлами
Контекст: Чтобы агент мог модифицировать кодовую базу, ему необходимы инструменты прямого взаимодействия с файловой системой. Для этого создается реестр инструментов на базе паттерна Dispatch Table (таблица диспетчеризации / команда). Каждый инструмент содержит две части: meta (строгое JSON-описание интерфейса функции для языковой модели) и метод execute (непосредственная JavaScript-функция, исполняемая средой Node.js).
Тайминг: , ,
Выгода: Модульное добавление любых новых возможностей (запуск bash-команд, git commit, поиск по коду через grep, вызовы curl) за 2 минуты без изменения логики ядра агента.
Как применить:
- Шаг 1: Создание реестра инструментов — Создайте файл
tools-registry.jsс реализацией базовых операций чтения и записи файлов:
// tools-registry.js
import fs from 'node:fs';
import path from 'node:path';
export const toolsRegistry = {
read_file: {
meta: {
type: 'function',
function: {
name: 'read_file',
description: 'Считывает полное содержимое текстового файла по указанному относительному пути.',
parameters: {
type: 'object',
properties: {
path: {
type: 'string',
description: 'Относительный путь к файлу в рабочей директории проекта'
}
},
required: ['path'],
additionalProperties: false
},
strict: true
}
},
async execute({ path: filePath }) {
const resolvedPath = path.resolve(filePath);
if (!fs.existsSync(resolvedPath)) {
return `Ошибка: файл по пути "${filePath}" не найден.`;
}
return fs.readFileSync(resolvedPath, 'utf-8');
}
},
write_file: {
meta: {
type: 'function',
function: {
name: 'write_file',
description: 'Создает новый файл или полностью перезаписывает существующий с переданным содержимым.',
parameters: {
type: 'object',
properties: {
path: {
type: 'string',
description: 'Относительный путь к файлу для записи'
},
content: {
type: 'string',
description: 'Полный исходный код или текст, который необходимо записать в файл'
}
},
required: ['path', 'content'],
additionalProperties: false
},
strict: true
}
},
async execute({ path: filePath, content }) {
const resolvedPath = path.resolve(filePath);
const dir = path.dirname(resolvedPath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
fs.writeFileSync(resolvedPath, content, 'utf-8');
return `Успех: файл "${filePath}" успешно записан (${content.length} байт).`;
}
}
};Результат: Декларативно описанный набор безопасных инструментов, полностью готовый к передаче в OpenAI API и синхронному выполнению на локальной машине разработчика.
8. Оркестрация агентского цикла (Agentic Loop) и запуск кодинг-агента
Контекст: Сердце любой агентской системы (Claude Code, Devin, Cursor Agent) — это агентский цикл (Agentic Loop). Когда пользователь отдает команду (например, «перенеси приветствие в отдельный файл и подключи его в index.js»), модель не может выполнить это за один шаг. Агент должен войти в цикл: сначала запросить чтение index.js, получить файл, проанализировать его, сгенерировать вызов создания нового файла welcome.js, затем сгенерировать вызов перезаписи index.js, и только после подтверждения успешности всех операций выдать финальный отчет пользователю.
Тайминг: , ,
Выгода: Полная автономность в решении комплексных инженерных задач: агент сам находит файлы, читает код, рефакторит модули и вносит изменения в кодовую базу в автоматическом режиме.
Как применить:
- Шаг 1: Сборка класса Agent — Создайте файл
agent.js, объединяющий память, адаптер и реестр инструментов в единый управляемый цикл с лимитом итераций:
// agent.js
import { aiAdapter } from './ai-adapter.js';
import { MemoryRepository } from './memory-repository.js';
import { toolsRegistry } from './tools-registry.js';
export class Agent {
#memory = new MemoryRepository();
#maxIterations = 10;
async run(userPrompt) {
// 1. Сохраняем пользовательский промпт в память
this.#memory.save({
role: 'user',
content: userPrompt
});
// 2. Извлекаем мета-описания всех зарегистрированных инструментов
const toolsMeta = Object.values(toolsRegistry).map(t => t.meta);
// 3. Запускаем агентский цикл
for (let i = 0; i < this.#maxIterations; i++) {
const messages = this.#memory.getAll();
const response = await aiAdapter.ask(messages, toolsMeta);
// Сохраняем сырой ответ ассистента (включая tool_calls) в историю
this.#memory.save(response.rawMessage);
// Если модель не запросила вызов инструментов — возвращаем финальный ответ
if (!response.toolCalls || response.toolCalls.length === 0) {
return response.content;
}
// Если есть запросы на вызов инструментов — выполняем каждый по очереди
for (const toolCall of response.toolCalls) {
const toolName = toolCall.function.name;
const callId = toolCall.id;
let toolArgs = {};
try {
toolArgs = JSON.parse(toolCall.function.arguments);
} catch (e) {
toolArgs = {};
}
const tool = toolsRegistry[toolName];
let toolResult = '';
if (tool) {
try {
toolResult = await tool.execute(toolArgs);
} catch (err) {
toolResult = `Ошибка выполнения инструмента ${toolName}: ${err.message}`;
}
} else {
toolResult = `Ошибка: инструмент "${toolName}" не существует в реестре.`;
}
// Сохраняем результат работы функции в память с ролью tool
this.#memory.save({
role: 'tool',
tool_call_id: callId,
content: String(toolResult)
});
}
}
return 'Превышен лимит итераций агента. Задача остановлена во избежание зацикливания.';
}
}- Шаг 2: Финализация входной точки приложения — Обновите
index.js, связав CLI со сборкой агента:
// index.js
import readline from 'node:readline';
import { styleText } from 'node:util';
import { Agent } from './agent.js';
const agent = new Agent();
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
prompt: styleText('green', 'in-engine > ')
});
console.log(styleText('cyan', '=== In-Engine Coding Agent запущен ==='));
rl.prompt();
rl.on('line', async (line) => {
const input = line.trim();
if (!input) {
rl.prompt();
return;
}
if (input === 'exit' || input === 'quit') {
process.exit(0);
}
try {
const answer = await agent.run(input);
process.stdout.write(`\n${styleText('yellow', answer)}\n\n`);
} catch (error) {
process.stderr.write(`\nОшибка: ${error.message}\n\n`);
}
rl.prompt();
});- Шаг 3: Тестирование автономной работы и рекурсивного самоизменения — Запустите агента и отдайте сложную составную инструкцию:
npm run startin-engine > Создай файл greetings.js, в котором экспортируй функцию getGreeting(name), возвращающую строку с приветствием. Затем создай файл test-greet.js, импортируй туда эту функцию и выполни ее с именем "VibeCoder".Результат: Агент автоматически инициирует цепочку вызовов write_file, создаст необходимые файлы в директории проекта и выведет отчет об успешном завершении задачи.
AI-агент для кодинга своими руками: ответы на частые вопросы
В: Чем наш созданный агент принципиально отличается от Claude Code, Cursor или OpenDevin?
О: Фундаментальная архитектура (CLI + Agentic Loop + Tool Calling + Сессионная память) абсолютно идентична. Коммерческие продукты отличаются более широким набором встроенных инструментов (выполнение bash-команд, linter, семантический поиск по AST/эмбеддингам), продвинутыми алгоритмами сжатия контекста (Sub-agents) и сложными системными промптами с защитными механизмами.
В: Почему модель иногда пытается вызвать функцию с неправильными параметрами?
О: Это происходит из-за неточного или двусмысленного описания в поле description объекта meta в toolsRegistry. Чтобы модель не ошибалась, всегда используйте точные описания параметров, указывайте "strict": true и заполняйте массив required всеми обязательными полями.
В: Зачем сохранять в историю промежуточные сообщения вызова инструментов (tool_calls), если пользователю нужен только итоговый код?
О: LLM работает по принципу авторегрессионного восстановления цепочки рассуждений. Если передать результат выполнения функции с ролью tool, не передав перед этим сообщение ассистента с tool_calls и соответствующим call_id, API OpenAI вернет критическую ошибку валидации контекста.
В: Как добавить агенту возможность выполнять команды в терминале (Bash / Terminal)?
О: Достаточно добавить в toolsRegistry новый инструмент run_bash_command с вызовом стандартного метода Node.js child_process.execSync. Однако для безопасности критически важно настроить подтверждение от пользователя (Human-in-the-loop) перед выполнением опасных команд вроде rm -rf или git push.
В: Почему контекст в сессии нельзя раздувать бесконечно, если современные модели поддерживают 128k–1M токенов?
О: Чем длиннее контекст, тем дороже обходится каждый следующий запрос (вы платите за все предыдущие токены на каждом шаге цикла) и тем выше вероятность деградации внимания модели («Lost in the Middle»), когда агент забывает первоначальные системные инструкции и начинает совершать логические ошибки в коде.
В: Что такое протокол MCP (Model Context Protocol) и как он соотносится с нашей архитектурой?
О: MCP — это стандартизированный протокол подключения инструментов и источников данных к LLM через stdin/stdout или HTTP/SSE. Наш реестр toolsRegistry можно обернуть в MCP-клиент, что позволит агенту подключать готовые серверы инструментов от Anthropic, GitHub, PostgreSQL и браузерной автоматизации без переписывания локального кода.
AI-агент для кодинга своими руками: инструменты и ссылки из видео
- OpenAI API Platform — Платформа для управления API-ключами, лимитами моделей и мониторинга затрат на инференс —
https://platform.openai.com - Node.js Readline Documentation — Официальная документация встроенного модуля управления потоками ввода/вывода в CLI —
https://nodejs.org/api/readline.html - Claude Code — Терминальный агент для разработки от компании Anthropic — упомянут в видео
- Cursor IDE — Популярный AI-редактор кода с интегрированными агентскими возможностями — упомянут в видео
- Ollama — Локальный инференс-движок для запуска open-source моделей (Llama, DeepSeek, Qwen) — упомянут в видео
Конспект создан на основе видео «Как устроен Claude Code / Cursor изнутри? Пишем свой Coding Agent на JS за 1 час» канала IT-KAMASUTRA. Все права на оригинальный материал принадлежат авторам. Источник: https://www.youtube.com/watch?v=ItAgfR-l-_k