Prompt
How do I set up a tool-calling framework for integrating external APIs into an AI agent workflow?
Latest observation
A good tool-calling framework has 5 parts:
- A tool registry — defines what external APIs the agent can call.
- A request schema — tells the model exactly how to ask for a tool.
- A planner/executor loop — sends user input to the model, executes tool calls, and feeds results back.
- Safety/validation — validates arguments, limits access, and handles failures.
- Observability — logs calls, latency, errors, and tool usage.
Here’s a practical way to set it up.
1) Define the tools you want the agent to use
Model tools as small, well-scoped functions. Each tool should do one thing.
Examples:
search_web(query)get_weather(city, date)create_ticket(title, priority, description)lookup_customer(email)send_email(to, subject, body)
Keep tools:
- narrow
- deterministic where possible
- idempotent if feasible
- documented with clear inputs/outputs
2) Create structured schemas for each tool
Use JSON Schema or a similar format so the model returns structured arguments.
Example tool spec:
{
"name": "get_weather",
"description": "Get current weather for a city",
"parameters": {
"type": "object",
"properties": {
"city": { "type": "string", "description": "City name" },
"units": { "type": "string", "enum": ["metric", "imperial"] }
},
"required": ["city"]
}
}
Why this matters:
- reduces ambiguity
- makes validation easy
- prevents malformed API calls
3) Implement a tool execution layer
This layer maps tool calls to real API calls.
Pseudo-code:
tools = {
"get_weather": get_weather_api,
"search_web": search_web_api,
"create_ticket": create_ticket_api
}
def execute_tool(name, arguments):
if name not in tools:
raise ValueError(f"Unknown tool: {name}")
validate_arguments(name, arguments)
return tools[name](**arguments)
Important:
- validate inputs before calling external APIs
- sanitize strings and enforce allowlists
- use timeouts and retries
- handle API errors gracefully
4) Build the agent loop
The workflow usually looks like this:
- User asks a question.
- Send user message + tool definitions to the model.
- Model either:
- answers directly, or
- returns a tool call
- If tool call is returned:
- execute the tool
- send tool result back to model
- Model uses the result and either:
- calls another tool, or
- gives a final answer
Pseudo-code:
messages = [
{"role": "user", "content": "What's the weather in Paris?"}
]
response = model.chat(messages, tools=tool_specs)
if response.tool_call:
tool_result = execute_tool(
response.tool_call.name,
response.tool_call.arguments
)
messages.append(response.assistant_message)
messages.append({
"role": "tool",
"name": response.tool_call.name,
"content": tool_result
})
final_response = model.chat(messages, tools=tool_specs)
print(final_response.content)
else:
print(response.content)
5) Add guardrails
Tool calling can be powerful, so add controls:
Authorization
Only allow specific tools for specific users or tasks.
Input validation
Reject invalid or suspicious parameters.
Rate limiting
Prevent overuse of expensive or rate-limited APIs.
Confirmation for risky actions
For actions like:
- sending emails
- deleting records
- creating payments
ask the user to confirm before executing.
Tool output filtering
Don’t blindly trust tool outputs; treat them as untrusted data.
6) Separate read-only and write actions
A strong pattern is to split tools into:
- Read tools: search, fetch, inspect
- Write tools: create, update, delete, send
This makes it easier to:
- allow safe autonomous use of read-only tools
- require confirmation for write tools
7) Make tool responses concise and machine-friendly
Tool results should be structured, not verbose text.
Good example:
{
"city": "Paris",
"temperature": 18,
"units": "metric",
"condition": "Cloudy"
}
Avoid returning long paragraphs unless needed.
8) Handle multi-step workflows
Some tasks require multiple tools:
- get customer record
- look up order
- create support ticket
- send confirmation email
The agent loop should support repeated tool calls until completion.
You may want a limit like:
- max 5 tool calls per request
- stop if the model repeats the same call
- stop on low confidence or errors
9) Add logging and tracing
Log:
- user request
- chosen tool
- arguments
- latency
- tool response
- errors
- final answer
This helps with:
- debugging
- monitoring costs
- spotting prompt/tool misuse
- evaluating performance
10) A simple architecture
A common implementation looks like this:
User → Agent Orchestrator → LLM → Tool Executor → External API → LLM → User
Where the orchestrator handles:
- prompt construction
- tool schema injection
- tool call parsing
- retries/fallbacks
- conversation state
11) Recommended design patterns
Router pattern
Use one model or classifier to decide which tool family to use.
Planner-executor pattern
One component plans steps; another executes them.
Human-in-the-loop for sensitive actions
Use user approval before side effects.
Capability-based tool access
Different agents get different tool permissions.
12) Minimal example structure
class Agent:
def __init__(self, llm, tools):
self.llm = llm
self.tools = tools
def run(self, user_input):
messages = [{"role": "user", "content": user_input}]
while True:
response = self.llm.chat(messages=messages, tools=self.tools)
if response.get("tool_call"):
name = response["tool_call"]["name"]
args = response["tool_call"]["arguments"]
result = execute_tool(name, args)
messages.append(response["message"])
messages.append({
"role": "tool",
"name": name,
"content": result
})
else:
return response["content"]
13) Best practices
- Keep tool descriptions clear and unambiguous
- Use strict schemas
- Validate arguments server-side
- Add timeouts and retries
- Limit tool access by role
- Confirm destructive actions
- Track everything in logs
- Return structured results
- Test with adversarial prompts
14) If you’re using a modern LLM API
Most tool-calling APIs let you provide:
- tool definitions
- model-generated tool call requests
- tool results as follow-up messages
The exact syntax varies by provider, but the architecture is the same.
If you want, I can also give you:
- a Python reference implementation,
- a Node.js example,
- or a production-ready architecture with retries, auth, and logging.