Solutions — Three CrewAI Exercises
Lesson 09 · Building Your Own Agents · worked answers with commentary
Worked solutions for the three homework tasks. Read them after you’ve had your own swing — each section includes a “common mistake” box, and comparing your draft to the “why this not that” commentary is where the lesson actually lands.
How to use this page
Each snippet shows only the lines that change relative to hello_crew.py from the
Lab 09 walk-through. Drop them in, re-run with your ANTHROPIC_API_KEY set, and
compare the printed output to the “expected behaviour” notes. If something
doesn’t match, the “common mistake” box is the first place to look.
Solution to Task 1 — Swap the writer’s model to a cheaper one
One-line change on the writer agent — researcher stays on Sonnet:
writer = Agent(
role="Technical Writer",
goal="Write a 2-3 sentence brief based on the research",
backstory="...",
llm="anthropic/claude-3-5-haiku-latest", # was sonnet
verbose=True,
)
Why this works. The writer’s task is pure transformation — given the researcher’s bullets, rephrase them as prose. Cheap models handle that well; you rarely need Sonnet-grade reasoning to turn three bullet points into a paragraph. The researcher’s task, by contrast, involves judgement — which sources matter, are they authoritative, is this primary or a tertiary blog summary? — which usually justifies the more capable model.
The general rule. The role doing the hardest reasoning gets the best model; the
role doing rote rephrasing gets the cheap model. This is one of CrewAI’s underappreciated
wins — per-agent llm= makes per-role routing trivial. You don’t need a
separate framework, a router agent, or any wiring; just pick the right model in each
Agent(...) constructor and the cost split takes care of itself.
Expected behaviour. Total cost drops noticeably on the writer call (Haiku is roughly 1/12th the price of Sonnet 3.5 at the time of writing). Brief quality stays acceptable for a 2–3 sentence transformation; you may notice slightly less elegant phrasing, but the facts the researcher gathered come through intact.
Common mistake — putting the cheapest model on the reviewer
The instinct is “the reviewer just checks things, give it the cheap model.” This is backwards. Reviewing requires more judgement than writing, not less — the reviewer has to spot factual errors, weak claims, style violations, and missing context, often without re-doing the underlying research. A cheap reviewer rubber-stamps everything and you lose the entire benefit of having a reviewer in the crew. If anything, put the most capable model on the reviewer and a mid-tier model on the writer.
Solution to Task 2 — verbose=False and a structured summary print
Turn off CrewAI’s debug noise everywhere, then read each agent’s output explicitly via
crew.tasks[N].output:
researcher = Agent(role="...", goal="...", backstory="...", llm=MODEL, verbose=False)
writer = Agent(role="...", goal="...", backstory="...", llm=MODEL, verbose=False)
crew = Crew(
agents=[researcher, writer],
tasks=[research_task, write_task],
process=Process.sequential,
verbose=False,
)
result = crew.kickoff(inputs={"topic": "Linux kernel 6.14 networking"})
print(f"Researcher said:\n{crew.tasks[0].output}\n")
print(f"Writer said:\n{crew.tasks[1].output}\n")
print(f"Final result:\n{result}\n")
Why this matters. verbose=True is great for the first run —
you can see exactly which LLM call the agent made, what it returned, and how the next task picked
it up. But in production you want structured access to artefacts, not a wall of stdout
log lines you have to grep. crew.tasks[N].output is that structured handle: ship it
to your logging backend, your database, your evals harness, whatever — one field per task,
cleanly separated.
What result actually is. The return value of kickoff()
is just the last task’s output (which is also crew.tasks[-1].output). For
debugging, verbose=True is faster. For production, log
crew.tasks[N].output per-task so you can audit each agent’s contribution
independently — and so a failed reviewer doesn’t mean you lose the
researcher’s work.
Common mistake — reading task output before kickoff() returns
crew.tasks[N].output is populated only after the task has run. Reading it
before kickoff() returns gives you None (or, in some versions, an
exception). If you need to react to intermediate task outputs as they complete — say, to
stream a progress update to a UI — use task callbacks instead, not polling. The
callback= argument on Task(...) fires when that specific task
finishes, with the output as its argument.
Solution to Task 3 — Shared “house style” via backstory composition
Define the style string once, append it to every agent’s backstory:
HOUSE_STYLE = (
"Write in British English. Use Oxford commas. "
"Never use the phrase 'in conclusion'."
)
researcher = Agent(
role="Senior Researcher",
goal="Find 3 short factual statements about the topic",
backstory=(
"You are a meticulous researcher. You prefer primary sources. "
+ HOUSE_STYLE
),
llm=MODEL,
verbose=True,
)
writer = Agent(
role="Technical Writer",
goal="Write a 2-3 sentence brief based on the research",
backstory=(
"You are a concise technical writer. You never invent facts. "
+ HOUSE_STYLE
),
llm=MODEL,
verbose=True,
)
Why string composition, not subclassing. CrewAI 0.86 does not ship a first-class
“crew-wide system prompt” primitive — backstory is the only hook
for persona-shaped context, and it lives on the individual Agent. Composing shared
rules into per-agent backstories is the supported pattern. In a larger codebase, factor this into
a helper:
def make_agent(role: str, goal: str, role_specific_backstory: str, llm: str) -> Agent:
return Agent(
role=role,
goal=goal,
backstory=f"{role_specific_backstory} {HOUSE_STYLE}",
llm=llm,
verbose=True,
)
Now every new agent in the crew inherits the house style for free, and updating the style is a one-line change in one place — the discipline of “don’t copy a paragraph into every constructor” is enforced by the function signature, not by reviewer vigilance.
Expected behaviour. The writer’s brief uses British spelling (organisation, not organization), keeps Oxford commas in lists of three or more, and avoids the banned phrase. The researcher’s bullets don’t have many style choices to make at the word-rephrasing level, but the orthography matches and any prose fragments respect the rules.
Common mistake — subclassing Agent to encode shared style
It’s tempting to write class StyledAgent(Agent): ... and override
__init__ to inject HOUSE_STYLE. Don’t. That fights the
framework — CrewAI’s Agent is a Pydantic model with validation rules,
and subclassing it ties you to internal layout that may change between minor versions. String
composition (or the make_agent helper above) is the path of least resistance and
survives every CrewAI upgrade unchanged.
Closing tip. You now have the CrewAI primitives in your fingertips: roles, tasks, sequential process, per-agent models and tools, and shared style via backstory composition. Lesson 10 puts them to work in the canonical CrewAI shape — a 3-agent crew (planner + worker + reviewer) that produces a structured brief. See you there.