Track
Попросите одну модель за один проход исследовать тему, написать статью, отредактировать её и отполировать финальную версию — чаще всего вы получите посредственный результат по всем четырём пунктам. У модели нет чёткой роли в каждый момент времени, поэтому она жонглирует всеми задачами сразу и редко выполняет их хорошо. Разделение работы между специализированными агентами обычно даёт лучший итог, потому что каждый агент может сосредоточиться на своей части и выполнить её качественно.
В этом руководстве мы соберём рой агентов, который делает именно это. Мы используем CrewAI для координации агентов и Gemini 3.5 Flash для их рассуждений и написания текста. В рое четыре участника: Исследователь для сбора актуальной информации, Писатель для подготовки первого черновика, Редактор для повышения ясности и точности и Менеджер для координации процесса.
Мы также дадим Исследователю доступ к вебу в реальном времени через Olostep, чтобы он мог подтягивать свежие источники, а не опираться только на внутренние знания модели. В итоге у вас будет рабочий иерархический рой, который берёт тему и возвращает готовую к публикации статью — и чёткое понимание, когда такой подход оправдывает затраты, а когда — нет.
Что такое рой агентов?
Рой агентов — это группа ИИ‑агентов, которые совместно выполняют задачу. Вместо того чтобы просить одну модель исследовать, писать, редактировать и финализировать всё самостоятельно, рой агентов делит работу между несколькими специализированными агентами. У каждого агента есть чёткая роль, а система координирует их, чтобы получить более сильный конечный результат.
Преимущество — в специализации. Агент, ориентированный на исследования, концентрируется на поиске и проверке источников; агент, ориентированный на написание, — на структуре и ясности; а редактор сверяет черновик с исходными исследованиями, не отвлекаясь на только что написанный текст.
Координирующий агент связывает шаги вместе и решает, что делать дальше. В рое, который мы строим, эти роли напрямую соответствуют Исследователю, Писателю, Редактору и Менеджеру.

