Агенти Harness, Loop та Graph: три шари, що лежать в основі надійних AI-агентів
Чому ненадійні агенти зазвичай не мають певного рівня: кріплення навколо моделі, цикл, заснований на даних, та чіткий граф, разом із шаблонами коду та чек-листом для виправлення помилок.
Джерела: Harness, Loop, and Graph: What I Learned Building the Same AI Agent Three Different Ways
Імена, які здаються знайомими, але насправді незрозумілі
Фрази „agent harness engineering“ та „loop engineering“ так часто зустрічаються у соціальних публікаціях та статтях у блогах, що починають здаватися зрозумілими. Знайомість — це омана, і невелика помилка зазвичай її розкриває.
Уявіть дослідницького агента, завданням якого є збір статей з 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 року він мав безстановий ядро, яке працювало за допомогою звичайних балансерів навантаження без функції «sticky sessions», розширення Tasks від AWS для обробки завдань, що тривають довше одного запиту, а також суворішу автентифікацію, засновану на OAuth та OpenID Connect. У джерелі зазначається понад десять тисяч публічних серверів у продакшені та понад 97 мільйонів завантажень SDK щомісяця. Створення інструментального шару сьогодні, який не є 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 простою мовою: стан, пам’ять та оркестрація — Перш ніж писати код: що означають оркестрація, час виконання, агенти зі станом, короткострокова та довгострокова пам’ять, багатоагентні системи та участь людини в LangGraph.
- Створення системи DeepAgents Harness: дозволи, підагенти та схвалення людини — Практичний підхід до створення планувальника системи з використанням DeepAgents: бекенди для файлів, правила дозволів, підагенти, механізми схвалення, навички та MCP, а також ситуації, коли оцінка моделі все ще дає помилки.