track
Ber du en enda modell att i ett svep göra research om ett ämne, skriva ett utkast, granska det och putsa den slutliga versionen, får du oftast något som är mediokert på alla fyra. Modellen har ingen tydlig roll i stunden, så den försöker göra allt samtidigt och lyckas sällan riktigt bra med något. Att dela upp arbetet mellan specialiserade agenter brukar ge ett starkare resultat, eftersom varje agent kan fokusera på att göra sin del ordentligt.
I den här guiden bygger vi en agent‑svärm som gör just detta. Vi använder CrewAI för att samordna agenterna och Gemini 3.5 Flash för deras resonemang och skrivförmåga. Svärmen har fyra medlemmar: en Researcher som samlar aktuell information, en Writer som tar fram första utkastet, en Reviewer som förbättrar tydlighet och korrekthet, och en Manager som koordinerar arbetsflödet.
Vi ger också Researcher live‑åtkomst till webben via Olostep, så att den kan hämta färska källor istället för att bara förlita sig på modellens interna kunskap. I slutet har du en fungerande hierarkisk svärm som tar ett ämne och returnerar en publiceringsklar artikel — och en tydlig bild av när detta angreppssätt är värt kostnaden och när det inte är det.
Vad är en agent‑svärm?
En agent‑svärm är en grupp AI‑agenter som samarbetar för att slutföra en uppgift. Istället för att be en enda modell att på egen hand göra research, skriva, granska och färdigställa allt, delar en agent‑svärm upp arbetet mellan flera specialiserade agenter. Varje agent har en tydlig roll och systemet samordnar dem för att skapa ett starkare slutresultat.
Fördelen är specialisering. En research‑fokuserad agent kan ägna sig åt att hitta och verifiera källor, en skriv‑fokuserad agent kan fokusera på struktur och tydlighet, och en granskare kan kontrollera utkastet mot den ursprungliga researchen utan att bli distraherad av texten den just skrev.
En koordinerande agent binder ihop stegen och bestämmer vad som ska hända härnäst. I svärmen vi bygger motsvarar dessa roller direkt Researcher, Writer, Reviewer och Manager.

Detta är dock inte gratis. Fler agenter betyder fler modellanrop, mer tid och fler ställen där arbetsflödet kan fallera; en avvägning vi återkommer till i slutet när du sett vad det faktiskt kostar att köra.
Vad är CrewAI?
CrewAI är ett öppen källkods‑ramverk för att bygga multiagent‑system. Det ger ett rent sätt att definiera agenter, tilldela dem uppgifter och välja hur de samordnas, så att du kan fokusera på rollerna och arbetsflödet snarare än rören som kopplar ihop dem.
Tre begrepp gör huvuddelen av arbetet.
- Agenter är de enskilda arbetarna, var och en definierad av en roll, ett mål och en bakgrundshistoria som formar hur den beter sig.
- Uppgifter beskriver arbetet som ska göras och vilket resultat varje uppgift ska ge.
- Ett crew för samman agenter och uppgifter och kör dem enligt en vald process
Processen kan vara antingen sekventiell, där uppgifter körs i en fast ordning, eller hierarkisk, där en manager‑agent delegerar arbete till andra och validerar deras output. Här använder vi den hierarkiska processen, med Manager som sköter delegeringen.
CrewAI hanterar också modellåtkomst via LiteLLM, ett open source‑bibliotek som erbjuder ett enhetligt gränssnitt mot många leverantörer. Det är det som låter oss definiera en enda Gemini 3.5 Flash-modell och återanvända den i alla fyra agenterna, vilket vi sätter upp i stegen nedan.
Bygga en agent‑svärm med CrewAI och Gemini 3.5 Flash
Låt oss komma igång med installationen.
1. Förutsättningar
Innan du börjar, se till att du har:
- Python 3.11 eller senare installerat
- Jupyter Notebook eller JupyterLab installerat
- Grundläggande förståelse för Python och notebooks
- Ett Google AI Studio‑konto
- Fakturering aktiverad eller krediter tillagda på ditt Google‑konto för Gemini‑användning
- Ett kostnadsfritt Olostep‑konto
2. Installera beroenden
Starta Jupyter Notebook eller JupyterLab och öppna en notebook med den senaste Python‑kärnan som finns i din miljö. Kör sedan följande cell för att installera nödvändiga paket.
%pip install --quiet -U \
crewai \
crewai-tools \
google-genai \
litellm \
olostep \
nest-asyncio
När installationen är klar, kör nästa cell för att kontrollera installerade paketversioner. Det hjälper att bekräfta att allt installerades korrekt.
import importlib.metadata as md
for pkg in [
"crewai",
"crewai-tools",
"google-genai",
"litellm",
"olostep",
"nest-asyncio",
]:
print(f"{pkg:>16} {md.version(pkg)}")
Du bör se utdata liknande detta:
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. Ställ in miljövariabler
Du behöver två API‑nycklar för det här projektet.
Skapa först en Gemini API‑nyckel i Google AI Studio. Öppna Google AI Studio, gå till avsnittet API key och skapa en ny nyckel för ditt projekt. Gemini erbjuder en kostnadsfri nivå, men den har begränsad användning och hastighetsgränser. För att testa och köra Gemini mer tillförlitligt, koppla ditt Google‑faktureringskonto eller lägg till krediter på ditt Google‑konto så att användningen kan debiteras när du går över gränserna för den kostnadsfria nivån.
Skapa sedan en Olostep API‑nyckel. Registrera dig på Olostep, öppna Olosteps instrumentpanel och generera en API‑nyckel från sidan för API‑nycklar. Vi använder den här nyckeln för att ge Researcher‑agenten live‑åtkomst till webben för sökning och sid‑skrapning.
När båda nycklarna är klara, spara dem som miljövariabler. Gemini kommer att driva CrewAI‑agenterna, medan Olostep låter Researcher‑agenten samla in aktuell information från webben.
Efter att du skapat båda nycklarna, spara dem som miljövariabler innan du kör notebooken.
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"
Kör nu följande cell för att kontrollera att Gemini API‑nyckeln har laddats korrekt.
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")
Du bör se:
Gemini API key loaded
4. Konfigurera Gemini 3.5 Flash via LiteLLM
I det här projektet använder vi Gemini 3.5 Flash som delad LLM för alla CrewAI‑agenter. Gemini 3.5 Flash är utformad för snabba, agentiska och kodfokuserade arbetslaster. Den är användbar här eftersom våra agenter behöver resonera i flera steg, följa instruktioner, arbeta med verktyg och producera strukturerade utdata.
Gemini 3.5 Flash stöder också stort kontextfönster, långa utdata, thinking och verktygsanvändning, vilket gör den till ett starkt val för agent‑arbetsflöden. Eftersom en agent‑svärm kan anropa modellen flera gånger över Researcher, Writer, Reviewer och Manager, hjälper en Flash‑modell till att hålla arbetsflödet responsivt samtidigt som den ger starkt resonemang och skrivkvalitet.
Vill du jämföra modellen med andra toppmoderna LLM:er rekommenderar jag våra guider om Gemini 3.5 Flash vs GPT-5.5 och Gemini 3.5 Flash vs Claude Opus 4.8.
Vi kommer att nå Gemini via LiteLLM. LiteLLM är ett open source‑bibliotek som ger ett enhetligt gränssnitt för att anropa många LLM‑leverantörer, inklusive Gemini, OpenAI, Anthropic, Bedrock, Vertex AI och andra. I den här konfigurationen använder CrewAI LiteLLM i bakgrunden så att vi kan skicka Gemini‑modellnamnet i ett enkelt format provider/model.
Nu skapar vi en delad CrewAI‑LLM och återanvänder den för alla agenter. Det håller uppsättningen enkel eftersom Researcher, Writer, Reviewer och Manager alla använder samma Gemini‑modell.
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)")
Du bör se utdata liknande detta:
LLM ready: gemini-3.5-flash
Provider API: Google Gemini (via LiteLLM)
Detta bekräftar att CrewAI nu kan nå Gemini via LiteLLM. I nästa steg skickar vi vidare det delade gemini_llm‑objektet till varje agent.
5. Lägg till live‑åtkomst till webben
Researcher‑agenten behöver tillgång till aktuell information. För att göra detta skapar vi ett anpassat Olostep‑sökverktyg och ger det till Researcher‑agenten senare.
Detta verktyg kan fungera i två lägen. Det kan returnera enbart söksammanfattningar, vilket går snabbare, eller skrapa resultatsidorna och returnera sidinnehåll i markdown. Vi lägger också till en liten sökbudget så att agenten inte fortsätter att anropa webbverktyget för många gånger.
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."
)
Börja med att ladda Olostep API‑nyckeln från miljön.
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
Skapa nu det anpassade CrewAI‑verktyget.
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)
Avslutningsvis, skapa en instans av verktyget.
olostep_search_tool = OlostepSearchTool()
print(f"Tool ready: {olostep_search_tool.name}")
Du bör se utdata liknande detta:
Olostep API key loaded.
Tool ready: olostep_web_search
Detta bekräftar att Olosteps webbsöksverktyg är redo. Senare kopplar vi detta verktyg till Researcher‑agenten så att den kan samla in aktuell information från webben.
6. Skapa agenterna
Nu skapar vi agenterna för vår svärm. Vi definierar tre arbetsagenter och en manager‑agent. Arbetsagenterna gör huvuduppgifterna, medan managern koordinerar arbetsflödet.
Researcher‑agenten använder Olostep‑verktyget för att hitta aktuell information. Writer‑agenten förvandlar researchen till ett utkast. Reviewer‑agenten förbättrar utkastet. Manager‑agenten avgör hur arbetet ska delegeras i den hierarkiska besättningen.
Skapa arbetsagenterna
Vi börjar med de tre arbetsagenterna.
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,
)
Skapa sedan Writer‑agenten.
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,
)
Skapa nu Reviewer‑agenten.
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,
)
Skapa manager‑agenten
Avslutningsvis, skapa Manager‑agenten.
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,
)
Managern konfigureras separat när vi bygger den hierarkiska besättningen. Därför har den ännu inte lagts till i listan över arbetsagenter.
7. Definiera uppgifter
Nu definierar vi uppgifterna för agent‑svärmen. Dessa uppgifter tilldelas inte direkt till arbetsagenterna. Istället avgör Manager‑agenten vilken arbetare som ska hantera varje uppgift.
I detta arbetsflöde är den första uppgiften research, den andra skrivande och den tredje granskning. Varje uppgift inkluderar också ett förväntat resultat så att agenterna vet exakt vad de ska producera.
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."
),
)
Skapa sedan skrivuppgiften. Denna uppgift använder research‑uppgiften som kontext, så att Writer kan bygga artikeln från den insamlade researchen.
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],
)
Skapa nu granskningsuppgiften. Denna uppgift använder både research‑ och skrivuppgifterna som kontext, så att Reviewer kan kontrollera artikeln mot den ursprungliga researchen.
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],
)
Lägg till sist alla tre uppgifter i en lista.
tasks = [research_task, write_task, review_task]
print(f"Defined {len(tasks)} manager-delegated tasks.")
Du bör se:
Defined 3 manager-delegated tasks.
8. Bygg arbetsflödet
Nu kopplar vi ihop agenterna och uppgifterna till ett CrewAI‑arbetsflöde. Vi använder en hierarkisk process där Manager‑agenten koordinerar arbetsagenterna.
I denna konfiguration läggs Researcher, Writer och Reviewer till som arbetsagenter. Manager skickas separat via manager_agent, eftersom den ansvarar för delegering och samordning.
from crewai import Crew, Process
crew = Crew(
agents=[researcher, writer, reviewer],
tasks=tasks,
manager_agent=manager,
process=Process.hierarchical,
verbose=True,
memory=False,
)
Skriv nu ut detaljerna för besättningen för att bekräfta att arbetsflödet sattes ihop korrekt.
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}")
Du bör se utdata liknande detta:
Hierarchical crew assembled.
Workers : ['Senior Research Analyst', 'Blog Post Writer', 'Senior Editor']
Manager : Content Coordinator
Tasks : 3
Process : Process.hierarchical
Detta bekräftar att den hierarkiska agent‑svärmen är redo. I nästa steg kör vi besättningen på ett ämne och genererar det slutliga svaret.
9. Kör agent‑svärmen
Nu kan vi köra agent‑svärmen. Vi anger ett ämne och startar arbetsflödet med kickoff_async().
Jupyter kör redan en händelseloop i bakgrunden. Därför applicerar vi först nest_asyncio, så att det asynkrona CrewAI‑arbetsflödet kan köras i notebooken.
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")
Du bör se:
nest_asyncio applied: CrewAI can now run inside this kernel.
Definiera sedan ett ämne och skapa en hjälpfunktion för att köra besättningen.
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)
När arbetsflödet startar ger CrewAI först uppgiften till Manager‑agenten. Managern delegerar sedan research‑arbetet till Senior Research Analyst. Researchern använder verktyget olostep_web_search för att söka på webben efter information om adaptiva trösklar, LLM‑verktygsanvändning, minne och beslut att anropa verktyg.

I loggen använder Researcher både rena sökanrop och skrapningsanrop. Sökbudgeten utnyttjas fullt ut, 6 av 6 totala webbanrop och 3 av 3 upptäcktsanrop. Ett extra sök‑endast‑anrop blockeras eftersom gränsen för upptäcktsökningar redan har nåtts. Det visar att budgetreglerna i vårt anpassade Olostep‑verktyg fungerar korrekt.

Efter researchfasen fortsätter Manager arbetsflödet genom att delegera skriv‑ och granskningsstegen. Agenterna tar fram research‑anteckningar, skriver utkastet och polerar sedan det slutliga svaret. Körningen slutförs framgångsrikt och returnerar ett CrewOutput‑objekt.
Du bör se utdata liknande detta:
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
Detta betyder att hela svärmen kördes framgångsrikt. Managern koordinerade arbetsflödet, Researcher samlade in aktuell information, Writer skapade utkastet och Reviewer förbättrade slutresultatet.
10. Visa resultatet
När svärmen har körts klart kan vi visa den slutliga artikel som genererats av besättningen. Värdet result.raw innehåller det slutliga svaret från arbetsflödet.
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))
Detta renderar slututdata som formaterad markdown i notebooken.
I de flesta fall kommer den genererade bloggen redan att vara välstrukturerad, med flera rubriker, tydliga avsnitt, diagram eller visuella förklaringar där det är lämpligt, och en stark slutsats.
Kombinationen av research, skrivande och redaktionell granskning hjälper till att ta fram innehåll som är redo för publicering. Efter att ha granskat utfallet skulle jag personligen ge grönt ljus för publicering.

