CrewAI — фреймворк интересный. Он похож на самый быстрый способ удивить своего босса: легкий, у него очень низкий порог входа, он по дизайну нацелен на мультиагентность и из него можно очень быстро собирать MVP с вау-эффектом. В статье поговорим о том как создавать агентов на фреймворке, что у них внутри, где фреймворк хорош, а куда брать его не нужно.
Мультиагентная система без подходящей задачи — это, как говорится, токены на ветер, поэтому мы сколотим банду агентов, которые нам будут анализировать arxiv-статьи про LLM и посмотрим как это работает.
CrewAI появился в 2023 году как надстройка над LangChain, используя его компоненты для работы с LLM, инструментами и памятью. Главной идеей был процесс упрощения создания именно мультиагентных систем через ролевую архитектуру. Вышло неплохо, идея зашла, после чего через год фреймворк решили переписать с нуля в пользу самостоятельности и легковесности.
Вся архитектура держится на том, что агент - это автономная личность, у которой есть цели и задачи. И таких личностей можно собирать в банду (Crew) и совместно решать что-то большое.
Пройдем по концептам прям по коду.
Вот как легко создается агент (Agent):
# Создание минимального агента
minimal_agent = Agent(
role='Дата-саетист',
goal='Анализировать данные',
backstory='Опытный сотрудник',
tools=[like_this_post, like_author],
)
Личность создана: role
— отвечает за то, кем агент будет себя ощущать, это некая профессиональная идентичность, goal
— конкретизация что именно будет делать эта личность, backstory
— это некий опыт в профессии. Tools
— понятно, это передаваемые инструменты, тут все как у всех — любая функция через декоратор превращается в вызываемый инструмент.
Одиночные агенты во фреймворке неэффективны, потому что по своему дизайну и заточен под объединение вот таких личностей в команду — Crew. Поэтому код выше под капотом превращается в следующий системный промпт:
# Фактический промпт агента в команде выглядит примерно так:
"""
You are a {role}.
Your personal goal is: {goal}
{backstory}
You are working with a team of other specialized agents.
Each agent has their own expertise. Focus on your area of specialization.
When you complete your task, your output will be used by other agents.
Make your output clear and structured.
"""
Параметр role
принимает строку, но CrewAI не валидирует её содержимое. Можно писать что угодно, но это попадет промпт и работать не будет. По поводу ролей и опыт — здесь классический trade-off между тем насколько детально все описывать: с одной стороны детальнее, тем лучше и более прогнозируемым будет цепочка рассуждения и ответ, с другой стороны — все это постоянные токены и забиваемый ими контекст. Это system prompt, а значит он вызывается каждый вызов агента.
Эмпирическая граница эффективности находится примерно на 50-150 токенах для полного определения роли (role + goal + backstory). Это примерно 35-100 слов английского текста.
Здесь очень важным является умение емко и правильно сформулировать роли амбивалентность свести к минимуму. Просто "менеджер" не годится, потому что это слишком широко и непонятно. Кратко, однозначно, понятно и согласовано.
Да, пример, который я приводил выше - как раз таки плохой, а вот пример хороший:
llm_researcher = Agent(
role='Исследователь LLM',
goal='Анализировать научные статьи о LLM и выделять ключевые технические инсайты',
backstory='5 лет в исследовании NLP и LLM, опыт работы с трансформерами, файнтюнингом и промпт-инженерингом. Читаю arXiv ежедневно.'
)
В backstory можно напихать все те грязные хаки, которые прекрасно работают с рефреймингом в LLM.
Помимо обязательных параметров, есть и служебные (для трейсинга, объяснимости и тд), например: verbose (показ логов), cache (кеш инструментов), allow_delegation (может ли он делегировать задачу) и другие.
Заонбордили электронного сотрудничка? Давайте уже накинем ему работки.
За работку у нас отвечает Task. Это некая исполняемая единица работы, которая собирается в структурированный промпт и управляет execution loop агента. Когда мы создаем Task, то определяем три ключевых компонента: что нужно сделать (description), какой результат ожидается (expected_output), и кто это делает (agent).
Вот так:
# Задача
analysis_task = Task(
description='Проанализировать статью',
expected_output='Сделать краткую выжимку на 2 предложения',
agent=researcher
)
Здесь agent=researcher
создает жесткую связь между задачей и агентом. Но есть так же и механизмы делегирования, когда агент во время выполнения своей задачи может принять решение, что часть работы лучше передать другому агенту с более подходящими навыками. За это отвечает параметр allow_delegation=True
и это добавляет в промпт вот что:
system_prompt_with_delegation = f"""
.... все как у обычной роли ....
When you encounter a task that requires specialized expertise outside your role,
you can delegate it to another agent using the delegation tool.
Available action: delegate(agent_name, task_description)
Think carefully about when delegation is appropriate.
"""
Под капотом делегирование — это специальный внутренний tool calling с подзадачей. Делегирование может быть как peer-to-peer (когда попросил коллегу), так и начальник-подчиненный, если так настроены ролевые модели и их возможности делегирований.
Окей, с делегирование разобрались, что же происходит когда навешена задача? Как и всегда, обогащается промпт всем, из чего агент и задача состоят:
### Схема упрощенная, но общую логику передает
final_prompt = f"""
{system_base}
{tools_section}
{context_section}
{task_section}
Think step by step. Use tools when necessary.
When you have completed the task, provide your final answer.
"""
Когда финальный промпт сформирован, начинается execution loop агента, который выглядит следующим образом:
# Псевдокод того, что происходит внутри
def execute_task(task, agent):
prompt = compile_prompt(task, agent)
iteration = 0
while iteration < agent.max_iter:
# Вызываем LLM
response = agent.llm.generate(prompt)
# LLM может вернуть два типа ответа:
# 1. Вызов инструмента (tool call)
# 2. Финальный ответ (task completion)
if response.is_tool_call:
# Агент решил использовать инструмент
tool_name = response.tool_name
tool_params = response.tool_params
# Выполняем инструмент
tool_result = execute_tool(tool_name, tool_params)
# Добавляем результат в контекст
prompt += f"\n\nTool Result:\n{tool_result}\n\nContinue working on the task."
iteration += 1
continue
elif response.is_final_answer:
# Агент считает задачу выполненной
task.output = response.answer
return task.output
else:
# Агент продолжает рассуждать
prompt += f"\n\n{response.reasoning}\n\nContinue."
iteration += 1
# Достигнут max_iter
raise MaxIterationsError("Agent exceeded maximum iterations")
Ключевой момент: каждая итерация добавляет новую информацию в промпт. Это создает нарастающий контекст, где агент видит всю историю своих действий и результатов инструментов. Между итерациями внутренний промпт растет и это важно понимать. Но на выход дальше уходит именно финальный ответ, которые попадает в дальнейший контекст.
Если же задача передается другому агенту, то все это тоже вставляется в общий контекст:
--- Context from previous work ---
TASK: Extract key findings from paper.pdf
RESULT:
1. Novel attention mechanism reduces complexity
2. 30% improvement on benchmark X
3. Works with models up to 70B parameters
4. Training time reduced by 2x
5. Open source implementation available
--- End of context ---
Да, на всякий случай. Промпт — это отдельная текстовая сущность, а контекст — это собранный воедино результат промптов + их фреймворкошные добавления.
Task может иметь флаг async_execution=True
и тогда используется asyncio для паралельного выполнения задач.
Итоговый flow у Task
Task превращается в многослойный промпт: system message агента + descriptions инструментов + context от предыдущих задач + описание текущей задачи + expected output. Этот промпт запускает execution loop, где LLM итеративно рассуждает, вызывает инструменты и добавляет результаты в контекст. Когда агент решает, что задача выполнена, его финальный ответ сохраняется в task.output
и может быть использован другими задачами через механизм context
. Вся эта оркестрация скрыта внутри фреймворка, но если мы делаем систему, то всю эту внутрянку надо знать.
Помните я говорил про делегирование? Делегирование удваиваем количество вызовов LLM, так как его сначала получает один агент, затем передает другому. Предлагаю свободно пофантазировать какое количество токенов будет гоняться туда-сюда, если у нас будет большая задача, несколько команд и сложное делегирование задач между ними.
Crew — это оркестратор и механизм выполнения (execution engine), который координирует работу агентов и задач.
Код элегантно минимальный:
# Минимальная команда
crew = Crew(
agents=[agent],
tasks=[task]
)
# Запуск
result = crew.kickoff()
print(result)
Crew поддерживает два фундаментально разных режима координации агентов: sequential и hierarchical. Выбор между ними определяет всю архитектуру агентной системы.
В sequential процессе задачи выполняются строго в том порядке, в котором они объявлены. Это детерминированный конвейер, где каждая задача жестко привязана к своему агенту через параметр agent=...
, контекст передается автоматически через механизм context=[previous_task]
, и фреймворк гарантирует, что зависимые задачи получат output своих предшественников. Самый главный недостаток — конвейнер едет до упора, даже если в середине задачи понятно, что она не решается, но система уже не может остановиться.
В hierarchical процессе появляется дополнительный слой управления — внутренний Manager агент. Этот менеджер сам анализирует все задачи и доступных агентов, затем принимает решения о распределении работы. Здесь уже можно не указывать кому накинуть задачку, решение будет принято в рантайме на основе анализа описания и компетенций агентов. Порядок задач все еще важен, но менеджер может принять решение выполнять его в другом порядке. Главный минус — менеджер (=запрос к ллмке) вызывается после каждого всего (выполнения, назначения и тд), а это все сильно увеличивает пожирание токенов.
Память в CrewAI — это векторное хранилище исторического контекста через ChromaDB. При включении memory=True
активируются три типа: классические short-term (текущая сессия), long-term (постоянная) и новая интересная — entity memory (извлеченные сущности: люди, места, концепции). Каждая задача и её результат превращаются в векторные эмбеддинги, а при старте новой задачи делается семантический поиск по истории и все релевантное добавляется в промпт.
Планирование
Planning — дополнительная фаза перед выполнением, где отдельный запрос к LLM анализирует все задачи и создает оптимизированный план выполнения (execution plan). Когда planning=True
, фреймворк не сразу запускает задачи, а сначала формирует метапромпт с описаниями задач и агентов, затем все анализирует и может предложить декомпозицию сложных задач. Возможность явно интересная, но есть у меня внутренние подозрения, что может что-то пойти не так.
Callbacks
Callbacks — это возможность встраивания кастомной логики в процессе выполнения (execution flow). Есть Task callback (выполняется после выполнения задачи и хорош для метрик, логирования) и Step callback (вызывается после каждого шага агента, хорош для дебага). Оба выполняются синхронно в основном потоке, поэтому должны быть легковесными, потому что блокирующие.
Full Output
По умолчанию crew.kickoff()
возвращает только результат последней задачи. С full_output=True
возвращается структурированный объект с полями tasks_outputs
(список результатов всех задач) и final_output
(последняя задача). Полезно, если нам важен не только последний итог, а в том числе и какие-то промежуточные этапы.
Я обещал, что потестирую мультиагентную систему, которая будет анализировать технические статьи про LLM. Подойдем мы к этому делу творчески и сделаем трех агентов: один будет у нас жутким душнилой (который уже никому не верит и ничего не ждет), охоника за инновациями и их руководителя, который знает обоих и принимает взвешенное решение.
В этой задаче я не гонюсь за оптимизациями, поэтому на рефрейминге можно оторваться:
Душнила:
dushnila = Agent(
role='Principal ML Engineer и профессиональный скептик',
goal='Критически проанализировать статью и выявить все слабые места, проблемы и ограничения',
backstory="""Ты Principal ML Engineer с 15 годами опыта. Ты повидал ВСЕ:
десятки хайповых статей, которые не воспроизводятся, модели, которые работают только
на cherry-picked датасетах, "революционные" методы, дающие +0.1% к метрике.
Ты глубоко скептичен ко всему новому. Твой первый вопрос всегда: "А где подвох?"
Ты ищешь недостатки методологии, сомнительные эксперименты, натянутые выводы,
отсутствие важных baseline'ов, нечестные сравнения.
Ты не веришь громким заявлениям без solid доказательств. Ты видел слишком много
красивых графиков, за которыми скрывается overfitting или data leakage.
Твоя задача — быть занудой и найти ВСЕ причины, почему это может не работать.
Ты душнила, но честный душнила.""",
tools=[read_pdf],
llm='gpt-4o-mini',
verbose=True
)
Позитивщик — обратный, руководитель же пытается сбалансировать оценку первых двух.
Минимальный код — 141 строчка, с трейсингом и всеми промптами/принтами — 457.
Ну классно же? Но и это не все, роли и задачи можно создавать через yaml-конфиги, тогда вся логика вообще управляется через них, что очень круто.
##### роли ######
skeptic:
role: "Скептик"
goal: "Найти проблемы и слабые места в статье"
backstory: |
Опытный Principal ML Engineer с 15 годами опыта.
Видел все хайповые статьи и знает, где искать подвох.
llm: "gpt-4o-mini"
verbose: true
##### задачи ######
analyze_skeptic:
description: "Прочитай статью {pdf_path} и проведи критический анализ. Найди все проблемы."
expected_output: "Критический анализ с выявленными проблемами"
agent: "skeptic"
+ питошка, который это все читает и собирает
Итак, давайте смотреть результат!
И вот итоги (для гурманов):
Вполне недурно!
Когда хорошо использовать | Для сценариев с несколькими агентами (анализ, дебаты, синтез мнений, исследовательские пайплайны). Подходит для прототипов и MVC. |
Для чего не подходит | Для сложных контролируемых процессов или прогнозируемых flow. |
Production-ready | ❌ Низкая — больше исследовательский / прототипный инструмент. Требует ручного контроля ошибок, трейсинга и токенов. |
Легкость входа | ✅ Высокая — простой синтаксис ( |
Потребление токенов | ⚠️ Средне-высокое — каждый агент и задача генерирует отдельные запросы к LLM, токены быстро накапливаются. |
Фреймворк очень прикольный, дающий мультиагентность с 20 строчек кода. Собирать какие-то простые версии на нем очень легко, особенно там, где агентам нужно договариваться — это производит вау-эффект. Все как в моем бенчмарке, не обманул!
К сожалению, этот же вау-эффект создает ожидания, что все на самом деле очень легко. Но подняв капот, честно говоря, лично я увидел классную игрушку и страшную пожиралку токенов (большая часть из которых будут влетать в кэш, но все же), которая без жестких оптимизаций, глубокой проработки агентного флоу, без evals, трейсинга, контекст-инжиниринга и всего прочего вообще слабопригодна для чего-то сильно серьезного.
Это не значит, что на ней этого делать нельзя, это значит именно что БЕЗ этих штук будет больно и опасно. Примерно вот так (мем я уже использовал, но он слишком хорош и его хочется вставлять в каждую статью про агентов):
Спасибо!
Мой тг-канальчик Agentic World и другие статьи: