
Современные кодинг-помощники кажутся магией. Достаточно описать нужное вам на хотя бы немного понятными словами, после чего они сами читают файлы, редактируют проект и пишут работающий код.
Но вот что я вам скажу: в основе этих инструментов не лежит магия. Для них достаточно примерно двухсот строк простого Python.
Давайте с нуля напишем собственный функциональный кодинг-агент.
Ментальная модель
Прежде, чем приступать к написанию кода, надо разобраться, что же происходит, когда мы используем агента. По сути, это просто беседа с мощной LLM, обладающей набором инструментов.
Вы отправляете сообщение («Создай новый файл с функцией hello world»)
LLM решает, что ей нужен инструмент, и отвечает структурированным вызовом инструмента (или несколькими вызовами инструментов)
Ваша программа выполняет этот инструмент локально (создаёт файл)
Результат передаётся LLM
LLM использует этот контекст для дальнейшей работы или ответа
Вот и весь цикл. На самом деле LLM вообще никак не взаимодействует с вашей файловой системой. Она всего лишь просит выполнять действия, и ваш код выполняет их.
Три инструмента, которые нам понадобятся
Нашему кодинг-агенту необходимы три функции:
Чтение файлов, чтобы LLM могла видеть ваш код
Создание списка файлов, чтобы она могла ориентироваться в проекте
Редактирование файлов, чтобы можно было давать ему команды для создания и изменения кода
Вот и всё. У агентов продакшен-уровня наподобие Claude Code есть и другие инструменты, например, grep, bash, websearch и так далее, но, как мы увидим ниже, даже трёх инструментов достаточно для того, чтобы творить нечто невероятное.
Предварительная настройка
Начнём мы с базовых импортов и клиента API. Я буду пользоваться OpenAI, но подойдёт и любой другой сервис LLM:
import inspect
import json
import os
import anthropic
from dotenv import load_dotenv
from pathlib import Path
from typing import Any, Dict, List, Tuple
load_dotenv()
claude_client = anthropic.Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])
Добавим цветов терминала, чтобы вывод было удобнее читать:
YOU_COLOR = "\u001b[94m"
ASSISTANT_COLOR = "\u001b[93m"
RESET_COLOR = "\u001b[0m"
И утилиту для ресолвинга файловых путей (чтобы file.py превращался в /Users/you/project/file.py):
def resolve_abs_path(path_str: str) -> Path:
"""
file.py -> /Users/you/project/file.py
"""
path = Path(path_str).expanduser()
if not path.is_absolute():
path = (Path.cwd() / path).resolve()
return path
Реализуем инструменты
Стоит отметить, что docstrings функций инструментов должны быть подробными, потому что они будут использоваться LLM для рассуждений о том, какие инструменты необходимо вызывать во время беседы. Детальнее мы разберём это чуть ниже.
Инструмент 1: чтение файлов
Самый простой инструмент. Получаем имя файла, возвращаем его содержимое:
def read_file_tool(filename: str) -> Dict[str, Any]:
"""
Gets the full content of a file provided by the user.
:param filename: The name of the file to read.
:return: The full content of the file.
"""
full_path = resolve_abs_path(filename)
print(full_path)
with open(str(full_path), "r") as f:
content = f.read()
return {
"file_path": str(full_path),
"content": content
}
Мы возвращаем словарь, потому что LLM требуется структурированный контекст происходящего.
Инструмент 2: создание списка файлов
Ходим по папкам, создавая списки их содержимого:
def list_files_tool(path: str) -> Dict[str, Any]:
"""
Lists the files in a directory provided by the user.
:param path: The path to a directory to list files from.
:return: A list of files in the directory.
"""
full_path = resolve_abs_path(path)
all_files = []
for item in full_path.iterdir():
all_files.append({
"filename": item.name,
"type": "file" if item.is_file() else "dir"
})
return {
"path": str(full_path),
"files": all_files
}
Инструмент 3: редактирование файлов
Это самый сложный инструмент, но всё равно достаточно понятный. Он обрабатывает два случая:
Создание нового файла, когда
old_strпустаЗамена текста нахождением
old_strи заменой её наnew_str
def edit_file_tool(path: str, old_str: str, new_str: str) -> Dict[str, Any]:
"""
Replaces first occurrence of old_str with new_str in file. If old_str is empty,
create/overwrite file with new_str.
:param path: The path to the file to edit.
:param old_str: The string to replace.
:param new_str: The string to replace with.
:return: A dictionary with the path to the file and the action taken.
"""
full_path = resolve_abs_path(path)
if old_str == "":
full_path.write_text(new_str, encoding="utf-8")
return {
"path": str(full_path),
"action": "created_file"
}
original = full_path.read_text(encoding="utf-8")
if original.find(old_str) == -1:
return {
"path": str(full_path),
"action": "old_str not found"
}
edited = original.replace(old_str, new_str, 1)
full_path.write_text(edited, encoding="utf-8")
return {
"path": str(full_path),
"action": "edited"
}
Правило здесь такое: пустая old_str означает «создать этот файл». Если она не пуста, то нужно найти и заменить. Настоящие IDE добавляют сложное поведение при сбое в случае ненайденной строки, но и этого вполне достаточно.
Перечень инструментов
Нам нужно как-то находить инструменты по именам:
TOOL_REGISTRY = {
"read_file": read_file_tool,
"list_files": list_files_tool,
"edit_file": edit_file_tool
}
Учим LLM пользоваться нашими инструментами
LLM должна знать, какие инструменты есть и как их вызывать. Мы генерируем это знание динамически из сигнатур функций и docstrings:
def get_tool_str_representation(tool_name: str) -> str:
tool = TOOL_REGISTRY[tool_name]
return f"""
Name: {tool_name}
Description: {tool.__doc__}
Signature: {inspect.signature(tool)}
"""
def get_full_system_prompt():
tool_str_repr = ""
for tool_name in TOOL_REGISTRY:
tool_str_repr += "TOOL\n===" + get_tool_str_representation(tool_name)
tool_str_repr += f"\n{'='*15}\n"
return SYSTEM_PROMPT.format(tool_list_repr=tool_str_repr)
А также в самом системном промпте:
SYSTEM_PROMPT = """
Ты помощник в кодинге, цель которого - помогать в решении задач кодинга.
У тебя есть доступ к набору инструментов, которые ты можешь применять. Вот список инструментов:
{tool_list_repr}
Когда тебе нужно использовать инструмент, отвечай ровно одной строкой в таком формате: 'tool: TOOL_NAME({{JSON_ARGS}})' и больше ничем.
Используй компактный однострочный JSON с двойными кавычками. После получения сообщения tool_result(...) продолжай выполнение задачи.
Если инструмент не требуется, отвечай обычным образом.
"""
И это здесь самое важное — мы просто говорим LLM: «Вот твои инструменты, вот формат для их вызова». LLM сама разберётся, когда и как их использовать.
Парсинг вызова инструментов
Когда LLM отвечает, нам нужно распознавать, что она просит нас запустить инструмент:
def extract_tool_invocations(text: str) -> List[Tuple[str, Dict[str, Any]]]:
"""
Return list of (tool_name, args) requested in 'tool: name({...})' lines.
The parser expects single-line, compact JSON in parentheses.
"""
invocations = []
for raw_line in text.splitlines():
line = raw_line.strip()
if not line.startswith("tool:"):
continue
try:
after = line[len("tool:"):].strip()
name, rest = after.split("(", 1)
name = name.strip()
if not rest.endswith(")"):
continue
json_str = rest[:-1].strip()
args = json.loads(json_str)
invocations.append((name, args))
except Exception:
continue
return invocations
Это простой парсинг текста. Ищем строки, начинающиеся с tool:, извлекаем имя функции и JSON-аргументы.
Вызов LLM
Тонкая обёртка вокруг API:
def execute_llm_call(conversation: List[Dict[str, str]]):
system_content = ""
messages = []
for msg in conversation:
if msg["role"] == "system":
system_content = msg["content"]
else:
messages.append(msg)
response = claude_client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=2000,
system=system_content,
messages=messages
)
return response.content[0].text
Цикл агента
Теперь мы соединяем всё вместе. Именно тут и происходит «магия»:
def run_coding_agent_loop():
print(get_full_system_prompt())
conversation = [{
"role": "system",
"content": get_full_system_prompt()
}]
while True:
try:
user_input = input(f"{YOU_COLOR}You:{RESET_COLOR}:")
except (KeyboardInterrupt, EOFError):
break
conversation.append({
"role": "user",
"content": user_input.strip()
})
while True:
assistant_response = execute_llm_call(conversation)
tool_invocations = extract_tool_invocations(assistant_response)
if not tool_invocations:
print(f"{ASSISTANT_COLOR}Assistant:{RESET_COLOR}: {assistant_response}")
conversation.append({
"role": "assistant",
"content": assistant_response
})
break
for name, args in tool_invocations:
tool = TOOL_REGISTRY[name]
resp = ""
print(name, args)
if name == "read_file":
resp = tool(args.get("filename", "."))
elif name == "list_files":
resp = tool(args.get("path", "."))
elif name == "edit_file":
resp = tool(args.get("path", "."),
args.get("old_str", ""),
args.get("new_str", ""))
conversation.append({
"role": "user",
"content": f"tool_result({json.dumps(resp)})"
})
Структура кода:
Внешний цикл: получаем пользовательский ввод, добавляем в беседу
-
Внутренний цикл: вызываем LLM, проверяем вызовы инструментов
Если инструменты не требуются, печатаем ответ и выходим из внутреннего цикла
Если инструменты нужны, исполняем их, добавляем результаты в беседу и начинаем цикл снова
Внутренний цикл продолжается, пока LLM отвечает, не запрашивая инструменты. Это позволяет агенту объединять в цепочку несколько вызовов инструментов (чтение файла, его редактирование и подтверждение изменений).
Запускаем нашего агента
if __name__ == "__main__":
run_coding_agent_loop()
Теперь вы можете вести такие беседы:
Вы: Создай новый файл с именем hello.py и реализуй в нём hello world
Агент вызывает edit_file path="hello.py", old_str="", new_str="print(‘Hello World’)"
Помощник: Готово! Создан hello.py с реализацией hello world.
Или многоэтапные разговоры:
Вы: Отредактируй hello.py, добавив в него функцию перемножения двух чисел
Агент вызывает read_file для просмотра текущего содержимого, а затем вызывает edit_file для добавления функции.
Помощник: Добавлена функция умножения в hello.py.
Разница между нашей системой и продакшен-инструментами
Всего у нас получилось около 200 строк. В продакшен-инструментах наподобие Claude Code также имеется:
Более качественная обработка ошибок и поведения при сбоях
Потоковые ответы для улучшения UX
Более умное управление контекстом (суммаризация длинных файлов и так далее)
Дополнительные инструменты (выполнение команд, поиск по кодовой базе и так далее)
Процедуры подтверждения для деструктивных операций
Но их базовый цикл остаётся точно таким же, который создали мы. LLM решает, что делать, ваш код исполняет это, результаты передаются обратно. В этом и заключается вся архитектура.
Попробуйте сами
Полные исходники состоят из примерно 200 строк. В качестве домашнего задания подставьте в них тот сервис LLM, с которым вы работаете, настройте системный промпт, добавьте новые инструменты. Вас приятно поразит мощь этого простого паттерна.
Комментарии (18)

