3
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?

もう普通のBotには戻れない!?自律行動するGPT-5.2 LINEエージェントbotの作り方

Last updated at Posted at 2026-01-26

はじめに

「ユーザーの入力をただ返すだけのBotはもう飽きた…」
「最新情報も調べて、時間も確認して、経路案内までしてほしい!」

そう思ったことありませんか?

今回紹介するのは Google Apps Script × OpenAI GPT-5.2 × Brave Search APIで自律行動する AI エージェントBotです。ユーザーの質問に応じて、必要なら勝手に外部ツール(Web検索・時刻取得・乗換案内)を呼び出して回答してくれるLINE Botです。

↓こんな感じの作りました!!

image.png

今回の構成

特徴

今回の構成の特徴を簡潔にまとめると以下の4つかなと思います!

  • Google Apps Script+APIだけのシンプルな構成!

  • OpenAI GPT-5.2 のツール呼び出し(function calling)を活用

  • Brave Search API を使って外部検索

  • 会話履歴を保持してコンテキスト対応

詳しい解説

image.png

図は今回の「自律行動するGPT-5.2LINEエージェントBot」がどのように動作しているかを示したアーキテクチャです。

LINE
ユーザーは LINE 上で message(テキスト)を送受信します。

Google Apps Script(GAS)
今回の Bot の 中心部分です。以下の5つの主要処理を担当します。

  • Webhook受信 & 返信
  • ホワイトリスト
  • 会話履歴の管理
  • GPT-5.2 とのやりとり
  • ツールの定義

PropertiesService(GAS内部なので上と同じではある)
GAS内のキーバリュー型ストアであり環境変数と同じ場所にあります。これにより会話履歴を残すことでGPT は過去の文脈を理解して自然に会話できます。

OpenAI GPT-5.2 API
エージェント本体です。処理の中心は以下の3つです。

  • ユーザーメッセージの解析(function calling)

    「駅名が2つある → search_transit」
    「今何時 → get_current_time」
    「ニュース系の質問 → search_web」
    「その他 → そのままLLMが返信」

  • 必要なツールの呼び出し
    上記のようにツールを使う判断がされた場合JSONで指示を出しGAS がツール(search_web等)を実行します。

  • 返信の生成

ツール

ツール名 動作
get_current_time 「今何時?」に必ず使用GAS内で現在時刻を生成して返す。
search_web Brave Search API を使用 Web検索、ニュース、技術情報の取得に使う。
search_transit 駅名が2つ含まれている場合に自動発火しYahoo!路線検索URLを生成する。(今後どこかのAPIから取ってこれるようになると良い)

~function callingについて詳しくは公式ドキュメントにて~

まずは使ってみる

まずは挨拶 しっかり返してくれてますね。

image.png

現在の時間も正しく返してくれていますね。

image.png

本日のニュースも検索を使って教えてくれます。

image.png

会話クリア,リセット,/resetで会話履歴をリセットできます。

image.png

実装コード

const CHANNEL_ACCESS_TOKEN = PropertiesService.getScriptProperties().getProperty('CHANNEL_ACCESS_TOKEN');
const OPENAI_API_KEY = PropertiesService.getScriptProperties().getProperty('OPENAI_API_KEY');
const BRAVE_API_KEY = PropertiesService.getScriptProperties().getProperty('BRAVE_API_KEY');
const LINE_ENDPOINT = 'https://api.line.me/v2/bot/message/reply';
const OPENAI_ENDPOINT = 'https://api.openai.com/v1/chat/completions';
const BRAVE_SEARCH_ENDPOINT = 'https://api.search.brave.com/res/v1/web/search';

// 会話履歴の保持数
const MAX_CONVERSATION_HISTORY = 20;

// 許可するユーザーIDのリスト
const ALLOWED_USER_IDS = [
  '//ユーザーID',
];

function doGet(e) {
  return ContentService.createTextOutput('AI Agent Bot is running!');
}

function doPost(e) {
  try {
    if (!e?.postData?.contents) return ContentService.createTextOutput('OK');

    const events = JSON.parse(e.postData.contents).events || [];
    events.forEach(event => {
      if (event.type === 'message' && event.message.type === 'text') {
        // 許可されていないユーザーには何も返さない
        if (!isUserAllowed(event.source.userId)) {
          return;
        }
        const reply = handleMessage(event.source.userId, event.message.text);
        replyToUser(event.replyToken, reply);
      }
    });
    return ContentService.createTextOutput('OK');
  } catch (error) {
    return ContentService.createTextOutput('OK');
  }
}

