iT邦幫忙

2026 iThome 鐵人賽

DAY 30
0
Modern Web

我推的Laravel S2!系列 第 30

我推的Laravel S2|Day 29:實戰演練:LINE Bot + OpenAI 聊天機器人,Git 版控與 Render 部署上線

  • 分享至 

  • xImage
  •  

一句話破題

這是這系列最長的一篇,也是把前面 28 天知識串起來的實戰演練:先幫 blog-app 加一個能回答讀者問題的 LINE 聊天機器人(串接 OpenAI),接著走一次 Git 版控流程,最後真正把專案部署上線。三件事合起來剛好是「開發 → 版控 → 上線」的完整故事線。

第一站:LINE Bot 基礎設定

LINE Developers 建立一個 Provider 跟 Messaging API Channel,取得兩個關鍵憑證:

LINE_BOT_CHANNEL_ACCESS_TOKEN=xxx
LINE_BOT_CHANNEL_SECRET=xxx

LINE Developers Console 設定畫面截圖

安裝官方 SDK:

composer require linecorp/line-bot-sdk

建立 Webhook 路由與 Controller

// routes/api.php
Route::post('/line/webhook', LineWebhookController::class);
// app/Http/Controllers/LineWebhookController.php
use LINE\Clients\MessagingApi\Api\MessagingApiApi;
use LINE\Clients\MessagingApi\Configuration;
use LINE\Parser\EventRequestParser;
use LINE\Webhook\Model\MessageEvent;
use LINE\Webhook\Model\TextMessageContent;

class LineWebhookController extends Controller
{
    public function __construct(
        protected ChatbotService $chatbot,
    ) {}

    public function __invoke(Request $request)
    {
        $parsedEvents = EventRequestParser::parseEventRequest(
            $request->getContent(),
            config('services.line.channel_secret'),
            $request->header('x-line-signature'),
        );

        foreach ($parsedEvents->getEvents() as $event) {
            if ($event instanceof MessageEvent && $event->getMessage() instanceof TextMessageContent) {
                ReplyToLineMessage::dispatch(
                    replyToken: $event->getReplyToken(),
                    userMessage: $event->getMessage()->getText(),
                );
            }
        }

        return response()->noContent();
    }
}

EventRequestParser::parseEventRequest() 會用 Channel Secret 驗證這個請求真的來自 LINE(防止偽造請求),驗簽失敗會直接拋出例外,這正好呼應 Day 16 學過的請求偽造防護精神,只是這裡是應用層級的簽章驗證,不是框架內建的 CSRF 機制。

為什麼要透過 Queue Job 而不是同步處理

注意上面的程式碼把實際回覆邏輯派送成一個 Job(ReplyToLineMessage::dispatch()),而不是像早期草稿那樣直接同步呼叫 $this->chatbot->reply()。這是刻意的設計,理由值得說清楚:LINE 平台對 Webhook 的回應時間有嚴格限制(通常是幾秒鐘內),如果 Controller 直接同步呼叫 OpenAI API(Day 26 的 HTTP Client,網路延遲加上模型生成時間,經常需要幾秒到十幾秒),很可能在 OpenAI 回應之前就已經超過 LINE 的逾時限制,導致 LINE 判定這次 Webhook 呼叫失敗、觸發重試機制——重試又會再次觸發同一段耗時邏輯,容易演變成同一則訊息被處理好幾次。

把實際處理邏輯丟進 Day 19 學過的 Queue,Controller 只需要「確認收到、快速回應 LINE」跟「把工作排進佇列」兩件事,符合 Day 19 那句「不需要讓使用者等待的工作」精神——只是這裡「使用者」換成了「LINE 平台的 Webhook 呼叫方」:

// app/Jobs/ReplyToLineMessage.php
class ReplyToLineMessage implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public function __construct(
        public string $replyToken,
        public string $userMessage,
    ) {}

    public function handle(ChatbotService $chatbot): void
    {
        $chatbot->reply($this->replyToken, $this->userMessage);
    }
}

handle() 方法直接型別提示 ChatbotService $chatbot——這是 Day 19 提過的、Job 的 handle() 方法本身也享有 Service Container 的方法注入能力,不需要在建構子裡塞進這個依賴。