WondeRu
10.01.2026 13:08Доверие теряется, тк в первом же абзаце реализации: «берем openai», а по факту создается антропик инстанс

Pro-Sh
10.01.2026 13:08Хорошо. Статья именно то, что снимает ореол «агенты = особая магия». По факту это чат, где модели дали три руки: list_files, read_file, edit_file. Всё остальное — UX и страховочные барьеры. То есть Claude Code — это не “супер-LLM”, это “LLM + ремни безопасности + каска + регламент”. Самое полезное здесь даже не код, а ментальная модель: LLM не трогает вашу ФС, она только просит, а вы решаете, выполнять ли. В корпоративном/закрытом контуре это вообще золото (факт фактический) : можно дать агенту ровно тот набор инструментов, который не превратит его в junior с root’ом.
Немного «а вот тут начинается взрослая жизнь»:
edit_file“по подстроке” — это как деплой «по ощущениям»: пока работает — красиво, как сломается — будете искать, какой именно символ агент “улучшил”. На практике лучше учить агента предлагать unified diff и применять его через patch, плюс проверять, что diff применился чисто.
Очень не хватает инструмента “сделай вид, что ты взрослый”: run_tests/lint/typecheck. Без этого агент пишет код, как студент на экзамене: уверенно, быстро и без запуска.
И да, path safety: если не ограничить root проекта, однажды получите легендарное: «Создал файл /etc/hosts с полезными импортами».

