Homework — Structural Evals, an MCP Second Tool, and a Haiku Budget
Lesson 12 · Cost, Latency & Evals Across Frameworks · ~60 min
All three tasks below build on the Lab 12 workflow: token accounting on the L04 research
assistant, a latency budget calculation, and the MCP-bridge sidebar that connects a Claude
SDK agent to the stub MCP server at agents/_eval/mcp_stub/server.py. Keep the
Lab 12 environment open as your base — each task assumes you can run
run_research_agent(question) from L04 and that the MCP stub launches cleanly
via stdio.
Before you start
Confirm that python -m agents.lesson04_tool_heavy_agent.agent "What changed in
Linux kernel 6.14's networking stack?" returns a dict with report,
cost_usd, and latency_s keys, and that
python agents/_eval/mcp_stub/server.py --self-test prints
parse ok. If either fails, revisit the lab guide before tackling the
homework — debugging structural evals is much easier on a working baseline.
Task 1 — Write a structural eval for the L04 research assistant
Create eval_structural.py at the repo root (or inside
agents/_eval/ — either is fine, just be consistent with your imports).
Import run_research_agent from L04 and assert that the returned
report dict has the following structural shape:
intro— a string of length ≥ 20 chars.bullets— a list of length 2–5.sources— a list of length ≥ 1.
Run the eval on three different questions — pick ones with different shapes (a factual lookup, an open-ended “what changed” question, and a comparison-style question). Print a one-line OK/FAIL per question with the structural counts so a failure is obvious at a glance:
[OK ] 'What changed in Linux kernel 6.14...' -> {'has_intro': True, 'n_bullets': 3, 'n_sources': 2}
[FAIL] 'Compare ext4 and btrfs for the 6.14 era' -> {'has_intro': True, 'n_bullets': 1, 'n_sources': 2}
✓ Self-check: at least one question passes; failures (if any) emit a clear assertion-style message that names the dimension that failed (intro / bullets / sources) and the actual count. Do not hard-fail the whole script on the first failure — the point of a structural eval is to surface all the failures in one run, not just the first one.
Why a structural eval, not a content eval?
A structural eval can't tell you whether the bullets are correct — that's what an LLM-as-judge or held-out test set is for. What it can tell you is whether the agent's output is shaped right: did it return the keys downstream code expects, in plausible quantities? This is the eval you write before the content eval, because broken shape is the most common failure mode and the cheapest to detect. Treat it as a smoke test, not a verdict.
Task 2 — Add an MCP-hosted second tool (pkb_count)
Extend the stub MCP server at agents/_eval/mcp_stub/server.py with a second
tool, pkb_count() -> int, that returns the number of entries in the
in-process PKB dict. Then update the client-side
agent_with_mcp.py (from the Lab 12 MCP-bridge sidebar) so the agent
sees the new tool — remember the agent only knows about tools you wire
into its tools=[...] list.
# In agents/_eval/mcp_stub/server.py, inside list_tools():
Tool(
name="pkb_count",
description="Return the number of entries in the PKB.",
inputSchema={"type": "object", "properties": {}, "required": []},
),
# And in call_tool():
elif name == "pkb_count":
return [TextContent(type="text", text=str(len(PKB)))]
On the client side, add a second @tool wrapper in
agent_with_mcp.py that proxies the MCP call:
@tool
async def pkb_count() -> str:
"""Return the number of entries currently in the PKB."""
result = await session.call_tool("pkb_count", {})
return result.content[0].text
Run the agent with the prompt “How many entries are in the knowledge base?” and inspect the tool-call trace.
✓ Self-check: the agent calls pkb_count exactly once (not twice, not zero times) and quotes the returned number verbatim in its reply. If it calls pkb_search first to “look around” before counting, that's a tool-selection failure worth noting — add a sentence to the tool's description telling the model to use it directly for “how many” questions and re-run.
Why an MCP-hosted tool and not just another @tool?
You could add pkb_count directly as an in-process @tool and
skip MCP entirely. The reason to host it on the MCP server is portability:
the same tool, with no changes, can be picked up by any MCP-aware client — Claude
SDK, LangGraph, CrewAI, or a future framework. This is the bridge between the
Building Agents with Claude course and the Creating MCP Servers
course: tools written for one are reusable in the other.
Task 3 — Latency & cost budget for a slimmer model (Haiku)
Re-run agents/lesson04-tool-heavy-agent/agent.py with
model="claude-3-5-haiku-latest" instead of the default Sonnet. Measure two
numbers across three runs of the same question (pick one from Task 1):
- Cost per task — average across the three runs.
- Median latency — wall-clock seconds from
run_research_agententry to return.
Record the Sonnet baseline (from your Lab 12 numbers) alongside the Haiku numbers in a small markdown table:
| Model | Cost/run | Median latency | Quality (vibe) |
|-----------------------------|----------|----------------|----------------|
| claude-3-5-sonnet-latest | $0.034 | 18s | sources cited, bullets specific |
| claude-3-5-haiku-latest | $0.008 | 9s | bullets sometimes vague |
Then write a 2–3 sentence recommendation: for which class of workload would you pick Haiku, and for which class would you pick Sonnet?
✓ Self-check: Haiku should land roughly 3–5× cheaper and 2× faster than Sonnet on the same question. The answer quality may degrade (sometimes substantially) for the research-assistant shape — Haiku is great at transformation, weaker at multi-source synthesis. Your recommendation should name the dimension that wins for each workload (e.g., “Haiku for triage / classification / first-pass; Sonnet for any agent where source attribution matters”).
Don't pick a model from the L11 scorecard alone
It's tempting to look at the L11 scorecard and conclude “Haiku is the cheap-cost winner.” That scorecard was measured on each framework's Sonnet runs — it's a framework comparison, not a model comparison. Model choice is a separate axis from framework choice, and it deserves its own measurement on your own questions. The whole point of this homework is to teach you to keep those two axes distinct.