Skip to main content

On This Page

Laravel AI Agents in Production: Tool Calling Pattern Cuts Chatbot Limit

3 min read
Share

These articles are AI-generated summaries. Please check the original sources for full details.

Building AI Agents in PHP: Tool Calling with Laravel

Aditya Kumar, a Laravel tech lead, published a pattern for building AI agents with tool calling in Laravel. His production order-support agent averages under a cent per resolved conversation on GPT-4.1-mini.

Why This Matters

Chatbots that can only generate text confidently fabricate data like order counts or shipping statuses. Kumar’s tool-calling pattern grounds AI responses in real database queries and services via a controlled loop, with costs averaging under a cent per conversation on GPT-4.1-mini, replacing engineer time that cost more per minute.

Key Insights

  • Tool calling allows AI models to execute real functions like database queries, not just generate text.
  • Authorization must live in the tool code (e.g., auth()->id() in OrderLookupTool), not in prompts, because attackers can steer tool selection via crafted inputs.
  • MAX_ROUNDS is critical: a loop cap of 5 prevents the model from ping-ponging between tools indefinitely and burning tokens.
  • Return errors as tool results instead of throwing exceptions—models handle failures gracefully and retry with fixed arguments.
  • Token costs are 3–5x a plain chat reply per round; keep tool schemas tight and register only context-relevant tools.
  • Tool registry mapping names to classes enables validation and error handling in a single execute() method.

Working Examples

Tool contract interface defining the structure for AI tools in Laravel.

interface Tool
{
    public function name(): string;
    public function description(): string;
    /** JSON Schema for the arguments the model may pass. */
    public function parameters(): array;
    /** @param array $args validated arguments from the model */
    public function handle(array $args): string;
}

Example of a real Laravel tool that looks up orders with authorization scoping.

class OrderLookupTool implements Tool
{
    public function name(): string
    {
        return 'order_lookup';
    }
    public function description(): string
    {
        return 'Look up an order by its number. Returns status, items, and shipping info.';
    }
    public function parameters(): array
    {
        return [
            'type' => 'object',
            'properties' => [
                'order_number' => [
                    'type' => 'string',
                    'description' => 'The order number, e.g. ORD-2041',
                ],
            ],
            'required' => ['order_number'],
        ];
    }
    public function handle(array $args): string
    {
        $order = Order::where('number', $args['order_number'])
            ->where('user_id', auth()->id()) // ← the line that matters
            ->first();

        if (! $order) {
            return json_encode(['error' => 'Order not found for this account.']);
        }

        return json_encode([
            'number' => $order->number,
            'status' => $order->status,
            'items' => $order->items->pluck('name'),
            'shipped_at' => $order->shipped_at?->toDateString(),
        ]);
    }
}

AgentRunner loop that executes up to 5 rounds of tool calls with OpenAI API integration.

class AgentRunner
{
    private const MAX_ROUNDS = 5;

    public function __construct(private ToolRegistry $tools) {}

    public function run(array $messages): string
    {
        foreach (range(1, self::MAX_ROUNDS) as $round) {
            $response = Http::withToken(config('services.openai.key'))
                ->timeout(30)
                ->post('https://api.openai.com/v1/chat/completions', [
                    'model' => 'gpt-4.1-mini',
                    'messages' => $messages,
                    'tools' => $this->tools->schemas(),
                ])->throw()->json('choices.0.message');

            if (empty($response['tool_calls'])) {
                return $response['content'] ?? '';
            }

            $messages[] = $response;

            foreach ($response['tool_calls'] as $call) {
                $result = $this->tools->execute(
                    $call['function']['name'],
                    json_decode($call['function']['arguments'], true) ?? [],
                );

                $messages[] = [
                    'role' => 'tool',
                    'tool_call_id' => $call['id'],
                    'content' => $result,
                ];
            }
        }

        return 'I could not complete that in a reasonable number of steps.';
    }
}

Practical Applications

  • Use case: Order lookup in Laravel support agents using OrderLookupTool. Pitfall: Missing authorization scoping inside tool handle()—the model is an untrusted caller that can be steered by crafted prompts.
  • Use case: Report generation or shipping status checks in production apps. Pitfall: Omitting MAX_ROUNDS causes infinite loops and high token costs—every round carries full conversation history plus tool schemas.

References:

Continue reading

Next article

Agentic Coding Shifts Dev Work to Strategy, Demands Human Taste and Community Feedback

Related Content