Kwent
10.01.2026 13:08Теперь и комментарии ллмками пишем?)

achekalin
10.01.2026 13:08Поспешил человек высказаться, а писать было лень.
Но и ллм вполне разумное написала.
Другое дело, что это proof of concept, а не продакшен-волшебная палочка.
Но вот diff-ы точно нужны!
Ну и экономия токенов не помешала бы: гонять туда сюда по целому файлу (а, будем реалистами, там скоро и каталоги полетят) - это прямо "на что бы потратить стопятьсот долларов денег и как бы сжечь стопятьсот киловатт энергии?!"

Pro-Sh
10.01.2026 13:08Да, это ответ, который сформулировать наша система (Тестируем свою систему локально/в закрытом контуре конечно же, но которая может мониторить источники, искать статьи по смыслу, ставит пометки -тезисы/риски/что проверить-, связывает с похожими материалами и сама пингует, когда появляется реально важное под наш профиль. Плюсы: меньше шума, быстрее разбор, прозрачные причины алертов и никакого “джуна с root’ом” — всё в пределах заданных правил и инструментов. Скоро будет статья и репозиторий для скачивания на гитах. ) — мы её сейчас как раз тестируем на реальных обсуждениях. При этом по смыслу я писал про приземлённые вещи: агент = LLM + ограниченные инструменты (list/read/edit) + human‑in‑the‑loop; edit_file “по подстроке” в проде лучше заменять на unified diff + patch; и обязательно иметь run_tests/lint/typecheck и path safety. Скоро выпустим статью про саму систему, и сборку можно будет скачать на гитах — кину ссылку, как выложим. PS скоро это без времени. пока вопросики.

