Выбор фреймворка агента в зависимости от формы нагрузки: шесть осей и путь принятия решения
Выберите фреймворк агента в зависимости от характера нагрузки: LangGraph, CrewAI, AutoGen, Temporal или никакого, оцениваемый по детерминизму, сроку службы, идемпотентности, возможностям аудита и стоимости.
Источники: Your AI Agent Framework Is Probably the Wrong One Here’s How to Actually Choose
Вопрос о том, какую рамку агентов использовать, — это неверный вопрос для начала. Лучше спросить, какова структура рабочей нагрузки, поскольку рамка определяется ответом на этот вопрос и не должна выбираться изолированно на основе данных из обзоров. Команды, которые уже на первой неделе принимают решение, основываясь на постах в социальных сетях, часто тратят месяцы с третьего по шестой на понимание того, что основные предположения рамки не соответствуют тому, что они создают. Обычно вина не в самой рамке. Никто не определил заранее требования к рабочей нагрузке перед тем, как выбрать инструмент для их реализации.
Вместо сравнения функций этот подход определяет шесть осей, влияющих на соответствие требованиям, показывает, как они выглядят в коде, а затем формирует путь принятия решений. Приводится пример работы такого агента — системы для сортировки сигналов тревоги в инфраструктуре. Этот пример прост, реалистичен и охватывает все оси. Агенту необходимо определить, является ли сигнал тревоги реальной проблемой, принять меры путем отправки уведомления кому-либо или автоматического устранения проблемы, иногда ждать ответа от человека несколько часов, и никогда не отправлять уведомление инженеру, находящемуся в режиме дежурства, дважды в 4 часа утра из-за повторной попытки обработки.
Первая ось: насколько детерминированным должно быть разветвление?
Эту ось пропускают, рассматривая её позже. Честным тестом будет вопрос о том, пострадает ли кто-либо, если для одних и тех же входных данных 2% случаев приведут к другому пути выполнения кода. Некоторые ветви допускают вариативность, например при выборе тона для письма-краткого обзора.
# A branch where non-determinism is FINE — picking a tone for a summary email.
# If the agent occasionally phrases things slightly differently, nobody's paged.
def draft_summary_tone(context: dict) -> str:
return llm_call(
prompt=f"Summarize this incident in a {context['audience']}-appropriate tone.",
temperature=0.7, # variability here is a feature, not a bug
)
Другие функции таковыми не являются, например, принятие решения о том, стоит ли будить человека в 4 утра или автоматически применять решение. Это решение должно приниматься кодом, а не через запрос от пользователя.
# A branch where non-determinism is NOT fine — deciding whether to page a human
# at 4am versus auto-remediating. This must be code, not a prompt.
def route_alert(alert: dict) -> str:
if alert["severity"] == "critical" and alert["service"] in PAGE_ALWAYS_SERVICES:
return "page_oncall"
if alert["auto_remediation_available"] and alert["confidence"] > 0.9:
return "auto_remediate"
if alert["severity"] == "critical":
return "page_oncall"
return "log_and_monitor"
Когда важные решения в рамках рабочего процесса имеют вид второй функции — детерминированной, поддающейся аудиту и представленной в виде обычного кода, который хотел бы прочитать специалист по соблюдению стандартов, фреймворк должен изолировать такую логику от LLM. Условные ребра LangGraph отлично подходят для этой цели, поскольку функция маршрутизации написана на обычном Python, а модель взаимодействует только с теми частями, которые ей намеренно передаются, такими как классификация или составление текста.
CrewAI и AutoGen построены иначе: они позволяют собственному мышлению агента определять дальнейшие действия. Это преимущество, когда цель — творческая гибкость, но недостаток, если гибкость иногда приводит к пропуску проверки соответствия стандартам. Если большинство важных веток должны быть детерминированными, один только этот факт исключает значительную часть подобных решений из числа возможных.
Второй аспект: как долго существует единица работы?
Кратковременный элемент начинается и завершается в рамках одного HTTP-запроса. Это та область, где не требуется никакая фреймворк-среда, поскольку её использование принесёт лишь чистую нагрузку.
# Short-lived: starts and finishes inside one HTTP request.
# This is the "no framework needed" zone — a framework here is pure overhead.
async def handle_summarize_request(request: SummarizeRequest) -> SummarizeResponse:
text = await fetch_document(request.doc_id)
summary = await llm_summarize(text, max_tokens=300)
return SummarizeResponse(summary=summary)
Устройство с длительным сроком службы работает иначе. Сообщение о событии может оставаться в ожидании подтверждения от человека в течение шести часов, пока инженер дежурства спит, а затем возобновить работу в другом процессе после того, как процедура развертывания заменила подлежащие ему контейнеры.
# Long-lived: this alert might sit in "awaiting human ack" for six hours
# while the on-call engineer is asleep, then resume on a completely
# different process after a deploy rotated the pods underneath it.
class AlertTriageWorkflow:
async def run(self, alert: dict) -> dict:
decision = await self.classify_and_route(alert)
if decision == "page_oncall":
await self.page(alert)
await self.wait_for_ack(timeout_hours=1) # this line is the whole ballgame
...
Если задача надежно выполняется за несколько секунд и полностью находится в памяти одного процесса, то вопрос о использовании фреймворка, скорее всего, преувеличен. Обычная асинхронная функция с декоратором повторных попыток превосходит любой фреймворк с точки зрения удобства обслуживания, поскольку не требует никакой координации, а изучение модели состояния LangGraph или запуск кластера Temporal для безсостоянийного конечного пункта обработки данных — это затраты без выгоды.
Как только единица работы может существовать дольше процесса, поскольку она ожидает действий человека, выполнения пакетной задачи или соблюдения SLA другой команды, становится необходимой устойчивая обработка, измеряемая в часах. Ни один из трех популярных фреймворков для оркестрации LLM не был разработан как слой обеспечения устойчивости. В таком случае речь идет о решениях вроде Temporal или, по крайней мере, о ручно созданной системе с сохранением состояния и очередью задач с возможностью возобновления выполнения шагов.
Третья ось: что, если шаг выполнится дважды?
Команды обычно обнаруживают эту проблему во время анализа инцидентов. Первая версия, приведённая ниже, кажется нормальной в демо-версии, но на самом деле представляет собой инцидент, готовый произойти, поскольку повторная попытка выполнения операции снова запускает сервис.
# BEFORE — looks fine in a demo, is a live incident waiting to happen
async def auto_remediate(alert: dict):
await restart_service(alert["service"]) # what if this activity gets retried?
Вторая версия по своей конструкции является идемпотентной.
# AFTER — idempotent by construction
async def auto_remediate(alert: dict, idempotency_key: str):
if await remediation_ledger.already_applied(idempotency_key):
logger.info("remediation already applied, skipping", key=idempotency_key)
return await remediation_ledger.get_result(idempotency_key)
result = await restart_service(alert["service"])
await remediation_ledger.record(idempotency_key, result)
return result
Ключ идемпотентности должен браться из чего-то стабильного в бизнес-данных, такого как идентификатор оповещения, и никогда не из UUID, генерируемого при каждой попытке, иначе попытки повторного выполнения перестают быть безопасными. Это относится к функционалу инструмента, а не к какой-либо фреймворк-среде, однако это влияет на то, какие фреймворки можно использовать, поскольку некоторые затрудняют отслеживание того, происходило ли повторное выполнение. Механизм выполнения AutoGen на основе чата не подает никаких структурных сигналов о том, что вызов инструмента является попыткой повтора или новым решением. С точки зрения протокола разговора агент просто решает снова вызвать инструмент, и остается незамеченным, является ли это повторным выполнением или дублированием действия.
Когда побочные эффекты имеют высокую стоимость, представляют опасность или связаны с деньгами и изменениями инфраструктуры, необходима фреймворк-среда или самостоятельный слой под ней, который рассматривает вопрос о том, уже происходили ли побочные эффекты, как вопрос первостепенной важности, а не как что-то, что можно вывести из протокола чата.
Четвертая ось: кто и как будет читать решение позже?
Во время анализа после события важен запись аудита, независимая от используемой структуры, независимо от того, что инициировало выполнение шагов.
# A framework-agnostic audit record — this is what actually matters
# in a postmortem, regardless of what orchestrated the steps.
@dataclass
class DecisionRecord:
alert_id: str
timestamp: float
step: str
reasoning: str # what the LLM said, verbatim
decision: str # the structured outcome, not prose
confidence: float | None
human_override: bool
async def log_decision(record: DecisionRecord):
await audit_store.insert(record)
# Also emit as a structured log line — cheap insurance for when
# the audit store itself is the thing that's down during an incident.
logger.info("agent_decision", **asdict(record))
Каждая фреймворк-среда позволяет вести логирование. Вопрос заключается в том, обеспечивает ли её структура автоматически чёткий, поддающийся поиску след принятия решений, или же для этого необходимо бороться с абстракциями. Объект состояния LangGraph проходит через узлы с именами, поэтому запрос о том, каким было состояние непосредственно перед принятием решения о маршрутизации, представляет собой прямой поиск в TypedDict, который никогда не преобразуется в свободный текст. Чтобы восстановить те же факты, необходимо парсить записи диалога AutoGen, поскольку его нативный формат хранения — проза, а не структурированное состояние. В ситуациях, где требуется соблюдение нормативов, при наличии аудиторской группы или привычки к тщательному анализу после событий, этот аспект заслуживает большего внимания, чем указывается в маркетинговых материалах.
Пятый аспект: что ограничивает скорость работы команды?
Этот показатель редко фигурирует в технических сравнительных таблицах, но часто определяет исход дела. Стартап из четырех человек, который проверяет, смогут ли агенты вообще выполнить задачу, не должен тратить три недели на изучение ограничений детерминизма Temporal перед тем, как написать хоть какую-то бизнес-логику.
# Week-one prototype: prove the concept fast, accept the debt knowingly.
from crewai import Agent, Task, Crew
triage_agent = Agent(role="Alert Triage", goal="Decide how to handle infra alerts")
crew = Crew(agents=[triage_agent], tasks=[Task(description="Triage: {alert}", agent=triage_agent)])
crew.kickoff(inputs={"alert": alert_payload})
Это совершенно нормальное явление первой недели работы. Ошибка заключается не в том, что для прототипа был выбран CrewAI, а в том, что команда через шесть месяцев больше не возвращалась к этому выбору, когда на кону оказались реальные обязанности оператора и изменения инфраструктуры, поскольку прототип уже функционировал. Выбор фреймворка имеет срок действия, зависящий от количества операций в системе; запись этой даты при выборе предотвращает её установку по умолчанию.
Шестой аспект: какова задержка и бюджет на расходы за каждое решение?
Каждое решение о маршрутизации, требующее вызова LLM, умножается на количество этапов в многоподразделенческом диалоге, а при объеме сигналов в сотни в день с всплесками до тысяч во время инцидентов счет быстро растет.
# Expensive pattern: every routing decision is its own LLM call,
# multiplied across a multi-agent conversation with several turns.
# At alert volumes (hundreds/day, sometimes bursts of thousands during
# a real incident), this is a real line item, not a rounding error.
async def route_via_llm(alert: dict) -> str:
return await llm_call(f"How should we handle this alert? {alert}")
# Cheaper, faster, and more auditable: cheap deterministic pre-filtering
# in code, LLM reserved for genuinely ambiguous cases.
async def route_alert_efficiently(alert: dict) -> str:
if alert["service"] in KNOWN_NOISY_SERVICES and alert["severity"] == "low":
return "log_and_monitor" # zero LLM calls for the common case
if alert["signature"] in KNOWN_REMEDIATION_PLAYBOOK:
return "auto_remediate" # deterministic lookup, zero LLM calls
return await llm_call(f"Novel alert, needs judgment: {alert}") # LLM only when genuinely needed
Это скорее дисциплина архитектуры, чем ося фреймворка, которая помогает любому из них. Фреймворки, позволяющие легко сочетать дешевый детерминистический код с дорогостоящими вызовами моделей, экономят реальные деньги при больших объемах работы. LangGraph обеспечивает это, как и движок рабочих процессов вроде Temporal, который оборачивает вызов LLM в одну операцию. Альтернативой является нативный подход, при котором всё направляется через еще один этап общения с посредничеством модели — это стандартное поведение AutoGen при установке параметра speaker_selection_method в значение auto.
Путь принятия решения, а не дерево решений
Диаграмма потоков, дающая один ответ, была бы недобросовестной, поскольку области перекрываются. Приведенный ниже путь ближе к тому, как на самом деле происходит принятие решения.
Is this unit of work stateless and finishes in seconds?
└─ YES → skip the framework entirely. Plain functions + retries. Ship it.
└─ NO, continue.
Does it need to survive process restarts / wait on humans for hours-to-days?
└─ YES → you need durable execution (Temporal or equivalent) as the backbone,
regardless of what else you pick for the reasoning layer.
└─ NO, continue.
Are the important branches safety- or compliance-critical
(money, infra changes, irreversible external actions)?
└─ YES → LangGraph-style explicit graphs, keep LLM scoped to narrow nodes.
└─ NO, mostly exploratory/creative → CrewAI or AutoGen are legitimate defaults.
Is this still a prototype whose findings might get thrown away?
└─ YES → optimize for speed of iteration over long-term correctness,
but write down when you'll revisit that tradeoff.
При применении к агенту для сортировки сигналов получается многоуровневая структура. Она выдерживает перезагрузки и ожидает действий человека, поэтому требуется надежный уровень выполнения. Самая важная её ветвь связана с критически важной безопасностью — выбор между отправкой уведомлений и автоматическим устранением проблем, в связи с чем в каждом шаге необходимо явное маршрутизирование в стиле LangGraph. Кроме того, система давно вышла за рамки этапа прототипирования, поскольку уже включает в себя механизм отправки уведомлений. Временные аспекты становятся основой, при этом шаг оркестрации LLM в рамках определённого диапазона выступает одной из действий внутри этой структуры.
@activity.defn
async def classify_alert_activity(alert: dict) -> dict:
# LangGraph-style graph runs here — bounded reasoning, deterministic routing —
# inside an activity Temporal will retry and time-box like any other side effect.
result = alert_triage_graph.invoke({"alert": alert, "audit_log": []})
return {"decision": result["decision"], "confidence": result["confidence"]}
@workflow.defn
class AlertTriageWorkflow:
def __init__(self):
self._acked = False
@workflow.signal
async def acknowledge(self):
self._acked = True
@workflow.run
async def run(self, alert: dict) -> dict:
classification = await workflow.execute_activity(
classify_alert_activity, alert,
start_to_close_timeout=timedelta(seconds=20),
retry_policy=workflow.RetryPolicy(maximum_attempts=3),
)
if classification["decision"] == "page_oncall":
await workflow.execute_activity(page_oncall, alert, start_to_close_timeout=timedelta(seconds=10))
await workflow.wait_condition(lambda: self._acked, timeout=timedelta(hours=1))
if not self._acked:
await workflow.execute_activity(escalate_to_secondary, alert, start_to_close_timeout=timedelta(seconds=10))
elif classification["decision"] == "auto_remediate":
await workflow.execute_activity(
auto_remediate, alert, f"remediate-{alert['id']}",
start_to_close_timeout=timedelta(minutes=2),
retry_policy=workflow.RetryPolicy(maximum_attempts=2),
)
return {"alert_id": alert["id"], "decision": classification["decision"]}
На практике выбор подходящей фреймворковой платформы выглядит именно так, и это не однозначный ответ, который можно дать за одну строку. Это решение, связанное с композицией компонентов, а умение составлять такие композиции и является ключевой навыком.
Часто повторяющиеся ошибки
- Выбор на основе количества звезд на GitHub. Популярность указывает на поддержку сообщества и количество онлайн-решений. Однако она ничего не говорит о том, соответствует ли основная модель выполнения требованиям к надежности и детерминизму, необходимым для работы системы.
- Превращение прототипной фреймворковой платформы в постоянную решение. Прототип, который временно хорошо работал, превращается в производственную систему, поскольку переписывание механизмов оркестрации кажется бесполезным с точки зрения добавления новых функций. На самом деле это повышает надежность, которая остается незаметной до тех пор, пока не исчезнет.
Настоящее решение
Не существует идеальной рамочной структуры для агентов, и именно предположение об её существовании приводит к тому, что многие из этих систем перепроектируются уже через восемнадцать месяцев. Существует оптимальная рамочная структура для конкретной нагрузки с заданным набором из шести свойств. Дисциплина, необходимая для работы в команде, заключается в том, чтобы сначала определить эти свойства, прежде чем вообще приступать к использованию какой-либо рамочной структуры; после этого выбор обычно становится очевидным. Если этого не сделать, слой оркестрации будет переписан в течение года, независимо от выбранной рамочной структуры, поскольку проблема всегда заключалась не в самой рамочной структуре, а в отсутствии понимания того, что именно нужно создать, прежде чем решать, как это сделать.
Связанные материалы
- Оптимизация агента Terraform с помощью DSPy: сначала создайте верификатор, затем оптимизатор — агенты Terraform формируют схемы. Сначала разработайте детерминистичный верификатор, установите базовый уровень для агента, а затем используйте DSPy и GEPA для оптимизации его файла с параметрами работы с честными результатами.
- Стриминговые глубокие агенты LangChain: реальные планы, вызовы инструментов и токены — замените блокирующий вызов на поток, чтобы пользователи могли наблюдать за работой глубокого агента: три режима потока, пространства имен для подагентов и фильтрация токенов для краткого изложения.