[Đất đai AI 06/10] AI client PHP 8.x: adapter Chat Completions, timeout, lỗi an toàn và chế độ mô phỏng

28 lượt xem · 2 trả lời

1. 12:12 13/08/2026
CHUỖI THỰC HÀNH: CHATBOT AI HỎI ĐÁP ĐẤT ĐAI — PHP 8.x + MySQL 9.x Lưu ý trách nhiệm: chatbot chỉ tổng hợp thông tin tham khảo từ nguồn do quản trị viên phê duyệt. Không dùng để kết luận quyền sử dụng đất, tranh chấp, quy hoạch hoặc nghĩa vụ tài chính cụ thể. Không nhập CCCD, số sổ, ảnh giấy tờ, địa chỉ/tọa độ chính xác hay dữ liệu cá nhân vào chat. # 06/10 — AI client: khóa mẫu, fake mode và timeout Ta tách cuộc gọi AI vào một lớp duy nhất. Nhờ vậy website có thể chạy bằng `AI_FAKE_MODE=true` mà không cần API key thật; khi đổi nhà cung cấp, bạn chỉ chỉnh adapter/cấu hình, không chạm router hay database. Ví dụ dùng endpoint dạng `/chat/completions` phổ biến; hãy đọc tài liệu chính thức của nhà cung cấp bạn chọn để khớp tên model và response. ## 1. Nhắc lại cấu hình khóa — chỉ là ví dụ Trong `.env.example` hoặc tài liệu hướng dẫn, luôn để dạng sau: ```dotenv # KHÔNG phải khóa thật. Đừng thay chuỗi này trong source code. AI_API_KEY=YOUR_AI_API_KEY_HERE AI_FAKE_MODE=true AI_BASE_URL=https://api.example.invalid/v1 AI_MODEL=your-model-name ``` Khi lên production, người vận hành tự tạo `.env` riêng và thay `YOUR_AI_API_KEY_HERE` bằng khóa của họ trên máy chủ. `.env` nằm trong `.gitignore`. Không ghi lệnh `var_dump($_ENV)`, không đưa header Authorization vào log, không gửi khóa lên frontend. Khi `AI_FAKE_MODE=true`, `AI_API_KEY` có thể giữ nguyên nhãn mẫu. ## 2. Tạo `app/AiClient.php` ```php <?php declare(strict_types=1); final class AiClient { /** @return array{answer:string, model:string} */ public function answer(string $systemPrompt, string $userPrompt): array { if (Config::bool('AI_FAKE_MODE', true)) { return [ 'answer' => "[Chế độ mô phỏng] Hệ thống đã tìm thấy nguồn tham khảo. Hãy đặt AI_FAKE_MODE=false và cấu hình khóa tại .env trên máy chủ để nhận câu trả lời AI thực tế.", 'model' => 'fake-mode', ]; } $apiKey = Config::require('AI_API_KEY'); if ($apiKey === 'YOUR_AI_API_KEY_HERE' || strlen($apiKey) < 12) { throw new RuntimeException('AI chưa được cấu hình: thay nhãn khóa mẫu trong .env trên máy chủ.'); } $url = rtrim(Config::require('AI_BASE_URL'), '/') . '/chat/completions'; $payload = [ 'model' => Config::require('AI_MODEL'), 'messages' => [ ['role' => 'system', 'content' => $systemPrompt], ['role' => 'user', 'content' => $userPrompt], ], 'temperature' => 0.1, 'max_tokens' => 700, ]; $curl = curl_init($url); if ($curl === false) throw new RuntimeException('Không khởi tạo được HTTP client.'); curl_setopt_array($curl, [ CURLOPT_POST => true, CURLOPT_POSTFIELDS => json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR), CURLOPT_RETURNTRANSFER => true, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => max(5, min(60, (int)Config::get('AI_TIMEOUT_SECONDS', '25'))), CURLOPT_HTTPHEADER => [ 'Content-Type: application/json', 'Authorization: Bearer ' . $apiKey, ], ]); $raw = curl_exec($curl); $status = (int)curl_getinfo($curl, CURLINFO_RESPONSE_CODE); $error = curl_error($curl); curl_close($curl); if (!is_string($raw) || $status < 200 || $status >= 300) { // Không log raw response vì có thể chứa dữ liệu nhạy cảm hoặc header từ proxy. error_log("AI request failed: HTTP={$status}; curl=" . ($error !== '' ? $error : 'n/a')); throw new RuntimeException('Dịch vụ AI đang tạm thời không phản hồi.'); } try { $data = json_decode($raw, true, 64, JSON_THROW_ON_ERROR); $answer = $data['choices'][0]['message']['content'] ?? null; } catch (Throwable) { throw new RuntimeException('Dịch vụ AI trả về dữ liệu không hợp lệ.'); } if (!is_string($answer) || trim($answer) === '') throw new RuntimeException('Dịch vụ AI không có nội dung trả lời.'); return ['answer' => trim($answer), 'model' => (string)($data['model'] ?? 'unknown')]; } } ``` ## 3. Nạp class Thêm dòng này vào cuối `app/Bootstrap.php`: ```php require __DIR__ . '/KnowledgeService.php'; require __DIR__ . '/AiClient.php'; ``` Các điểm cố ý trong code: timeout ngắn, giới hạn token, nhiệt độ thấp, không lộ raw API response, và không có khóa bí mật nào nằm trong mã. Bài 07 sẽ ghép KnowledgeService + AiClient thành ChatService có guardrail, rate limit và lưu lịch sử tối thiểu.
0 Facebook
2. 14:47 12/10/2026
Trả lời Quản Trị Viên CHUỖI THỰC HÀNH: CHATBOT AI HỎI ĐÁP ĐẤT ĐAI — PHP 8.x + MySQL 9.x Lưu ý trách nhiệm: chatbot chỉ tổng hợp thông tin tham…
Cách tách adapter và bật `AI_FAKE_MODE` rất thực tế khi dev local hoặc chạy CI mà không muốn tốn quota API thật. Em thấy phần nhắc nhở không log header `Authorization` và không `var_dump($_ENV)` quan trọng — nhiều project vẫn lọt lỗi này khi debug vội. Về timeout, anh/chị có định cấu hình khác nhau cho stream và non-stream không? Em đang cân nhắc set connect timeout ngắn (3–5s) nhưng read timeout dài hơn cho stream response, chưa chắc đã tối ưu cho mọi nhà cung cấp.
0
3. 21:50 26/10/2026
Trả lời Khánh Linh Cách tách adapter và bật `AI_FAKE_MODE` rất thực tế khi dev local hoặc chạy CI mà không muốn tốn quota API thật. Em thấy…
Mình thấy tách timeout cho stream và non-stream là hợp lý, nhất là khi adapter có cấu hình riêng. Với non-stream, nên có connect timeout ngắn (3–5s) và read timeout vừa phải, kèm tổng thời gian chờ để tránh treo request. Với stream, connect timeout vẫn ngắn, nhưng read timeout nên hiểu là thời gian chờ giữa các chunk chứ không phải toàn bộ phiên; nếu đặt một read timeout dài cho cả stream thì dễ giữ kết nối chết lâu hơn cần thiết. Thực tế nên có thêm idle timeout và tổng deadline riêng, đồng thời cho phép cấu hình theo từng nhà cung cấp vì hành vi chunk/keep-alive khác nhau. Cũng đừng quên retry có kiểm soát: chỉ retry lỗi kết nối hoặc 5xx, không retry khi đã nhận một phần stream để tránh trùng nội dung. Bạn đang dùng cURL hay Guzzle cho phần stream, và đã thử đo thời gian giữa các chunk thực tế chưa?
0
Vui lòng đăng nhập để tham gia trả lời chủ đề này.
Điều hướng nhanh

Bạn muốn khám phá điều gì?

Tìm chủ đề, bài viết hoặc đi thẳng đến một khu vực của cộng đồng.

Esc để đóng Ctrl K để mở lại