เบื้องหลัง 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

Production-Ready CodeIgniter 4 + Docker พร้อม CI/CD ใน 10 นาที

โดย CyberMAN



🐳 Docker + CI/CD + CodeIgniter 4

Production-Ready CodeIgniter 4 + Docker
พร้อม CI/CD ใน 10 นาที

ตั้งค่าสภาพแวดล้อม PHP ระดับ Production ด้วย Docker, Nginx, MySQL, Redis และ GitHub Actions ในขั้นตอนเดียว

🕐 อ่าน 8 นาที⚡ PHP-FPM 8.2🐳 Docker Compose🔁 GitHub Actions

ปัญหาที่นักพัฒนา PHP ทุกคนเจอ

ทุกครั้งที่เริ่มโปรเจกต์ CodeIgniter 4 ใหม่ ก่อนที่จะเขียน business logic สักบรรทัดเดียว เราต้องเสียเวลาหลายชั่วโมงกับการ setup: ทำให้ Docker ทำงานได้, ตั้งค่า Nginx, เชื่อม PHP-FPM, ติดตั้ง MySQL กับ Redis, ทำให้ PHPUnit รันได้ใน container และสุดท้ายก็ต้องนั่งหาว่าทำไม CI pipeline ถึง fail ตั้งแต่ push ครั้งแรก

เวลาที่ใช้ setup คือ "เวลาสูญเปล่า" ไม่ได้ช่วย ship feature และก็ต้องทำซ้ำในทุกโปรเจกต์

💡 บทความนี้จะพาคุณตั้งแต่ศูนย์ไปถึง environment ที่ tested และพร้อม production จริงๆ ด้วย CodeIgniter 4 + Docker stack ภายใน 10 นาที พร้อม GitHub Actions CI/CD ที่รัน PHPUnit, PHPStan และ Rector อัตโนมัติ

Stack ที่ใช้ในโปรเจกต์นี้

stack ทั้งหมดถูกออกแบบมาให้ครอบคลุมตั้งแต่ development ไปจนถึง production โดยไม่ต้องเปลี่ยน config อะไรมาก

ComponentVersionหน้าที่
PHP-FPM8.2FastCGI runtime สำหรับรัน PHP
CodeIgniter4.7MVC framework + Shield authentication
Nginx1.28-alpineReverse proxy และ serve static files
MySQL8.4Primary database
Redis7-alpineCache, sessions และ queue backend
SupervisorsystemPID-1 process manager ใน production
PHPStanlevel 6Static analysis ตรวจ code ก่อน deploy
PHPUnit10.5Test suite พร้อม pcov driver

โครงสร้างไฟล์และ Docker Compose

โปรเจกต์แบ่งเป็นสองโหมดชัดเจน: Dockerfile.dev สำหรับ development (มี pcov + git support) และ Dockerfile สำหรับ production (ใช้ Supervisor เป็น PID-1)

docker-compose.yml
version: '3.9'

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile.dev
    volumes:
      - .:/var/www/html
    depends_on:
      - db
      - redis
    environment:
      CI_ENVIRONMENT: development
      DB_HOST:     db
      REDIS_HOST:  redis

  nginx:
    image: nginx:1.28-alpine
    ports:
      - "8080:80"
    volumes:
      - ./nginx/default.conf:/etc/nginx/conf.d/default.conf
    depends_on:
      - app

  db:
    image: mysql:8.4
    environment:
      MYSQL_ROOT_PASSWORD: secret
      MYSQL_DATABASE:      ci4app
    volumes:
      - db_data:/var/lib/mysql

  redis:
    image: redis:7-alpine

volumes:
  db_data:

รัน Stack ทั้งหมดด้วยคำสั่งเดียว

ทุกอย่างถูก wrap ไว้ใน Makefile คำสั่ง make setup จะ build images, start containers, รอ MySQL + Redis พร้อม, รัน migrations, seed database และ hit healthcheck endpoint อัตโนมัติ

terminal
# Build, start, migrate, seed — ทุกอย่างในคำสั่งเดียว
make setup

# ตรวจสอบว่า stack ทำงานปกติ
curl http://localhost:8080/health

ถ้า stack ทำงานถูกต้อง จะได้ response JSON ดังนี้:

healthcheck response
{
  "status":    "ok",
  "database":  "ok",
  "redis":     "ok",
  "timestamp": "2026-06-26T15:00:00+00:00"
}
✅ เห็น JSON นี้ = สำเร็จ!หมายความว่า PHP-FPM, Nginx, MySQL, Redis ทำงานครบ, migrations รันเรียบร้อย และ healthcheck ผ่านทั้งหมด

3 การตัดสินใจสำคัญที่ไม่ค่อยมีคนพูดถึง

1. ใช้ Supervisor เป็น PID-1 แทน php-fpm โดยตรง

ถ้ารัน php-fpm เป็น PID-1 ตรงๆ จะเกิดปัญหา zombie processes สะสมใน container ที่รันนานๆ Supervisor จัดการ signal forwarding ได้ถูกต้อง, reap child processes และ restart service ที่ crash อัตโนมัติ

Dockerfile (production)
FROM php:8.2-fpm-alpine

RUN apk add --no-cache supervisor

# คัดลอก config ของ supervisor
COPY supervisord.conf /etc/supervisor/conf.d/app.conf

# Healthcheck via FastCGI
HEALTHCHECK --interval=10s --timeout=3s \
  CMD cgi-fcgi -bind -connect 127.0.0.1:9000 || exit 1

CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/supervisord.conf"]

2. SSH Key ใน Docker Container — ทำให้ถูกต้องและปลอดภัย

ถ้า app ต้องใช้ git clone จาก private repo หรือ deploy ผ่าน SSH ภายใน container การจัดการ SSH key ต้องทำอย่างระมัดระวัง ห้าม hardcode key ใน Dockerfile เด็ดขาด ให้ใช้ Docker build argument แทน

⚠️ ข้อควรระวังอย่า COPY ~/.ssh/id_rsa /root/.ssh/id_rsa ตรงๆ ใน Dockerfile เพราะ key จะถูก bake เข้าไปใน image layer และถ้าใครดึง image ไปก็จะได้ private key ของคุณไปด้วย
Dockerfile (SSH key ปลอดภัย)
# รับ SSH key ผ่าน build argument
ARG ssh_prv_key
ARG ssh_pub_key

RUN mkdir -p /root/.ssh && \
    echo "$ssh_prv_key" > /root/.ssh/id_rsa && \
    echo "$ssh_pub_key" > /root/.ssh/id_rsa.pub && \
    chmod 600 /root/.ssh/id_rsa && \
    chmod 600 /root/.ssh/id_rsa.pub

# ลบ key หลัง build เสร็จ (ใช้ --squash)
terminal — build command
docker build \
  --build-arg ssh_prv_key="$(cat ~/.ssh/id_rsa)" \
  --build-arg ssh_pub_key="$(cat ~/.ssh/id_rsa.pub)" \
  --squash \
  -t myapp .

3. สองสภาพแวดล้อม ไฟล์เดียว .env

ใช้ .env แยก config ระหว่าง dev และ production อย่าเขียน credential ลงใน docker-compose.yml โดยตรง ใช้ variable substitution แทน

.env
CI_ENVIRONMENT=development
DB_HOST=db
DB_PORT=3306
DB_DATABASE=ci4app
DB_USERNAME=ci4user
DB_PASSWORD=secret
REDIS_HOST=redis
REDIS_PORT=6379

GitHub Actions CI/CD Pipeline

ทุก push และ pull request จะ trigger workflow อัตโนมัติ รัน PHPUnit, PHPStan และ Rector เพื่อให้มั่นใจว่า code ที่จะ merge เข้า main ผ่าน quality gate ทุกข้อ

.github/workflows/ci.yml
name: CI Pipeline

on:
  push:
    branches: [main, develop]
  pull_request:

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Build & Start Stack
        run: make setup

      - name: PHPStan — Static Analysis
        run: docker compose exec app vendor/bin/phpstan analyse --level=6

      - name: PHPUnit — Run Tests
        run: docker compose exec app vendor/bin/phpunit --coverage-text

      - name: Rector — Code Quality Check
        run: docker compose exec app vendor/bin/rector process --dry-run
🔁 Flow ทำงานอย่างไร
  1. Push code → GitHub Actions เริ่มทำงาน
  2. make setup build Docker stack ทั้งหมด
  3. PHPStan วิเคราะห์ type safety ระดับ 6
  4. PHPUnit รัน test suite พร้อม code coverage
  5. Rector ตรวจ code style ตาม PHP 8.2 standards
  6. ผ่านทุก step → merge ได้, fail → block merge ทันที

Docker Best Practices ที่ต้องรู้

1

ใช้ Alpine image เสมอ

เลือก php:8.2-fpm-alpine และ nginx:alpine แทน full Debian image ขนาดเล็กกว่า 5–10 เท่า attack surface น้อยกว่า และ pull เร็วกว่ามาก

2

ห้ามใส่ secret ใน image layer

ใช้ Docker build argument หรือ Docker secrets สำหรับ sensitive data ทุกชนิด ไม่ว่าจะเป็น API keys, SSH keys หรือ database passwords ถ้า key เข้าไปใน layer แล้วจะดึงออกยากมาก

3

ใช้ Healthcheck ทุก service

Docker รู้ว่า container "start" แต่ไม่รู้ว่า service พร้อมรับ request แล้ว Healthcheck บอก Docker Compose ให้รอ service จริงๆ พร้อมก่อนจึงเริ่ม service ถัดไป

4

แยก Dockerfile ระหว่าง dev และ production

Dockerfile.dev มี pcov, xdebug, git, tools ต่างๆ ส่วน Dockerfile production ต้องเบาที่สุด ไม่ต้องการ dev dependency ใดๆ ทั้งนั้น


สรุป: ทำไมต้อง Setup แบบนี้?

การ setup CI4 + Docker stack แบบนี้ให้ประโยชน์ชัดเจนสามด้าน:

  • ความเร็ว — ไม่ต้องเสียเวลา setup ซ้ำทุกโปรเจกต์ make setup คำสั่งเดียวพร้อมพัฒนา
  • ความสม่ำเสมอ — ทุก developer ใน team รันบน environment เดียวกันทุกครั้ง ไม่มีปัญหา "works on my machine"
  • คุณภาพ — CI/CD pipeline บังคับให้ code ผ่าน test, static analysis และ code quality check ก่อน merge ทุกครั้ง

สำหรับมือใหม่ที่เริ่มต้นกับ CodeIgniter 4 การมี stack แบบนี้ตั้งแต่ต้นจะช่วยให้โฟกัสกับการเขียน business logic ได้เลย แทนที่จะมานั่ง debug environment ปัญหา

🚀 ลองทำตามเลยวันนี้!