Главная / Статьи

Инженерия хардвера: среда выполнения, превращающая LLM в агента

Что на самом деле позволяет работать агенту для программирования: цикл, механизм вызова инструментов, способный выдерживать ошибочные запросы, а также функции разрешения/запроса/отклонения прав доступа, показанные на примере примерно 600 строк простого кода на Python.

Издатель: REACTAPP.TOP13 мин чтения2950 слов

Источники: Harness Engineering: Build the Runtime That Turns an LLM Into an Agent (Part 1/3)

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

Каждый, кто использовал API для дополнения текста в чате, знает про простую часть работы: отправка сообщений, получение ответа, возможно, вызов функций. Сложная часть — это путь от этого до агента, способного находиться в репозитории, редактировать файлы, запускать тесты и восстанавливаться после повреждения чего-либо. Фреймворк преодолевает этот путь. Это среда выполнения вокруг модели: инструменты, разрешения, изоляция, обработка контекста, управление памятью и восстановление от ошибок. Модель делает только одно — принимает текст и возвращает текст. Фреймворк — это программа, которая повторяет этот цикл, запускает инструменты, обеспечивает безопасность и взаимодействует с вами в терминале. Если модель — это двигатель, то фреймворк — это автомобиль.

Сам по себе модель языка не может открыть ваши файлы, запустить pytest, отредактировать код, вспомнить разговор прошлого вторника или помешать себе ввести команду rm -rf /. Всё, что она может делать, — это генерировать следующее сообщение. Всё остальное требует дополнительных механизмов:

  • Цикл: постоянно вызывать модель до тех пор, пока она не перестанет запрашивать инструменты.
  • Инструменты: средства для выполнения действий (оболочка, файлы, редактирование).
  • Разрешения: запрашивать разрешение перед выполнением рискованных действий и блокировать наиболее опасные из них.
  • Песочница: препятствовать выполнению опасных действий на уровне операционной системы.
  • Контекст: ограничивать объем разговора так, чтобы его можно было отправить.
  • Память: хранение навыков, списка дел, файла сессии, который можно возобновить.
  • Подагенты: исследование кодовой базы без перегрузки основного разговора.

В этой статье рассматриваются первые три аспекта, которые превращают чат-бота в агента. Остальные элементы — сандбоксинг, управление контекстом, память и субагенты — представляют собой дополнительные слои на той же основе.

Чат-бот против агента

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

Люди начали называть это инженерией креплений — довольно пафосное название для простой идеи: Claude Code, Cursor и Codex являются лучшими инструментами именно потому, что у них лучшие крепления, а не только лучшие модели.

Пример помогает лучше, чем определения. Вы вводите: «Исправьте несработавший тест в test_parser.py.» Далее следует следующее:

Turn 1:  LLM reads the test file         → bash("cat test_parser.py")
Turn 2:  LLM runs the test to see error  → bash("pytest test_parser.py")
Turn 3:  LLM reads the source file       → read_file("parser.py")
Turn 4:  LLM edits the bug               → str_replace("parser.py", old, new)
Turn 5:  LLM re-runs the test            → bash("pytest test_parser.py")
Turn 6:  Tests pass → LLM says "Done! Fixed the off-by-one error in line 42."

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

Лексика

Несколько терминов встречаются повторно.

  • LLM: функция, преобразующая список сообщений в ещё одно сообщение. У неё нет памяти о предыдущих вызовах API. Если вы хотите, чтобы казалось, что она помнит, нужно снова отправить историю.
  • Токен: единица текста, которую считает модель; примерно три четверти английского слова. Фраза «Hello world» состоит примерно из двух токенов, а 100-строчный файл на Python — примерно из 500 токенов.
  • Окно контекста: количество токенов, которые модель может видеть одновременно. Значения около 128 тыс. для моделей класса GPT-4 и примерно 200 тыс. для Claude кажутся огромными, пока агент 40 раз не вставит результат выполнения команды grep -r в чат.
  • Вызов инструмента: модель не запускает ваш код. Она формирует запрос вида {"name": "bash", "arguments": {"command": "ls"}}, после чего ваш Python-скрипт выполняет команду ls и возвращает результат.
  • Без состояния: каждый вызов call_llm(messages) является автономным, а список messages служит памятью.

Не требуется опыт в машинном обучении. Достаточно базовых знаний Python (функции, циклы, словари, try/except), небольшого опыта работы с API в стиле OpenAI и наиболее распространённых команд терминала (ls, grep, cat).

Эталонная реализация

Пример агента состоит примерно из 600 строк кода на Python без использования каких-либо фреймворков; в нем используются четыре библиотеки: openai, rich, prompt-toolkit и pyyaml. Приведённые ниже фрагменты — это реальный код, а не псевдокод, и к концу вы сможете узнать те же элементы в приложениях Claude Code или Cursor. Чтобы следовать за примером, скопируйте проект и установите его в режиме редактирования:

git clone https://github.com/shrinidhi-mahishi/coding-harness.git
cd coding-harness
pip install -e .

Может использоваться любой конечный пункт, совместимый с OpenAI: OpenRouter, локальный сервер или сам OpenAI. Укажите настройки в файле ~/.agents/env:

BASE_URL=https://openrouter.ai/api/v1
API_KEY=sk-or-...
MODEL=deepseek/deepseek-v4-flash

Настоящие переменные окружения переопределяют содержимое этого файла. Команда coding-harness, указанная в pyproject.toml как точка входа в coding_harness.agent:main, запускает терминальную сессию. Вы вводите команды, и внутренний цикл продолжает работу до тех пор, пока у агента не останется запросов к инструментам.

Вся конструкция состоит из семи компонентов: цикл агента («сердцебиение» системы), инструменты («руки»), разрешения (кто может что делать), песочница (безопасность, обеспечиваемая ядром), инжиниринг контекста (чистое окно для работы), навыки, задачи и сессии (память), а также субагенты (использование контекста во временных целях). Здесь рассматриваются только первые три компонента.

Важные файлы:

coding_harness/agent.py         # two nested loops
coding_harness/llm.py           # one API call + the system prompt
coding_harness/tools.py         # tool functions + defensive execute()
coding_harness/permissions.py   # allow / ask / deny
coding_harness/config.py        # BASE_URL, API_KEY, MODEL

Не существует ни базы данных, ни веб-сервера, ни библиотеки оркестрации. «Фреймворк» состоит из двух циклов while True и словаря с функциями инструментов. При первом ознакомлении просмотрите файлы config.py, llm.py, agent.py, tools.py и permissions.py в указанном порядке.

1. Цикл агента

Claude Code, Cursor и Codex используют один и тот же цикл:

  1. Отправлять диалог модели.
  2. Если модель запрашивает инструмент, запустить его.
  3. Добавить результат в диалог и вернуться к шагу 1.
  4. Когда запросов к инструментам больше нет, работа завершена, и отображается ответ.

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

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

messages = [
    {"role": "system",    "content": "You are a coding agent..."},
    {"role": "user",      "content": "Fix the failing test"},
    {"role": "assistant", "content": "I'll start by reading the test file."},
    {"role": "tool",      "content": "... contents of test file ..."},
]

Существует четыре роли. Роль системы хранит постоянные инструкции, пользователь — это вы, ассистент — это модель, а инструмент передает результаты, полученные от нее. Модель принимает список и генерирует следующий элемент; этот список является ее единственной «памятью». Скрытого второго хранилища не существует, поэтому если список неверен, агент тоже действует неверно.

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

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

while True:
    message, usage = call_llm(messages)

    messages.append(message.model_dump(exclude_none=True))
        if message.content:
            ui.agent(message.content)
        # No tool calls? The agent is done thinking.
        if not message.tool_calls:
            break
        # Execute each tool and feed the result back
        for tool_call in message.tool_calls:
            args, result = execute(tool_call)
            ui.tool(tool_call.function.name, args, result)
            messages.append({
                "role": "tool",
                "tool_call_id": tool_call.id,
                "content": result,
            })

