LINE Bot × Cloudflare Workers AI|サーバーレスでしゃべるBotを作ってみた(2025年版)

技術

「LINEでちょっとAIに聞けたら便利なのに」と思って、Cloudflare Workers AIとLINE Botを組み合わせてみました。独自ドメインのサブドメインをWorkerに割り当てて、Webhookを受け取り、署名検証をして、AIの返答をPushで返すという流れです。実際に動かしてみた手順をまとめておきます。


はじめに

最近はChatGPTやClaudeといったAIを直接ブラウザや専用アプリで使うことが当たり前になってきました。ただ、日常のやりとりの中で気軽に呼び出したいときってありませんか?たとえば友だちにLINEで相談するように、「ちょっと天気は?」「旅行先でおすすめは?」「この技術どうなってる?」みたいなノリでAIに聞けたら便利だな、と。

そんなときに考えたのが、LINE BotにAIを組み込むというアプローチです。LINEは日本ではほぼ生活インフラといっていいくらい普及しているし、自分のスマホにも当然入っている。そこにCloudflare Workersを挟んでAIの頭脳をつないでしまえば、身近なチャット相手としてAIを呼び出せるんじゃないか、と。

Cloudflare Workersを選んだ理由はいくつかあります。まず無料枠が太っ腹で、試すだけならお金を気にせず始められること。そしてWorkers AIが提供されていて、サーバーレスでそのままモデルを叩けること。さらに独自ドメインを簡単に割り当てられるので、Webhook用のエンドポイントを自分のドメインで用意できるのもポイントでした。

実際にやってみると、署名検証やReplyとPushの違いなど、LINE特有のクセには少しハマりましたが、流れがつかめると拍子抜けするほどシンプルでした。今回の記事では、その一連の手順を自分の体験をもとに整理して紹介します。


LINE公式アカウントの準備

まずはLINE公式アカウントを作るところから始めます。2024年9月から仕様が変わり、LINE Developers Consoleから直接Messaging APIチャネルを作ることはできなくなりました。そのため、LINE公式アカウントを開設し、Official Account ManagerからMessaging APIを有効化するのが入口になります。

Messaging APIを有効化すると、裏でMessaging APIチャネルが自動で生成され、Channel IDやChannel secretが割り当てられます。さらに、ここからチャネルアクセストークンを発行します。開発中は長期のアクセストークンを発行しておくと楽です。

その後、LINE Developers Consoleにアクセスすると、プロバイダーに新しいチャネルが追加されているのが確認できます。Webhook URL(今回は https://linebot.your-domain.com/webhook)を入力して「Verify」で接続確認し、成功したら「Use webhook」を有効化しましょう。


Cloudflare Workersの準備(ダッシュボードで進める場合)

Cloudflareの管理画面にログインし、左メニューから Workers & Pages を開いて新しいWorkerを作成します。名前は自由でOKです。作成した時点で *.workers.dev のドメインで動作確認できます。

次に、このWorkerをLINEのWebhook用サブドメインに割り当てます。Workerの設定画面 → トリガー → ドメインとルート から カスタムドメインを追加 を選び、たとえば

linebot.your-domain.com

のようなサブドメインを登録してください。DNSや証明書は自動で用意され、数分待てばHTTPSでアクセスできるようになります。

ここで環境変数を登録します。**「設定 → 変数とシークレット」**から以下を追加しておきましょう。

  • LINE_CHANNEL_SECRET(署名検証に使う)
  • LINE_CHANNEL_ACCESS_TOKEN(Messaging API呼び出しに使う)

さらに、Workers AIを呼び出すためのバインディングを設定します。**「設定 → バインディング → AIバインディング」**を開き、名前を AI_BINDING にすればOK。これでコード側で env.AI_BINDING.run(...) として呼べるようになります。


CLI派向け:wranglerを使う場合

コマンドラインでまとめて設定したい場合は wrangler を使います。

# Cloudflareアカウントにログイン
npx wrangler login

# 新しいプロジェクトを初期化
npx wrangler init linebot-worker
cd linebot-worker

環境変数(シークレット)は次のように投入します。

npx wrangler secret put LINE_CHANNEL_SECRET
npx wrangler secret put LINE_CHANNEL_ACCESS_TOKEN

AIバインディングは wrangler.toml に追記します。

[ai]
binding = "AI_BINDING"

準備が整ったらデプロイします。

npx wrangler deploy

ログをリアルタイムで確認したいときは tail を使います。

npx wrangler tail linebot-worker

Webhookの実装(エントリーポイント)

export interface Env {
  LINE_CHANNEL_SECRET: string;
  LINE_CHANNEL_ACCESS_TOKEN: string;
  AI_BINDING: Ai;
}

type LineEvent = {
  type: string;
  replyToken?: string;
  source?: { userId?: string; type?: string; groupId?: string; roomId?: string };
  message?: { id: string; type: string; text?: string };
};

const MODEL = "@cf/meta/llama-3.3-70b-instruct-fp8-fast";

export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
    const url = new URL(request.url);

    if (request.method === "GET" && url.pathname === "/webhook") {
      return new Response("OK (health)", { status: 200 });
    }
    if (request.method !== "POST" || url.pathname !== "/webhook") {
      return new Response("Not Found", { status: 404 });
    }

    const bodyText = await request.text();
    const ok = await verifyLineSignature(bodyText, request.headers.get("x-line-signature") || "", env.LINE_CHANNEL_SECRET);
    if (!ok) return new Response("Bad signature", { status: 401 });

    const payload = JSON.parse(bodyText) as { events: LineEvent[] };
    const tasks: Promise<void>[] = [];

ここでは、GET /webhook にアクセスがあった場合は「OK」を返すようにしてヘルスチェックに使えるようにしました。本番用のPOSTリクエストでは、まず署名検証を通して正当なリクエストかどうかを確認しています。


イベント処理とAI応答

    for (const ev of payload.events || []) {
      if (ev.type === "message" && ev.message?.type === "text" && ev.replyToken) {
        const userText = ev.message.text?.trim() || "";
        const userId = ev.source?.userId;

        // ① 即時ACK
        await replyMessage(env, ev.replyToken, [{ type: "text", text: "了解、少しお待ちください…" }]);

        // ② 裏でAI処理
        if (userId) {
          tasks.push((async () => {
            await startLoading(env, userId, 8);

            const messages = [
              { role: "system", content: "あなたは日本語で簡潔・実用的に答えるアシスタントです。" },
              { role: "user", content: userText }
            ];

            const ai = await env.AI_BINDING.run(MODEL, {
              messages,
              temperature: 0.2,
              max_tokens: 256
            });

            const text = typeof ai?.response === "string" ? ai.response : JSON.stringify(ai);
            await pushMessage(env, userId, [{ type: "text", text: text.slice(0, 4800) }]);
          })());
        }
      }
    }

    tasks.forEach((p) => ctx.waitUntil(p));
    return new Response("OK", { status: 200 });
  }
};

