I built a "do-anything" agent. It could search the web, edit files, send Slack messages, open PRs, query Postgres, and summarize PDFs. On the demo slide it looked like a junior engineer. In production it looked like one on three espressos and no ticket. It booked the right meeting on the wrong calendar. It opened a PR that reformatted half the repo. It answered a support question with last quarter's pricing. Every failure had the same root cause: the agent had too many hands and no job description. If your general-purpose agent keeps almost-working on everything and finishing nothing, this is the post I wish I'd read first. The fix usually isn't a smarter model. It's a narrower job. Generalists are impressive. Specialists ship. A Swiss-army knife is a great keychain. It is a terrible chef's knife and screwdriver if you need both in the same hour — mediocre versions of each tool, plus a blister. General agents are the same trick. One system prompt, twelve tools, infinite ambition. The model has to constantly re-decide what kind of worker it is before it decides what to do next. That extra deliberation is where loops, hallucinations, and "helpful" side quests are born. A specialist agent gets one sentence of identity: "You triage GitHub issues labeled bug and write a minimal repro." Suddenly the tool list shrinks, success criteria fit on a sticky note, and failure modes become debuggable. The night the generalist lost to a 40-line specialist My do-anything agent had inbox, docs-search, and CRM tools, plus a "be helpful" prompt the size of a terms-of-service page. A customer asked whether plan tier X included SSO. It searched docs (good), found an outdated blog post (bad), "confirmed" the tier in the CRM (fine), and sent a confident wrong answer (career-limiting). I didn't fix it with another prompt paragraph. I deleted nine tools and wrote one specialist: Job: Answer billing/plan questions only. Tools: get_account_plan(account_id), get_plan_features(plan_id), draft_reply(thread_id, body). Rule: If the feature isn't in get_plan_features, say you don't know and escalate — never browse the public blog. Wrong-answer rate fell off a cliff. Not because the model got smarter. Because it stopped freestyling outside a tiny, true dataset. A tiny specialist scaffold you can steal from dataclasses import dataclass from typing import Callable, Dict @dataclass class Specialist: name: str job: str # one sentence tools: Dict[str, Callable] max_steps: int = 8 escalate_to: str | None = None def get_account_plan(args): return db.accounts.get(args["account_id"]).plan_id def get_plan_features(args): return db.plans.get(args["plan_id"]).features def draft_reply(args): return helpdesk.draft(args["thread_id"], args["body"]) billing_agent = Specialist( name="billing_specialist", job=( "Answer plan/feature questions using account + plan data only. " "If a feature isn't listed, say you don't know and escalate." ), tools={ "get_account_plan": get_account_plan, "get_plan_features": get_plan_features, "draft_reply": draft_reply, }, max_steps=6, escalate_to="human_support", ) def run_specialist(agent: Specialist, goal: str) -> str: """Model picks a tool or finishes. No freestyle side quests.""" scratch = [] for _ in range(agent.max_steps): decision = llm.decide( job=agent.job, goal=goal, tool_names=list(agent.tools), scratch=scratch, ) if decision.action == "finish": return decision.output if decision.action == "escalate": return f"ESCALATE->{agent.escalate_to}: {decision.output}" if decision.action not in agent.tools: scratch.append(f"rejected unknown tool: {decision.action}") continue result = agent.tools[decision.action](decision.args) scratch.append(f"{decision.action} -> {result!r}") return f"ESCALATE->{agent.escalate_to}: max steps hit" Copy the contract, not the fake llm.decide guts: one job sentence, three tools, hard max steps, explicit escalate path. How to carve a specialist out of your generalist Pick the failure that hurts most — the ticket that pages you, not the demo that impresses. Write the job in one sentence a new hire could follow on day one. Commas and caveats mean two jobs — split them. List the minimum true tools. Source-of-truth reads beat web browse. Writes stay narrow (draft_reply, not send_anything). Add a kill switch. max_steps + escalate_to are product, not polish. Measure one number the specialist owns: wrong-answer rate, time-to-repro, PRs reverted. Do this three times and you have boring agents you can test, monitor, and replace independently — multi-agent without the hype slide. Analogies worth stealing in standup Hospital, not superhero: triage + specialist + handoff beats one doctor doing surgery, billing, and janitorial. Unix pipes > monolith scripts: small programs that do one thing well compose; god-objects rot. Menu vs buffet: a buffet agent samples everything and overeats; a menu agent plates three orders. Agentic AI gets more reliable when you stop asking one model to be your entire company. What to build this week Skip the orchestration framework. Pick one painful workflow: Bug triage: read_issue, search_code, post_repro_checklist Weekly metrics: run_sql, render_chart, open_draft_mr Oncall summary: fetch_alerts, fetch_deploys, draft_incident_doc Ship one. Put a max-step guard on it. Compare error rate to the generalist on the same task. If the specialist doesn't win, your job sentence is mushy or your tools point at mushy data. The punchline I wanted one agent that could do everything. What I needed was three agents that could each do one thing without inventing the rest. Prompts make agents polite. Tools make them capable. Job descriptions make them safe. If you've retired a Swiss-army agent for a boring specialist, drop its one-sentence job + tool names in the comments. I want a museum of narrow agents that actually ship.