Bu yazıda, Laravel’da bir AI chatbot’un nasıl daha iyi bir kullanıcı deneyimi sunabileceğini keşfedeceğiz. İlk sürümde, bir mesaj gönderdiğinizde yanıtı almak için beş saniye beklemek zorundasınız. Ancak bu süreç, kullanıcı deneyimini olumsuz etkileyebiliyor. Kullanıcıların, gelen yanıtları anında görebilmeleri, hız algısını artırarak daha iyi bir deneyim sunacaktır. Bu nedenle streaming özelliğini kullanacağız.
<h2>
<a name="why-sse-and-not-websockets" href="#why-sse-and-not-websockets">
</a>
Neden SSE ve WebSockets değil?
</h2>
<p>WebSockets çift yönlü iletişim sunar ve uzun süreli sunucu gerektirir. Ancak chat tamamlama için yalnızca tek yönlü iletişim gerekir: sunucu → tarayıcı. SSE tam olarak bunu sağlar ve Laravel 11 ile birlikte <code>response()->eventStream()</code> ile birlikte gelmektedir.</p>
<h2>
<a name="1-stream-from-the-openai-client" href="#1-stream-from-the-openai-client">
</a>
1. OpenAI istemcisinden akış
</h2>
<p>Burada, ilk bölümdeki <code>ChatService</code>'i koruyarak akış özelliği ekliyoruz. <code>openai-php</code> istemcisi <code>createStreamed()</code> metodunu sunar ve bu metod delta'ların bir iterator'unu döner:</p>
<div class="highlight js-code-highlight">
<pre class="highlight php"><code><span class="cp"><?php </span>namespace App\Services;
use Generator;
use OpenAI\Laravel\Facades\OpenAI;
class ChatService
{
// … SYSTEM_PROMPT ve reply() buradan …
<span class="cd">/
* @param array<int array="" string="" content:=""> $history
*/</int></span>
<span class="k">public</span> <span class="k">function</span> <span class="n">streamReply</span><span class="p">(</span><span class="kt">array</span> <span class="nv">$history</span><span class="p">,</span> <span class="kt">string</span> <span class="nv">$userMessage</span><span class="p">):</span> <span class="kt">Generator</span>
<span class="p">{</span>
<span class="nv">$stream</span> <span class="o">=</span> <span class="nc">OpenAI</span><span class="o">::</span><span class="nf">chat</span><span class="p">()</span><span class="o">-></span><span class="nf">createStreamed</span><span class="p">([</span>
<span class="s1">'model'</span> <span class="o">=></span> <span class="s1">'gpt-4o-mini'</span><span class="p">,</span>
<span class="s1">'messages'</span> <span class="o">=></span> <span class="p">[</span>
<span class="p">[</span><span class="s1">'role'</span> <span class="o">=></span> <span class="s1">'system'</span><span class="p">,</span> <span class="s1">'content'</span> <span class="o">=></span> <span class="k">self</span><span class="o">::</span><span class="no">SYSTEM_PROMPT</span><span class="p">],</span>
<span class="mf">...</span><span class="nv">$history</span><span class="p">,</span>
<span class="p">[</span><span class="s1">'role'</span> <span class="o">=></span> <span class="s1">'user'</span><span class="p">,</span> <span class="s1">'content'</span> <span class="o">=></span> <span class="nv">$userMessage</span><span class="p">],</span>
<span class="p">],</span>
<span class="s1">'max_tokens'</span> <span class="o">=></span> <span class="mi">500</span><span class="p">,</span>
<span class="p">]);</span>
<span class="k">foreach</span> <span class="p">(</span><span class="nv">$stream</span> <span class="k">as</span> <span class="nv">$response</span><span class="p">)</span> <span class="p">{</span>
<span class="nv">$delta</span> <span class="o">=</span> <span class="nv">$response</span><span class="o">-></span><span class="n">choices</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="o">-></span><span class="n">delta</span><span class="o">-></span><span class="n">content</span><span class="p">;</span>
<span class="k">if</span> <span class="p">(</span><span class="nv">$delta</span> <span class="o">!==</span> <span class="kc">null</span><span class="p">)</span> <span class="p">{</span>
<span class="k">yield</span> <span class="nv">$delta</span><span class="p">;</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>}
<p><code>Generator</code>, burada doğru bir dönüş tipidir; kontrolcü, tam yanıtı bellek içinde tutmadan parçaları geldiği gibi iletebilir.</p>
<h2>
<a name="2-the-streaming-controller" href="#2-the-streaming-controller">
</a>
2. Akış kontrolcüsü
</h2>
<p>Laravel 11.19+ ile birlikte <code>response()->eventStream()</code>, SSE formatını ( <code>event:</code> / <code>data:</code> satırları) sizin için otomatik olarak yönetir:</p>
<div class="highlight js-code-highlight">
<pre class="highlight php"><code><span class="cp"><?php </span>namespace App\Http\Controllers;
use App\Services\ChatService;
use Illuminate\Http\Request;
class ChatStreamController extends Controller
{
public function __invoke(Request $request, ChatService $chat)
{
$validated = $request->validate([
‘message’ => [‘required’, ‘string’, ‘max:2000’],
]);
<span class="nv">$history</span> <span class="o">=</span> <span class="nv">$request</span><span class="o">-></span><span class="nf">session</span><span class="p">()</span><span class="o">-></span><span class="nf">get</span><span class="p">(</span><span class="s1>'chat_history'</span><span class="p">,</span> <span class="p">[]);</span>
<span class="c1">// KRİTİK: oturum kilidini serbest bırakın. Akış yanıtı uzun süreli bir istektir — varsayılan dosya/veritabanı oturum sürücüsü ile bu, bu kullanıcıdan gelen diğer tüm istekleri (sayfa yüklemeleri de dahil) akış sona erinceye kadar engelleyecektir.</span>
<span class="nv">$request</span><span class="o">-></span><span class="nf">session</span><span class="p">()</span><span class="o">-></span><span class="nf">save</span><span class="p">();</span>
<span class="k">return</span> <span class="nf">response</span><span class="p">()</span><span class="o">-></span><span class="nf">eventStream</span><span class="p">(</span><span class="k">function</span> <span class="p">()</span> <span class="k">use</span> <span class="p">(</span><span class="nv">$request</span><span class="p">,</span> <span class="nv">$chat</span><span class="p">,</span> <span class="nv">$history</span><span class="p">,</span> <span class="nv">$validated</span><span class="p">)</span> <span class="p">{</span>
<span class="nv">$full</span> <span class="o">=</span> <span class="s1">''</span><span class="p">;</span>
<span class="k">foreach</span> <span class="p">(</span><span class="nv">$chat</span><span class="o">-></span><span class="nf">streamReply</span><span class="p">(</span><span class="nv">$history</span><span class="p">,</span> <span class="nv">$validated</span><span class="p">[</span><span class="s1>'message'</span><span class="p">])</span> <span class="k">as</span> <span class="nv">$chunk</span><span class="p">)</span> <span class="p">{</span>
<span class="nv">$full</span> <span class="mf">.</span><span class="o">=</span> <span class="nv">$chunk</span><span class="p">;</span>
<span class="k">yield</span> <span class="nv">$chunk</span><span class="p">;</span>
<span class="p">}</span>
<span class="c1">// akış tamamlandığında geçmişi kalıcı hale getirin</span>
<span class="nv">$history</span><span class="p">[]</span> <span class="o">=</span> <span class="p">[</span><span class="s1>'role'</span> <span class="o">=></span> <span class="s1>'user'</span><span class="p">,</span> <span class="s1>'content'</span> <span class="o">=></span> <span class="nv">$validated</span><span class="p">[</span><span class="s1>'message'</span><span class="p">]];</span>
<span class="nv">$history</span><span class="p">[]</span> <span class="o">=</span> <span class="p">[</span><span class="s1>'role'</span> <span class="o">=></span> <span class="s1>'assistant'</span><span class="p">,</span> <span class="s1>'content'</span> <span class="o">=></span> <span class="nv">$full</span><span class="p">];</span>
<span class="nv">$request</span><span class="o">-></span><span class="nf">session</span><span class="p">()</span><span class="o">-></span><span class="nf">put</span><span class="p">(</span><span class="s1>'chat_history'</span><span class="p">,</span> <span class="nb">array_slice</span><span class="p">(</span><span class="nv">$history</span><span class="p">,</span> <span class="o">-</span><span class="mi">20</span><span class="p">));</span>
<span class="nv">$request</span><span class="o">-></span><span class="nf">session</span><span class="p">()</span><span class="o">-></span><span class="nf">save</span><span class="p">();</span>
<span class="p">},</span> <span class="n">headers</span><span class="o">:</span> <span class="p">[</span>
<span class="s1>'X-Accel-Buffering'</span> <span class="o">=></span> <span class="s1>'no'</span><span class="p">,</span> <span class="c1">// nginx'e akışı tamponlamamasını söyleyin</span>
<span class="p">]);</span>
<span class="p">}</span>}


