Agente de IA não é mágica. É um loop.
Você manda uma mensagem para o modelo, ele responde. Se a resposta pede para executar alguma ferramenta (buscar no banco, chamar uma API, ler um arquivo), você executa e manda o resultado de volta. O modelo responde de novo. Repete até terminar. É isso.
Esse loop simples é o que separa um chatbot comum de um agente. E você consegue implementar isso em PHP hoje mesmo, sem framework especial, sem biblioteca de agentes, sem abstrações elaboradas. Vou mostrar como eu faria isso em produção.
Conectando na API da Anthropic
A Anthropic não tem um SDK oficial para PHP ainda. Não faz falta: a API é REST pura, e com Guzzle você resolve em poucos minutos.
composer require guzzlehttp/guzzle
Com isso instalado, você cria um cliente básico:
use GuzzleHttp\Client;
class AnthropicClient
{
private Client $http;
private string $model = 'claude-sonnet-4-6';
public function __construct(private string $apiKey)
{
$this->http = new Client([
'base_uri' => 'https://api.anthropic.com/v1/',
'headers' => [
'x-api-key' => $apiKey,
'anthropic-version' => '2023-06-01',
'content-type' => 'application/json',
],
]);
}
public function messages(array $payload): array
{
$response = $this->http->post('messages', ['json' => $payload]);
return json_decode($response->getBody()->getContents(), true);
}
}
Olha o header anthropic-version. Ele é obrigatório. Sem ele você recebe um 400 que não explica nada, e vai gastar uns 20 minutos lendo a documentação antes de perceber o óbvio. Aprendi isso da forma difícil num projeto que precisava subir no dia.
Definindo ferramentas para o agente
O modelo não executa código. Ele declara que quer usar uma ferramenta, você executa no seu PHP e devolve o resultado. A declaração das ferramentas vai junto com a requisição.
Imagine um agente de suporte que pode consultar o status de pedidos:
$tools = [
[
'name' => 'buscar_pedido',
'description' => 'Busca informações de um pedido pelo número. Retorna status, itens e previsão de entrega.',
'input_schema' => [
'type' => 'object',
'properties' => [
'numero_pedido' => [
'type' => 'string',
'description' => 'Número do pedido no formato #12345',
],
],
'required' => ['numero_pedido'],
],
],
];
A description da ferramenta importa muito, de verdade. O modelo usa ela para decidir quando chamar e com quais parâmetros. Se você escreve “busca pedido” sem mais detalhes, o modelo vai errar o timing ou chamar com input errado. Seja específico como você seria em um comentário de código que um colega júnior vai ler às 23h antes de um deploy.
O loop do agente
Aqui a coisa fica interessante. Você não faz uma chamada só: você itera até o modelo indicar que terminou.
function rodarAgente(AnthropicClient $client, string $mensagem, array $tools): string
{
$messages = [
['role' => 'user', 'content' => $mensagem],
];
$maxIteracoes = 10;
for ($i = 0; $i < $maxIteracoes; $i++) {
$response = $client->messages([
'model' => 'claude-sonnet-4-6',
'max_tokens' => 1024,
'tools' => $tools,
'messages' => $messages,
]);
$stopReason = $response['stop_reason'];
if ($stopReason === 'end_turn') {
foreach ($response['content'] as $block) {
if ($block['type'] === 'text') {
return $block['text'];
}
}
}
if ($stopReason === 'tool_use') {
$messages[] = ['role' => 'assistant', 'content' => $response['content']];
$toolResults = [];
foreach ($response['content'] as $block) {
if ($block['type'] !== 'tool_use') {
continue;
}
$resultado = executarFerramenta($block['name'], $block['input']);
$toolResults[] = [
'type' => 'tool_result',
'tool_use_id' => $block['id'],
'content' => json_encode($resultado),
];
}
$messages[] = ['role' => 'user', 'content' => $toolResults];
}
}
return 'Agente não conseguiu concluir em tempo hábil.';
}
O executarFerramenta é a parte que você escreve para cada caso. No exemplo de pedidos:
function executarFerramenta(string $nome, array $input): array
{
return match ($nome) {
'buscar_pedido' => buscarPedidoNoBanco($input['numero_pedido']),
default => ['erro' => "Ferramenta desconhecida: {$nome}"],
};
}
Simples assim. O modelo raciocina sobre o que buscar e como responder. Você só executa e devolve dados brutos.
O que acontece na prática
Quando um usuário pergunta “qual o status do meu pedido #54321?”, o fluxo é esse: o modelo recebe a mensagem, decide que precisa chamar buscar_pedido, você consulta o banco, devolve algo como {"status": "em transporte", "previsao": "27/06/2026", "transportadora": "Correios"}, e o modelo escreve uma resposta humanizada com esses dados.
O usuário vê “Seu pedido #54321 está a caminho e chega amanhã pelos Correios” sem saber que houve uma consulta ao banco no meio. Isso é diferente de um chatbot com respostas fixas. O modelo adapta ao contexto, ao tom da conversa, ao que o usuário perguntou de verdade.
E sabe o que acontece quando você coloca mais ferramentas? O agente começa a encadear chamadas sozinho. Você expõe buscar_pedido e buscar_endereco_cliente e, quando faz sentido, o modelo chama as duas em sequência antes de responder. Sem você programar essa lógica explicitamente.
Cuidados que vêm de projeto real
Coloca o limite de iterações. Coloquei $maxIteracoes = 10 no exemplo e não é por capricho. Em produção você encontra edge cases onde o modelo fica pedindo a mesma ferramenta com inputs levemente diferentes, geralmente por causa de uma description ambígua. Sem esse limite você bota um loop infinito no servidor e o próximo erro que você vê é o PHP-FPM matando o processo por timeout.
Guarda o histórico de mensagens se você quer continuidade de conversa. Cada chamada à API é stateless, a memória fica com você. Em projeto de suporte com sessões de usuário, eu persisto o array $messages no Redis com TTL de 30 minutos. Funciona bem.
Monitora os tokens com atenção. A API cobra por tokens de input e output, e o histórico cresce a cada iteração do loop. Para agentes com ferramentas verbosas, vi conversas longas chegando a R$0,05 por interação. Barato unitariamente, mas com 10 mil conversas por dia isso escala. Vale medir antes de abrir para volume.
Trata os erros da API. A Anthropic retorna 529 quando está com alta demanda e 429 quando você estoura o rate limit do seu tier. Com Guzzle você resolve isso com RetryMiddleware, exponential backoff com 3 tentativas resolve 95% dos casos transitórios.
Por que PHP e não Python?
Sinceramente, porque a maior parte dos sistemas de negócio que eu conheço é PHP. E-commerce em Laravel, ERP customizado em PHP puro, sistema de gestão que roda há 8 anos. Esses sistemas têm lógica de negócio valiosa, banco de dados cheio de dados reais, integrações com fornecedores e processadores de pagamento que levaram anos para estabilizar.
Não tem sentido reescrever tudo em Python só para ter um agente. O agente vive em PHP, usa as classes de serviço que você já tem, chama os repositories que já estão testados. A integração com Laravel é natural: suas ferramentas chamam services, os services chamam o banco. Você não reescreve nada.
Comecei a usar esse padrão num sistema de e-commerce no começo de 2025, com PHP 8.2 e Laravel 11. O agente de suporte substituiu um script de respostas automáticas que tinha 400 linhas de if/else e ainda assim não cobria metade dos casos. O código novo tem menos de 200 linhas e resolve quase tudo.
O que mostrei aqui é suficiente para ter algo funcionando hoje à tarde. Literalmente.