Вы запускаете мультиагентную систему. Генератор пишет код. Оценщик проверяет. Через час система возвращает 10 000 строк, завернутых в бесконечный цикл рефакторинга. Потому что контекст вытек, агенты забыли, что строили, и теперь они отчаянно переписывают модуль логина, хотя задача была "дописать платежный шлюз".
Знакомо? Это не баг LLM, это отсутствие обвязки. Если вы читали нашу статью про проклятие длинного контекста — вы знаете, что агенты деградируют на длинных сессиях. Но мультиагентная архитектура без жесткой смирительной рубашки — это гарантированный путь к хаосу.
В этой статье я расскажу про harness design — архитектурный паттерн, который превращает группу агентов в предсказуемый конвейер. Не очередной "умный промпт", а системный слой, управляющий итерациями, контекстом и валидацией.
Проблема: мультиагент не равен автоматизации
В статье про кейс Anthropic с +90% производительности мы видели, как разделение на роли резко улучшает результат. Но есть подвох: в исследовании Anthropic каждый агент работал с четкими границами контекста. В реальном проекте эти границы размываются.
Возьмем задачу "дописать приложение". У вас есть существующий код, тесты, требования. Задача длится не один вызов, а серию итераций. Классический паттерн генератор-оценщик (вдохновленный GAN) выглядит так:
- Агент-генератор пишет код.
- Агент-оценщик проверяет его логику, покрытие, стиль.
- Оценщик отправляет замечания назад. Цикл повторяется.
Звучит логично. Но на практике:
- Генератор переписывает целые файлы, хотя нужно было исправить одну строку.
- Оценщик начинает галлюцинировать зависимости — требует импортировать несуществующие библиотеки.
- После 5 итераций контекст становится огромным, агенты "забывают" первоначальный запрос.
- Стоимость API вызовов взлетает, а качество падает.
Причина: нет развязки. Агенты напрямую общаются через общий контекст. Они не управляют памятью, не знают, когда остановиться, и не умеют восстанавливаться после сбоя.
Решение: Harness — обвязка, а не оркестратор
Оркестратор (типа LangGraph, AutoGen) просто вызывает агентов по очереди. Harness — это системный слой, который владеет состоянием. Он решает:
- Когда запускать агента.
- Что он видит (контекстная сегментация).
- Что делать с его выводом (валидация, откат, merge).
- Когда остановиться (критерий схождения).
Представьте, что вы — техлид, а агенты — джуны. Вы не даете им весь код проекта и не ждете, пока они сами разберутся. Вы выдаете таск с четким скоупом, проверяете пулл-реквест и откатываете, если тесты упали. Harness — это ваша роль, автоматизированная.
Архитектура включает три компонента:
- State Machine — управляет жизненным циклом задачи: инициализация, генерация, валидация, откат, завершение.
- Context Manager — режет контекст на порции, чистит историю, извлекает релевантные части из репозитория. Без него контекст распухает.
- Validation Gate — запускает юнит-тесты, линтеры, проверяет API-контракты. Не доверяет LLM-оценщику, а использует железные проверки.
Пошаговый план: как построить Harness для дописывания приложения
Допустим, у вас есть проект на Python с тестами. Нужно добавить новый эндпоинт. Вместо того чтобы отдать это одному агенту, вы создаете Harness.
1 Определите миссию и скоуп
Harness получает задачу и сразу создает JSON-спецификацию: цель, изменяемые файлы, ограничения (не трогать ядро, не менять API существующих методов). Это предотвращает рефакторинг половины проекта.
2 Контекстный бутстрап
Context Manager вычитывает только те файлы, которые относятся к задаче: модель, роутер, тесты. Он убирает лишнее: конфиги, документацию, логи. Это снижает шум и стоимость.
3 Цикл генерации и валидации с откатом
Генератор получает чистый контекст и задачу. Он производит diff. Harness применяет diff к временной ветке, запускает тесты. Если тесты упали — харнес откатывает изменения, логирует ошибку и отправляет генератору только текст ошибки (без всего контекста, который уже был). Это ключевая фишка: обратная связь минимальна, чтобы не засорять контекст.
4 Параллельные треки для больших задач
Если задача затрагивает несколько модулей, Harness может запустить параллельные циклы для каждого модуля, а затем автоматически смержить изменения, проверяя интеграционные тесты. Это как разделение на суб-агентов, о котором мы говорили в статье про суб-агентов — каждый отвечает за свой кусок.
Нюансы и ошибки, которые взорвут вашу архитектуру
Я видел, как команды копируют паттерн генератор-оценщик и получают не улучшение, а головную боль. Вот три грабли.
1. Контекстный бюджет глобальный, а не локальный
Часто харнес передает агенту всю историю итераций. Через 10 шагов контекст становится гигантским, агент начинает видеть в коде то, чего нет. Как надо: каждый вызов агента — это фиксированный контекст: скоуп задачи + текущий diff + одно сообщение от валидатора. История хранится только в State Machine, но не закидывается агенту.
2. Синхронные блокировки
Если валидатор ждет, пока генератор закончит, а генератор ждет валидатора — вы получили deadlock. Как надо: харнес запускает генератор, получает результат, отправляет его валидатору, но не блокирует поток. Может быть параллельная очередь задач. Вспомните архитектуру без роутинга — иногда лучше убрать посредников и сделать прямой пайплайн с очередями.
3. Доверие LLM-оценщику
Оценщик часто пропускает баги, потому что его промпт недостаточно строгий. Как надо: валидация должна включать исполнение кода — юнит-тесты, type checker, линтер. Оценщик-LLM используется только для проверки соответствия требованиям ("использовал ли правильный паттерн?"). Без железных проверок ваш харнес — это просто дорогая переписка агентов.
Пример: Harness-класс на Python
Ниже — упрощенный, но рабочий скелет. Он не зависит от конкретного LLM-провайдера. Суть в архитектуре.
import subprocess, json, tempfile, os
from enum import Enum
class Step(Enum):
BOOTSTRAP, GENERATE, VALIDATE, ROLLBACK, DONE = range(5)
class Harness:
def __init__(self, repo_path: str, max_iterations: int = 10):
self.repo_path = repo_path
self.state = Step.BOOTSTRAP
self.iteration = 0
self.max_iterations = max_iterations
self.diff_stack = []
def context_for_generator(self, task_spec: dict) -> str:
# загружаем только релевантные файлы
files = task_spec['input_files']
context = '\n\n'.join(open(os.path.join(self.repo_path, f)).read() for f in files)
return context + '\nЗадача: ' + task_spec['instruction']
def apply_diff(self, diff_text: str):
with tempfile.NamedTemporaryFile(mode='w', suffix='.diff') as f:
f.write(diff_text)
f.flush()
result = subprocess.run(['git', 'apply', f.name], cwd=self.repo_path,
capture_output=True, text=True)
if result.returncode != 0:
raise RuntimeError(f'Patch failed: {result.stderr}')
self.diff_stack.append(diff_text)
def rollback_last(self):
if self.diff_stack:
subprocess.run(['git', 'checkout', '.'], cwd=self.repo_path, check=True)
self.diff_stack.clear()
def run_tests(self) -> bool:
result = subprocess.run(['pytest', '--tb=short', '-x'], cwd=self.repo_path,
capture_output=True, text=True)
if result.returncode == 0:
return True
# записываем только ошибки, не весь контекст
self.last_error = result.stderr[-2000:]
return False
def run(self, task_spec: dict):
self.state = Step.GENERATE
while self.iteration < self.max_iterations:
if self.state == Step.GENERATE:
context = self.context_for_generator(task_spec)
# вызов LLM — заглушка
diff_text = self.llm_generate(context, task_spec)
try:
self.apply_diff(diff_text)
except RuntimeError as e:
self.state = Step.ROLLBACK
continue
self.state = Step.VALIDATE
elif self.state == Step.VALIDATE:
if self.run_tests():
self.state = Step.DONE
break
else:
self.state = Step.GENERATE
self.iteration += 1
self.rollback_last()
# ROLLBACK и прочее опущено для краткости
if self.state == Step.DONE:
print('Задача выполнена. Итераций:', self.iteration)
else:
print('Не удалось за ' + str(self.max_iterations) + ' итераций')
Обратите внимание: генератор получает только сырой контекст, а ошибки проходят через отдельный канал. Это предотвращает перегрузку контекста. Такой подход перекликается с идеей stateful memory из статьи про проектирование AI-агента — память должна быть внешней, а не встроенной в промпт.
Когда Harness — это overkill
Не пихайте харнес в каждую задачу. Если генерация кода занимает один вызов LLM и не требует валидации — простой агент справится лучше. Как я писал в статье "Один против всех", мультиагент — это инструмент для сложных задач, а не модная игрушка.
Правило: Если задача занимает меньше 10 минут ручного кодинга — не стройте харнес. Но если вы планируете автоматизировать целые эпики — обвязка окупится после первой же недели работы без перегоревших токенов.
В итоге: без харнеса мультиагент — это просто дорогой хаос. С харнесом он становится предсказуемым конвейером, где каждый шаг контролируется кодом, а не промптом. И помните: лучше один хорошо спроектированный харнес, чем десять агентов с божественным промптом.