Однако это не бесплатно. Больше агентов — это больше вызовов модели, больше времени и больше мест, где рабочий процесс может дать сбой; к этому компромиссу мы вернёмся в конце, когда вы увидите фактическую стоимость запуска.
Что такое CrewAI?
CrewAI — это фреймворк с открытым исходным кодом для создания многоагентных систем. Он даёт удобный способ определять агентов, назначать им задачи и выбирать схему координации, чтобы вы могли сосредоточиться на ролях и рабочем процессе, а не на «сантехнике», которая их связывает.
Три понятия выполняют основную работу.
- Агенты — это отдельные исполнители, каждый из которых определяется ролью, целью и предысторией, влияющей на его поведение.
- Задачи описывают выполняемую работу и ожидаемый результат для каждой из них.
- Команда (crew) объединяет агентов и задачи и запускает их по выбранному процессу
Процесс может быть последовательным, когда задачи идут в фиксированном порядке, или иерархическим, когда агент‑менеджер делегирует работу другим и валидирует их результат. Здесь мы используем иерархический процесс, где делегацией управляет Менеджер.
CrewAI также обеспечивает доступ к моделям через LiteLLM, библиотеку с открытым исходным кодом, предоставляющую единый интерфейс для множества провайдеров. Это позволяет нам определить одну модель Gemini 3.5 Flash и повторно использовать её во всех четырёх агентах, как мы настроим ниже.
Создание роя агентов с CrewAI и Gemini 3.5 Flash
Приступим к настройке.
1. Необходимые условия
Перед началом убедитесь, что у вас есть:
- Установлен Python 3.11 или новее
- Установлен Jupyter Notebook или JupyterLab
- Базовое понимание Python и ноутбуков
- Аккаунт Google AI Studio
- Включено выставление счетов или на аккаунт Google добавлены кредиты для использования Gemini
- Бесплатный аккаунт Olostep
2. Установка зависимостей
Запустите Jupyter Notebook или JupyterLab и откройте ноутбук с последним доступным в вашей среде ядром Python. Затем выполните следующую ячейку для установки необходимых пакетов.
%pip install --quiet -U \
crewai \
crewai-tools \
google-genai \
litellm \
olostep \
nest-asyncio
После завершения установки выполните следующую ячейку, чтобы проверить версии установленных пакетов. Это помогает убедиться, что всё установлено корректно.
import importlib.metadata as md
for pkg in [
"crewai",
"crewai-tools",
"google-genai",
"litellm",
"olostep",
"nest-asyncio",
]:
print(f"{pkg:>16} {md.version(pkg)}")
Вы должны увидеть примерно такой вывод:
crewai 1.14.6
crewai-tools 1.14.6
google-genai 2.8.0
litellm 1.87.1
olostep 1.1.0
nest-asyncio 1.6.0
3. Задайте переменные окружения
Для этого проекта понадобятся два ключа API.
Сначала создайте ключ API Gemini в Google AI Studio. Откройте Google AI Studio, перейдите в раздел API key и создайте новый ключ для вашего проекта. У Gemini есть бесплатный тариф, но с ограничениями по использованию и скорости. Для более стабильной проверки и запуска Gemini подключите платёжный аккаунт Google или добавьте кредиты на аккаунт Google, чтобы списывать оплату при превышении лимитов бесплатного тарифа.
Затем создайте ключ API Olostep. Зарегистрируйтесь в Olostep, откройте панель управления Olostep и сгенерируйте ключ API на странице API keys. Мы используем этот ключ, чтобы дать агенту‑Исследователю доступ к вебу в реальном времени для поиска и скрейпинга страниц.
Когда оба ключа готовы, сохраните их как переменные окружения. Gemini будет обеспечивать работу агентов CrewAI, а Olostep позволит агенту‑Исследователю собирать актуальную информацию из интернета.
После создания ключей сохраните их как переменные окружения перед запуском ноутбука.
PowerShell:
$env:GEMINI_API_KEY="your-gemini-key"
$env:OLOSTEP_API_KEY="your-olostep-key"
macOS/Linux:
export GEMINI_API_KEY="your-gemini-key"
export OLOSTEP_API_KEY="your-olostep-key"
Теперь выполните следующую ячейку, чтобы проверить, что ключ API Gemini загружен корректно.
import os
api_key = os.getenv("GEMINI_API_KEY") or os.getenv("GOOGLE_API_KEY")
if not api_key:
raise RuntimeError(
"Set GEMINI_API_KEY or GOOGLE_API_KEY in your environment, "
"then restart the notebook kernel."
)
print(f"Gemini API key loaded")
Вы должны увидеть:
Gemini API key loaded
4. Настройте Gemini 3.5 Flash через LiteLLM
В этом проекте мы используем Gemini 3.5 Flash как общий LLM для всех агентов CrewAI. Gemini 3.5 Flash создан для быстрых, «агентных» и ориентированных на код нагрузок. Он подходит для этого проекта, потому что нашим агентам нужно рассуждать в несколько шагов, следовать инструкциям, работать с инструментами и выдавать структурированные результаты.
Gemini 3.5 Flash также поддерживает большой контекст, длинные ответы, «мышление» и использование инструментов, что делает его сильным выбором для агентных рабочих процессов. Поскольку рой агентов может вызывать модель несколько раз — через Исследователя, Писателя, Редактора и Менеджера — использование модели Flash помогает сохранить отзывчивость, обеспечивая при этом качественные рассуждения и письмо.
Если вы хотите сравнить модель с другими передовыми LLM, рекомендуем наши статьи Gemini 3.5 Flash vs GPT-5.5 и Gemini 3.5 Flash vs Claude Opus 4.8.
Мы будем обращаться к Gemini через LiteLLM. LiteLLM — библиотека с открытым исходным кодом, предоставляющая единый интерфейс для вызова многих провайдеров LLM, включая Gemini, OpenAI, Anthropic, Bedrock, Vertex AI и др. В этой схеме CrewAI использует LiteLLM «под капотом», поэтому мы можем передать имя модели Gemini в простом формате provider/model.
Теперь создадим общий LLM CrewAI и переиспользуем его для всех агентов. Это упрощает настройку, поскольку Исследователь, Писатель, Редактор и Менеджер будут использовать одну и ту же модель Gemini.
from crewai import LLM
GEMINI_MODEL = "gemini/gemini-3.5-flash"
gemini_llm = LLM(
model=GEMINI_MODEL,
api_key=os.getenv("GEMINI_API_KEY") or os.getenv("GOOGLE_API_KEY"),
temperature=1.0,
)
print(f"LLM ready: {gemini_llm.model}")
print("Provider API: Google Gemini (via LiteLLM)")
Вы должны увидеть примерно такой вывод:
LLM ready: gemini-3.5-flash
Provider API: Google Gemini (via LiteLLM)
Это подтверждает, что CrewAI теперь может обращаться к Gemini через LiteLLM. На следующих шагах мы передадим общий объект gemini_llm каждому агенту.
5. Добавьте доступ к вебу в реальном времени
Агент‑Исследователь нуждается в доступе к актуальной информации. Для этого мы создадим пользовательский инструмент поиска Olostep и позже передадим его агенту‑Исследователю.
Инструмент может работать в двух режимах. Он может возвращать только итоги поиска — быстрее, или же скрейпить страницы результатов и возвращать содержимое страниц в markdown. Мы также добавим небольшой бюджет на поиск, чтобы агент не вызывал веб‑инструмент слишком много раз.
from pydantic import Field, PrivateAttr
from crewai.tools import BaseTool
try:
from olostep import Olostep, Olostep_BaseError
except ImportError:
raise ImportError(
"The 'olostep' package is not installed. Run %pip install -U olostep."
)
Сначала загрузите ключ API Olostep из окружения.
olostep_api_key = os.getenv("OLOSTEP_API_KEY")
if not olostep_api_key or olostep_api_key == "your-olostep-api-key-here":
print(
"The Olostep tool will fail at call time unless OLOSTEP_API_KEY "
"is set in the environment."
)
else:
print("Olostep API key loaded.")
_olostep_client = Olostep(api_key=olostep_api_key) if olostep_api_key else None
Теперь создайте пользовательский инструмент CrewAI.
class OlostepSearchTool(BaseTool):
"""Search the web with a strict research-call budget."""
name: str = "olostep_web_search"
description: str = (
"Search the live web. Set scrape=false for fast discovery and "
"scrape=true for full-page evidence. You have at most 3 discovery "
"searches without scraping and 6 external search calls total. After "
"3 discovery searches, use scrape=true on the strongest query or "
"domain. Each call returns at most 3 results. Inputs: query, scrape, "
"limit, and optional include_domains."
)
default_limit: int = Field(default=3, ge=1, le=3)
max_chars_per_page: int = Field(default=4000, ge=500, le=12000)
_total_calls: int = PrivateAttr(default=0)
_discovery_calls: int = PrivateAttr(default=0)
_call_history: list[dict] = PrivateAttr(default_factory=list)
def _run(
self,
query: str,
scrape: bool = False,
limit: int | None = None,
include_domains: list[str] | None = None,
) -> str:
if _olostep_client is None:
return (
"OLOSTEP_API_KEY is not set. Set it in the environment "
"and restart the kernel."
)
if self._total_calls >= 6:
return (
"Research search budget exhausted: 6 external calls have "
"already been used. Stop searching and complete the task "
"with the collected sources."
)
if not scrape and self._discovery_calls >= 3:
return (
"Discovery-search limit reached. Do not run another "
"scrape=False search. Use scrape=True on the strongest query "
"or selected domains, or finish with the sources already found."
)
clean_query = query.strip()
if not clean_query:
return "Search query cannot be empty."
result_limit = max(1, min(limit or self.default_limit, 3))
kwargs = {"query": clean_query, "limit": result_limit}
if scrape:
kwargs["scrape_options"] = {
"formats": ["markdown"],
"timeout": 25,
}
if include_domains:
kwargs["include_domains"] = include_domains
self._total_calls += 1
if not scrape:
self._discovery_calls += 1
call_record = {
"number": self._total_calls,
"query": clean_query,
"scrape": scrape,
"limit": result_limit,
"domains": include_domains or [],
"status": "started",
"results": 0,
}
self._call_history.append(call_record)
try:
search = _olostep_client.searches.create(**kwargs)
except Olostep_BaseError as error:
call_record["status"] = "error"
call_record["error"] = f"{type(error).__name__}: {error}"
return (
f"Olostep search error: {type(error).__name__}: {error}\n"
f"Budget used: {self._total_calls}/6 total calls; "
f"{self._discovery_calls}/3 discovery calls."
)
if not search.links:
call_record["status"] = "no_results"
return (
f"No results found for query: {clean_query!r}\n"
f"Budget used: {self._total_calls}/6 total calls; "
f"{self._discovery_calls}/3 discovery calls."
)
call_record["status"] = "success"
call_record["results"] = len(search.links)
chunks = [
f"## Web results for: {clean_query!r}",
f"Page scraping: {'enabled' if scrape else 'disabled'}",
(
f"Budget used: {self._total_calls}/6 total calls; "
f"{self._discovery_calls}/3 discovery calls."
),
]
for index, link in enumerate(search.links, 1):
url = link.get("url", "")
title = link.get("title") or url or "Untitled result"
description = link.get("description") or "No description provided."
chunks.append(
f"### {index}. {title}\n"
f"URL: {url}\n"
f"Summary: {description}"
)
markdown = link.get("markdown_content")
if scrape and markdown:
body = markdown.strip()
if len(body) > self.max_chars_per_page:
body = body[: self.max_chars_per_page] + "\n...[truncated]"
chunks.append(f"Page content:\n{body}")
return "\n\n".join(chunks)
Наконец, создайте экземпляр инструмента.
olostep_search_tool = OlostepSearchTool()
print(f"Tool ready: {olostep_search_tool.name}")
Вы должны увидеть примерно такой вывод:
Olostep API key loaded.
Tool ready: olostep_web_search
Это подтверждает, что инструмент веб‑поиска Olostep готов. Позже мы прикрепим его к агенту‑Исследователю, чтобы он мог собирать актуальную информацию из интернета.
6. Создайте агентов
Теперь создадим агентов для нашего роя. Определим трёх исполнителей и одного менеджера. Исполнители выполняют основные задачи, а менеджер координирует рабочий процесс.
Агент‑Исследователь будет использовать инструмент Olostep для поиска актуальной информации. Агент‑Писатель превратит исследования в черновик. Агент‑Редактор улучшит черновик. Агент‑Менеджер будет решать, как делегировать работу в иерархической команде.
Создайте исполнителей
Начнём с трёх агентов‑исполнителей.
from crewai import Agent
researcher = Agent(
role="Senior Research Analyst",
goal=(
"Find accurate, current, and well-sourced information about {topic}. "
"Produce concrete, verifiable points for the writer."
),
backstory=(
"You are a meticulous research analyst. You decide whether each web "
"search needs page scraping. Use scrape=False when titles, URLs, and "
"summaries are enough. Use scrape=True when you need full-page evidence "
"to verify a claim or understand a source in detail. Cite source URLs."
),
llm=gemini_llm,
tools=[olostep_search_tool],
verbose=True,
allow_delegation=False,
)
Далее создайте агента‑Писателя.
writer = Agent(
role="Blog Post Writer",
goal=(
"Turn the research into a complete, publication-ready educational "
"article on {topic}. Include a strong title, an unlabeled italic "
"summary, TL;DR, detailed body sections, conclusion, and FAQs. Add a "
"table only when it makes a comparison or decision easier to understand."
),
backstory=(
"You are a seasoned technology educator and writer. You create "
"practical, approachable articles similar in structure to high-quality "
"DataCamp content, without copying its wording or branding. You use "
"clear headings, short paragraphs, concrete examples, and sourced facts."
),
llm=gemini_llm,
verbose=True,
allow_delegation=False,
)
Теперь создайте агента‑Редактора.
reviewer = Agent(
role="Senior Editor",
goal=(
"Review the draft for factual accuracy, clarity, structure, and "
"completeness. Ensure it follows the required article order and return "
"a polished, publication-ready version."
),
backstory=(
"You are a detail-oriented senior editor for an educational technology "
"publication. You verify claims, improve flow, remove repetition, and "
"ensure the title is followed by an unlabeled italic summary and TL;DR. "
"You keep a table only when it adds real value, remove any Key Takeaways "
"section, and ensure Conclusion appears immediately before FAQs."
),
llm=gemini_llm,
verbose=True,
allow_delegation=False,
)
Создайте агента‑менеджера
Наконец, создайте агента‑Менеджера.
manager = Agent(
role="Content Coordinator",
goal=(
"Coordinate research, writing, and review for {topic}. Delegate each "
"task to the correct specialist and ensure the reviewed article is the "
"final result."
),
backstory=(
"You are an experienced editorial lead. You keep the workflow simple: "
"research first, writing second, and editorial review last."
),
llm=gemini_llm,
verbose=True,
allow_delegation=True,
)
Менеджер настраивается отдельно при сборке иерархической команды. Поэтому его ещё не добавляли в список агентов‑исполнителей.
7. Определите задачи
Теперь определим задачи для роя агентов. Эти задачи не назначаются напрямую агентам‑исполнителям. Вместо этого агент‑Менеджер решит, какому исполнителю поручить каждую задачу.
В этом процессе первая задача — исследование, вторая — написание, третья — редактура. Каждая задача также включает ожидаемый результат, чтобы агенты точно понимали, что необходимо произвести.
from crewai import Task
research_task = Task(
description=(
"Delegate to the Senior Research Analyst. Research '{topic}' using at "
"most 6 web-tool calls. Use no more than 3 calls with scrape=False, "
"then use scrape=True only when detailed evidence is needed. Collect "
"5-7 useful findings, source URLs, and common reader questions."
),
expected_output=(
"Clear markdown research notes with 5-7 sourced findings and 3-5 "
"potential FAQ questions."
),
)
Далее создайте задачу написания. Она использует задачу исследований в качестве контекста, чтобы Писатель мог построить статью на основе собранных материалов.
write_task = Task(
description=(
"Delegate to the Blog Post Writer. Write one complete educational "
"markdown article on '{topic}', approximately 1,000-1,500 words. Start "
"with one H1 title and an unlabeled italic summary. Then include TL;DR, "
"an introduction, useful body sections, an optional table only when it "
"improves understanding, a Conclusion, and FAQs. Use source links."
),
expected_output=(
"One continuous publication-ready markdown article with title, italic "
"summary, TL;DR, body, Conclusion, and 3-5 FAQs."
),
context=[research_task],
)
Теперь создайте задачу рецензирования. Она использует в качестве контекста и исследования, и написание, чтобы Редактор мог сверить статью с исходными материалами.
review_task = Task(
description=(
"Delegate to the Senior Editor. Review the article for accuracy, "
"clarity, structure, and source support. Return only the corrected full "
"article. Keep the same continuous article format and ensure FAQs are "
"the final section."
),
expected_output=(
"Only the final reviewed markdown article, beginning with its H1 title "
"and ending with the final FAQ answer."
),
context=[research_task, write_task],
)
Наконец, добавьте все три задачи в список.
tasks = [research_task, write_task, review_task]
print(f"Defined {len(tasks)} manager-delegated tasks.")
Вы должны увидеть:
Defined 3 manager-delegated tasks.
8. Постройте рабочий процесс
Теперь свяжем агентов и задачи в один рабочий процесс CrewAI. Мы используем иерархический процесс, в котором агент‑Менеджер координирует агентов‑исполнителей.
В этой схеме Исследователь, Писатель и Редактор добавляются как исполнители. Менеджер передаётся отдельно через manager_agent, потому что он отвечает за делегацию и координацию.
from crewai import Crew, Process
crew = Crew(
agents=[researcher, writer, reviewer],
tasks=tasks,
manager_agent=manager,
process=Process.hierarchical,
verbose=True,
memory=False,
)
Теперь выведите сведения о команде, чтобы убедиться, что рабочий процесс собран правильно.
print("Hierarchical crew assembled.")
print(f" Workers : {[a.role for a in crew.agents]}")
print(f" Manager : {crew.manager_agent.role}")
print(f" Tasks : {len(crew.tasks)}")
print(f" Process : {crew.process}")
Вы должны увидеть примерно такой вывод:
Hierarchical crew assembled.
Workers : ['Senior Research Analyst', 'Blog Post Writer', 'Senior Editor']
Manager : Content Coordinator
Tasks : 3
Process : Process.hierarchical
Это подтверждает, что иерархический рой агентов готов. На следующем шаге мы запустим команду на теме и сгенерируем финальный ответ.
9. Запустите рой агентов
Теперь можно запустить рой агентов. Мы передадим тему и начнём рабочий процесс с помощью kickoff_async().
Jupyter уже запускает цикл событий в фоне. Поэтому мы сначала применим nest_asyncio, чтобы асинхронный рабочий процесс CrewAI мог выполняться внутри ноутбука.
try:
import nest_asyncio
nest_asyncio.apply()
print("nest_asyncio applied: CrewAI can now run inside this kernel.")
except ModuleNotFoundError:
print("nest_asyncio not installed. Run: pip install nest-asyncio")
Вы должны увидеть:
nest_asyncio applied: CrewAI can now run inside this kernel.
Далее задайте тему и создайте вспомогательную функцию для запуска команды.
from datetime import datetime
topic = "Learning Adaptive Thresholds for LLM Tool Use: When to Rely on Memory and When to Call a Tool"
async def run_crew(topic: str):
"""Run the CrewAI crew asynchronously."""
print(f"Starting crew at {datetime.now().isoformat(timespec='seconds')}")
print(f"Topic: {topic!r}\n")
result = await crew.kickoff_async(inputs={"topic": topic})
print(f"\nFinished at {datetime.now().isoformat(timespec='seconds')}")
print(f"Result type: {type(result).__name__}")
return result
result = await run_crew(topic)
При запуске рабочего процесса CrewAI сначала передаёт задачу агенту‑Менеджеру. Менеджер затем делегирует исследовательскую работу Старшему аналитику‑исследователю. Исследователь использует инструмент olostep_web_search для поиска информации в интернете об адаптивных порогах, использовании инструментов LLM, памяти и решениях о вызове инструментов.

