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

Агенты Harness, Loop и Graph: три уровня, лежащих в основе надежных ИИ-агентов

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

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

Источники: Harness, Loop, and Graph: What I Learned Building the Same AI Agent Three Different Ways

Имена, кажущиеся знакомыми, но на самом деле не понятные

Фразы «инженерия управления агентом» и «инженерия циклов» встречаются так часто в постах в социальных сетях и статьях на блогах, что создаётся впечатление, будто их все понимают. Однако кажущаяся знакомость обманчива, и небольшая ошибка обычно это выявляет.

Представьте исследовательского агента, задачей которого является сбор статей с arXiv, их краткое изложение и составление еженедельного обзора. Три пробных запуска проходят успешно. Затем запрос к API с ограничением скорости проваливается бесшумно — код ничего не замечает, — и агент начинает создавать краткое содержание статей, которые он даже не открывал. Вина не в модели и не в запросе; проблема заключается в отсутствии способа у кода отличить действительно прочитанную статью от правдоподобной выдумки. Эта проблема реальна, у неё есть название, и корректировки запросов её не решают.

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

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

Индустрия отреагировала на это тем, что больше не рассматривает «агента» как единый элемент. Речь идет о трех отдельных инженерных проблемах, объединенных в модели.

LAYER            QUESTION IT ANSWERS                         FAILS AS
----------------------------------------------------------------------
Harness           What can the agent see and touch?          Missing tools,
                   (environment, tools, memory,               lost state, blown
                   permissions, execution limits)             budgets, leaks
----------------------------------------------------------------------
Loop               How does one attempt turn into a           Confident wrong
                   correct one? (retry, verify, stop)          answers, infinite
                                                                spinning, silent
                                                                failure
----------------------------------------------------------------------
Graph              What is the actual shape of the work?       Wrong step order,
                   (sequence, branches, parallel paths,         no recovery path,
                   human checkpoints)                           no visibility
----------------------------------------------------------------------

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

Инструментарий: что может видеть и использовать агент

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

Распространённой первоначальной моделью мышления относительно «предоставления агенту инструментов» является написание нескольких функций на Python и описание их в системном промпте. Это подходит для демонстрации. Однако такой подход не справляется с задачами, требующими более одного окна контекста, или с вызовами инструментов, которые терпят неудачу в 2 часа ночи без наблюдения. На практике оказывается, что такая система несёт на себе шесть функций.

  • Внедрение контекста определяет, что видит модель перед каждым шагом рассуждения: сообщение пользователя, а также загруженные документы, предыдущие разговоры, состояние проекта и корпоративные правила. Чрезмерная коррекция приводит к проблемам. Включение всей структуры файлов и истории изменений в git в каждый вызов снижает точность, поскольку модель затопляется нерелевантными данными, и более компактный, тщательно отобранный контекст всегда эффективнее большого необработанного контекста.
  • Интерфейсы действий преобразуют решение в конкретный эффект в мире программы: это может быть запрос к API, команда в терминале, запись в базу данных или клик в браузере. Здесь играет роль Протокол контекста модели, который сильно изменился в этом году. Он появился в конце 2024 года как стандартный способ для агентов находить и вызывать инструменты, позволяя командам избегать необходимости создания индивидуальной интеграции для каждого API. К моменту разработки спецификации в июле 2026 года у него уже была безсостоянийная основа, работающая через обычные балансировщики нагрузки без использования функции «липких сессий», расширение Tasks от AWS для обработки задач, длительность выполнения которых превышает один запрос, а также более строгая система авторизации, основанная на OAuth и OpenID Connect. В источнике указывается, что существует более десяти тысяч публичных серверов в производстве, а количество загрузок SDK превышает 97 миллионов в месяц. Сегодня для создания инструментов на основе слоя, отличного от MCP или аналогичных решений, требуется веское обоснование.
  • Персистентность — это всё то, что должно сохраняться после перезагрузки, сбоя или закрытия окна контекста, такое как точки контроля, состояние сессии, векторная память и история изменений в Git. Первый исследовательский агент не сохранял ничего из этого, поэтому прерывание соединения уничтожало сорок минут выполненной работы. Создание небольшой JSON-точки контроля после каждой завершённой подзадачи способствовало надёжности больше, чем любые изменения в инструкциях.
  • Механизмы управления выполнением включают правила повторных попыток, таймауты, лимиты на использование токенов и ресурсов, маршрутизацию моделей, ограничения на создание дочерних агентов, а также этапы, на которых человек обязан подтвердить действия. Без них агент, застрявший в цикле попыток и сбоев на всю ночь, может превратить выполнение задачи в бесконечный процесс.
  • Безопасность и управление включают принцип минимальных привилегий при доступе к инструментам, обработку конфиденциальных данных, использование сандбоксов, логи аудита и списки разрешенных действий. Код, написанный моделью, должен всегда выполняться в сандбоксе. Один из кодирующих агентов был пойман на попытке выполнить команду rm -rf в папке, которую он по ошибке принял за артефакт сборки. Злого умысла не было — это была ошибка в сочетании с наличием прав на нанесение ущерба.
  • Наблюдаемость подразумевает отслеживание действий агента, логи вызовов инструментов, использование токенов, время задержки и результаты оценки. Всё это кажется скучным, пока агент не начинает странно вести себя в производственной среде, и тогда отсутствуют записи о его реальных действиях по сравнению с тем, что предполагалось.

