Lesson 09 CrewAI Intro
Theme:

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.