function getUserData(userId, key) {
  const data = PropertiesService.getScriptProperties().getProperty(`user_${userId}_${key}`);
  return data ? JSON.parse(data) : null;
}

function saveUserData(userId, key, data) {
  PropertiesService.getScriptProperties().setProperty(`user_${userId}_${key}`, JSON.stringify(data));
}

function deleteUserData(userId, key) {
  PropertiesService.getScriptProperties().deleteProperty(`user_${userId}_${key}`);
}

function getConversationHistory(userId) {
  return getUserData(userId, 'conversation') || [];
}

function saveConversationHistory(userId, history) {
  if (history.length > MAX_CONVERSATION_HISTORY) {
    history = history.slice(-MAX_CONVERSATION_HISTORY);
  }
  saveUserData(userId, 'conversation', history);
}

function clearConversationHistory(userId) {
  deleteUserData(userId, 'conversation');
  return '会話履歴をクリアしました。新しい会話を始めましょう!';
}

function isUserAllowed(userId) {
  if (ALLOWED_USER_IDS.length === 0) return true;
  return ALLOWED_USER_IDS.includes(userId);
}

function handleMessage(userId, message) {
  const msg = message.trim();

  // デバッグ用:ツール一覧表示
  if (msg === 'ツール' || msg === 'tools' || msg === '/tools') {
    const tools = getToolDefinitions();
    let response = '【利用可能なツール】\n\n';
    tools.forEach((tool, index) => {
      response += `${index + 1}. ${tool.function.name}\n`;
      response += `   ${tool.function.description}\n\n`;
    });
    response += '※AIが自動的に必要なツールを選んで使用します';
    return response;
  }

  if (msg === '会話クリア' || msg === 'リセット' || msg === '/reset') {
    return clearConversationHistory(userId);
  }

  return chatWithAI(userId, message);
}

// ツール定義
function getToolDefinitions() {
  return [
    {
      type: 'function',
      function: {
        name: 'get_current_time',
        description: '現在の日時を取得します',
        parameters: {
          type: 'object',
          properties: {},
          required: []
        }
      }
    },
    {
      type: 'function',
      function: {
        name: 'search_web',
        description: 'Web検索を実行して最新情報を取得します。最新のニュース、天気、技術情報、イベント情報など、リアルタイムの情報が必要な場合に使用してください。',
        parameters: {
          type: 'object',
          properties: {
            query: {
              type: 'string',
              description: '検索クエリ(日本語または英語)'
            }
          },
          required: ['query']
        }
      }
    },
    {
      type: 'function',
      function: {
        name: 'search_transit',
        description: '電車・バスの乗り換え案内を検索します。出発地と目的地を指定して経路を検索できます。',
        parameters: {
          type: 'object',
          properties: {
            from: {
              type: 'string',
              description: '出発地の駅名またはバス停名'
            },
            to: {
              type: 'string',
              description: '目的地の駅名またはバス停名'
            }
          },
          required: ['from', 'to']
        }
      }
    }
  ];
}

// Brave Search APIを使用したWeb検索
function searchWeb(query) {
  if (!BRAVE_API_KEY) {
    return 'Brave Search APIキーが設定されていません。スクリプトプロパティにBRAVE_API_KEYを設定してください。';
  }

  try {
    const url = `${BRAVE_SEARCH_ENDPOINT}?q=${encodeURIComponent(query)}&count=10`;
    const response = UrlFetchApp.fetch(url, {
      method: 'get',
      headers: {
        'Accept': 'application/json',
        'X-Subscription-Token': BRAVE_API_KEY
      },
      muteHttpExceptions: true
    });

    const statusCode = response.getResponseCode();
    if (statusCode !== 200) {
      return `検索APIエラー (${statusCode}): ${response.getContentText()}`;
    }

    const result = JSON.parse(response.getContentText());

    let searchResults = `「${query}」の検索結果:\n\n`;

    // Web検索結果
    if (result.web && result.web.results && result.web.results.length > 0) {
      searchResults += '【検索結果】\n';
      result.web.results.slice(0, 5).forEach((item, index) => {
        searchResults += `${index + 1}. ${item.title}\n`;
        if (item.description) {
          searchResults += `   ${item.description}\n`;
        }
        searchResults += `   ${item.url}\n\n`;
      });
    }

    // ニュース結果(あれば)
    if (result.news && result.news.results && result.news.results.length > 0) {
      searchResults += '【関連ニュース】\n';
      result.news.results.slice(0, 3).forEach((item, index) => {
        searchResults += `${index + 1}. ${item.title}\n`;
        if (item.description) {
          searchResults += `   ${item.description}\n`;
        }
        searchResults += `   ${item.url}\n\n`;
      });
    }

    // 結果がない場合
    if ((!result.web || !result.web.results || result.web.results.length === 0) &&
        (!result.news || !result.news.results || result.news.results.length === 0)) {
      return `「${query}」の検索結果が見つかりませんでした。`;
    }

    return searchResults;

  } catch (error) {
    return `検索中にエラーが発生しました: ${error.toString()}`;
  }
}

