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

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

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

islaevdenn
16.06.2026 07:39Идея выглядит сыровато и рискованно. Дать модели доступ к bash и файловой системе это прямой путь к проблемам, особенно без строгой изоляции и ограничений.
Karroplan
кто-то забыл функцию get_tool_registry.... что-то типа:
Genius_Russian_Coders
Нативный tool calling надёжнее текстовых Action, но даже Claude/GPT иногда путают аргументы в сложных схемах. Спасает валидация с возвратом ошибки в контекст — на втором шаге модель обычно исправляется.