เบื้องหลัง LLM API: เมื่อโค้ด PHP ของคุณคุยกับ AI

โดย CyberMAN



LLM APIPHP / Laravel / CI4AI Integration

เบื้องหลัง LLM API:
เมื่อโค้ด PHP ของคุณคุยกับ AI

เปิดฝากล่องดำ — ทำความเข้าใจว่าเกิดอะไรขึ้นจริงๆ ตั้งแต่กด send ไปจนถึงได้รับคำตอบ

ทำไมต้องรู้เรื่องนี้?

หลายคนเริ่มต้น integrate LLM เข้าโปรเจกต์ PHP ด้วยการ copy วาง curl จาก docs แล้ว "มันก็ทำงานได้" — แต่พอ response แปลก, token หมด, หรือ latency พุ่งขึ้น ก็ไม่รู้จะ debug จากไหน

บทความนี้จะพาคุณเปิดฝากล่องดำนั้น ตั้งแต่ HTTP request ออกไปจนถึง token ไหลกลับมา พร้อม code ตัวอย่างจริงสำหรับ CodeIgniter 4 และ Laravel ที่นำไปใช้ได้เลย

💡
บทความนี้เหมาะสำหรับผู้ที่รู้จัก PHP พื้นฐานและเคยสร้าง REST API มาบ้างแล้ว ถ้ายังไม่คุ้นเรื่อง HTTP ลองอ่านซีรีส์ PHP RESTful Web Service ก่อนได้เลย

1. ภาพรวม: Request เดินทางอย่างไร?

เมื่อคุณเรียก LLM API เช่น OpenAI, Claude, หรือ Gemini สิ่งที่เกิดขึ้นจริงๆ คือ HTTP POST request ธรรมดาๆ ไปยัง endpoint ของ provider ไม่มีเวทมนตร์ซ่อนอยู่เลย

PHP Appสร้าง payload
HTTPS POSTJSON body
LLM Servertokenize + infer
ResponseJSON / Stream
PHP Appparse + แสดงผล

สิ่งที่ต่างจาก API ทั่วไปคือ ฝั่ง server ใช้เวลาประมวลผลนานกว่าปกติมาก (ตั้งแต่ 1 วินาทีไปจนถึงหลายสิบวินาที) และ response อาจส่งกลับมาแบบ streaming คือทยอยส่งทีละ chunk แทนที่จะรอให้ครบแล้วส่งพร้อมกัน

2. โครงสร้าง Request ที่ต้องรู้

ทุก LLM API มีโครงสร้าง JSON body คล้ายกัน ลองดู format ของ OpenAI-compatible API ซึ่งใช้ได้กับหลาย provider:

request-payload.json
{
  "model":       "gpt-4o-mini",       // โมเดลที่ต้องการใช้
  "messages": [
    { "role": "system",    "content": "You are a helpful assistant." },
    { "role": "user",      "content": "สรุปบทความนี้ให้หน่อย..." }
  ],
  "max_tokens":  500,              // จำกัด output token
  "temperature": 0.7,              // 0 = deterministic, 2 = wild
  "stream":      false             // true = ส่งแบบ chunk
}
⚠️
ระวัง context window! ทุก message ใน messages[] นับ token ทั้งหมด ถ้าคุณส่ง conversation ยาวๆ หรือ document ใหญ่ๆ ค่าใช้จ่ายพุ่งขึ้นแน่นอน วางแผน context management ตั้งแต่ต้น

3. เรียก LLM API จาก CodeIgniter 4

CI4 มี CURLRequest built-in ใน HTTP Client ทำให้ไม่ต้องพึ่ง library ภายนอก:

app/Services/LlmService.php
<?php

namespace App\Services;

use CodeIgniter\HTTP\CURLRequest;

class LlmService
{
    private CURLRequest $client;
    private string      $apiKey;

    public function __construct()
    {
        $this->apiKey = env('OPENAI_API_KEY');
        $this->client = service('curlrequest', [
            'baseURI' => 'https://api.openai.com/v1/',
            'timeout' => 30,
        ]);
    }