LINE Bot 訊息處理流程:使用者傳訊息 → Webhook 驗簽 → 立即回 204 避免逾時 → 派送 Queue Job → ChatbotService 呼叫 OpenAI → 透過 LINE Reply API 回覆使用者

串接 OpenAI

composer require openai-php/laravel
php artisan openai:install
OPENAI_API_KEY=xxx
// app/Services/ChatbotService.php
use OpenAI\Laravel\Facades\OpenAI;
use LINE\Clients\MessagingApi\Api\MessagingApiApi;
use LINE\Clients\MessagingApi\Model\ReplyMessageRequest;
use LINE\Clients\MessagingApi\Model\TextMessage;

class ChatbotService
{
    public function __construct(
        protected MessagingApiApi $lineApi,
    ) {}

    public function reply(string $replyToken, string $userMessage): void
    {
        try {
            $result = OpenAI::chat()->create([
                'model' => 'gpt-4o-mini',
                'messages' => [
                    ['role' => 'system', 'content' => '你是 blog-app 部落格的客服機器人,用繁體中文簡短回答讀者問題。'],
                    ['role' => 'user', 'content' => $userMessage],
                ],
            ]);

            $replyText = $result->choices[0]->message->content;
        } catch (\Throwable $e) {
            Log::error('OpenAI 呼叫失敗', ['error' => $e->getMessage()]);
            $replyText = '抱歉,我現在有點忙不過來,請稍後再試一次。';
        }

        $this->lineApi->replyMessage(new ReplyMessageRequest([
            'replyToken' => $replyToken,
            'messages' => [new TextMessage(['text' => $replyText])],
        ]));
    }
}

ChatbotService 依賴 MessagingApiApi(LINE 回覆 API 的客戶端),實際綁定可以在 AppServiceProvider 裡用 Day 23 學過的方式組裝:

$this->app->singleton(MessagingApiApi::class, function () {
    return new MessagingApiApi(
        config: (new Configuration())->setAccessToken(config('services.line.channel_access_token')),
    );
});

try/catch 這段是刻意補上的——呼應 Day 26 提過的「不能假設外部 API 永遠成功」,OpenAI 服務偶爾會逾時或回傳錯誤,如果沒有妥善處理,使用者會完全收不到任何回應(而不是至少看到一句「抱歉現在有點忙」),這種「優雅降級」的處理方式在串接任何外部 AI 服務時都值得養成習慣。

這整段串接,本質上是 Day 26 HTTP Client 概念的延伸(openai-php/laravellinecorp/line-bot-sdk 底層都是包裝好的 HTTP 呼叫)、Day 19 Queue 背景處理、與 Day 23 依賴注入實戰的組合應用——原理都是前面天數教過的,今天只是把它們組合成一個真正能用的功能。

第二站:Git 版本控制

blog-app 開發到這裡,如果還沒建立版本控制,先確認 .gitignore 排除該排除的內容(vendor/node_modules/.env——.env 含機密資訊絕對不能進版控,Day 6 提過的加密機制是給真的需要分享 .env 內容時的另一條路,日常開發直接排除在外最單純):

git init
git add .
git commit -m "Initial commit: blog-app with LINE bot integration"

到 GitHub 建立一個新的 repository,推送上去:

git remote add origin https://github.com/your-username/blog-app.git
git branch -M main
git push -u origin main

Git Flow:分支策略走一遍

實務上不會直接在 main 上開發,常見的 Git Flow 精神:

git checkout -b develop
git checkout -b feature/line-chatbot develop

# 開發完成,提交變更
git add .
git commit -m "feat: 新增 LINE 聊天機器人串接 OpenAI"
git push -u origin feature/line-chatbot

接著到 GitHub 開一個 Pull Request,從 feature/line-chatbot 合併回 develop,讓團隊成員 Code Review 後再合併,最後 develop 穩定後才合併進 main 準備部署——這套流程的核心精神是「main 永遠是可以部署的穩定狀態」,功能開發都在獨立分支進行,不直接衝擊主線。

Git Flow 分支圖:從 develop 切出 feature/line-chatbot,開發完成後開 PR 合併回 develop,develop 穩定後才合併進 main 觸發部署

提交訊息的慣例

上面的 commit message 用了 feat: 前綴,這是社群廣泛採用的 Conventional Commits 慣例,幾個常見前綴:

前綴 用途
feat: 新功能
fix: 修正錯誤
refactor: 重構,不改變外部行為
test: 新增或調整測試
docs: 文件調整
chore: 建置流程、套件更新等雜項

這套慣例的實際價值不只是「看起來整齊」——如果團隊一致遵守,配合工具(例如自動產生 CHANGELOG 的套件)能直接從 commit 歷史自動整理出版本更新紀錄,不需要每次發版都手動回想「這個版本到底改了什麼」。Day 27 提過的 CI 流程,也常會設定規則檢查 commit message 是否符合這套格式,確保長期累積下來的版控歷史保持一致、可讀。

第三站:部署到 Render

Render 是一個對 Laravel 專案友善的雲端平台,用 Docker 容器部署。

準備 Dockerfile

FROM richarvey/nginx-php-fpm:latest

COPY . .

ENV SKIP_COMPOSER=1
ENV WEBROOT=/var/www/html/public
ENV PHP_ERRORS_STDERR=1
ENV RUN_SCRIPTS=1
ENV REAL_IP_HEADER=1

RUN composer install --no-dev --optimize-autoloader

CMD ["/start.sh"]

這裡有個容易踩雷的細節:網路上的 Laravel + Render 教學(包含我自己以前寫的)常會直接給一個綁死的映像檔標籤,例如 richarvey/nginx-php-fpm:2.0.03.1.6。這種版本號會過期——部署 blog-app(Laravel 13、PHP 8.3+)時,請直接查 Docker Hub 上的最新標籤,確認內建 PHP 版本滿足 8.3+,不要照抄。如果找不到明確對應的標籤,也可以改用 Laravel 官方維護的 Sail production 映像檔當基礎。

Render Dashboard 設定

  1. 在 GitHub 建一個 deploy 分支(或直接用 main,視你的部署策略而定)。
  2. Render Dashboard 建立新的 Web Service,連接你的 GitHub repository。
  3. 設定環境變數(APP_KEYphp artisan key:generate --show 產生後貼上、DB_* 系列、LINE_BOT_*OPENAI_API_KEY 等)。
  4. 建立一個 Render 提供的 PostgreSQL 資料庫,把連線資訊填進對應的環境變數。
  5. 開啟 Auto-Deploy,之後 git push 到指定分支就會自動觸發重新部署。

Render

部署時自動執行 Migration

正式環境每次部署新版本時,如果有新的 Migration(Day 9 一路建立的資料表結構變更),需要在新版本程式碼上線的同時執行 php artisan migrate。Render 的 Web Service 設定裡可以指定一個「Release Command」,在每次部署、應用程式真正對外服務之前執行:

php artisan migrate --force

--force 是必要的——migrate 指令在正式環境(APP_DEBUG=false)預設會詢問確認,避免有人不小心在正式環境誤跑 migration;--force 明確表示「我知道這是正式環境,請直接執行」,這是自動化部署流程裡標準的做法。同時記得部署流程也該包含 Day 6/7/12 提過的快取指令:

php artisan config:cache
php artisan route:cache
php artisan view:cache

這幾個指令在正式環境上線流程裡幾乎是標準配備,能明顯提升回應速度,Day 6/7/12 各自介紹過它們個別的效果,部署時建議一次全部執行。

Worker 也要記得部署

Day 19 提過 Queue Worker 是一個獨立的常駐行程,Render 這類平台通常需要額外建立一個「Background Worker」服務(區別於處理 HTTP 請求的 Web Service),執行:

php artisan queue:work --tries=3

這是很容易在第一次部署時漏掉的一步——如果只部署了 Web Service,blog-app 的 HTTP 請求能正常運作,但 Day 19 派送的 Job(包括今天的 LINE 回覆邏輯)會一直堆在佇列裡,永遠不會被實際執行,因為根本沒有 Worker 行程在監聽處理。

Render 部署架構:GitHub push 觸發 Auto-Deploy,Web Service、Background Worker、Cron Job 三個獨立服務共用同一個 PostgreSQL 資料庫

強制 HTTPS

// app/Providers/AppServiceProvider.php
public function boot(): void
{
    if ($this->app->environment('production')) {
        URL::forceScheme('https');
    }
}

上線後:怎麼確認一切正常運作

部署完成不代表工作結束,這裡提供一份簡短的上線後檢查清單,串起這系列學過的工具:

  1. 打 Day 5 設定的健康檢查端點/up),確認應用程式基本存活。
  2. 檢查 Day 15 提過的日誌,確認沒有大量非預期的 Error/Critical 等級訊息。
  3. 透過 Day 28 提過的 Telescope(如果有在正式環境限制存取後開啟)或 Sentry,觀察前幾筆真實請求的處理狀況。
  4. 實際發一則 LINE 訊息測試整條流程,確認 Webhook 驗簽、Queue 派送、OpenAI 呼叫、LINE 回覆整條鏈路都正常運作。
  5. 確認 Day 25 的排程任務有被正確觸發(如果部署平台需要額外設定 cron,記得一併設定,Render 這類平台通常需要另外建立一個 Cron Job 服務執行 php artisan schedule:run)。

常見錯誤與踩雷點

  • .env 不小心被 git add . 加進版控:務必在第一次 commit 之前就確認 .gitignore 正確排除 .env,如果已經不小心提交過,光是之後補加 .gitignore 沒用,還需要用 git rm --cached .env 把它從版控歷史移除(更徹底的做法需要改寫歷史,這裡不展開,重點是養成一開始就排除的習慣)。
  • Render 環境變數忘記設定 APP_KEY:沒有這個值,應用程式會直接啟動失敗(加密機制需要這把金鑰),部署前務必確認核心環境變數都已經正確設定。
  • LINE Webhook 驗簽用錯 Secretchannel_secretchannel_access_token 是兩個不同的憑證,驗簽用的是 channel_secret,如果搞混會導致所有 Webhook 請求都被判定為驗簽失敗。
  • 部署映像檔版本沒對應到專案實際的 PHP 需求:延續前面提到的版本號問題,如果映像檔內建的 PHP 版本低於 Laravel 13 要求的 8.3,部署會直接失敗或出現難以排查的相容性問題,部署前務必確認映像檔的 PHP 版本。
  • Webhook Controller 直接同步呼叫 OpenAI,導致 LINE 判定逾時、觸發重複處理:這是今天特別強調、也是實務上很容易在第一版直接同步寫的疏漏,記得透過 Queue 背景處理。
  • 只部署了 Web Service,忘記部署 Queue Worker:前面提過的重點,Job 會一直堆積、永遠不會被執行,使用者傳訊息給 LINE Bot 完全沒有回應,卻很難第一時間意識到問題出在「Worker 根本沒在跑」而不是程式邏輯錯誤。

小結

今天把 blog-app 從「一個能在本機跑的專案」推進到「一個真正上線、能被外部使用者互動的服務」:串接 LINE Bot + OpenAI 打造聊天機器人功能(並學到透過 Queue 背景處理避免 Webhook 逾時的實務考量)、用 Git Flow 走一次團隊協作的版控流程、部署到 Render 並確保 HTTPS、Migration 自動執行、Worker 部署,最後補上一份上線後的檢查清單。部署映像檔的版本號,是這篇少數需要你根據部署當下的實際情況自行查證的地方。

千里之堤,潰於蟻穴 — 《韓非子・喻老》

明日預告

Day 30,這個系列的最後一天:回顧整趟 30 天的旅程,並展望 Laravel 13 裡幾個很值得關注的新能力。


上一篇
我推的Laravel S2|Day 28:套件生態管理:Composer 衝突處理、Telescope、Sanctum、Octane、Socialite、Laravel Boost
下一篇
我推的Laravel S2|Day 30:結語與展望:系列總結 + Laravel 13 新能力巡禮
系列文
我推的Laravel S2!31
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言