
Bedrock AgentCore で作った居酒屋検索AIエージェント「シンバシくん」をLINE公式アカウントに登録し Messaging API でチャットボットにしてみた
製造ビジネステクノロジー部の小林です。
前回の記事では、Amazon Bedrock AgentCore を使って新橋専門居酒屋検索 AI エージェント「シンバシくん」を作りました。
前回はターミナルから API を叩いて会話する形でしたが、今回は「シンバシくん」を LINE Bot 化して、スマホから気軽に居酒屋を検索できるようにしていきます。
完成したもの
LINE で「美味しいホルモンが食べたい」と送ると...


新橋エリアに特化した居酒屋検索 AI エージェント「シンバシくん」が、条件に合うお店をサクッと探しておすすめを返してくれます。
さらに、検索だけでなく“会話”も可能です。ちょっとした相談ベースで条件を詰めたり、追加要望を投げたりできます。

アーキテクチャ
今回の全体構成は次のとおりです。

作ってみる
LINE Developers の準備
LINE 公式アカウントで Messaging API を利用するため、まずは LINE Developers 側の登録・初期設定を行います。
アカウント作成
LINE Developers にアクセスします。
LINE アカウントでログインします。

続いて ビジネス ID を作成します。

開発者情報を登録します。

登録が完了すると、LINE Developers のコンソールにアクセスできるようになります。

プロバイダー作成
まずは Bot を紐づけるための プロバイダーを作成します。
コンソール上で「プロバイダーを作成」をクリックします。

プロバイダー名を入力します(例:shinbashi-kun)。

作成が完了すると、作成したプロバイダーが一覧に表示されます。

Messaging API チャネル作成
続いて、Messaging API の チャネルを作成します。
チャネル作成の画面で「Messaging API」を選択します。

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

Lambda 関数の作成
次に、LINE から「シンバシくん」を呼び出すための Lambda 関数を AWS コンソールから作成します。
- 関数名:shinbashi-kun-line-bot
- ランタイム:Python 3.14
- アーキテクチャ:x86_64

このあとの手順で、作成した Lambda を Webhook の受け口として設定し、Messaging API と接続していきます。
環境変数の設定
先ほど作成した Lambda 関数に環境変数を設定していきます。
その前準備として、まずは チャネルアクセストークンを発行します。
LINE Developers のコンソールで 「Messaging API 設定」タブを開きます。
画面下部にある 「チャネルアクセストークン」→「発行」をクリックします。

チャネルアクセストークンが発行されます。

準備ができたら、Lambda 関数の環境変数に以下の値を登録します。
- キー値
- LINE_CHANNEL_SECRET:チャネルシークレット
- LINE_CHANNEL_ACCESS_TOKEN:チャネルアクセストークン
- AGENTCORE_RUNTIME_ARN:AgentCore の ARN

タイムアウトの設定
AgentCore の呼び出しは状況によって時間がかかることがあるため、念のため Lambda のタイムアウトを 60 秒に設定しておきます。

コードの実装
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 のリクエスト仕様
verify_signature(署名検証)
def _verify_signature(body: str, signature: str) -> bool:
LINE プラットフォームからの正当なリクエストであることを確認します。
- チャネルシークレットとリクエストボディから HMAC-SHA256 ハッシュを計算
- x-line-signature ヘッダーの値と比較して一致するか確認
これにより、第三者によるなりすましリクエストを防ぎます。
Lambda 関数 URL の設定
今回は API Gateway は使わず、Lambda 関数 URL を利用して Lambda に直接アクセスできる HTTPS エンドポイントを作成します。

作成後に発行される URL を控えておきます(このあと LINE の Webhook に設定します)。
IAM 権限の追加
Lambda から AgentCore を呼び出すために、Lambda が利用している IAM ロールに権限を追加します。今回は対象ロールに インラインポリシーを追加しました。

LINE 側の Webhook 設定
続いて、LINE Developers 側で Webhook を設定します。
Messaging API 設定を開き、Webhook URL に先ほど控えた Lambda 関数 URL を入力します。

「Webhook の利用」をオンにします。

これで、LINE にメッセージが送られたタイミングで、LINE プラットフォームから Lambda 関数 URL に Webhook が POST されるようになります。
LINE プロフィール画像の作成
LINE を開いてみると、シンバシくんが友だちに追加されています。せっかくなので、プロフィール画像もそれっぽく用意してみます。今回は Google の Nano Banana を使って、シンバシくんのアイコン画像を生成してみました。




新橋っぽさもあって、いい感じのシンバシくんができました。
動作確認
それでは、実際に LINE からシンバシくんを呼び出して動かしてみます。
まずは LINE を開き、シンバシくんとのトーク画面を表示します。

今日はホルモン気分なので、「美味しいホルモンが食べたい。」と送ってみます。

すると、シンバシくんが新橋エリアの候補を検索して、おすすめのホルモン屋さんを提案してくれました。


「炭火焼ホルモン ぐう」が気になりますね。

前回の会話の流れも引き継げているようなので、このままもう少しやり取りしてみます。



混み具合の確認や、予約に向けた段取りまで手伝ってくれるのは助かります。なお、予約機能は実装されていません。画面に表示される予約 ID はシミュレーション用です。今後の拡張を検討しています!
料金について
日本の料金プラン例では、無料(コミュニケーションプラン)の無料メッセージ通数は月 200 通までです。最新の料金体系は公式ページをご参照ください。
おわりに
今回は LINE の Messaging API を使って、「シンバシくん」を LINE Bot 化し、スマホからサクッと新橋の居酒屋検索ができるようにしました。
ターミナルから API を叩いて会話していた前回と比べて、普段使いしやすい形に一気に近づいたと思います。
次回は、認証機能の強化と 自動予約機能の実装に挑戦する予定です。
たとえばグループ LINE で「新橋 焼き鳥 4 人」と送るだけで、おすすめ店舗が返ってきて、そのまま予約まで完了する ── そんな体験が作れたら嬉しいですね。
おまけ
前回の記事でも触れましたが、現時点では AgentCore の Memory 機能を未実装のため、会話の文脈は インメモリの一時データとして保持しています。
そのため、Lambda の実行環境が新しくなると、インメモリに保持していた会話履歴が失われるため、新しいセッションでは前の会話文脈が利用できなくなります。実際、会話のログからもその様子が確認できます。

次回、余裕があれば Memory 機能も組み込んで、より自然に会話を引き継げるようにしたいですね。