Код находится в файле agent.py. Функция call_llm(messages) отправляет полную историю запросов, и модель возвращает текст, результаты вызовов инструментов или и то, и другое. Функция model_dump() просто преобразует объект API в словарь, который можно сохранить. Если вызовов инструментов нет, цикл прерывается. В противном случае они выполняются, их результаты отображаются, добавляются к истории, после чего цикл повторяется, чтобы модель могла увидеть произошедшее и решить, следует ли продолжать.

Внешний цикл ещё короче:

while True:
    user_input = ui.ask()
    if not user_input:
        break

    messages.append({"role": "user", "content": user_input})
        # ... inner agent loop runs here ...

Ваш текст добавляется в список, внутренний цикл выполняется до завершения работы агента, после чего программа снова ожидает. Здесь нет машин состояний — только два вложенных оператора while True, таких же, как у Claude Code и Cursor. Модель говорит или запрашивает инструменты, результаты работы инструментов возвращаются в messages, и цикл повторяется.

2. Инструменты: руки для модели

Без инструментов цикл представляет собой просто чат-бота с дополнительными шагами. Инструменты позволяют модели выполнять задачи: читать файлы, запускать команды, писать код, искать в репозитории.

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

Во многих учебниках упускается один важный момент: сами модели никогда не выполняют инструменты. Модель генерирует запрос вроде «запустить эту команду», а сам Python выполняет работу и возвращает результат. У модели нет оболочки — только форма для ввода данных.

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

bash является как самым мощным инструментом, так и самым вызывающим опасения. Вот его пример из файла tools.py:

def bash(command: str) -> str:
    """Run a shell command and return its combined stdout and stderr."""
    try:
        result = sandbox.run(command)
    except subprocess.TimeoutExpired as expired:
        return (
            f"Timed out after {expired.timeout}s and was killed. "
            "Narrow it down - search inside the working directory rather than /."
        )
    return (result.stdout + result.stderr) or "(no output)"

Модель видит следующую схему:

{
    "type": "function",
    "function": {
        "name": "bash",
        "description": "Run a shell command and return its combined stdout and stderr.",
        "parameters": {
            "type": "object",
            "properties": {
                "command": {
                    "type": "string",
                    "description": "The shell command to run",
                }
            },
            "required": ["command"],
        },
    },
}

Когда модель нуждается в списке содержимого каталога, она генерирует {"name": "bash", "arguments": {"command": "ls -la src/"}}. Код выполняет этот код, возвращает результат, и модель выбирает следующий шаг.

У агента семь инструментов, каждый из которых представляет собой функцию вместе с схемой:

  • bash выполняет команду оболочки внутри песочницы.
  • read_file читает файл.
  • write_file создаёт или перезаписывает файл.
  • str_replace выполняет точечную замену одной строки на другую.
  • read_skill загружает руководство SKILL.md.
  • write_todos заменяет весь список задач.
  • task запускает подагента и возвращает только его окончательный ответ.

Функция bash представляет собой простой оберточный код: она запускает sandbox.run с таймаутом в 60 секунд и возвращает вывод в стандартный и ошибочный потоки, либо сообщение об истечении времени с рекомендацией искать внутри каталога проекта, а не по всей директории /.

Рассматривайте вызовы инструментов как ненадежный ввод

Во многих постах типа «создайте агента за 20 строк» этот момент игнорируется. Вызов инструмента — это текст, написанный моделью, поэтому модель угроз соответствует веб-форме. Имя может отсутствовать, аргументы — не быть корректным JSON, а типы — неверными. Любой из этих факторов может привести к прерыванию сессии, если код не подготовлен должным образом.

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

execute() — защитный диспетчер, также находящийся в файле tools.py, работает в следующем порядке:

  1. Анализирует JSON-данные; если они повреждены, возвращается ошибка с указанием того, что аргументы не являются валидным JSON.
  2. Поиск по имени инструмента; если он неизвестен, выводится список реально существующих инструментов.
  3. Обращение к функции permissions.check() для получения решения о запрете, запросе или разрешении.
  4. Вызов соответствующей функции на Python.
  5. Ловля исключений типа TypeError и других ошибок; возвращается строка «Error: ...», но никогда не возвращается стек вызовов.

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

def execute(tool_call):
    name = tool_call.function.name

