Агентное программирование: LLM как исполнитель задач
Последние полгода я наблюдаю, как термин «AI-агенты» из академических статей перекочевал в продуктовые релизы. Claude может вызывать инструменты, GPT-4 выполняет задачи в несколько шагов, а фреймворки вроде LangGraph и CrewAI позволяют собирать мультиагентные системы. Разберёмся, что за этим стоит и где это реально полезно.
Простой промпт vs агент
Когда вы пишете в ChatGPT «напиши функцию сортировки» — это одноходовое взаимодействие. Вход → выход. Модель не проверяет свой код, не запускает тесты, не исправляет ошибки.
Агент — это LLM в цикле, у которого есть доступ к инструментам (tools) и способность принимать решения о следующем шаге. Вместо одного вызова модели агент работает итеративно:
Задача → Размышление → Действие → Наблюдение → Размышление → ... → Ответ Ключевые компоненты агента:
- LLM — «мозг», который решает, что делать дальше.
- Инструменты (tools) — функции, которые агент может вызывать: поиск в интернете, выполнение кода, запросы к API, чтение файлов.
- Цикл рассуждений — агент анализирует результат каждого действия и решает, нужно ли сделать ещё что-то.
- Память — контекст предыдущих шагов, чтобы не повторять действия.
Паттерн ReAct: Reasoning + Acting
Основной паттерн, который используют современные агенты — ReAct (Reasoning and Acting). Идея из одноимённой статьи 2022 года: модель чередует шаги рассуждения (Thought) и действия (Action).
Вот как это выглядит в логе агента:
Вопрос: Какая сейчас погода в Москве и стоит ли брать зонт?
Thought: Мне нужно узнать текущую погоду в Москве. Для этого я воспользуюсь
инструментом поиска погоды.
Action: get_weather(city="Moscow")
Observation: Температура: -2°C, облачно, вероятность осадков: 15%
Thought: Вероятность осадков 15% — это низкая вероятность. Зонт скорее всего
не понадобится, но стоит упомянуть температуру.
Answer: Сейчас в Москве -2°C, облачно. Вероятность осадков всего 15%,
так что зонт можно не брать, но оденьтесь тепло. Модель сама решила, какой инструмент вызвать, проинтерпретировала результат и сформулировала финальный ответ. Это принципиально отличается от жёстко заданного пайплайна, где порядок вызовов определён заранее.
Tool use / Function calling
Механизм, который делает агентов возможными — это function calling (вызов функций). Современные LLM обучены генерировать не просто текст, а структурированные вызовы функций в формате JSON.
Вы описываете доступные инструменты в виде JSON Schema, и модель решает, какой из них вызвать:
tools = [
{
"type": "function",
"function": {
"name": "execute_shell",
"description": "Execute a shell command on the server",
"parameters": {
"type": "object",
"properties": {
"command": {
"type": "string",
"description": "The shell command to execute"
}
},
"required": ["command"]
}
}
},
{
"type": "function",
"function": {
"name": "read_file",
"description": "Read contents of a file",
"parameters": {
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Path to the file"
}
},
"required": ["path"]
}
}
}
] Пишем простого агента
Давайте соберём минимального агента, который умеет выполнять shell-команды и читать файлы. Никаких фреймворков — чистый Python и API вызовы.
import json
import subprocess
import httpx
OLLAMA_URL = "http://localhost:11434/api/chat"
MODEL = "llama3.1:8b"
# Определяем инструменты
def execute_shell(command: str) -> str:
"""Выполняет shell-команду и возвращает вывод."""
try:
result = subprocess.run(
command, shell=True, capture_output=True,
text=True, timeout=30
)
output = result.stdout or result.stderr
return output[:2000] # Ограничиваем вывод
except subprocess.TimeoutExpired:
return "Error: command timed out after 30 seconds"
def read_file(path: str) -> str:
"""Читает содержимое файла."""
try:
with open(path, "r") as f:
content = f.read()
return content[:3000] # Ограничиваем размер
except FileNotFoundError:
return f"Error: file {path} not found"
# Маппинг имён к функциям
TOOLS = {
"execute_shell": execute_shell,
"read_file": read_file,
}
def call_tool(name: str, arguments: dict) -> str:
"""Вызывает инструмент по имени."""
if name in TOOLS:
return TOOLS[name](**arguments)
return f"Error: unknown tool {name}" Теперь основной цикл агента:
def run_agent(task: str, max_steps: int = 10):
"""Запускает агентный цикл."""
messages = [
{
"role": "system",
"content": (
"Ты — системный администратор. У тебя есть доступ к инструментам: "
"execute_shell (выполнить команду) и read_file (прочитать файл). "
"Используй их для выполнения задачи. Когда задача выполнена, "
"дай финальный ответ без вызова инструментов."
),
},
{"role": "user", "content": task},
]
for step in range(max_steps):
print(f"\n--- Шаг {step + 1} ---")
# Вызываем LLM
response = httpx.post(
OLLAMA_URL,
json={
"model": MODEL,
"messages": messages,
"tools": tools, # JSON Schema инструментов
"stream": False,
},
timeout=120,
)
result = response.json()
message = result["message"]
# Если модель решила вызвать инструмент
if message.get("tool_calls"):
for tool_call in message["tool_calls"]:
fn_name = tool_call["function"]["name"]
fn_args = tool_call["function"]["arguments"]
print(f"Вызов: {fn_name}({json.dumps(fn_args, ensure_ascii=False)})")
tool_result = call_tool(fn_name, fn_args)
print(f"Результат: {tool_result[:200]}...")
# Добавляем результат в историю
messages.append(message)
messages.append({
"role": "tool",
"content": tool_result,
})
else:
# Модель дала финальный ответ
print(f"\nОтвет: {message['content']}")
return message["content"]
return "Превышено максимальное количество шагов" Запускаем:
run_agent("Проверь, сколько свободного места на диске и какие процессы потребляют больше всего памяти") Вывод будет примерно таким:
--- Шаг 1 ---
Вызов: execute_shell({"command": "df -h /"})
Результат: Filesystem Size Used Avail Use% Mounted on
/dev/sda1 50G 32G 16G 67% /...
--- Шаг 2 ---
Вызов: execute_shell({"command": "ps aux --sort=-%mem | head -10"})
Результат: USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1234 2.1 15.3 ...
--- Шаг 3 ---
Ответ: На диске занято 32 GB из 50 GB (67%). Свободно 16 GB.
Больше всего памяти потребляют: ... Агент сам решил, что нужно выполнить две команды, проинтерпретировал их вывод и дал человеко-читаемый ответ.
Обзор фреймворков
Писать агентов с нуля — полезное упражнение, но для продакшена есть готовые решения.
LangGraph — от создателей LangChain. Агент описывается как граф состояний: узлы — это действия, рёбра — условия переходов. Гибко, но многословно. Подходит для сложных workflow с ветвлениями.
from langgraph.prebuilt import create_react_agent
from langchain_community.chat_models import ChatOllama
from langchain_core.tools import tool
@tool
def get_server_status(hostname: str) -> str:
"""Check if server is reachable and get uptime."""
result = subprocess.run(
["ssh", hostname, "uptime"],
capture_output=True, text=True, timeout=10,
)
return result.stdout or result.stderr
llm = ChatOllama(model="llama3.1:8b")
agent = create_react_agent(llm, tools=[get_server_status])
result = agent.invoke({
"messages": [("user", "Проверь статус серверов web-01 и web-02")]
}) CrewAI — фреймворк для мультиагентных систем. Каждый агент имеет свою роль, цель и набор инструментов. Агенты могут делегировать задачи друг другу. Подходит для сценариев, где нужна специализация: один агент ищет информацию, другой пишет код, третий ревьюит.
Claude tool use — Anthropic встроил function calling прямо в API Claude. Работает надёжнее большинства open-source моделей — Claude реже галлюцинирует имена инструментов и лучше формирует аргументы. Для продакшен-агентов это мой выбор номер один.
Autogen (Microsoft) — ориентирован на диалоговые мультиагентные сценарии. Агенты общаются друг с другом в чат-формате. Интересная архитектура, но на практике сложно контролировать поведение.
Реальные кейсы применения
DevOps-агент. У нас есть внутренний бот, который умеет: проверить статус сервисов, посмотреть логи за последние N минут, перезапустить контейнер, проверить SSL-сертификаты. По сути, дежурный инженер первой линии. За месяц он сэкономил команде примерно 15 часов рутины.
Code review агент. Получает diff из PR, анализирует изменения, ищет типичные проблемы (незакрытые ресурсы, отсутствие обработки ошибок, проблемы с безопасностью). Не заменяет ревью человеком, но ловит очевидные вещи до того, как ревьюер потратит на них время.
Data analysis агент. Получает вопрос на естественном языке («сколько новых пользователей зарегистрировалось за последнюю неделю?»), генерирует SQL-запрос, выполняет его, форматирует результат. Для менеджеров, которые не знают SQL — это магия.
Когда НЕ стоит использовать агентов
Агенты — это не универсальное решение. Есть случаи, когда они вредны:
Детерминированные задачи. Если вы знаете точную последовательность шагов — напишите обычный скрипт. Агент добавит непредсказуемость, задержку и стоимость. Деплой не должен зависеть от того, правильно ли LLM интерпретировала вывод команды.
Критичные операции. Агент с доступом к продакшен-базе — это страшно. Даже с ограничениями. Любой tool calling должен проходить через approval step для деструктивных операций. Пусть агент предложит действие, а человек подтвердит.
Простые CRUD. Если задача сводится к «возьми данные из A, преобразуй, положи в B» — это пайплайн, а не агент. Не нужно тянуть LLM туда, где хватает jq и curl.
Высоконагруженные сценарии. Каждый шаг агента — это вызов LLM (сотни миллисекунд или секунды). Для задач, требующих обработки тысяч элементов в минуту, агенты не подходят.
Безопасность
Отдельно про безопасность, потому что это критично:
# ПЛОХО: агент может выполнить что угодно
def execute_shell(command: str) -> str:
return subprocess.run(command, shell=True, capture_output=True, text=True).stdout
# ЛУЧШЕ: whitelist разрешённых команд
ALLOWED_COMMANDS = ["df", "free", "uptime", "docker ps", "systemctl status"]
def execute_shell_safe(command: str) -> str:
base_cmd = command.split()[0]
if base_cmd not in ALLOWED_COMMANDS:
return f"Error: command '{base_cmd}' is not allowed"
return subprocess.run(command, shell=True, capture_output=True, text=True).stdout Никогда не давайте агенту неограниченный доступ к shell, файловой системе или базе данных в продакшене. Принцип минимальных привилегий здесь критически важен.
Итог
Агентное программирование — это не хайп, а практичный инструмент для определённого класса задач. Ключевое: агент полезен там, где задача требует нескольких шагов, решения о следующем шаге зависят от результата предыдущего, и цена ошибки невысока. Для всего остального — классическая автоматизация надёжнее и дешевле.