Bedrock AgentCore で作った居酒屋検索AIエージェント「シンバシくん」をLINE公式アカウントに登録し Messaging API でチャットボットにしてみた

Bedrock AgentCore で作った居酒屋検索AIエージェント「シンバシくん」をLINE公式アカウントに登録し Messaging API でチャットボットにしてみた

2026.01.18

製造ビジネステクノロジー部の小林です。

前回の記事では、Amazon Bedrock AgentCore を使って新橋専門居酒屋検索 AI エージェント「シンバシくん」を作りました。
https://dev.classmethod.jp/articles/shoma-struggling-with-after-party-venue-search-built-shimbashi-izakaya-search-ai-agent-shimbashi-kun-with-bedrock-agentcore/

前回はターミナルから API を叩いて会話する形でしたが、今回は「シンバシくん」を LINE Bot 化して、スマホから気軽に居酒屋を検索できるようにしていきます。

完成したもの

LINE で「美味しいホルモンが食べたい」と送ると...
スクリーンショット 2026-01-18 14.15.51
スクリーンショット 2026-01-18 16.21.54

新橋エリアに特化した居酒屋検索 AI エージェント「シンバシくん」が、条件に合うお店をサクッと探しておすすめを返してくれます。

さらに、検索だけでなく“会話”も可能です。ちょっとした相談ベースで条件を詰めたり、追加要望を投げたりできます。
スクリーンショット 2026-01-18 14.17.46

アーキテクチャ

今回の全体構成は次のとおりです。
スクリーンショット 2026-01-18 15.14.18

作ってみる

LINE Developers の準備

LINE 公式アカウントで Messaging API を利用するため、まずは LINE Developers 側の登録・初期設定を行います。

アカウント作成

LINE Developers にアクセスします。
https://developers.line.biz/ja/

LINE アカウントでログインします。
スクリーンショット 2026-01-18 12.29.05

続いて ビジネス ID を作成します。
スクリーンショット 2026-01-18 15.19.06

開発者情報を登録します。
スクリーンショット 2026-01-18 15.20.16

登録が完了すると、LINE Developers のコンソールにアクセスできるようになります。
スクリーンショット 2026-01-18 12.30.24

プロバイダー作成

まずは Bot を紐づけるための プロバイダーを作成します。
コンソール上で「プロバイダーを作成」をクリックします。
スクリーンショット 2026-01-18 12.31.48

プロバイダー名を入力します(例:shinbashi-kun)。
スクリーンショット 2026-01-18 15.32.57

作成が完了すると、作成したプロバイダーが一覧に表示されます。
スクリーンショット 2026-01-18 12.32.21

Messaging API チャネル作成

続いて、Messaging API の チャネルを作成します。
チャネル作成の画面で「Messaging API」を選択します。
スクリーンショット 2026-01-18 12.47.08

チャネル情報を入力して登録します。登録が完了すると Channel ID / Channel Secret が表示されます。
これらは後ほど使うので、必ず控えておきましょう。
なお、この時点では **Webhook URL は未設定(空欄)**のままで問題ありません。
スクリーンショット 2026-01-18 15.33.54

Lambda 関数の作成

次に、LINE から「シンバシくん」を呼び出すための Lambda 関数を AWS コンソールから作成します。

  • 関数名:shinbashi-kun-line-bot
  • ランタイム:Python 3.14
  • アーキテクチャ:x86_64

スクリーンショット 2026-01-18 15.42.49

このあとの手順で、作成した Lambda を Webhook の受け口として設定し、Messaging API と接続していきます。

環境変数の設定

先ほど作成した Lambda 関数に環境変数を設定していきます。
その前準備として、まずは チャネルアクセストークンを発行します。

LINE Developers のコンソールで 「Messaging API 設定」タブを開きます。
画面下部にある 「チャネルアクセストークン」→「発行」をクリックします。
スクリーンショット 2026-01-18 12.52.51

チャネルアクセストークンが発行されます。
スクリーンショット 2026-01-18 15.53.29

準備ができたら、Lambda 関数の環境変数に以下の値を登録します。

  • キー値
    • LINE_CHANNEL_SECRET:チャネルシークレット
    • LINE_CHANNEL_ACCESS_TOKEN:チャネルアクセストークン
    • AGENTCORE_RUNTIME_ARN:AgentCore の ARN

スクリーンショット 2026-01-18 15.55.06

タイムアウトの設定

AgentCore の呼び出しは状況によって時間がかかることがあるため、念のため Lambda のタイムアウトを 60 秒に設定しておきます。
スクリーンショット 2026-01-18 15.57.13

コードの実装

Lambda 関数のコードを実装します。

"""
LINE Bot Webhook Handler for シンバシくん
AWS Lambda + Bedrock AgentCore
"""
import json
import os
import hashlib
import hmac
import base64
import urllib.request
import boto3

# 環境変数
LINE_CHANNEL_SECRET = os.environ.get('LINE_CHANNEL_SECRET')
LINE_CHANNEL_ACCESS_TOKEN = os.environ.get('LINE_CHANNEL_ACCESS_TOKEN')
AGENTCORE_RUNTIME_ARN = os.environ.get('AGENTCORE_RUNTIME_ARN')

# AgentCore クライアント(Lambda起動時に1回だけ初期化)
agentcore_client = boto3.client('bedrock-agentcore', region_name='ap-northeast-1')

def lambda_handler(event, context):
    """
    LINE Webhook エントリーポイント

    LINEプラットフォームからのWebhookリクエストを受け取り、
    メッセージイベントを処理してAgentCoreに問い合わせる
    """
    body = event.get('body', '')
    signature = event.get('headers', {}).get('x-line-signature', '')

    # LINE署名検証(なりすまし防止)
    if not _verify_signature(body, signature):
        return {'statusCode': 403, 'body': 'Invalid signature'}

    # Webhookイベントを処理
    for evt in json.loads(body).get('events', []):
        if evt.get('type') == 'message' and evt['message'].get('type') == 'text':
            _handle_text_message(evt)

    return {'statusCode': 200, 'body': 'OK'}

def _verify_signature(body: str, signature: str) -> bool:
    """
    LINE署名を検証

    LINEプラットフォームからのリクエストであることを確認する
    https://developers.line.biz/ja/docs/messaging-api/receiving-messages/#verifying-signatures
    """
    if not LINE_CHANNEL_SECRET or not signature:
        return False

    expected = base64.b64encode(
        hmac.new(
            LINE_CHANNEL_SECRET.encode('utf-8'),
            body.encode('utf-8'),
            hashlib.sha256
        ).digest()
    ).decode('utf-8')

    return hmac.compare_digest(signature, expected)

def _handle_text_message(event: dict) -> None:
    """テキストメッセージを処理してAgentCoreに問い合わせ"""
    reply_token = event['replyToken']
    user_message = event['message']['text']
    user_id = event['source'].get('userId', 'anonymous')

    # セッションID(AgentCoreは33文字以上必要)
    session_id = f"line-{user_id}-0000000000000000"

    try:
        reply_text = _invoke_agentcore(user_message, session_id)
    except Exception as e:
        print(f"AgentCore error: {e}")
        reply_text = "すみません、エラーが発生しました。もう一度お試しください🙇"

    _reply_to_line(reply_token, reply_text)

def _invoke_agentcore(message: str, session_id: str) -> str:
    """
    AgentCore Runtime を呼び出し

    シンバシくん(居酒屋検索エージェント)にメッセージを送信し、
    検索結果を取得する
    """
    response = agentcore_client.invoke_agent_runtime(
        agentRuntimeArn=AGENTCORE_RUNTIME_ARN,
        runtimeSessionId=session_id,
        payload=json.dumps({"prompt": message}).encode('utf-8'),
        contentType='application/json',
        accept='application/json'
    )

    # StreamingBodyからレスポンスを読み取り
    streaming_body = response.get('response')
    if not streaming_body:
        return "検索結果が見つかりませんでした。"

    result = json.loads(streaming_body.read().decode('utf-8'))
    return result.get('result', '検索結果が見つかりませんでした。')

def _reply_to_line(reply_token: str, text: str) -> None:
    """
    LINEに返信

    Reply APIを使用してユーザーにメッセージを送信する
    https://developers.line.biz/ja/docs/messaging-api/sending-messages/#reply-messages
    """
    # LINEの文字数制限(5000文字)
    if len(text) > 5000:
        text = text[:4997] + "..."

    req = urllib.request.Request(
        'https://api.line.me/v2/bot/message/reply',
        data=json.dumps({
            'replyToken': reply_token,
            'messages': [{'type': 'text', 'text': text}]
        }).encode('utf-8'),
        headers={
            'Content-Type': 'application/json',
            'Authorization': f'Bearer {LINE_CHANNEL_ACCESS_TOKEN}'
        },
        method='POST'
    )

    try:
        with urllib.request.urlopen(req) as res:
            print(f"LINE reply: {res.status}")
    except urllib.error.HTTPError as e:
        print(f"LINE reply failed: {e.code} {e.reason}")

処理の全体像

今回の Lambda は、ざっくり次の流れで動きます。

lambda_handler()        # エントリーポイント
  ├── _verify_signature()   # LINE署名検証
  ├── _handle_text_message() # メッセージ処理
  │     ├── _invoke_agentcore()  # AgentCore呼び出し
  │     └── _reply_to_line()     # LINE返信

グローバル変数

agentcore_client = boto3.client('bedrock-agentcore', region_name='ap-northeast-1')

boto3 クライアントはグローバルで初期化しています。Lambda は実行環境(コンテナ)が再利用されるため、関数内で毎回初期化するのではなく、コールドスタート時に一度だけ作って使い回すようにしています。

lambda_handler(エントリーポイント)

