Языковая модель сама по себе умеет лишь одно — генерировать текст. Из неё выходит неплохой собеседник, который отвечает на вопросы в пределах того, что знает, но дальше этого дело не идёт: он не прочитает ваш файл, не запустит команду и не сходит за свежими данными в интернет. Чтобы такой «чат-бот» превратился в агента, который реально выполняет работу за вас, ему нужна возможность действовать в своём окружении — в нашем случае на компьютере, где он запущен.

Именно эту возможность дают агенту инструменты (tools). В этой статье мы с нуля на Python реализуем базовый набор инструментов и подключим их к простому агенту, превратив пассивного собеседника в исполнителя, который читает и пишет файлы, ищет по файловой системе, выполняет команды оболочки и загружает веб-страницы.

Что такое инструменты?

Инструмент — это программа или функция, которую вы предоставляете модели (LLM), чтобы она могла вызывать её самостоятельно. Инструмент может быть как совсем простым — обычной функцией на Python в том же коде агента, — так и сложным, например MCP-сервером (Model Context Protocol), который делает HTTP-запрос к API, читающему или обновляющему базу данных.

Как агенты используют инструменты?

Большие языковые модели выдают текст — так как же они могут пользоваться инструментами? Первые реализации вызова инструментов полагались на то, чтобы «подсказать» LLM вывести текст вроде Action: web_fetch, после чего обвязка агента (agent harness) разбирала этот текстовый вывод и запускала соответствующую функцию. Такой подход был не слишком надёжным: модель порой не вполне точно следовала ожидаемому от неё формату.

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

Улучшаем агента с помощью инструментов

За основу мы возьмём простого агента, которого собрали в прошлый раз.

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

В коде агента мы создадим подмодуль tools. В нём мы реализуем все инструменты и их схемы.

Весь код можно найти и склонировать в Github-репозитории.

Для начала — инструмент bash:

def run_bash(command: str) -> str:
    """Run a bash command and return its output."""
    result = subprocess.run(
        command, shell=True, text=True, capture_output=True
    )
    output = result.stdout
    if result.stderr:
        output += f"\nSTDERR:\n{result.stderr}"
    return output or "(no output)"

Это самый мощный инструмент. Позволив агенту выполнять команды bash, мы дадим ему возможность делать на компьютере что угодно. С одной стороны, это хорошо: нам не придётся реализовывать отдельный инструмент для каждой программы, которую можно просто запустить через bash и которой LLM и так умеет пользоваться. С другой стороны, это и самый опасный инструмент (по той же причине — он позволяет делать на компьютере что угодно). В дальнейшем мы вплотную займёмся безопасностью, чтобы избежать проблем.

Следующий инструмент — чтение файла (read file):

def read_file(path: str, offset: int = 1, limit: int = 200) -> str:
    """Read lines from a file, with optional offset and limit."""
    p = Path(path)
    if not p.exists():
        return f"Error: file not found: {path}"
    lines = p.read_text(errors="replace").splitlines()
    selected = lines[offset - 1: offset - 1 + limit]
    return "\n".join(f"{offset + i}: {line}" for i, line in enumerate(selected))

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

Следующий инструмент — поиск файлов по шаблону (glob files):

def glob_files(pattern: str, path: str = ".") -> str:
    """Find files matching a glob pattern inside a directory."""
    matches = glob_module.glob(f"{path}/**/{pattern}", recursive=True)
    matches += glob_module.glob(f"{path}/{pattern}")
    unique = sorted(set(matches))
    return "\n".join(unique) if unique else "(no matches)"

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

Следующий инструмент — grep:

def grep(pattern: str, path: str = ".", include: str = "*") -> str:
    """Search file contents for a regex pattern, optionally filtering by filename glob."""
    results = []
    for filepath in glob_module.glob(f"{path}/**/{include}", recursive=True):
        fp = Path(filepath)
        if not fp.is_file():
            continue
        try:
            for i, line in enumerate(fp.read_text(errors="replace").splitlines(), 1):
                if re.search(pattern, line):
                    results.append(f"{filepath}:{i}: {line}")
        except OSError:
            pass
    return "\n".join(results) if results else "(no matches)"

Этот инструмент ищет по содержимому файлов с помощью регулярных выражений и возвращает совпавшие строки вместе с путём к файлу и номером строки. Он отлично дополняет glob_files: сначала вы находите, какие файлы существуют, а затем ищете внутри них именно то содержимое, которое вас интересует. Необязательный параметр include позволяет ограничить поиск файлами, подходящими под шаблон имени, — это удобно, чтобы не искать в бинарных файлах или сузить область до конкретного языка.

Следующий инструмент — запись файла (write file):

def write_file(path: str, content: str) -> str:
    """Write content to a file, creating it if it does not exist."""
    p = Path(path)
    p.parent.mkdir(parents=True, exist_ok=True)
    p.write_text(content)
    return f"Wrote {len(content)} bytes to {path}"

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