// 乗り換え検索(Yahoo!路線情報のURLを生成)
function searchTransit(from, to) {
  try {
    // Yahoo!路線情報の検索URLを生成
    const baseUrl = 'https://transit.yahoo.co.jp/search/result';
    const searchUrl = `${baseUrl}?from=${encodeURIComponent(from)}&to=${encodeURIComponent(to)}`;

    let result = `【乗り換え案内】\n\n`;
    result += `出発: ${from}\n`;
    result += `到着: ${to}\n\n`;
    result += `詳細な経路はこちら:\n${searchUrl}\n\n`;
    result += `※Yahoo!路線情報で最新の運行情報、所要時間、料金を確認できます。`;

    return result;
  } catch (error) {
    return `乗り換え検索中にエラーが発生しました: ${error.toString()}`;
  }
}

// ツール実行
function executeToolCall(toolName, args) {
  switch (toolName) {
    case 'get_current_time':
      const now = new Date();
      return `現在時刻: ${now.getFullYear()}${now.getMonth()+1}${now.getDate()}${now.getHours()}:${String(now.getMinutes()).padStart(2,'0')}:${String(now.getSeconds()).padStart(2,'0')}`;

    case 'search_web':
      return searchWeb(args.query);

    case 'search_transit':
      return searchTransit(args.from, args.to);

    default:
      return 'ツールが見つかりません';
  }
}

function chatWithAI(userId, userMessage) {
  if (!OPENAI_API_KEY) {
    return 'AI機能を使用するにはOPENAI_API_KEYを設定してください。';
  }

  try {
    const history = getConversationHistory(userId);

    const messages = [
      {
        role: 'system',
        content: `あなたは高度な推論能力を持つAIエージェントです。以下のツールを積極的に活用してください:

【必ず使用すべきツール】
1. get_current_time - 時刻や日付に関する質問には必ずこのツールを使用
2. search_web - 最新情報、知識、事実確認が必要な場合は必ず使用
3. search_transit - 乗り換えや経路案内の質問には必ず使用

【重要】
- 時刻を聞かれたら、必ずget_current_timeツールを呼び出してください
- 検索が必要な質問には、必ずsearch_webツールを使用してください
- 駅名や場所が2つ出てきたら、search_transitツールを使用してください
- ツールなしで答えられない質問には、必ず適切なツールを使用してください`
      }
    ];

    history.forEach(msg => {
      messages.push({
        role: msg.role,
        content: String(msg.content)
      });
    });

    messages.push({
      role: 'user',
      content: userMessage
    });

    const tools = getToolDefinitions();

    // 最大3回までツール呼び出しループ
    let loopCount = 0;
    const maxLoops = 3;

    // 使用したツールを記録
    const usedTools = [];

    while (loopCount < maxLoops) {
      loopCount++;

      const response = UrlFetchApp.fetch(OPENAI_ENDPOINT, {
        method: 'post',
        headers: {
          'Content-Type': 'application/json',
          'Authorization': `Bearer ${OPENAI_API_KEY}`
        },
        payload: JSON.stringify({
          model: 'gpt-5.2',
          messages: messages,
          tools: tools,
          reasoning_effort: 'medium'
        }),
        muteHttpExceptions: true
      });

      const result = JSON.parse(response.getContentText());

      if (result.error) {
        return 'AI APIでエラーが発生しました:\n' + result.error.message;
      }

      const choice = result.choices[0];
      const assistantMessage = choice.message;

      messages.push(assistantMessage);

      // ツール呼び出しがない場合は終了
      if (choice.finish_reason !== 'tool_calls' || !assistantMessage.tool_calls) {
        let aiReply = assistantMessage.content;

        // 使用したツールを明記
        if (usedTools.length > 0) {
          aiReply += `\n\n---\n🔧 使用したツール: ${usedTools.join(', ')}`;
        }

        history.push({
          role: 'user',
          content: userMessage
        });
        history.push({
          role: 'assistant',
          content: aiReply
        });

        saveConversationHistory(userId, history);
        return aiReply;
      }

      // ツール呼び出しを実行(3パターン)
      assistantMessage.tool_calls.forEach(toolCall => {
        const toolName = toolCall.function.name;
        const toolArgs = JSON.parse(toolCall.function.arguments);
        const toolResult = executeToolCall(toolName, toolArgs);

        // 使用したツールを記録その後返信に記載する(コード的には上)
        if (!usedTools.includes(toolName)) {
          usedTools.push(toolName);
        }

        messages.push({
          role: 'tool',
          tool_call_id: toolCall.id,
          content: toolResult
        });
      });
    }

    // ループ上限に達した場合に送信
    return 'エージェントの処理が複雑すぎます。質問を簡単にしてもう一度お試しください。';

  } catch (error) {
    return 'エラーが発生しました。しばらくしてからもう一度お試しください。\n\nエラー: ' + error.toString();
  }
}