pythondef lambda_handler(event, context):

Lambda のエントリーポイントです。ここでは主に以下の処理を行います。

  • LINE 署名の検証(なりすまし防止)
  • テキストメッセージイベントのみを処理対象にする
  • 常に 200 OK を返す(LINE プラットフォーム要件に合わせる)

参考:Messaging API のリクエスト仕様
https://developers.line.biz/en/reference/messaging-api/nojs/?utm_source=openai#request-body

verify_signature(署名検証)

def _verify_signature(body: str, signature: str) -> bool:

LINE プラットフォームからの正当なリクエストであることを確認します。

  • チャネルシークレットとリクエストボディから HMAC-SHA256 ハッシュを計算
  • x-line-signature ヘッダーの値と比較して一致するか確認

これにより、第三者によるなりすましリクエストを防ぎます。

Lambda 関数 URL の設定

今回は API Gateway は使わず、Lambda 関数 URL を利用して Lambda に直接アクセスできる HTTPS エンドポイントを作成します。
スクリーンショット 2026-01-18 15.59.12

作成後に発行される URL を控えておきます(このあと LINE の Webhook に設定します)。

IAM 権限の追加

Lambda から AgentCore を呼び出すために、Lambda が利用している IAM ロールに権限を追加します。今回は対象ロールに インラインポリシーを追加しました。
スクリーンショット 2026-01-18 16.07.27

LINE 側の Webhook 設定

続いて、LINE Developers 側で Webhook を設定します。
Messaging API 設定を開き、Webhook URL に先ほど控えた Lambda 関数 URL を入力します。
スクリーンショット 2026-01-18 16.04.00

「Webhook の利用」をオンにします。
スクリーンショット 2026-01-18 16.06.18

これで、LINE にメッセージが送られたタイミングで、LINE プラットフォームから Lambda 関数 URL に Webhook が POST されるようになります。

LINE プロフィール画像の作成

LINE を開いてみると、シンバシくんが友だちに追加されています。せっかくなので、プロフィール画像もそれっぽく用意してみます。今回は Google の Nano Banana を使って、シンバシくんのアイコン画像を生成してみました。
スクリーンショット 2026-01-18 16.12.58
スクリーンショット 2026-01-18 16.13.19
スクリーンショット 2026-01-18 16.13.29
スクリーンショット 2026-01-18 16.14.46
新橋っぽさもあって、いい感じのシンバシくんができました。

動作確認

それでは、実際に LINE からシンバシくんを呼び出して動かしてみます。
まずは LINE を開き、シンバシくんとのトーク画面を表示します。
スクリーンショット 2026-01-18 16.18.25

今日はホルモン気分なので、「美味しいホルモンが食べたい。」と送ってみます。
スクリーンショット 2026-01-18 16.20.21

すると、シンバシくんが新橋エリアの候補を検索して、おすすめのホルモン屋さんを提案してくれました。
スクリーンショット 2026-01-18 16.21.28
スクリーンショット 2026-01-18 16.21.54

「炭火焼ホルモン ぐう」が気になりますね。
スクリーンショット 2026-01-18 16.22.57
前回の会話の流れも引き継げているようなので、このままもう少しやり取りしてみます。

スクリーンショット 2026-01-18 16.24.24
スクリーンショット 2026-01-18 16.25.00
スクリーンショット 2026-01-18 16.25.56
混み具合の確認や、予約に向けた段取りまで手伝ってくれるのは助かります。なお、予約機能は実装されていません。画面に表示される予約 ID はシミュレーション用です。今後の拡張を検討しています!

料金について

日本の料金プラン例では、無料(コミュニケーションプラン)の無料メッセージ通数は月 200 通までです。最新の料金体系は公式ページをご参照ください。
https://developers.line.biz/ja/docs/messaging-api/pricing/

おわりに

今回は LINE の Messaging API を使って、「シンバシくん」を LINE Bot 化し、スマホからサクッと新橋の居酒屋検索ができるようにしました。

ターミナルから API を叩いて会話していた前回と比べて、普段使いしやすい形に一気に近づいたと思います。

次回は、認証機能の強化と 自動予約機能の実装に挑戦する予定です。
たとえばグループ LINE で「新橋 焼き鳥 4 人」と送るだけで、おすすめ店舗が返ってきて、そのまま予約まで完了する ── そんな体験が作れたら嬉しいですね。

おまけ

前回の記事でも触れましたが、現時点では AgentCore の Memory 機能を未実装のため、会話の文脈は インメモリの一時データとして保持しています。

そのため、Lambda の実行環境が新しくなると、インメモリに保持していた会話履歴が失われるため、新しいセッションでは前の会話文脈が利用できなくなります。実際、会話のログからもその様子が確認できます。
スクリーンショット 2026-01-18 16.58.29
次回、余裕があれば Memory 機能も組み込んで、より自然に会話を引き継げるようにしたいですね。

この記事をシェアする

FacebookHatena blogX

関連記事