I rewrote the system prompt fourteen times.
Version 3 was "be thorough." Version 7 was a 900-word manifesto about edge cases. Version 12 threatened the model if it hallucinated a file path. Version 14 still booked a meeting on the wrong calendar and cheerfully summarized the disaster.
The agent wasn't dumb. It was unarmed.
If you've been stuck in prompt-tweaking hell with an AI agent that keeps almost-working, this is the post I wish I'd read first. The fix usually isn't another paragraph of instructions. It's giving the thing a real tool.
A prompt is a pep talk before a job interview. A tool is the résumé, the laptop, and the door badge.
You can coach someone forever on "be professional and double-check details." If they can't open the calendar API, they will invent a meeting time that sounds right. LLMs are the same — world-class at sounding competent, terrible at touching the real world unless you wire them up.
That's the agentic AI shift in one sentence: stop asking the model to pretend it did something. Give it a function it can actually call.
My agent had one job: when a GitHub issue is labeled needs-repro
, clone the repo, run the failing test, and paste the output back.
I tried better prompts first — "always clone before you speculate," "never invent stack traces," "if you're unsure, say so." It invented a beautifully formatted stack trace from a file that didn't exist. From the model's point of view, a plausible stack trace is the assignment when the only tool it has is "generate text."
Then I registered three boring tools: clone_repo(url)
, run_tests(path)
, comment_on_issue(number, body)
. No poetry. Next run: clone → run → paste real output. The intelligence didn't jump. The surface area of reality did.
import json, subprocess
from dataclasses import dataclass
from typing import Callable
@dataclass
class Tool:
name: str
description: str
schema: dict
handler: Callable[[dict], str]
def run_tests(args: dict) -> str:
path = args["path"]
result = subprocess.run(
["pytest", path, "-q"],
capture_output=True, text=True, timeout=120
)
return (result.stdout or result.stderr)[-4000:]
TOOLS = {
"run_tests": Tool(
name="run_tests",
description="Run pytest on a path; return the tail of output.",
schema={
"type": "object",
"properties": {"path": {"type": "string"}},
"required": ["path"],
},
handler=run_tests,
),
}
def tools_for_llm() -> list[dict]:
"""Send names/descriptions/schemas — not handlers."""
return [{
"type": "function",
"function": {
"name": t.name,
"description": t.description,
"parameters": t.schema,
},
} for t in TOOLS.values()]
def dispatch(tool_name: str, arguments_json: str) -> str:
tool = TOOLS[tool_name]
return tool.handler(json.loads(arguments_json))
Notice what the prompt no longer has to say: "please don't invent pytest output." The model can't invent what run_tests
already returned. Reality is a function return value, not a writing style.
1. If the model must touch the world, it needs a tool.
Email, APIs, files, tests, databases — tools, not adjectives in a system prompt.
2. Prefer boring, narrow tools over one god-tool.
run_tests(path)
beats do_whatever(shell_command)
. Narrow tools are easier to log and sandbox. God-tools are how you wake up to a deleted directory and a polite apology.
3. Put constraints in code, not in prose.
Timeouts, allow-lists, max bytes, path sandboxes belong in the handler. The model is a creative writer under deadline. Your handler is the adult in the room.
4. Log tool calls like production traffic.
Name, args, duration, success/fail. When an agent goes weird, you won't debug prompt vibes — you'll debug the trace: it called clone_repo with a typo URL three times.
Imagine a pizza shop where the only employee is a poet. Hand them a long script about greeting customers and using the oven. Without an oven dial, a ticket printer, and a delivery map, you don't have a pizza shop — you have spoken-word night with cheese anxiety.
Agents are the same. Prompting is coaching the poet. Tool-use is installing the oven. Multi-agent setups multiply the moral: researchers need search, coders need file/test tools, reviewers need diffs. More agents with no tools is just a group chat with confidence issues.
Prompts are great at role ("prefer failing closed"), output shape ("short summary after tools"), and tie-breaking ("prefer the read-only tool"). They're bad at physics. "Don't hallucinate test output" is physics. Solve physics with a tool.
Pick one task your current bot fakes:
fetch_invoice
, list_open_prs
, tail_logs
).If success jumps, you didn't find a better spell. You handed the wizard a screwdriver.
Agentic AI feels magical in demos because demos hide the plumbing. In production, the magic is mostly tool surface area + boring guardrails + a short honest prompt.
Stop asking your agent to role-play competence. Give it hands. Keep the prompt short enough that you can still read it without scrolling past your own despair.
Your turn: What's one tool you wish your agent had yesterday — the specific function, not a vibe? Drop the name and two args in the comments. I'm collecting a hall-of-fame of "why didn't I add this sooner" tools.