    public function chat(string $userMessage): string
    {
        $payload = [
            'model'      => 'gpt-4o-mini',
            'messages'  => [
                ['role' => 'system', 'content' => 'ตอบเป็นภาษาไทย กระชับ และถูกต้อง'],
                ['role' => 'user',   'content' => $userMessage],
            ],
            'max_tokens' => 500,
        ];

        $response = $this->client->post('chat/completions', [
            'headers' => [
                'Authorization' => 'Bearer ' . $this->apiKey,
                'Content-Type'  => 'application/json',
            ],
            'json' => $payload,
        ]);

        $data = json_decode($response->getBody(), true);

        // ดึงคำตอบจาก response structure
        return $data['choices'][0]['message']['content'] ?? '';
    }
}

เรียกใช้จาก Controller

app/Controllers/AiController.php
public function ask()
{
    $question = $this->request->getPost('question');

    if (empty($question)) {
        return $this->response
            ->setStatusCode(422)
            ->setJSON(['error' => 'กรุณาระบุคำถาม']);
    }

    $llm    = new LlmService();
    $answer = $llm->chat($question);

    return $this->response->setJSON([
        'answer' => $answer,
    ]);
}

4. เรียก LLM API จาก Laravel

Laravel มี HTTP Client ที่ wrap Guzzle ไว้ใช้งานง่ายมาก และรองรับ retry, timeout, และ async ได้ตั้งแต่ out-of-the-box:

app/Services/LlmService.php
<?php

namespace App\Services;

use Illuminate\Support\Facades\Http;
use Illuminate\Http\Client\RequestException;

class LlmService
{
    public function chat(string $userMessage, string $systemPrompt = ''): string
    {
        $messages = [];

        if ($systemPrompt) {
            $messages[] = ['role' => 'system', 'content' => $systemPrompt];
        }
        $messages[] = ['role' => 'user', 'content' => $userMessage];

        $response = Http::withToken(config('services.openai.key'))
            ->timeout(30)
            ->retry(2, 1000)           // retry 2 ครั้ง รอ 1 วิ
            ->post('https://api.openai.com/v1/chat/completions', [
                'model'      => 'gpt-4o-mini',
                'messages'  => $messages,
                'max_tokens' => 500,
            ])
            ->throw();                  // โยน exception ถ้า 4xx/5xx

        return $response->json('choices.0.message.content', '');
    }
}
Laravel tip: ใส่ key ใน config/services.php และอ่านจาก .env ผ่าน OPENAI_API_KEY=... อย่า hard-code API key ใน source code เด็ดขาด!

5. อ่าน Response ให้เป็น

Response ที่ได้กลับมามีโครงสร้างที่ควรรู้จัก:

{
  "id": "chatcmpl-abc123" ← unique id ของ request นี้
  "choices": [{
    "message": {
      "role": "assistant" ← บทบาทของผู้ตอบ
      "content": "คำตอบอยู่ตรงนี้..."
    },
    "finish_reason": "stop" ← stop/length/content_filter
  }],
  "usage": {
    "prompt_tokens": 45 ← token ที่คุณส่งไป
    "completion_tokens": 312← token ที่ AI ตอบกลับ
    "total_tokens": 357← รวม = เงินที่เสียไป 💸
  }
}

finish_reason สำคัญมาก — ถ้าเป็น length แปลว่าคำตอบถูกตัดออกเพราะ max_tokens หมด ต้องเพิ่ม limit หรือแบ่ง task ให้เล็กลง

6. เปรียบเทียบ LLM API ยอดนิยมสำหรับ PHP Dev

ProviderEndpoint BaseAuth HeaderStreamingเหมาะสำหรับ
OpenAIapi.openai.com/v1Bearer token✅ SSEPrototype เร็ว, ecosystem ใหญ่
Anthropic Claudeapi.anthropic.com/v1x-api-key header✅ SSELong context, เหตุผลซับซ้อน
Google Geminigenerativelanguage.googleapis.comAPI key param✅ SSEContext window ใหญ่ที่สุด
Ollama (local)localhost:11434/apiไม่ต้องใช้✅ JSON streamDev/test ประหยัด, ข้อมูลลับ
OpenRouteropenrouter.ai/api/v1Bearer token✅ SSEเปลี่ยนโมเดลได้ทีเดียว
🔮
Pro tip: ถ้าอยากเปลี่ยน provider ได้ง่ายในอนาคต ให้สร้าง interface LlmInterface และ implement แต่ละ provider แยกกัน แล้วผูกผ่าน service container ของ Laravel หรือ DI ของ CI4