В журнале Исследователь использует и запросы только‑поиск, и запросы со скрейпингом. Бюджет поиска израсходован полностью: 6 из 6 общих веб‑вызовов и 3 из 3 ознакомительных вызовов. Одна дополнительная заявка только‑поиск блокируется, потому что достигнут лимит ознакомительных поисков. Это показывает, что бюджетные правила в нашем пользовательском инструменте Olostep работают корректно.

После этапа исследований Менеджер продолжает рабочий процесс, делегируя шаги написания и рецензирования. Агенты готовят исследовательские заметки, создают черновик статьи, а затем полируют финальный ответ. Запуск завершается успешно и возвращает объект CrewOutput.
Вы должны увидеть примерно такой вывод:
Starting crew at 2026-06-05T13:54:07
Topic: 'Learning Adaptive Thresholds for LLM Tool Use: When to Rely on Memory and When to Call a Tool'
Crew Execution Started
Task Started
Agent: Content Coordinator
Agent: Senior Research Analyst
Tool: olostep_web_search
Budget used: 6/6 total calls; 3/3 discovery calls
Finished at 2026-06-05T13:59:29
Result type: CrewOutput
Это означает, что весь рой отработал успешно. Менеджер скоординировал процесс, Исследователь собрал актуальную информацию, Писатель создал черновик, а Редактор улучшил финальный результат.
10. Просмотрите результат
После завершения работы роя мы можем отобразить финальную статью, сгенерированную командой. Значение result.raw содержит итоговый ответ рабочего процесса.
from IPython.display import Markdown, display
raw = result.raw
if "Blog Post" in raw:
raw = raw.split("Blog Post", 1)[1].strip()
display(Markdown(raw))
Это отрендерит финальный вывод как отформатированный markdown внутри ноутбука.
В большинстве случаев сгенерированный блог будет хорошо структурирован: с несколькими заголовками, понятными разделами, диаграммами или наглядными пояснениями там, где это уместно, и сильным выводом.
Сочетание исследований, написания и редакторской вычитки помогает получить контент, готовый к публикации. После просмотра результата я бы лично дал зелёный свет к публикации.