function replyToUser(replyToken, message) {
  UrlFetchApp.fetch(LINE_ENDPOINT, {
    method: 'post',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${CHANNEL_ACCESS_TOKEN}`
    },
    payload: JSON.stringify({
      replyToken: replyToken,
      messages: [{ type: 'text', text: String(message) }]
    }),
    muteHttpExceptions: true
  });
}

作成手順

LINE Developers

  1. LINE Developers で Botアカウントを作る
    上記リンクにアクセスしビジネスアカウントを作成
  2. 新規プロバイダー作成
  3. 新規チャネル作成(Messaging API)
  4. チャネルができたら「Messaging API設定」を開きWebhookの利用をオンにする
  5. チャネルアクセストークンを確認する
  6. LINE Official Account Managerを開き応答設定からあいさつメッセージをオフにする

Google Apps Script

  1. 新しいプロジェクトを作成

  2. GAS にコードを貼り付ける
    下記のホワイトリストを先にやるとスムーズです。めんどくさい場合はコードのホワイトリスト部分を削除して貼り付けてください

  3. スクリプトプロパティに API Key を設定(各API Key取得法は割愛)

    スクリプトプロパティ Key スクリプトプロパティ Value
    CHANNEL_ACCESS_TOKEN LINEの長期アクセストークン
    OPENAI_API_KEY OpenAIのAPIキー
    BRAVE_API_KEY Brave Search API
  4. メニュー「デプロイ」

    新しいデプロイ → 種類 → Webアプリ
    実行ユーザー → 自分
    【重要】アクセス権 → 全員
    スクリーンショット 2025-12-03 001713.png

  5. デプロイ時のURLをLINEのWebhookに貼り付け
    LINE Developers(Messaging API設定)へ戻りWebhook URL に GAS の WebアプリURLをペーストし「Webhookを有効化」をオンにする
    スクリーンショット 2025-12-03 001729.png

スクリーンショット 2025-12-03 001755.png

注意
デプロイしなおす場合LINE Developers(Messaging API設定)へ戻りWebhook URL に GAS の WebアプリURLをペーストし更新ボタンを押す操作をその都度やる必要があります。

ホワイトリスト

今回生成AIのAPIを不正に叩かれないようにするためホワイトリストをで制限をしています。そのため上記のBOTコードのホワイトリストの設定を行わないと返答がされません。

詳しくは↓

最後に

今回紹介した GPT-5.2 自律エージェント LINE Bot は、これまでの「ただ返すだけのBot」とはまったく違うAIエージェントを活用したBOTであり簡単に考えて動くAIアシスタントを LINE 上だけで実現できます。
この記事が「AIで何か作ってみたい!」という人の背中を押し、もっと自由にAIを活用するきっかけになれば嬉しいです。読んでいただきありがとうございました!

参考資料

3
0
1

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
3
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?