Därefter kan vi skriva ut en körningsspårning. Det hjälper oss att förstå hur svärmen arbetade, vilka agenter som deltog, hur många webbanrop som användes och hur varje uppgift hanterades.
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.")
Du bör se utdata liknande detta:

Denna spårning visar att den hierarkiska svärmen fungerade som förväntat. Managern koordinerade alla tre uppgifter, Researcher hanterade researchsteget, Writer skapade artikelutkastet och Reviewer förbättrade slutsvaret.
Detta visar dock också en viktig begränsning: multiagent‑arbetsflöden kan bli dyra.
I denna körning användes 6 externa webbanrop och flera LLM‑anrop över Manager, Researcher, Writer och Reviewer. En enskild körning kan kosta upp till cirka $0,28, och att skapa denna guide kostade cirka $2,78 under testningen. Det är högt för ett enkelt tutorial‑projekt.

Avslutande tankar
Agent‑svärmar och multiagent‑arbetsflöden ser bra ut på papper. Idén att ha en Researcher, Writer, Reviewer och Manager som arbetar tillsammans känns kraftfull, och i den här guiden var slutresultatet faktiskt tillräckligt bra för att publiceras med mindre redigering.
Men i praktiken kan denna uppsättning kosta mer, ta längre tid och skapa fler felpunkter. I mitt fall kostar en körning cirka $0,28 och att skapa den här guiden kostar cirka $2,78. Det är mycket för ett enkelt tutorial‑projekt.
För verkliga applikationer skulle jag inte alltid använda en fullständig multiagent‑svärm. Jag skulle föredra en mer programmatisk uppsättning där enkla uppgifter hanteras av kod eller regler, och endast komplexa uppgifter lämnas över till agenter. Vi kan också minska kostnaderna genom att begränsa verktygsanrop, använda färre agenter, korta ner uppmaningar och använda ett linjärt arbetsflöde istället för ett hierarkiskt.
Självklart, när vi optimerar för mycket är det kanske inte längre en sann agent‑svärm. Det blir mer som ett regelbaserat AI‑arbetsflöde. Därför är målet balans: använd agent‑svärmar när de tillför verkligt värde, men håll det enkelt när de inte gör det.