# The model might send broken JSON
    try:
        args = json.loads(tool_call.function.arguments)
    except json.JSONDecodeError as broken:
        return {}, f"Error: arguments were not valid JSON ({broken})."
    # The model might hallucinate a tool name
    if name not in TOOLS:
        return args, f"Error: no tool named '{name}'. Available: {', '.join(TOOLS)}."
    # Run through the permission layer, then call the function
    try:
        action, reason = check(name, args)
        if action == "deny":
            return args, f"Blocked by policy: {reason}"
        if action == "ask" and not ui.approve(reason):
            return args, "The user denied this tool call."
        return args, TOOLS[name](**args)
    except TypeError as mismatch:
        return args, f"Error: wrong arguments for {name} ({mismatch})."
    except Exception as failure:
        return args, f"Error: {name} failed - {type(failure).__name__}: {failure}"

Предположим, что модель создаёт инструмент под названием search_code. JSON-данные в порядке, но название отсутствует в списке TOOLS, поэтому диспетчер сообщает, что такого инструмента нет, и перечисляет доступные: bash, read_file, write_file, str_replace, read_skill, write_todos и task. В следующем ходе модель использует bash с командой grep -r "auth" .. Ничего не сломалось, и это именно то, что нужно: неправильный вызов инструмента никогда не должен приводить к сбою сессии. Ошибки — это просто дополнительные данные ввода.

3. Разрешения: кто может что делать

При наличии bash агент становится полезным, но именно он позволяет выполнять команды вроде rm -rf / или отправлять конфиденциальную информацию куда-либо. Если агент может выполнять любые команды, он может удалять файлы, загружать мусор или связываться с внешними системами. Если вы будете подтверждать каждую команду, весь день уйдёт на одобрение простой команды ls. Необходим третий путь.

Ответ — три уровня, как светофор:

Уровень Значение Примеры
allow работает бесшумно ls, grep, cat, git status, pytest
ask ожидает вашего решения mkdir, python script.py, npm install
deny блокируется даже при согласии rm, sudo, curl, wget, git push

Правила представляют собой шаблоны glob, где * соответствует чему угодно, и побеждает последнее подходящее правило. Из файла permissions.py:

BASH_RULES = {
    "*":                   "ask",     # default: ask for everything
    # read-only: let them through
    "ls*":                 "allow",
    "cat *":               "allow",
    "grep *":              "allow",
    "git status*":         "allow",
    "git diff*":           "allow",
    "pytest*":             "allow",
    # risky: never, even if the user says yes
    "rm *":                "deny",
    "sudo *":              "deny",
    "curl *":              "deny",
    "git push*":           "deny",
}

Значение * устанавливает стандартный режим запроса разрешения. Команды, доступные только для чтения, разрешаются, затем опасные команды блокируются полностью, а все остальные, такие как python script.py или npm install, остаются в режиме запроса разрешения.

Модель может использовать хитрые приемы. Команда вроде grep foo | rm -rf / начинается безвредно, но заканчивается катастрофически. Поэтому проверщик разделяет команду по символам |, ; и &&, учитывая кавычки, и применяет самое строгое решение на основе каждой части.

def decide(command):
    """Rate every part of a compound command; the strictest verdict wins."""
    verdicts = []
    for part in split_command(command):
        action = "ask"
        for pattern, rule in BASH_RULES.items():
            if fnmatch(part, pattern):
                action = rule
        verdicts.append(action)

    for strictest in ("deny", "ask"):
            if strictest in verdicts:
                return strictest
        return "allow"

На практике команда grep foo | rm -rf / отклоняется, поскольку запрещена команда rm. Команда ls && mkdir test запрашивает разрешение, так как команда mkdir не включена в список разрешённых действий. Команда cat file.py | grep TODO разрешена, поскольку обе части являются только для чтения.

Разрешения и сандбоксы выполняют разные функции. Разрешения касаются пользовательского опыта: стоит ли прерывать пользователя ради этого действия? Они не являются средством безопасности, поскольку умная команда может выглядеть как ls, но на самом деле быть чем-то другим. Сандбокс является средством безопасности, поскольку он определяет, допускается ли действие вообще с технической точки зрения. Настоящему агенту нужны и те, и другие.

Что это даёт нам

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

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