0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ローカルLLM と LINE Bot を組み合わせた話【LINEDC】

Posted at

(この記事は LINEDC の Advent Calendar 2025 の記事です)

はじめに

記事の内容

今年、LINE DC のイベント登壇でも扱った「ローカルLLM + LINE Bot」に関する話の記事です。

関連するスライド・アーカイブ動画

以下は、今年9月・10月の LINE DC のイベントで使ったスライドや、その時のアーカイブ動画です。

スライド(9月の分)

アーカイブ動画(9月の分)

スライド(10月の分)

アーカイブ動画(10月の分)

「ローカルLLM + LINE Bot」について

上記の登壇に関する内容

上記 2回の登壇では、どちらも「ローカルLLM + LINE Bot」に関する話をしました。

9月に話した内容

9月に話した内容に関する情報は、以下のとおりです。

2025-12-24_22-53-56.jpg

2025-12-24_22-54-26.jpg

2025-12-24_22-55-16.jpg

LINE Bot の応答を、ローカルLLM で生成するというものでした。

10月に話した内容

10月に話した内容に関する情報は、以下のとおりです。

2025-12-24_22-56-44.jpg

2025-12-24_22-56-59.jpg

基本構成は 9月と同じですが、巨大なモデルを扱える PC を使うことで、ローカルLLM の部分で gpt-oss-120b を扱えるようになっています。

LINE Bot の実装に関して

上記で用いた LINE Bot の実装について書いていきます。

前提となる内容

この後に掲載したコードを用いる際の前提について、リストで書いておきます。

  • ローカルLLM の処理
    • LM Studio を使ってローカルサーバーを立ち上げ
      • LM Studio でローカルLLM用のモデルを読み込み
  • トークンなど
    • LINE関連のトークンなどは環境変数で設定

コード

コードは、以下のとおりです。コード冒頭でインポートしているパッケージを用いています。

import { Agent } from "@mastra/core/agent";
import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
import * as line from "@line/bot-sdk";
import express from "express";

const { CHANNEL_SECRET, CHANNEL_ACCESS_TOKEN } = process.env;

if (!CHANNEL_SECRET || !CHANNEL_ACCESS_TOKEN) {
  console.error(
    "[env error] CHANNEL_SECRET / CHANNEL_ACCESS_TOKEN が未設定です"
  );
  process.exit(1);
}

// ===== LM Studio (OpenAI互換) プロバイダ =====
// LM Studio のローカルサーバーの URL は http://localhost:1234/v1
const lmstudio = createOpenAICompatible({
  name: "lmstudio",
  baseURL: "http://localhost:1234/v1",
  apiKey: "lm-studio", // ダミー・空で OK
});

const agent = new Agent({
  name: "LMStudio",
  instructions: "日本語で簡潔に答えてください。",
  // model: lmstudio.chatModel("gemma-3-270m-it"),
  model: lmstudio.chatModel("jan-v1-4b"),
});

const config = { channelSecret: CHANNEL_SECRET };
const client = new line.messagingApi.MessagingApiClient({
  channelAccessToken: CHANNEL_ACCESS_TOKEN,
});

const app = express();

// 動作確認用
app.get("/", (_req, res) => {
  res.status(200).send("OK");
});

app.post("/bot", line.middleware(config), async (req, res) => {
  try {
    const results = await Promise.all((req.body.events || []).map(handleEvent));
    res.json(results);
  } catch (err) {
    console.error("[callback error]", err);
    res.status(500).end();
  }
});

async function handleEvent(event) {
  if (event.type !== "message" || event.message?.type !== "text") {
    return null;
  }

  const userText = (event.message.text || "").trim();
  const aiText = await generateAnswer(userText);

  // 生成結果が空ならオウム返し
  const replyText = aiText && aiText.trim() ? aiText.trim() : userText;
  const capped = replyText.slice(0, 5000);

  return client.replyMessage({
    replyToken: event.replyToken,
    messages: [{ type: "text", text: capped }],
  });
}

async function generateAnswer(input) {
  try {
    const result = await agent.generateVNext(input);
    return result?.text ?? "";
  } catch (e) {
    console.error("[mastra generate error]", e);
    return "";
  }
}

app.listen(3000, () => {
  console.log("listening on 3000");
});

このコードの内容で、上で動画で掲載していたデモの内容を実現していました。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?