Tool Design
Building the Hands of Your Agent
Why Tools Matter
An AI agent without tools is just a chatbot. It can talk about looking up a customer's account, but it can't actually do it. Tools are what turn language models from conversationalists into operators.
The quality of your agent is bounded by the quality of its tools. A brilliant planning loop with sloppy tool definitions will hallucinate parameters, misinterpret results, and fail silently. A simple loop with well-designed tools will reliably get work done.
The principle: Design tools for the LLM, not for a human developer. The model reads your tool's name, description, and schema to decide when and how to call it. Every word matters.
Anatomy of a Tool
Every tool has four parts:
| Component | Purpose | Example |
|---|---|---|
| Name | How the LLM refers to it — must be unambiguous | `search_crm_contacts` not `search` |
| Description | When to use it and what it returns — the LLM's decision guide | "Search CRM contacts by name, email, or company. Returns up to 10 matching contacts with their deal history." |
| Input Schema | JSON Schema defining exact parameters | `{ name?: string, email?: string, company?: string, limit?: number }` |
| Execute Function | The actual implementation that runs when called | Database query, API call, computation |
const searchContactsTool = {
name: "search_crm_contacts",
description:
"Search CRM contacts by name, email, or company. " +
"Returns up to 10 matching contacts with their deal history. " +
"Use this when the user asks about a specific person or account.",
parameters: {
type: "object" as const,
properties: {
name: { type: "string", description: "Contact's full or partial name" },
email: { type: "string", description: "Contact's email address" },
company: { type: "string", description: "Company name to filter by" },
limit: { type: "number", description: "Max results (default 10, max 50)" },
},
required: [],
},
execute: async (params: {
name?: string;
email?: string;
company?: string;
limit?: number;
}) => {
const results = await db.contacts.search({
...params,
limit: Math.min(params.limit ?? 10, 50),
});
return { contacts: results, total: results.length };
},
};JSON Schema for Input Validation
The LLM generates tool call arguments as JSON. Your schema is both documentation and validation:
{
"type": "object",
"properties": {
"priority": {
"type": "string",
"enum": ["low", "medium", "high", "critical"],
"description": "Ticket priority level"
},
"assignee_email": {
"type": "string",
"format": "email",
"description": "Email of the team member to assign this ticket to"
}
},
"required": ["priority"]
}Output Contracts
Tool outputs should be structured, predictable, and informative:
{ success: true, contact_id: "abc-123" } beats "Contact created successfully."{ items: [...], total: number }. Every tool that mutates should return { success: boolean, id: string }.Error Handling
Tools will fail. Networks time out, APIs return 500s, users ask for things that don't exist. Your error handling determines whether the agent recovers or crashes:
execute: async (params) => {
try {
const result = await externalApi.call(params);
return { success: true, data: result };
} catch (error) {
if (error.status === 404) {
return { success: false, error: "not_found",
message: "No contact found with that email. Try searching by name instead." };
}
if (error.status === 429) {
return { success: false, error: "rate_limited",
message: "API rate limit reached. Try again in 30 seconds." };
}
return { success: false, error: "internal",
message: "Failed to search contacts. The CRM service may be down." };
}
}Key rule: Never throw exceptions from tool execution. Always return a structured error the model can reason about. The model can then tell the user what happened and suggest alternatives.
Tool Registry Pattern
As your agent grows, you'll have dozens of tools. A registry pattern keeps them organized and enables dynamic discovery:
class ToolRegistry {
private tools = new Map<string, Tool>();
register(tool: Tool) {
this.tools.set(tool.name, tool);
}
// Return schemas for the LLM to choose from
getSchemas(): ToolSchema[] {
return Array.from(this.tools.values()).map((t) => ({
name: t.name,
description: t.description,
parameters: t.parameters,
}));
}
// Execute a tool by name
async execute(name: string, params: unknown) {
const tool = this.tools.get(name);
if (!tool) return { success: false, error: `Unknown tool: ${name}` };
return tool.execute(params);
}
}This pattern lets you add tools per user role, per module, or per context — the agent only sees what it needs. In the capstone, you'll build a registry that exposes different tool sets depending on whether the user is a sales rep, a manager, or an admin.
This is chapter 1 of Production AI Agents.
Get the full hands-on course for $100 and build the complete system. Your projects become your portfolio.
View course details