Ниже приведен минимальный, но функциональный инструмент на Python. Им управляет локальная модель Ollama, поэтому API-ключ не требуется, а переключение на хостинговую модель занимает всего одну строку кода.

import json
import subprocess
import requests
from datetime import datetime
OLLAMA_URL = "http://localhost:11434/api/chat"
MODEL = "llama3.1"
# --- Action surface: the only two things this agent is allowed to do ---
def read_file(path: str) -> str:
    with open(path, "r") as f:
        return f.read()[:4000]  # cap it, don't blow the context window
def run_tests() -> str:
    result = subprocess.run(
        ["pytest", "-q"], capture_output=True, text=True, timeout=60
    )
    return result.stdout[-2000:] + result.stderr[-2000:]
TOOLS = {"read_file": read_file, "run_tests": run_tests}
TOOL_SCHEMA = [
    {
        "type": "function",
        "function": {
            "name": "read_file",
            "description": "Read a file from the project",
            "parameters": {
                "type": "object",
                "properties": {"path": {"type": "string"}},
                "required": ["path"],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "run_tests",
            "description": "Run the project's pytest suite",
            "parameters": {"type": "object", "properties": {}},
        },
    },
]
# --- Persistence: cheap but real ---
def checkpoint(state: dict, path: str = "checkpoint.json"):
    with open(path, "w") as f:
        json.dump({**state, "saved_at": datetime.utcnow().isoformat()}, f)
# --- Execution control: hard ceilings, not suggestions ---
MAX_TOOL_CALLS = 8
def harness_run(user_task: str):
    messages = [{"role": "user", "content": user_task}]
    calls_made = 0
    while calls_made < MAX_TOOL_CALLS:
        response = requests.post(
            OLLAMA_URL,
            json={"model": MODEL, "messages": messages, "tools": TOOL_SCHEMA, "stream": False},
        ).json()
        msg = response["message"]
        messages.append(msg)
        if not msg.get("tool_calls"):
            return msg["content"]  # model is done, no more tools needed
        for call in msg["tool_calls"]:
            name = call["function"]["name"]
            args = call["function"].get("arguments", {})
            if name not in TOOLS:
                result = f"error: tool {name} is not permitted"
            else:
                result = TOOLS[name](**args)
            messages.append({"role": "tool", "content": str(result)})
            calls_made += 1
            checkpoint({"messages": messages, "calls_made": calls_made})
    return "stopped: hit the tool call ceiling before finishing"

Здесь нет ничего сложного, и в этом заключается урок: инструмент для управления моделью — это тщательный учет действий в отношении модели, которая сама не ведет учета. Для хостинговой модели замените блок requests.post на вызов SDK OpenAI или Anthropic. Схема инструмента и процедура сохранения состояния остаются без изменений, поскольку они не зависят от того, кто отвечает.

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

Цикл: как одна попытка превращается в успешную

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

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

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

Техника Ральфа Уиггума и причины эффективности примитивных циклов

Паттерн под названием «Ральф Уиггум» стал популярным после того, как инженер Джеффри Хантли описал его в 2025 году. По сути, он крайне прост: цикл в bash постоянно передаёт один и тот же запрос кодирующему агенту до завершения задачи, причём прогресс фиксируется в файлах и коммитах git, а не в памяти модели о ходе чата.

#!/bin/bash
# ralph.sh - crude but surprisingly effective
while true; do
  claude -p "$(cat PROMPT.md)" --dangerously-skip-permissions
  if git diff --quiet HEAD~1 HEAD -- STATUS.md && grep -q "DONE" STATUS.md; then
    echo "Task complete."
    break
  fi
  sleep 5
done

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

Вот небольшой цикл того же типа, основанный на доказательствах, который также можно запустить локально с Ollama; он рассматривает неудачный тест как единственно приемлемое значение для фразы «ещё не закончено».

def evidence_loop(task_description: str, max_attempts: int = 5):
    attempt = 0
    history = []
    while attempt < max_attempts:
        attempt += 1
        code = generate_patch(task_description, history)   # calls the model
        apply_patch(code)
        test_output = run_tests()                          # external evidence
        passed = "failed" not in test_output.lower()
        history.append({
            "attempt": attempt,
            "patch": code,
            "test_output": test_output,
            "passed": passed,
        })
        if passed:
            return {"status": "success", "attempts": attempt}
        # feed the *actual failure*, not a vague "try again"
        task_description = (
            f"{task_description}\n\nPrevious attempt failed with:\n{test_output}\n"
            "Fix the specific failure above."
        )
    return {"status": "gave_up", "attempts": attempt, "history": history}

Обратите внимание на последнюю строку перед возвратом, ведь именно она несёт смысл. Цикл не говорит «постарайтесь сильнее». Он подаёт модели реальную причину неудачи теста, поскольку конкретные внешние доказательства делают вторую попытку лучше первой, а не просто случайную повторную попытку. У большинства агентов наибольший прирост надёжности достигается путём замены вопроса «всё ли правильно?» на подлинную внешнюю проверку того типа, который соответствует заданию.

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

Граф: когда у задания есть реальные ветви

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

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

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

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

from langgraph.graph import StateGraph, END
from typing import TypedDict

class AgentState(TypedDict):
    task: str
    draft: str
    review_passed: bool
    attempts: int
def draft_node(state: AgentState) -> AgentState:
    state["draft"] = generate_draft(state["task"])
    return state
def review_node(state: AgentState) -> AgentState:
    state["review_passed"] = run_validator(state["draft"])
    state["attempts"] += 1
    return state
def needs_human(state: AgentState) -> str:
    if state["review_passed"]:
        return "approved"
    if state["attempts"] >= 3:
        return "escalate"   # branch: give up automating, ask a human
    return "retry"
graph = StateGraph(AgentState)
graph.add_node("draft", draft_node)
graph.add_node("review", review_node)
graph.set_entry_point("draft")
graph.add_edge("draft", "review")
graph.add_conditional_edges(
    "review",
    needs_human,
    {"approved": END, "retry": "draft", "escalate": "human_approval"},
)

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

В этом году Microsoft добавила нечто подобное в AutoGen под названием GraphFlow по той же причине. Несколько агентов или ветвящиеся рабочие процессы затрудняют понимание имплицитного цикла, тогда как граф можно изучить напрямую.

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

FRAMEWORK          ORCHESTRATION MODEL         STRONGEST AT               STATE / PERSISTENCE
--------------------------------------------------------------------------------------------
LangGraph           Directed graph, typed        Complex branching,        Built-in checkpointing,
                     state, conditional edges     long-running workflows,   time-travel debugging
                                                   human-in-the-loop
--------------------------------------------------------------------------------------------
CrewAI               Role-based "crew" of         Fast prototyping,         Sequential task output
                      agents, sequential or        multi-agent brainstorm   passing, lighter state
                      hierarchical process         style collaboration      model
--------------------------------------------------------------------------------------------
OpenAI Agents SDK     Explicit agent handoffs      Minimal-friction start   Ephemeral context
                                                    inside the OpenAI        variables by default
                                                    ecosystem
--------------------------------------------------------------------------------------------
AutoGen / GraphFlow   Multi-agent conversation     Agent-to-agent           Conversation history
                       plus optional graph layer   negotiation, graph-      plus graph state
                       for explicit control        based orchestration
--------------------------------------------------------------------------------------------

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

Как располагаются три слоя

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

+--------------------------------------------------------------+
|  GRAPH   - decides which node runs next, where branches      |
|            are, where a human has to approve                 |
+--------------------------------------------------------------+
|  LOOP    - inside a given node, drives attempt -> verify ->  |
|            revise until the evidence says stop               |
+--------------------------------------------------------------+
|  HARNESS - underneath both, provides the tools, memory,      |
|            permissions, and limits that make any of it       |
|            possible to execute safely                        |
+--------------------------------------------------------------+
|  MODEL   - the reasoning engine none of the above exists     |
|            without                                           |
+--------------------------------------------------------------+

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

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

SYMPTOM                                    LIKELY LAYER TO CHECK FIRST
----------------------------------------------------------------------
Agent doesn't know something it should      Harness -> context injection
Agent "can't" do something it should be     Harness -> action surface /
  able to do                                 permissions
Agent forgets earlier progress after a      Harness -> persistence
  restart or long run
Costs or run time spiral unexpectedly       Harness -> execution control
Agent confidently reports success on        Loop -> swap confidence checks
  work that's actually wrong                 for external evidence
Agent retries the same mistake              Loop -> is it seeing the actual
  repeatedly without improving                failure, or just "try again"?
Agent takes the wrong path when a task      Graph -> is the branching logic
  has more than one possible outcome          explicit, or implicit in a
                                               single prompt?
No human ever gets a chance to catch a      Graph -> is there an approval
  bad decision before it ships                node, or is it all automatic?

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

Открытые вопросы

Некоторые вопросы остаются нерешёнными, и было бы неправильно делать окончательные выводы, притворяясь более уверенными, чем позволяют доказательства.

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

Во-вторых, подход Ральфа Уиггума кажется несколько несовершенным даже тогда, когда он работает. Предоставление модели одного запроса в замкнутом цикле и использование истории git в качестве единственной «памяти» напоминает временное решение проблемы без настоящей способности к сохранению информации. Этот подход подходит для небольших, узко определенных корректировок, но для ситуаций с настоящими ветками или последствиями ему пока нельзя доверять.

В-третьих, структуры графов могут продолжать меняться. Функция сохранения состояния в LangGraph отличная, но команды иногда выбирают целую структуру графов для трехэтапного процесса, который никогда не делится на ветки, просто потому что это модно. Рабочий процесс, представляющий собой прямую линию, может оставаться таковым.

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

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

Модель привлекает к себе внимание уже три года, но надёжность в реальных условиях обеспечивается инженерными решениями, связанными с ней. Эта работа гораздо менее привлекательна и гораздо более реалистична, чем утверждения о том, что модель недостаточно умна. Большинство времени она действительно умна. Просто ей не предоставлялись необходимая среда, обратная связь или «карта», которые позволили бы ей это показать.

Связанные материалы

  • Агенты, управляемые конфигурацией: создайте один агент для работы в реальном времени и быстро специализируйте его — Почему компаниям следует создавать один базовый агент и варьировать его с помощью внешних конфигураций: каталогов, реестров навыков, механизмов управления, процедур тестирования, LangGraph и способов обработки сбоев.
  • Использование механизмов harness, loop и graph: три вложенных уровня системы кодирующих агентов — Симптомы сбоев указывают на уровень, который необходимо исправить. Как взаимодействуют механизмы harness, loop и graph, где полезны многократные агенты, чего не могут гарантировать адверсарные проверки, и чек-лист для диагностики.
  • Практические заметки: MCP 2026: Протокол вырос — вот что такое производственный ИИ — Подробное руководство по практическим заметкам: MCP 2026: Протокол вырос — вот что такое производственный ИИ: контракты, проверки и блоки кода для команд, использующих эту модель.