AI agents and automation Assemble manually
How to build an agent with tool-calling
Tool-calling is the heart of a modern AI agent. In this guide, you’ll build a working agent from scratch, walking through the journey from concept to debugging and evaluation.
What is tool-calling, and why is it needed?
Tool-calling is a mechanism where an LLM doesn’t just generate text but instead forms a structured request to an external tool (API, function, script). This transforms the LLM from a “conversational interface” into an autonomous executor. Without tool-calling, an agent cannot interact with real-world systems: databases, CRMs, payment gateways, etc.
Agent’s thinking loop with tool-calling
flowchart TD
A[User] -->|Request| B[Context + Instruction]
B --> C[LLM: Planning]
C --> D{Tool needed?}
D -->|Yes| E[Generate tool_call]
D -->|No| F[Generate response]
E --> G[Execute tool]
G --> H[Tool result]
H --> I[LLM: Synthesize response]
I --> J[Response to user]
F --> J
Minimal example: Python + OpenAI
Example of an agent that can retrieve the current time via a tool:
import openai
# 1. Tool (actual function)
def get_current_time():
from datetime import datetime
return datetime.now().isoformat()
# 2. Tool description for the LLM
tools = [
{
"type": "function",
"function": {
"name": "get_current_time",
"description": "Returns the current time in ISO format",
"parameters": {"type": "object", "properties": {}}
}
}
]
# 3. Agent loop
messages = [{"role": "user", "content": "What time is it now?"}]
response = openai.chat.completions.create(
model="gpt-4o-mini",
messages=messages,
tools=tools,
tool_choice="auto"
)
choice = response.choices[0]
if choice.message.tool_calls:
# 4. Execute the tool
tool_call = choice.message.tool_calls[0]
if tool_call.function.name == "get_current_time":
result = get_current_time()
messages.append(choice.message)
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": result
})
# 5. Final response
final = openai.chat.completions.create(
model="gpt-4o-mini",
messages=messages
)
print(final.choices[0].message.content)
else:
print(choice.message.content)
Typical Errors During Assembly
- No validation for tool_call — The agent calls a non-existent tool or one with incorrect parameters. Solution: Always verify `tool_call.function.name` and `arguments` before execution.
- Cyclic calls — The agent keeps calling the same tool infinitely. Solution: Limit the number of tool calls per session (e.g., `max_steps=5`).
- Context leak — Tool results end up in the public log. Solution: Filter `tool_call` and `tool` messages before saving.
- Missing fallback — When the tool encounters an error, the agent “hangs.” Solution: Return a structured error (e.g., `{"error": "DB_TIMEOUT", "retry_after": 30}`).
Example: CRM assistant agent
Scenario: User requests “find client by email and show the last deal.”
- Tools: `find_customer(email)`, `get_last_deal(customer_id)`
- Context: Instruction + Dialogue History + Tool Descriptions
- Agent log:
Find the client ivan@company.com and show the last deal. → tool_call: find_customer(email="ivan@company.com") → {"id": "c123", "name": "Ivan Petrov"} → tool_call: get_last_deal(customer_id="c123") → {"deal": "SaaS Pro", "amount": 15000, "date": "2025-04-10"} → Ivan Petrov’s last deal: SaaS Pro (15,000 ₽, April 10).
Pre-launch checklist
- [ ] All `tool_call`s are handled in `try/except` with a fallback message
- [ ] Step limit (max_steps) is set and logged
- [ ] Tools do not return sensitive data (passwords, tokens)
- [ ] Audit: saves `tool_call.id`, `name`, `status`, `duration_ms`
- [ ] Tests: there are evals with mocked tools
- [ ] Human review: manual approval enabled for critical tools (e.g., fund debits)
Next step
After mastering basic tool-calling—move on to agent testing and orchestration of stepsIn a real project, you’ll combine tool-calling with memory, retries, and fallback logic—this creates resilient agent systems.