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

[Перевод] А король-то голый! Как написать свой Claude Code в 200 строках кода

emperor has no clothes

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

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

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

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

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

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

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

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

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

  5. 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)})" })

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

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

  2. Внутренний цикл: вызываем LLM, проверяем вызовы инструментов

    • Если инструменты не требуются, печатаем ответ и выходим из внутреннего цикла

    • Если инструменты нужны, исполняем их, добавляем результаты в беседу и начинаем цикл снова

Внутренний цикл продолжается, пока LLM отвечает, не запрашивая инструменты. Это позволяет агенту объединять в цепочку несколько вызовов инструментов (чтение файла, его редактирование и подтверждение изменений).

Запускаем нашего агента

if __name__ == "__main__": run_coding_agent_loop()

Теперь вы можете вести такие беседы:

Агент вызывает edit_file path="hello.py", old_str="", new_str="print(‘Hello World’)"

Или многоэтапные разговоры:

Агент вызывает read_file для просмотра текущего содержимого, а затем вызывает edit_file для добавления функции.

Разница между нашей системой и продакшен-инструментами

Всего у нас получилось около 200 строк. В продакшен-инструментах наподобие Claude Code также имеется:

  • Более качественная обработка ошибок и поведения при сбоях

  • Потоковые ответы для улучшения UX

  • Более умное управление контекстом (суммаризация длинных файлов и так далее)

  • Дополнительные инструменты (выполнение команд, поиск по кодовой базе и так далее)

  • Процедуры подтверждения для деструктивных операций

Но их базовый цикл остаётся точно таким же, который создали мы. LLM решает, что делать, ваш код исполняет это, результаты передаются обратно. В этом и заключается вся архитектура.

Попробуйте сами

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

Источник

Отказ от ответственности: Статьи, размещенные на этом веб-сайте, взяты из общедоступных источников и предоставляются исключительно в информационных целях. Они не обязательно отражают точку зрения MEXC. Все права принадлежат первоисточникам. Если вы считаете, что какой-либо контент нарушает права третьих лиц, пожалуйста, обратитесь по адресу service@support.mexc.com для его удаления. MEXC не дает никаких гарантий в отношении точности, полноты или своевременности контента и не несет ответственности за любые действия, предпринятые на основе предоставленной информации. Контент не является финансовой, юридической или иной профессиональной консультацией и не должен рассматриваться как рекомендация или одобрение со стороны MEXC.