JerryI
10.01.2026 13:08На самом деле вы можете попросить агента выдать вам весь список тулов. Там много всего интересного:)

Politura
10.01.2026 13:08Оно, конечно, прикольно, но слишком уж базовый уровень, который нихера в проде работать не будет, тупо потому-что изобретается свой протокол работы с тулзами, контекст подрастет и модель начнет тупить и вызывать их неправильно, ибо они только описаны в системном промпте, тогда как все современные модели, как правило, натренированны использовать стандартный протокол.
Для этого, там, где в статье идет вызов
claude_client.messages.create(помимо того, что передается, можно передать еще и список тулзов. А ответ модели в таком случае может быть не только текст, но и список вызовов тулзов, ибо за один ответ модель может подумать и вызвать несколько тулзов.Статья больше вредная, чем полезная, на мой взгляд, лучше сразу к нормальной работе приучаться, чем к уровню робких шагов начала 2024, когда еще не знали как тулзы вызывать. Ну и упоминание claude code в заголовке - чистый кликбейт, т.к. помимо вызова тулзов там еще дохера чего есть.
Упд: Ой, глянул оригинал - там в первой же строчке указывается, что он написан в январе 2025! Переводчик, такие вещи надо сразу указывать, что статья - антиквар.

amazingname
10.01.2026 13:08В AI IDE есть режим планирования и режим агента. Значит как минимум есть промпты которые готовят модель к решению задачи планирования или кодинга. Так же, когда ставишь задачу модель повторяет формулировку ещё раз сама себе другими словами. Это часть конкретной рассуждающий модели или это в коде агента? Модель строит план и затем идёт по пунктам, иногда не доводя работу до конца, если она слишком большая. Опять вопрос - это определяется агентом или рассуждающая моделью?
Иными словами, не совсем однозначно насколько король голый.

JerryI
10.01.2026 13:08VSCode Copilot как раз таки голый, если вытащить список тулов, то там будет manage_todo_list с простейшим crud, write, read, и также вызов субагентов в виде такой же функции. Никакой магии

achmed
10.01.2026 13:08Уж очень примитивно в сравнении с сегодняшними кодинг агентами, которые пишут код, запускают авто тесты, исправляют ошибки по провалам тестов, умеют работать долго в фоновом режиме без зацикливания, имеют глубокую интеграцию в ide и т. д. и т.п.

nickmanecannotbeempty
10.01.2026 13:08В Claude code есть:
Режим планирования
Thinking
Модель безопасности и пользовательских разрешений
Параллельный запуск специализированных субагентов
Защита от слишком большого ответа инструмента (например, при чтении огромного файла или результата выполнения шелл-команды, или списка файлов или http-ответа итд)
Автоматическое и ручное сжатие контекста
Поддержка пользовательских скиллов
MCP позволяющий подключать любые сторонние тулы
ACP позволяющий использовать Клода через API. Например, встраивать его в интерфейс IDE
Настройки, кастомные промпты, кастомные команды и правила безопасности уровня проекта и пользователя
Интеграция с IDE (видит открытый в данный момент файл и выделенную строку)
Инструмент редактирования файлов, основанный на diff
Предпросмотр изменений файла
Запуск шелл-команд в фоне и управление фоновыми процессами
Откат беседы и кода к предыдущим состояниям
База данных всех бесед с возможностью вернуться к любой из них
Динамический UI написанный на Ink, вылизанный почти до совершенства множеством итераций, основанных на тысячах пользовательских issues.
Это здоровенная махина, и ни в 200, ни в 20000 строк его не запихнуть.
А так-то да, написание helloworld-агента у вас прекрасно проиллюстрировано, но не стоит забывать, что реальные "промышленные" агенты — это 99.9% сложной логической обвязки, 0.1% LLM-вызовов.

DarthVictor
10.01.2026 13:08Весь Claude Code без промптов с .md расширением, около трёх десятков файлов на шелле, питоне и тайпскрипте.
Автор сильно упростил список инструментов современного агента, но и в 20000 и даже в 2000 он вполне влезет, потому что большинство указанного функционала — это просто вызов сторонних юниксовых функций, синтаксис которых нейронке уже известен.

Politura
10.01.2026 13:08Вы точно ни с чем не путаете? У меня исполняемый файл это бинарник на 178 мегабайт.

DarthVictor
10.01.2026 13:08Исходники Claude Code есть на гитхабе
https://github.com/anthropics/claude-codeТо что автор для вашего удобства упаковал зависимости в бинарник, не повод их считать исходником. Большая часть его логики вообще в одном не очень большом файле.
iBear
Хороший туториал, но метод Find-and-Replace (через old_str и new_str) — это не всегда однозначное решение для продакшена. Любая галлюцинация модели в один символ — и замена не сработает. Инструменты вроде Claude Code или Cursor занимаются именно тем, чтобы приземлить правки модели на реальный файл без ошибок в форматировании. Без перехода на diff-форматы такой агент быстро превратится в генератор синтаксических ошибок.
cher11
Такая проверка не просто так сделана. Уже много раз сталкивался с тем, что если модель придумала что-то в старой строке, то она и в новой напишет ерунду. А если ей сразу вернуть ошибку - перечитает файл и уже исправит нормально