Getting Started
In the previous step you learned how function calling works at the API level. Now you will design and build the tools themselves. This is where agents become genuinely useful — an agent that can read files, query databases, and call external services can accomplish real work.
Good tool design starts with thinking about what the LLM needs to know. Every tool has three parts: a name, a description, and an input schema. The description is the most important part, because the LLM uses it to decide when to invoke the tool:
tools = [
{
"name": "search_documents",
"description": "Search the knowledge base for documents matching a query. "
"Use this when the user asks about internal policies, procedures, "
"or company-specific information. Returns the top 5 matching excerpts.",
"input_schema": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Natural language search query"
},
"max_results": {
"type": "integer",
"description": "Maximum number of results to return (default: 5)",
"default": 5
}
},
"required": ["query"]
}
}
]
Key Concepts
Error recovery is what separates a demo from a production tool. Tools will fail: APIs go down, queries return empty results, file paths do not exist. Your tool implementations must catch errors and return useful messages that help the LLM adjust:
def run_query(query: str) -> str:
"""Execute a read-only SQL query against the database."""
if not query.strip().upper().startswith("SELECT"):
return "Error: Only SELECT queries are allowed for safety."
try:
conn = sqlite3.connect("data.db")
cursor = conn.execute(query)
columns = [desc[0] for desc in cursor.description]
rows = cursor.fetchall()
conn.close()
if not rows:
return "Query returned no results. Try broadening your search criteria."
return json.dumps({"columns": columns, "rows": rows[:50]})
except sqlite3.OperationalError as e:
return f"SQL error: {e}. Check table and column names with list_tables first."
Notice how each error message guides the LLM toward a corrective action. This pattern — returning actionable error messages rather than stack traces — is essential for reliable agents.
Tool composition is the practice of designing tools that work well together. In the database example, the agent naturally follows a workflow: list_tables to discover the schema, describe_table to understand column types, then run_query to get data. Design your tools to support these natural sequences.
Sandboxing is critical when tools execute code or modify state. For database tools, enforce read-only access. For file system tools, restrict operations to a specific directory. For code execution, use containers or restricted interpreters. Never trust LLM-generated input without validation.
Hands-On Practice
Start with the database agent exercise. Create a SQLite database with a few tables of sample data (employees, orders, products), then build the three tools described above. Test the agent with natural language questions like "Which employee had the most sales last month?" and observe how it chains tool calls together.
As you build, pay attention to how the quality of your tool descriptions affects the agent's behavior. Vague descriptions lead to incorrect tool selection. Precise descriptions with examples and constraints lead to reliable tool use.