Adding an AI Chatbot to a Laravel or Symfony Application
Adding an AI chatbot to an existing PHP app does not mean rewriting the backend in Python. Here is a concrete architecture: where the LLM lives, SSE streaming, business context, safe tool calls, and defending against prompt injection.
Most AI chatbot tutorials assume a Python backend. In reality, most business applications run on Laravel or Symfony, and there is no reason to rewrite them just to add a chat feature. The LLM lives behind an API; your existing PHP backend stays the entry point, keeps its auth, and stays in control of every action the chatbot can take.
Where the LLM lives
Two realistic options. Call a provider API directly (OpenAI, Anthropic, Mistral) from Laravel or Symfony over HTTP: simplest, no AI infrastructure. Or route through a dedicated Python service (FastAPI) once retrieval logic gets complex: cleaner separation, one more service to operate.
| Direct provider call from PHP | Dedicated Python (FastAPI) service | |
|---|---|---|
| Setup | An HTTP client and a key | A second deployable service |
| Best when | Chat, light context injection | RAG, embeddings, heavy pre/post-processing |
| Latency | One external hop | Two hops (PHP to service to provider) |
| Team | PHP only | PHP plus Python |
// app/Services/ChatService.php
final class ChatService
{
public function ask(string $question, array $history = []): string
{
$response = Http::withToken(config('services.anthropic.key'))
->withHeaders(['anthropic-version' => '2026-01-01'])
->post('https://api.anthropic.com/v1/messages', [
'model' => 'claude-sonnet-5',
'max_tokens' => 1024,
'system' => $this->systemPrompt(),
'messages' => [...$history, ['role' => 'user', 'content' => $question]],
]);
return $response->json('content.0.text');
}
}Stream the response instead of waiting for it
An LLM call takes several seconds. Waiting for the full response before showing anything hurts the experience; streaming tokens as they arrive over Server-Sent Events gives an immediate feeling of responsiveness even though full generation takes the same time.
- ✓Laravel: response()->stream(), flushing after each chunk received from the provider stream.
- ✓Symfony: StreamedResponse, same principle, write to the output buffer as data arrives.
- ✓Frontend: consume the stream with EventSource or fetch and a ReadableStream, no polling.
// Laravel controller
return response()->stream(function () {
foreach ($this->chat->stream($question, $history) as $token) {
echo "data: " . json_encode(['token' => $token]) . "\n\n";
ob_flush();
flush();
}
}, 200, ['Content-Type' => 'text/event-stream', 'X-Accel-Buffering' => 'no']);Business context: the real work is not the LLM call
Most of the effort is not calling the model, it is giving it the right context. Inject relevant application data (the current user's orders, the product catalogue, the internal docs that matter for this question) into the prompt. For a knowledge base, add lightweight vector search with pgvector directly inside PostgreSQL, no extra service, and retrieve the top few passages before calling the LLM.
Tool calls, done safely
When the chatbot needs to do something (look up an order, start a return), expose those as tools the model can call, but keep the model on the proposing side only. It returns a tool name and arguments; your code validates them against the current user's permissions and your business rules, then executes or refuses. The LLM never touches the database directly.
Never give the LLM direct write access to your database through unvalidated function calls. Any action, especially a destructive one, goes back through your existing business layer with its own authorization and validation. The LLM proposes; your code decides.
Defend against prompt injection
If the chatbot reads user-generated content or retrieved documents, treat all of it as untrusted: it can contain instructions aimed at your model. Keep the system prompt and the retrieved context clearly separated, never let retrieved text change what tools are allowed, scope every tool call to the authenticated user server-side, and run outputs through a moderation check before displaying them.
Cost, caching and fallback
- ✓Cache answers to frequent or identical questions so the same question asked a hundred times is not paid for a hundred times.
- ✓Always have a fallback: a static help page or a contact form when the model times out or errors.
- ✓Track tokens and cost per user or tenant, and rate-limit per user to avoid a billing surprise at scale.
FAQ
- Do I need to rewrite my PHP backend in Python for an AI chatbot?
- No. The LLM sits behind an HTTP API that Laravel or Symfony calls like any other service. Your PHP backend stays the entry point and keeps its auth and business rules. A dedicated Python service is only worth it once you have heavy retrieval or embedding work.
- How do I stream an LLM response in Laravel?
- Use response()->stream() with a text/event-stream content type, and echo plus flush each token as it arrives from the provider stream. Set X-Accel-Buffering to no so nginx does not buffer the response. On the frontend, consume it with EventSource.
- How do I give the chatbot business context?
- Inject the relevant application data into the prompt: the current user's records, the catalogue, the internal docs that matter for the question. For a knowledge base, add vector search with pgvector inside PostgreSQL and retrieve the top passages before the LLM call.
- How do I prevent prompt injection?
- Treat user content and retrieved documents as untrusted. Keep the system prompt separate from retrieved context, never let retrieved text expand what tools are allowed, scope every tool call to the authenticated user on the server, and moderate outputs before display.
- How much does an LLM chatbot cost to run?
- The variable cost is per token: the system prompt, the conversation history and the retrieved context on the way in, plus the generated answer on the way out. Caching frequent questions and trimming the history and context are the levers that keep it flat as traffic grows.
A well-integrated chatbot adds a thin layer to an existing Laravel or Symfony app: an HTTP service, a streaming endpoint, some business context, and a tool layer that keeps your code in control. The rest of your architecture, your auth, your database, your business rules, stays unchanged.
Want to add AI to your application?
AI & RAG Integration →