emperor has no clothes

Современные кодинг-помощники кажутся магией. Достаточно описать нужное вам на хотя бы немного понятными словами, после чего они сами читают файлы, редактируют проект и пишут работающий код.

Но вот что я вам скажу: в основе этих инструментов не лежит магия. Для них достаточно примерно двухсот строк простого Python.

Давайте с нуля напишем собственный функциональный кодинг-агент.

Ментальная модель

Прежде, чем приступать к написанию кода, надо разобраться, что же происходит, когда мы используем агента. По сути, это просто беседа с мощной LLM, обладающей набором инструментов.

  1. Вы отправляете сообщение («Создай новый файл с функцией hello world»)

  2. LLM решает, что ей нужен инструмент, и отвечает структурированным вызовом инструмента (или несколькими вызовами инструментов)

  3. Ваша программа выполняет этот инструмент локально (создаёт файл)

  4. Результат передаётся LLM

  5. LLM использует этот контекст для дальнейшей работы или ответа

Вот и весь цикл. На самом деле LLM вообще никак не взаимодействует с вашей файловой системой. Она всего лишь просит выполнять действия, и ваш код выполняет их.

Три инструмента, которые нам понадобятся

Нашему кодинг-агенту необходимы три функции:

  • Чтение файлов, чтобы LLM могла видеть ваш код

  • Создание списка файлов, чтобы она могла ориентироваться в проекте

  • Редактирование файлов, чтобы можно было давать ему команды для создания и изменения кода

Вот и всё. У агентов продакшен-уровня наподобие Claude Code есть и другие инструменты, например, grepbashwebsearch и так далее, но, как мы увидим ниже, даже трёх инструментов достаточно для того, чтобы творить нечто невероятное.

Предварительная настройка

Начнём мы с базовых импортов и клиента 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)})"
                })

Структура кода:

  1. Внешний цикл: получаем пользовательский ввод, добавляем в беседу

  2. Внутренний цикл: вызываем 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)


  1. iBear
    10.01.2026 13:08

    Хороший туториал, но метод Find-and-Replace (через ⁠old_str и ⁠new_str) — это не всегда однозначное решение для продакшена. Любая галлюцинация модели в один символ — и замена не сработает. Инструменты вроде Claude Code или Cursor занимаются именно тем, чтобы приземлить правки модели на реальный файл без ошибок в форматировании. Без перехода на diff-форматы такой агент быстро превратится в генератор синтаксических ошибок.


    1. cher11
      10.01.2026 13:08

      Такая проверка не просто так сделана. Уже много раз сталкивался с тем, что если модель придумала что-то в старой строке, то она и в новой напишет ерунду. А если ей сразу вернуть ошибку - перечитает файл и уже исправит нормально


  1. WondeRu
    10.01.2026 13:08

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


  1. 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 с полезными импортами».


    1. Kwent
      10.01.2026 13:08

      Теперь и комментарии ллмками пишем?)


      1. achekalin
        10.01.2026 13:08

        Поспешил человек высказаться, а писать было лень.

        Но и ллм вполне разумное написала.

        Другое дело, что это proof of concept, а не продакшен-волшебная палочка.

        Но вот diff-ы точно нужны!

        Ну и экономия токенов не помешала бы: гонять туда сюда по целому файлу (а, будем реалистами, там скоро и каталоги полетят) - это прямо "на что бы потратить стопятьсот долларов денег и как бы сжечь стопятьсот киловатт энергии?!"


      1. 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 скоро это без времени. пока вопросики.


    1. JerryI
      10.01.2026 13:08

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


  1. Politura
    10.01.2026 13:08

    Оно, конечно, прикольно, но слишком уж базовый уровень, который нихера в проде работать не будет, тупо потому-что изобретается свой протокол работы с тулзами, контекст подрастет и модель начнет тупить и вызывать их неправильно, ибо они только описаны в системном промпте, тогда как все современные модели, как правило, натренированны использовать стандартный протокол.

    Для этого, там, где в статье идет вызов claude_client.messages.create( помимо того, что передается, можно передать еще и список тулзов. А ответ модели в таком случае может быть не только текст, но и список вызовов тулзов, ибо за один ответ модель может подумать и вызвать несколько тулзов.

    Статья больше вредная, чем полезная, на мой взгляд, лучше сразу к нормальной работе приучаться, чем к уровню робких шагов начала 2024, когда еще не знали как тулзы вызывать. Ну и упоминание claude code в заголовке - чистый кликбейт, т.к. помимо вызова тулзов там еще дохера чего есть.

    Упд: Ой, глянул оригинал - там в первой же строчке указывается, что он написан в январе 2025! Переводчик, такие вещи надо сразу указывать, что статья - антиквар.


  1. amazingname
    10.01.2026 13:08

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

    Иными словами, не совсем однозначно насколько король голый.


    1. JerryI
      10.01.2026 13:08

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


  1. achmed
    10.01.2026 13:08

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


  1. 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-вызовов.


    1. DarthVictor
      10.01.2026 13:08

      Весь Claude Code без промптов с .md расширением, около трёх десятков файлов на шелле, питоне и тайпскрипте.

      Автор сильно упростил список инструментов современного агента, но и в 20000 и даже в 2000 он вполне влезет, потому что большинство указанного функционала — это просто вызов сторонних юниксовых функций, синтаксис которых нейронке уже известен.


      1. Politura
        10.01.2026 13:08

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


        1. DarthVictor
          10.01.2026 13:08

          Исходники Claude Code есть на гитхабе
          https://github.com/anthropics/claude-code

          То что автор для вашего удобства упаковал зависимости в бинарник, не повод их считать исходником. Большая часть его логики вообще в одном не очень большом файле.


  1. spacediver
    10.01.2026 13:08

    А вообще да, reducer)


  1. MKreGGo
    10.01.2026 13:08

    Ну, для написания скрипта hello world подойдет :)