テキストメッセージを受け取ったら、まず短いACKをReplyで返し、裏ではAIを呼び出して結果をPushで送る仕組みです。


ヘルパー関数

async function replyMessage(env: Env, replyToken: string, messages: Array<any>) {
  await fetch("https://api.line.me/v2/bot/message/reply", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${env.LINE_CHANNEL_ACCESS_TOKEN}`
    },
    body: JSON.stringify({ replyToken, messages })
  });
}

async function pushMessage(env: Env, to: string, messages: Array<any>) {
  await fetch("https://api.line.me/v2/bot/message/push", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${env.LINE_CHANNEL_ACCESS_TOKEN}`
    },
    body: JSON.stringify({ to, messages })
  });
}

async function startLoading(env: Env, chatId: string, seconds = 5) {
  await fetch("https://api.line.me/v2/bot/chat/loading/start", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${env.LINE_CHANNEL_ACCESS_TOKEN}`
    },
    body: JSON.stringify({ chatId, loadingSeconds: seconds })
  });
}

async function verifyLineSignature(bodyText: string, signature: string, channelSecret: string) {
  const enc = new TextEncoder();
  const key = await crypto.subtle.importKey(
    "raw",
    enc.encode(channelSecret),
    { name: "HMAC", hash: "SHA-256" },
    false,
    ["sign"]
  );
  const sigBuf = await crypto.subtle.sign("HMAC", key, enc.encode(bodyText));
  const expected = btoa(String.fromCharCode(...new Uint8Array(sigBuf)));
  return expected === signature;
}

ReplyとPushで役割を分け、ローディングアニメーションAPIで「考え中…」の演出も可能にしました。署名検証はChannel secretを使ったHMAC-SHA256です。


料金の目安

Cloudflare WorkersはFreeプランで1日10万リクエストまで無料。Workers AIは1日1万Neuronsまで無料で、超えると1000Neuronsあたり$0.011かかります。モデルによって入出力トークン単価が違いますが、例えば @cf/meta/llama-3.3-70b-instruct-fp8-fast だと入力100万トークンで$0.29、出力100万トークンで$2.25。軽い会話程度ならまず無料枠で収まります。

LINE側は日本の料金プランに従います。無料プランは月200通まで、ライトプランは月5000円で5000通、スタンダードプランは月1万5000円で3万通。スタンダードは超過分が従量課金になります。Push送信はカウント対象になるので運用時は注意が必要です。


まとめ

今回紹介したのは、Cloudflare WorkersとLINEを組み合わせて、自分だけのAIチャットボットを作る方法でした。LINE公式アカウントを用意してWebhookを設定し、CloudflareのWorkerに紐付ける。リクエストが飛んできたら署名検証をして、すぐにACKを返し、その裏でWorkers AIに処理を投げて最終的な回答をPushする。これだけの流れで、身近なLINEアプリにAIを呼び出せるようになります。

やってみると、思った以上にシンプルです。署名検証やReply/Pushの違いなど、LINE Bot独特のポイントはありますが、そこをクリアしてしまえばあとはほとんど「Glueコード」の世界。しかもCloudflareは無料枠が充実しているので、ちょっとした試作ならコストを気にせず遊べます。

「友だちに相談するようにAIに聞ける」という体験は、ブラウザでチャットするのとはまた違う気軽さがあります。スマホのホーム画面からLINEを開いて、「今日の天気は?」とか「最近のイヤホンでおすすめは?」と聞くと、すぐに返ってくる。それだけでAIがぐっと身近に感じられます。

実際に運用するなら、LINEの料金プランやPushの通数制限を意識する必要がありますが、まずは無料枠の範囲で気楽に試してみるのがおすすめです。用途を絞れば十分遊べますし、旅先のちょっとした調べ物や、生活の中の簡単な相談役として使うだけでも役立ちます。

今回のコードはあくまで最小限のサンプルなので、カスタマイズ次第でできることはまだまだ広がります。たとえば特定のキーワードで別のAPIを呼び出したり、社内の情報ベースにアクセスさせたり、趣味の分野に特化したQAボットにしたり…。LINEという日常的なインターフェースを入り口にして、自分の好きな機能をつなげていくとかなり面白いはずです。

ぜひ、自分のドメインとLINEアカウントで、この仕組みを試してみてください。気がつけば、あなたのLINEの友だちリストに「AIアシスタント」が自然に溶け込んでいるはずです。

コメント

タイトルとURLをコピーしました