7. Streaming Response — ทำให้ UX ดีขึ้น

แทนที่ user จะรอ 10 วินาทีแล้วเห็นข้อความโผล่ทีเดียว — Streaming ทำให้ข้อความไหลออกมาทีละ chunk เหมือน ChatGPT พิมพ์ให้เห็น

stream-example.php (vanilla PHP)
// ต้องปิด output buffering ก่อน
ob_end_clean();
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');

$ch = curl_init('https://api.openai.com/v1/chat/completions');
curl_setopt_array($ch, [
    CURLOPT_POST          => true,
    CURLOPT_HTTPHEADER    => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS    => json_encode([
        'model'    => 'gpt-4o-mini',
        'messages' => [['role' => 'user', 'content' => $prompt]],
        'stream'   => true,  // <-- ตรงนี้สำคัญ!
    ]),
    CURLOPT_WRITEFUNCTION => function($ch, $chunk) {
        echo $chunk;
        ob_flush();
        flush();
        return strlen($chunk);
    },
]);

curl_exec($ch);
curl_close($ch);
⚠️
ข้อจำกัดของ PHP streaming: Shared hosting หลายเจ้าปิด output buffering หรือ timeout เร็ว ถ้าต้องการ streaming UX ที่ดีจริงๆ แนะนำให้ทำ proxy endpoint แยก หรือใช้ JavaScript (fetch + ReadableStream) ไปเรียก API ตรงจาก frontend แทน

8. LLM ไม่ใช่เพื่อนร่วมทีม — มันคือ Probabilistic Compiler

มีแนวคิดที่น่าสนใจจากชุมชน dev ในปี 2026 คือ "ถ้าคุณ treat LLM เหมือนเพื่อนร่วมทีม คุณจะผิดหวัง แต่ถ้า treat มันเหมือน compiler คุณจะส่ง product ได้จริง"

ความหมายในทางปฏิบัติสำหรับ PHP dev คือ:

  • Input ต้องชัดเจน — ถ้า input มัว output ก็มัวแน่นอน ระบุ constraint ให้ครบ เช่น "ใช้ PHP 8.2, CI4, ห้ามแก้ public interface"
  • Context ต้องพอดี — ส่งเฉพาะสิ่งที่ AI ต้องรู้จริงๆ อย่า paste ทั้งไฟล์ถ้าไม่จำเป็น
  • ตรวจสอบ output เสมอ — LLM confident ได้แม้จะผิด (hallucination) อย่า merge โดยไม่ review
  • ใช้ไฟล์เป็น long-term memory — ให้ LLM เขียน decision/summary ลง markdown ไฟล์ใน repo แทนการเล่าซ้ำในทุก conversation

สรุป

การ integrate LLM เข้าโปรเจกต์ PHP ไม่ได้ซับซ้อนกว่าการเรียก REST API ทั่วไปเลย — มันคือ HTTP POST + JSON ธรรมดา สิ่งที่ต่างออกไปคือ latency ที่สูงกว่า, การจัดการ token budget, และการเข้าใจว่า response อาจมาแบบ streaming

สรุปขั้นตอนที่ควรจำ:

  1. สร้าง Service class แยก — อย่าเรียก API ตรงใน Controller
  2. เก็บ API key ใน .env เสมอ ห้าม commit เด็ดขาด
  3. ตรวจ finish_reason ทุกครั้ง — length หมายถึงคำตอบขาดหาย
  4. Monitor usage.total_tokens เพื่อควบคุมค่าใช้จ่าย
  5. ออกแบบ prompt ให้ชัด ระบุ constraint ครบ ก่อนค่อย optimize