Lesson 12 Cost, Latency & Evals Across Frameworks
Theme:

Solutions — Structural Eval, MCP Second Tool, Haiku Budget

Lesson 12 · Choosing & Shipping · worked answers with commentary

Below is one clean answer per homework task, plus the why-this-not-that commentary that distinguishes a passing solution from one that’s actually useful in production. Compare your attempt against these — the numbers in Task 3 in particular will look different on your machine, but the shape of the trade-off should match.

How to read the solutions

Each section opens with the code, then a common mistake paragraph, then the broader lesson. The code is meant to drop straight into the eval harness or the MCP stub — copy-paste, re-run, and you should see the new behaviour on the next pass.

Solution to Task 1 — a structural eval for the L04 report

Write eval_structural.py next to the eval harness (or inside agents/_eval/ if you prefer to keep eval code co-located). Import the agent entrypoint, run it against three different questions, and assert on the shape of the returned report dict — not on the prose itself.

"""Structural eval for the L04 research assistant."""
from agents.lesson04_tool_heavy_agent.agent import run_research_agent

QUESTIONS = [
    "What changed in Linux kernel 6.14's networking stack?",
    "What's notable in 6.14's filesystem changes?",
    "What scheduler improvements landed in 6.14?",
]

def evaluate(report: dict) -> dict:
    return {
        "has_intro": len(report.get("intro", "")) >= 20,
        "n_bullets": len(report.get("bullets", [])),
        "n_sources": len(report.get("sources", [])),
    }

for q in QUESTIONS:
    out = run_research_agent(q)
    e = evaluate(out["report"])
    ok = e["has_intro"] and 2 <= e["n_bullets"] <= 5 and e["n_sources"] >= 1
    print(f"[{'OK ' if ok else 'FAIL'}] {q!r}  -> {e}")

Run it three times; you should see at least one OK line and any FAIL lines should print the offending counts so you can see why they failed (zero sources? six bullets? a one-word intro?).

Common mistake

Asserting len(bullets) == 3 instead of a range. The LLM may legitimately produce 2 or 4 bullets depending on the question. The eval’s job is to flag structural failures — empty fields, malformed types, missing sources — not to enforce stylistic uniformity. A too-strict eval will fire on every reasonable output and you’ll learn to ignore it; an eval that fires only on real failures gets looked at every time it goes red.

Lesson. Structural evals are the cheapest, fastest eval rung. They don’t need a judge LLM, they don’t need a reference answer, and they catch the failure modes that matter most in production: the agent didn’t return what it promised to return. Stack them at the front of any eval pipeline — only run the expensive LLM-as-judge stages on the runs that pass the structural gate.

Solution to Task 2 — add an MCP-hosted pkb_count tool

Two changes: one server-side (the MCP stub gains a new tool), one client-side (the agent declares it so the LLM knows it’s available). Both edits are surgical.

Server-side — in agents/_eval/mcp_stub/server.py:

# In 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)))]

Client-side — add a second @tool in agent_with_mcp.py:

@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

Then ask the agent “How many entries are in the knowledge base?”. With the tool wired in, the agent should call pkb_count exactly once, get back a string like "7", and quote that number in its answer rather than hallucinating a count or refusing to answer.

Common mistake

Forgetting to add the new tool to the tools=[...] list when the agent is constructed. The MCP server can advertise it, the @tool decorator can register the Python wrapper, and the LLM still won’t know it exists until the agent is told to expose it. Symptom: the agent answers “I don’t have a way to count entries” even though the server is happily serving the tool. Fix: pass the new function into the agent’s tool list.

Lesson. MCP tools cross three boundaries (server, transport, client) and any of those boundaries can silently fail to wire up. When debugging, work outwards from the server: confirm the tool appears in list_tools() output (you can dump it in the connect handshake), then confirm the client sees it, then confirm the agent has it in its tool list. The most common failure is the last hop, because the others usually raise on misconfiguration and the last one just shrugs.

Solution to Task 3 — latency budget for a slimmer model

Re-run agent.py with model="claude-3-5-haiku-latest", time three runs each for Sonnet and Haiku, take the median, and price each run from the harness’s token counters. A representative result — your numbers will differ, but the shape of the trade-off should match:

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, occasional source omission

Roughly: Haiku is ~4× cheaper and ~2× faster, with a noticeable quality drop on this research-assistant shape. The cost / latency / quality triangle is tight — you almost never pick up two cleanly without giving back on the third.

Recommendation. Use Sonnet for any production agent where answer quality matters (research, customer-facing summaries, anything a human will read end to end). Use Haiku for triage / classification / “quick first pass” agents where the cost & latency win dwarfs the quality delta — routing, intent detection, draft generation that a Sonnet pass will polish.

Common mistake

Picking Haiku because it “scored higher” on the cost dimension of L11’s scorecard. The L11 scorecard measured frameworks against each other, all running Sonnet. The model axis is orthogonal to the framework axis — once you pick a framework, you still have to pick a model, and the same trade-offs apply on top of whatever the framework choice already cost you. Don’t double-count the cost win.

Lesson. Model choice is its own decision, made after the framework choice and run on the same task suite. Re-run your evals when you switch models, the way you’d re-run them when you switch frameworks. The numbers above are illustrative; your numbers, on your tasks, are the ones that matter.

What you’ve built

Across three small extensions you’ve added structural rigour (cheap, fast, no-judge evals at the front of the pipeline), MCP-hosted tool composition (a second tool added in two surgical edits), and multi-axis model selection (cost × latency × quality measured on the same task suite) to the cross-framework operations toolkit. That’s the production template for cost / latency / eval discipline in three pieces:

Closing tip: You’ve now built the operations layer that sits across all four frameworks. Lesson 13 closes the course with where the field is going — what’s settled, what’s still unsettled, and one warning about the half-life of any framework pick.

← Back to homework