Следующий инструмент — редактирование файла (edit file):

def edit_file(path: str, old_string: str, new_string: str) -> str:
    """Replace the first occurrence of old_string with new_string in a file."""
    p = Path(path)
    if not p.exists():
        return f"Error: file not found: {path}"
    original = p.read_text()
    if old_string not in original:
        return f"Error: string not found in {path}"
    p.write_text(original.replace(old_string, new_string, 1))
    return f"Edited {path}"

Если write_file заменяет всё содержимое файла целиком, то edit_file выполняет точечную замену строки. Это гораздо безопаснее, когда агенту нужно внести лишь небольшое изменение в существующий файл, поскольку так он не перезапишет случайно то содержимое, которое не читал. Это основной инструмент для кодинг-агентов, которым нужно править конкретные строки, не переписывая всё заново.

Последний инструмент — webfetch:

def webfetch(url: str) -> str:
    """Fetch a URL and return its full plain-text content (up to 2 MB)."""
    parsed = urlparse(url)
    if parsed.scheme not in ("http", "https"):
        return f"Error fetching {url}: unsupported scheme '{parsed.scheme}'."
    req = urllib.request.Request(url, headers={"User-Agent": "agent/1.0"})
    with urllib.request.urlopen(req, timeout=15) as resp:
        raw = b"".join(...).decode(charset, errors="replace")
    soup = BeautifulSoup(raw, "html.parser")
    text = soup.get_text(separator="\n", strip=True)
    return re.sub(r"\n{3,}", "\n\n", text).strip()

Этот инструмент загружает публичную веб-страницу и возвращает её содержимое в виде обычного текста. Он использует BeautifulSoup, чтобы убрать всю HTML-разметку, и модель получает только читаемый текст — это сохраняет контекст чистым и экономит токены. Инструмент работает только с URL по http и https и ограничивает ответ размером 2 МБ, чтобы не переполнять контекстное окно огромными страницами.

Когда все инструменты реализованы, нужно сообщить агенту об их существовании. Кроме того, агент должен знать, что делает каждый инструмент и какие параметры он принимает. Для этого мы определяем схему инструментов (tool schema) для модели:

def get_tool_schemas():
    return [
        {
            "type": "function",
            "function": {
                "name": "run_bash",
                "description": "Run a bash command on the user's machine and return the output.",
                "parameters": {
                    "type": "object",
                    "properties": {
                        "command": {
                            "type": "string",
                            "description": "The bash command to execute.",
                        }
                    },
                    "required": ["command"],
                },
            },
        },
        {
            "type": "function",
            "function": {
                "name": "read_file",
                "description": "Read lines from a file. Returns lines prefixed with line numbers.",
                "parameters": {
                    "type": "object",
                    "properties": {
                        "path": {"type": "string", "description": "Absolute or relative path to the file."},
                        "offset": {"type": "integer", "description": "First line to read (1-indexed). Defaults to 1."},
                        "limit": {"type": "integer", "description": "Maximum number of lines to return. Defaults to 200."},
                    },
                    "required": ["path"],
                },
            },
        },
        {
            "type": "function",
            "function": {
                "name": "glob_files",
                "description": "Find files matching a glob pattern (e.g. '**/*.py') inside a directory.",
                "parameters": {
                    "type": "object",
                    "properties": {
                        "pattern": {"type": "string", "description": "Glob pattern to match against file names."},
                        "path": {"type": "string", "description": "Root directory to search in. Defaults to '.'."},
                    },
                    "required": ["pattern"],
                },
            },
        },
        {
            "type": "function",
            "function": {
                "name": "grep",
                "description": "Search file contents for a regex pattern and return matching lines with file paths and line numbers.",
                "parameters": {
                    "type": "object",
                    "properties": {
                        "pattern": {"type": "string", "description": "Regular expression to search for."},
                        "path": {"type": "string", "description": "Directory to search in. Defaults to '.'."},
                        "include": {"type": "string", "description": "Filename glob to restrict which files are searched (e.g. '*.py'). Defaults to '*'."},
                    },
                    "required": ["pattern"],
                },
            },
        },
        {
            "type": "function",
            "function": {
                "name": "write_file",
                "description": "Write content to a file, creating it (and any missing parent directories) if it does not exist.",
                "parameters": {
                    "type": "object",
                    "properties": {
                        "path": {"type": "string", "description": "Path of the file to write."},
                        "content": {"type": "string", "description": "Full content to write to the file."},
                    },
                    "required": ["path", "content"],
                },
            },
        },
        {
            "type": "function",
            "function": {
                "name": "edit_file",
                "description": "Replace the first occurrence of a string in a file with a new string.",
                "parameters": {
                    "type": "object",
                    "properties": {
                        "path": {"type": "string", "description": "Path of the file to edit."},
                        "old_string": {"type": "string", "description": "Exact string to find and replace."},
                        "new_string": {"type": "string", "description": "String to replace it with."},
                    },
                    "required": ["path", "old_string", "new_string"],
                },
            },
        },
        {
            "type": "function",
            "function": {
                "name": "webfetch",
                "description": (
                    "Fetch a public URL (http/https only) and return its full plain-text content (up to 2 MB)."
                ),
                "parameters": {
                    "type": "object",
                    "properties": {
                        "url": {"type": "string", "description": "The URL to fetch (http/https)."},
                    },
                    "required": ["url"],
                },
            },
        },
    ]