Далее мы можем вывести трассировку выполнения. Это помогает понять, как работал рой, какие агенты участвовали, сколько было веб‑вызовов и как обрабатывалась каждая задача.
from collections import Counter
def role_name(value):
"""Return a readable role/name for CrewAI agent values."""
if value is None:
return "Not reported"
if isinstance(value, str):
return value
return getattr(value, "role", None) or getattr(value, "name", None) or str(value)
print("=" * 70)
print("SWARM EXECUTION TRACE")
print("=" * 70)
# Hierarchical crew structure
print("\nCOORDINATION")
print(f"Process : {crew.process}")
print(f"Manager : {role_name(crew.manager_agent)}")
print(f"Workers : {', '.join(role_name(agent) for agent in crew.agents)}")
print(
"Flow : Manager receives each task, delegates specialist work, "
"validates the response, and passes approved context to the next task."
)
Now print the web-tool usage recorded by the custom Olostep tool.
history = list(getattr(olostep_search_tool, "_call_history", []))
discovery_calls = sum(not item["scrape"] for item in history)
scrape_calls = sum(item["scrape"] for item in history)
successful_calls = sum(item["status"] == "success" for item in history)
print("\nWEB TOOL USAGE")
print(f"Total external calls : {len(history)}/6")
print(f"Without scraping : {discovery_calls}/3")
print(f"With scraping : {scrape_calls}")
print(f"Successful calls : {successful_calls}")
if history:
for item in history:
mode = "scrape" if item["scrape"] else "search only"
domains = ", ".join(item["domains"]) if item["domains"] else "all domains"
print(
f" {item['number']}. {mode}; status={item['status']}; "
f"results={item['results']}; scope={domains}"
)
print(f" Query: {item['query']}")
else:
print(" No Olostep calls were recorded.")
Finally, print the task-by-task trace.
print("\nTASK-BY-TASK TRACE")
agent_activity = Counter()
for index, task_out in enumerate(result.tasks_output, start=1):
task = crew.tasks[index - 1] if index <= len(crew.tasks) else None
reported_agent = role_name(getattr(task_out, "agent", None))
processed_by = sorted(getattr(task, "processed_by_agents", set()) or [])
participants = processed_by or [reported_agent]
for participant in participants:
agent_activity[role_name(participant)] += 1
delegations = getattr(task, "delegations", 0) if task else 0
used_tools = getattr(task, "used_tools", 0) if task else 0
tool_errors = getattr(task, "tools_errors", 0) if task else 0
start_time = getattr(task, "start_time", None) if task else None
end_time = getattr(task, "end_time", None) if task else None
duration = end_time - start_time if start_time and end_time else None
print(f"\n--- Task {index} ---")
print(f"Reported owner : {reported_agent}")
print(f"Processed by : {', '.join(map(role_name, participants))}")
print(f"Delegations : {delegations}")
print(f"Tool uses : {used_tools}")
print(f"Tool errors : {tool_errors}")
if duration is not None:
print(f"Duration : {duration}")
preview = getattr(task_out, "raw", "").strip().replace("\n", " ")
print(f"Output preview : {preview[:300]}{'...' if len(preview) > 300 else ''}")
print("\nAGENT ACTIVITY")
for agent, count in agent_activity.most_common():
print(f"{agent}: participated in {count} task(s)")
if not agent_activity:
print("CrewAI did not expose per-agent processing metadata for this run.")
Вы должны увидеть примерно такой вывод:

Эта трассировка показывает, что иерархический рой сработал как ожидалось. Менеджер скоординировал все три задачи, Исследователь выполнил этап исследований, Писатель создал черновик статьи, а Редактор улучшил финальный ответ.
Однако здесь также видна важная особенность: многоагентные рабочие процессы могут быть дорогими.
В этом запуске процесс использовал 6 внешних веб‑вызовов и несколько вызовов LLM между Менеджером, Исследователем, Писателем и Редактором. Один запуск может стоить до примерно $0,28, а создание этого руководства обошлось примерно в $2,78 во время тестирования. Это много для простого учебного проекта.

Итоговые мысли
Рои агентов и многоагентные рабочие процессы отлично выглядят на бумаге. Идея о том, что Исследователь, Писатель, Редактор и Менеджер работают вместе, кажется мощной, и в этом руководстве итог действительно получился достаточно хорошим для публикации с минимальными правками.
Но на практике такая настройка может стоить дороже, занимать больше времени и создавать больше точек отказа. В моём случае один запуск стоит около $0,28, а подготовка этого руководства — около $2,78. Это многовато для простого учебного проекта.
Для реальных приложений я бы не всегда использовал полноценный многоагентный рой. Я предпочёл бы более программный подход, где простые задачи обрабатываются кодом или правилами, а только сложные передаются агентам. Мы также можем снизить затраты, ограничив вызовы инструментов, сократив число агентов, укоротив подсказки и используя линейный процесс вместо иерархического.
Разумеется, если оптимизировать слишком сильно, это уже перестаёт быть настоящим роем агентов и превращается скорее в правило‑ориентированный ИИ‑процесс. Поэтому цель — баланс: используйте рои агентов, когда они реально добавляют ценность, и сохраняйте простоту, когда она уместна.