Introduction
In our last lesson, you learned how to implement the client-side of function calling: defining tool schemas and processing a tool_call response from an LLM. This gave you the power to make an agent act. Today, we shift our focus to the other side of that interaction. We will explore the art and science of designing a custom tool API.
This lesson directly addresses the learning outcome: Design a custom tool API, including a clear description, function signature, and input/output schemas for the LLM to use. Think of this as defining the "contract" between the LLM and your code. A well-designed tool is intuitive for the model to use, leading to more reliable and effective agentic behavior. For a top-tier AI engineer, designing clean, robust, and LLM-friendly tool APIs is a fundamental skill.
The Anatomy of an LLM Tool Interface
When you design an API for a human developer, you assume they can read documentation, infer context, and debug. When designing for an LLM, you must be far more explicit. The tool's interface is the only thing the model knows about its capabilities.
A comprehensive reading from APXML provides an excellent breakdown of the core components and design principles. We'll use it as our guide.
Designing Tool Interfaces for LLM Interaction
The article 'Designing Tool Interfaces for LLM Interaction' provides a foundational guide to creating effective tool APIs for agents. It details the essential components of a tool interface and the principles behind good design.
Please read the introduction and the first two main sections: 'The Anatomy of a Tool Interface' and 'Guiding Principles for Effective Interface Design'. Focus on understanding the four components of the interface and the five guiding principles, especially 'Clarity' and 'Atomicity'.
As you read, you saw that a well-designed tool interface for an LLM consists of four key parts, which together form the "contract" with the model:
- Tool Name: A clear, verb-noun name like
get_current_weatherorsend_email. This is the first signal the model uses for tool selection. - Tool Description: This is the most critical component. The description is essentially a prompt for the tool, telling the LLM what it does, when to use it, and what its purpose is. A well-crafted description is the primary driver of accurate tool selection.
- Input Parameters: The arguments the tool accepts, defined with a schema. Each parameter needs its own descriptive name, type (e.g.,
string,integer,boolean), and a clear description of its purpose and format. - Output Structure: A definition of what the tool returns. This helps the LLM anticipate the format of the tool's response and know how to use the information it receives.
Guiding Principles for Effective Tool Design
The reading outlined several principles that are crucial for creating robust tools. Let's crystallize the most important ones, keeping your goal of building production systems in mind.
1. Clarity and Expressiveness
Your names and descriptions must be unambiguous. An LLM doesn't have your implicit domain knowledge.
- Weak:
name: "process_data",description: "Processes user data."(What kind of processing? What data?) - Strong:
name: "calculate_monthly_loan_payment",description: "Calculates the monthly payment for a loan given the principal amount, annual interest rate, and loan term in years."
2. Atomicity and Composability
This is the Single Responsibility Principle from software engineering, applied to LLM tools. Each tool should do one thing and do it well. Avoid creating monolithic "god tools" that handle many operations via a complex set of mode flags.
Let's revisit the calculator example from the apxml.com article.
- Less Ideal (Monolithic): A single
calculatetool with anoperationparameter ('add','subtract', etc.). This forces the LLM to perform an extra reasoning step to choose the correct operation string, increasing the chance of error. - Better (Atomic): Separate tools like
add_numbers,subtract_numbers, andmultiply_numbers. If the user's intent is "add," the model can make a direct, unambiguous choice to use theadd_numberstool. This simplifies the decision-making process and makes the tools more composable for complex, multi-step tasks.
3. Considering the LLM's Perspective
Always ask: "How would a machine interpret this?"
- If a parameter requires a specific format (e.g., a date), state it explicitly in the description:
"The date for the query in YYYY-MM-DD format." - If a parameter name is ambiguous, clarify it. For a
queryparameter, specify:"The search term to find articles in the technical knowledge base."
Formalizing the Contract: JSON Schema and Pydantic
As you know from the previous lesson, these design principles are formally communicated to the model via a structured format, typically JSON Schema.
Let's look at the OpenAI documentation for a get_weather function. This is the concrete implementation of the principles we've just discussed.
The official OpenAI documentation provides a clear, practical example of how to define a function using JSON Schema.
Read the section 'Defining functions'. Pay close attention to the example JSON schema for the get_weather function. Note how it specifies the name, description, and the parameters object, which details the properties (location, units), their types, descriptions, and which are required.
While writing raw JSON schemas is possible, it's not ideal for production code. As we touched on last time, using Pydantic is the professional standard. It allows you to define your tool's input schema using Python's native type hints, making your code cleaner, more maintainable, and self-validating.
The following video provides a great refresher on using Pydantic for more complex tools, a practice you'll use constantly when building agents.
LLM Function Calling - AI Tools Deep Dive
In 'LLM Function Calling - AI Tools Deep Dive', Adam Lucek demonstrates how to move from raw JSON to Pydantic for defining complex tool interfaces.
Watch the segment from 14:45 to 17:56. Notice how a complex 'email campaign' tool with multiple, nested, and optional parameters is defined cleanly using a Pydantic BaseModel. This is the pattern you should aim to replicate in your own projects.
Advanced Design: Strict Mode and Best Practices
To build truly robust systems, you need to enforce your contract with the model strictly. OpenAI's strict mode is a powerful feature for this.
When you set "strict": true in your function definition, you are asking the model to guarantee its output will perfectly match your schema. This is not the default behavior. To use it, your schema must be fully constrained:
- All parameters must be listed in the
requiredarray (or be typed as optional, e.g.,["string", "null"]). - The
parametersobject must include"additionalProperties": false, preventing the model from inventing extra parameters.
Using strict mode is a best practice for production systems as it eliminates a whole class of potential parsing errors.
Test your understanding!
You need to design a tool for an LLM agent to send emails. The tool should be named send_email. It must have a recipient's email address and the email body. The subject line is optional and should default to "No Subject" if not provided by the LLM.
How would you define the JSON Schema for the parameters of this tool, aiming for a robust design that could support strict mode? Pay attention to types, descriptions, and the required field.
Show answer
A robust schema would look like this. Note how the optional subject is still listed as required, but its type includes null.
{
"type": "object",
"properties": {
"recipient_email": {
"type": "string",
"description": "The email address of the recipient."
},
"body": {
"type": "string",
"description": "The main content of the email, in plain text."
},
"subject": {
"type": ["string", "null"],
"description": "The subject line of the email. If not provided, a default will be used."
}
},
"required": ["recipient_email", "body", "subject"],
"additionalProperties": false
}
In your backend Python function, you would then handle the subject parameter like this: def send_email(recipient_email: str, body: str, subject: str | None = "No Subject").
Conclusion
You've now explored the critical principles of designing custom tool APIs for LLMs. This is where thoughtful software engineering meets the nuances of large language models. A well-designed tool doesn't just work; it guides the model toward the correct behavior reliably and efficiently.
Key Takeaways:
- Design for the Machine: Your tool's
nameand especially itsdescriptionare the primary drivers for the LLM's decision-making. Be clear, expressive, and unambiguous. - Embrace Atomicity: Follow the Single Responsibility Principle. Small, focused tools are easier for an LLM to select and combine than large, multi-purpose ones.
- The Schema is the Contract: Use JSON Schema (preferably via Pydantic) to create a formal, strict contract with the model, defining all parameters, their types, and their purpose.
- Enforce the Contract: Use
strictmode in production to ensure the model's output is always valid and parsable, preventing an entire category of runtime errors.
What's Next?
We've designed the public-facing API for our tool. We know what the contract looks like. In the next lesson, Implement the backend logic for a custom tool as a callable function, you will write the Python code that actually executes the tool's logic, completing the full picture from LLM intent to real-world action.