После этого мы можем интегрировать инструменты в наш прежний агентный цикл:

TOOL_REGISTRY = get_tool_registry()
TOOL_SCHEMAS = get_tool_schemas()

def handle_tool_calls(tool_calls, messages):
    """Execute each tool the LLM requested and append the results to messages."""
    for tool_call in tool_calls:
        name = tool_call.function.name
        args = json.loads(tool_call.function.arguments)

        print(f"  [tool] {name}({args})")

        if name not in TOOL_REGISTRY:
            result = f"Error: unknown tool '{
                name}'. Available tools: {list(TOOL_REGISTRY.keys())}"
        else:
            result = TOOL_REGISTRY[name](**args)

        print(f"  [tool result] {result[:200]}{
              '...' if len(result) > 200 else ''}")

        messages.append({
            "role": "tool",
            "tool_call_id": tool_call.id,
            "content": result,
        })


def agent_loop(client):
    messages = [
        {
            "role": "system",
            "content": (
                "You are a helpful assistant. You have tools to read and write files, "
                "search the file system, and fetch web pages. Use them to help the user."
            ),
        }
    ]

    while True:
        user_input = input("You: ")
        if user_input.lower() == "\\exit":
            break

        messages.append({"role": "user", "content": user_input})

        while True:
            response = client.chat.completions.create(
                model="gemma4",
                messages=messages,
                tools=TOOL_SCHEMAS,
                temperature=0.7,
            )

            message = response.choices[0].message

            messages.append(message)

            if message.tool_calls:
                handle_tool_calls(message.tool_calls, messages)
            else:
                print(f"Assistant: {message.content}")
                break

Что у нас получилось

Теперь у нас есть агент с вызовом инструментов, и он уже весьма мощный. Если попросить его сделать что-то за вас, он сможет задействовать все эти базовые инструменты для решения довольно сложных задач. По сути, это уже можно использовать как кодинг-агента или ассистента — и это действительно работает. Ему пока не хватает многих возможностей, которые есть у Claude Code или Hermes Agent, но мы постепенно к этому идём.

Что дальше?

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

Поэкспериментировать с таким агентом можно уже сейчас — но стоит помнить, что он умеет выполнять произвольные команды и переписывать файлы, то есть фактически делать на машине что угодно. Запускать автономного «исполнителя» прямо на рабочем ноутбуке рискованно: одна неудачная команда может задеть ваши же данные. Безопаснее держать его в изолированном окружении — например, развернуть агента и модель на ML-платформе Cloud4Y: отдельный контур с нужными ресурсами, где даже неудачный вызов bash не затронет ничего лишнего.

Комментарии (5)


  1. Karroplan
    16.06.2026 07:39

    кто-то забыл функцию get_tool_registry.... что-то типа:

    def get_tool_registry():
        return {
            "run_bash": run_bash,
            "read_file": read_file,
            ... and so on
        }


    1. Genius_Russian_Coders
      16.06.2026 07:39

      Нативный tool calling надёжнее текстовых Action, но даже Claude/GPT иногда путают аргументы в сложных схемах. Спасает валидация с возвратом ошибки в контекст — на втором шаге модель обычно исправляется.


  1. atues
    16.06.2026 07:39

    А не проще ли сформировать отдельный json-файл, считывать его и возвращать, чем расписывать все содержимое в виде портянки в функции get_tool_schemas()? Да и править его будет проще, чем влезать непосредственно в код imho


    1. Moog_Prodigy
      16.06.2026 07:39

      Наверное и так можно сделать, но по умолчанию для вызова MCP инструментов пишется именно вот такая портянка. Согласен, с одной стороны не очень удобно читать, зато все в одном - а с кучей файлов можно и забыть, где что. Типа json на jsone и джсоном погоняет. А так - один инструмент или набор - один файл. Один набор рисует, другой озвучивает, третий еще что-то делает. Можно конечно упихать вообще все в кучу, но это другая крайность.


  1. islaevdenn
    16.06.2026 07:39

    Идея выглядит сыровато и рискованно. Дать модели доступ к bash и файловой системе это прямой путь к проблемам, особенно без строгой изоляции и ограничений.