Homework — Three Extensions of the Brief Crew
Lesson 10 · Building Your Own Agents · ~45 min
All three tasks below extend agents/lesson10-multi-agent-crew/agent.py from the
Lab 10 walk-through — the planner + worker + reviewer crew that produces a structured
brief on a topic via run_brief_crew(topic). Keep that file open as your base;
each task is a focused extension that builds on the previous one.
Before you start
Make sure python agent.py "transformers" (or any topic in the in-memory KB)
runs end-to-end and prints a dict with plan, draft, and
review fields. If it doesn’t, revisit the lab guide before tackling
the homework — debugging the extensions is much easier on a working baseline.
Task 1 — Add a fourth agent (fact-checker) between worker and reviewer
Define a new agent that sits between the worker (drafter) and the reviewer. Its job is to verify each claim in the draft against the KB entry for the topic — not to rewrite, just to flag.
fact_checker = Agent(
role="Fact Checker",
goal="Verify each claim in the draft against the KB entry.",
backstory="You are a literal-minded checker. If a claim isn't in the KB, you flag it.",
llm=MODEL,
tools=[topic_lookup],
verbose=True,
)
Then add a fact_check_task between draft_task and
review_task, wired to read both upstream artefacts:
fact_check_task = Task(
description=(
"Look up the KB entry for the topic with topic_lookup. "
"For each claim in the draft, mark it OK or FLAGGED depending on "
"whether it appears in the KB entry. Output a short bullet list."
),
expected_output="A bullet list of claims, each marked OK or FLAGGED with a one-line reason.",
agent=fact_checker,
context=[plan_task, draft_task],
)
Register it in crew.tasks=[...] in the new order (plan → draft →
fact_check → review) and update review_task’s
context= to include fact_check_task so the reviewer
can read both the draft and the fact-checker’s findings. Re-run on the same topic.
✓ Self-check: the fact-checker’s output appears in the verbose log between draft and review, and the reviewer’s critique references the fact-checker’s flags (e.g., calls out an unsupported claim). Write a 2–3-sentence note in a comment at the top of your file on whether a 4-agent crew is better (more accurate review) or just slower (one extra LLM call per run) — both answers are defensible; the point is to notice the trade-off.
Why a separate fact-checker, not just a stricter reviewer?
You could fold this into the reviewer’s backstory and save one agent. The reason to split is single-responsibility: a checker that only verifies claims is easier to swap for a cheaper model, easier to evaluate (does it catch hallucinations?), and easier to disable when you don’t need it. That’s the multi-agent design argument — each agent has one job you can reason about in isolation.
Task 2 — Per-agent model routing for a real cost win
The worker is the most token-heavy step in this crew — it reads the plan and the KB entry and produces the longest output (the draft). The planner and reviewer are judgement-heavy but short. Route each agent to the right model:
- planner →
"anthropic/claude-3-5-sonnet-latest"(judgement) - worker →
"anthropic/claude-3-5-haiku-latest"(transformation only) - reviewer →
"anthropic/claude-3-5-sonnet-latest"(judgement)
Concretely, replace the shared MODEL on each agent with the per-agent string:
planner = Agent(role="Planner", goal=..., backstory=...,
llm="anthropic/claude-3-5-sonnet-latest", verbose=True)
worker = Agent(role="Worker", goal=..., backstory=...,
llm="anthropic/claude-3-5-haiku-latest",
tools=[topic_lookup], verbose=True)
reviewer = Agent(role="Reviewer", goal=..., backstory=...,
llm="anthropic/claude-3-5-sonnet-latest", verbose=True)
Run the crew on three different topics from your KB and record the per-run cost (from
crew_output.token_usage or whatever your CrewAI version exposes — you
may need to compute cost from token counts using current per-token pricing).
✓ Self-check: total cost drops ~30–40% vs. the all-Sonnet baseline, because the worker (the most token-heavy step) is now on Haiku. The brief quality should be essentially unchanged — the planner is still picking the structure and the reviewer is still catching mistakes. Both of those are judgement, which Sonnet is better at; the worker is just “take this plan and this KB entry, write 5 bullets,” which Haiku does fine.
This is the underrated CrewAI win
Per-agent model routing is one of those things that sounds obvious in retrospect but is genuinely hard to retrofit onto a single-agent setup — with one agent you pick one model for the whole loop. With three agents, each agent picks its own. On a production brief generator this can be the difference between a $0.04 run and a $0.015 run, at identical output quality. Always profile which agent is the token hog before reaching for prompt-tuning.
Task 3 — Replace the reviewer’s free-text output with a structured ReviewReport
Right now the reviewer returns prose. That’s fine for a human reading the brief, but your downstream code can’t branch on “ship it” vs. “needs another pass.” Define a Pydantic schema and have CrewAI parse the reviewer’s output into it:
from pydantic import BaseModel
class ReviewIssue(BaseModel):
severity: str # "low" | "medium" | "high"
description: str
class ReviewReport(BaseModel):
issues: list[ReviewIssue]
overall: str # "ship" | "revise" | "reject"
Add output_pydantic=ReviewReport to review_task and update the
reviewer’s expected_output to describe the schema in words (CrewAI
feeds the schema into the prompt for you, but the description helps the model). Then
update run_brief_crew to return the parsed object alongside the prose:
review_task = Task(
description="...same as before...",
expected_output=(
"A ReviewReport with a list of issues (each with severity low|medium|high "
"and a one-line description) and an overall verdict of 'ship', 'revise', or 'reject'."
),
agent=reviewer,
context=[plan_task, draft_task],
output_pydantic=ReviewReport,
)
def run_brief_crew(topic: str) -> dict:
crew_output = crew.kickoff(inputs={"topic": topic})
return {
"topic": topic,
"plan": str(plan_task.output),
"draft": str(draft_task.output),
"review": str(review_task.output),
"review_struct": review_task.output.pydantic.model_dump(),
}
Run on three topics and print just the review_struct for each.
✓ Self-check: review_struct is a dict with two keys — issues (a list of {severity, description} dicts) and overall (one of the three allowed strings). The downstream consumer can now write if result["review_struct"]["overall"] == "ship": publish(result["draft"]) — no regex on prose, no judgement call. This is the bridge from “the crew produces text a human reads” to “the crew produces a decision your pipeline acts on.”
Bonus — gate the publish step on overall
Add a tiny wrapper around run_brief_crew that returns the draft only when
overall == "ship" and otherwise returns the issues list as a
structured error. This is the shape of a real publishing workflow: the crew makes a
call, your code routes accordingly. We’ll formalise this scorecard-style pattern
in Module 6 (cross-framework evaluation).