はじめに
「弟が脳梗塞で倒れた。」
あまりにも非日常なLINEが突然来ました。
ちょうどAWSSummitで基調講演を聴いているところでした。
何ができるか分からないもやもゆ
そこからの24時間、ありとあらゆる情報が飛んできました。
- 先生が言っていたこと
- 次に聞くべきこと
- 家はどうするのか
- 手術予定
- リハビリ計画
- 関係各所への連絡
本人以上にショックで疲弊していく両親が見ていられなく、なにかできないかなと。
そんな思いで家族用のLINE Botを作りました。
名前は仮に Family Care Note Bot / 家族ケアメモBot としています。
目的はシンプルで、家族がLINEに送るだけで、メモ・ToDo・質問整理が残るBot です。
医療判断はしません。
あくまで家族内の情報整理に徹するBotです。
想定する読者層
- AWSとLINEを組み合わせて非エンジニアでも使いやすいLine botを作りたい
- 日曜エンジニアリングで個人開発してみたい初心者の方
- 家族が病気でも出来ることが少なく、何か役に立ちたいと思うすべてのご家族
作ったもの
LINEでBotにメッセージを送ると、メモとしてDynamoDBに保存されます。
また、リッチメニューから以下の操作ができます。
- メモを見る
- ToDoを見る
- ToDoを追加する
- ToDoを完了する
- AIに聞く
- 使い方を見る
最終的には、以下のようなUXになりました。
普通にLINEへ送る
→ メモとして保存
リッチメニューの「メモ見る」
→ 直近メモを表示
リッチメニューの「ToDo追加」
→ 次の1通だけToDoとして保存
リッチメニューの「ToDo見る」
→ 未完了ToDoを表示
→ Quick Replyで完了できる
毎日9:00 / 21:00
→ 今日の未完了ToDoをPush通知
リッチメニューの「AIに聞く」
→ 次の1通だけOpenAI APIへ送る
→ 医師に聞くことや文章化を手伝う
技術スタック
今回使ったものは以下です。
LINE Messaging API
LINE Rich Menu
LINE Reply API
LINE Push API
AWS Lambda
AWS Lambda Function URL
Amazon DynamoDB
Amazon EventBridge Scheduler
Amazon CloudWatch Logs
OpenAI API
Node.js
Lambdaのリージョンは東京リージョンです。
ap-northeast-1
全体アーキテクチャ
全体構成はこうです。
Webhook用LambdaとReminder用Lambdaは分けました。
Webhook Lambda
LINEからのWebhookを受けるメインLambdaです。
役割は以下です。
- LINE Webhookを受け取る
- LINE署名検証をする
- 通常メッセージをメモ保存する
-
/latestで直近メモを見る -
/todoでToDo追加モードにする -
/todosで未完了ToDoを見る - Quick ReplyでToDo完了する
-
/done 番号でToDo完了する -
/cancelで入力モードを解除する -
/askでAI質問モードにする - AI質問モード中の次の1通をOpenAI APIへ投げる
Reminder Lambda
EventBridge Schedulerから定時起動されるLambdaです。
役割は以下です。
- 毎日9:00 / 21:00に起動
- DynamoDBから未完了ToDoを取得
-
whenTextに「今日」が含まれるToDoを抽出 - LINE Push APIで通知
Webhookとは関係ないので、Reminder LambdaにはFunction URLは不要です。
Lambda Function URLについて
LINE Messaging APIからAWSへWebhookを送るために、今回はAPI Gatewayではなく Lambda Function URL を使いました。
Function URLの認証タイプは NONE です。
LINEからAWS IAM署名付きリクエストを送るわけではないためです。
ただし、URL自体は公開URLになるため、Lambda内部でLINEの署名検証をします。
LINE
→ Lambda Function URL
→ Lambda内で x-line-signature を検証
DynamoDB設計
DynamoDBは3テーブル構成です。
FamilyCareMemos
普通のLINEメッセージを保存するテーブルです。
Table: FamilyCareMemos
Partition Key: roomId
Sort Key: createdAt
主な属性は以下です。
roomId
createdAt
userId
userName
text
roomId には、LINEの groupId / roomId / userId のいずれかを入れます。
個別チャットでもグループでも動かせるようにしています。
FamilyCareTodos
ToDoを保存するテーブルです。
Table: FamilyCareTodos
Partition Key: roomId
Sort Key: todoId
主な属性は以下です。
roomId
todoId
task
whenText
assigneeName
assigneeUserId
isDone
createdAt
createdByUserId
createdByName
completedAt
completedByUserId
completedByName
whenText はあえて自由入力にしました。
例えば以下のような値を許容します。
今日18時
明日午前
病院行く前
次の面会まで
先生が来たら
厳密な期限管理よりも、家族が自然に入力できることを優先しました。
将来的には dueDate を追加して、日付正規化を入れる予定です。
FamilyCareSessions
一時的な入力モードを保存するテーブルです。
Table: FamilyCareSessions
Partition Key: roomId
Sort Key: userId
主な属性は以下です。
roomId
userId
mode
updatedAt
expiresAt
expiresAtEpoch
現在使っているmodeは以下です。
add_todo
ask_ai
add_todo は、リッチメニューの「ToDo追加」を押した後、次の1通だけToDoとして解釈するために使います。
ask_ai は、リッチメニューの「AIに聞く」を押した後、次の1通だけOpenAI APIへ送るために使います。
重要なのは、セッションを グループ単位ではなくユーザー単位 で持つことです。
これにより、母がToDo追加モードに入っている間に、他の家族が普通のメモを送っても誤ってToDo化されません。
リッチメニュー
最終的にリッチメニューは6分割にしました。
各ボタンのアクションは以下です。
メモ見る → /latest
ToDo見る → /todos
ToDo追加 → /todo
完了する → /todos
AIに聞く → /ask
使い方 → /help
「完了する」は /done ではなく /todos にしています。
理由は、完了対象をユーザーに番号で打たせるよりも、ToDo一覧を出してQuick Replyから選ぶ方が迷わないためです。
ToDo追加のUX改善
最初は以下のように入力していました。
/todo 兄 今日18時 リハビリの時間を確認する
ただ、家族に使ってもらうなら /todo を毎回打つのは面倒です。
そこで、リッチメニューの「ToDo追加」を押したら add_todo モードに入り、次の1通だけToDoとして扱うようにしました。
流れはこうです。
リッチメニュー「ToDo追加」
→ /todo が送られる
→ FamilyCareSessions に mode = add_todo を保存
→ Bot「次に だれが いつ なにを の形で送ってね」
→ ユーザー「兄 今日18時 リハビリの時間を確認する」
→ ToDoとして保存
→ modeを削除
これで、ユーザーはコマンドを覚えなくても使えます。
ToDo完了のUX改善
最初はToDo完了に以下のコマンドが必要でした。
/done 1
自分は問題なく使えますが、家族には少し面倒です。
そこで /todos の返信にQuick Replyを付けました。
未完了のToDoだよ。
1. リハビリの時間を確認する
いつ:今日18時
だれ:兄
2. 先生に聞くことをまとめる
いつ:明日午前
だれ:母
Quick Replyには以下を出します。
1を完了 → /done 1
2を完了 → /done 2
既存の /done 番号 ロジックをそのまま使えるので、実装コストが低く、UXはかなり改善します。
将来的にはFlex Message + Postbackで、ToDoごとに「完了する」ボタンを出す構成にしたいです。
Reminder Lambda
ToDoは作っただけだと忘れます。
そこで、Reminder Lambdaを作りました。
構成は以下です。
EventBridge Scheduler
→ Reminder Lambda
→ DynamoDB FamilyCareTodos
→ LINE Push API
→ 家族LINE
通知時刻は以下です。
毎日 9:00 JST
毎日 21:00 JST
Reminder Lambdaでは、まずはシンプルに以下の条件で通知対象を抽出しています。
isDone = false
かつ
whenText に「今日」が含まれる
dueDate がある場合は dueDate === 今日 も対象にできます。
通知文は以下のような形です。
今日の未完了ToDoだよ。
1. リハビリの時間を確認する
いつ:今日18時
だれ:兄
2. 先生に聞くことをまとめる
いつ:今日午前
だれ:母
終わったら、メニューの「ToDo見る」から完了してね。
AIに聞く機能
今回、OpenAI APIも組み込みました。
ただし、医療判断はさせません。
AIの役割は以下に限定しています。
- 医師に聞く質問を整理する
- 家族が伝えたいことを文章化する
- 説明内容を短く整理する
- ToDo化する
- 不安を煽らない
- 断定しない
やらないことは以下です。
- 診断
- 薬の判断
- 食事可否の判断
- 嚥下可否の判断
- リハビリ可否の判断
- 緊急度判定
- 予後の断定
- 治療方針の決定
最初の実装
最初は以下の一発コマンド方式で実装しました。
/ask 気管切開のあと先生に何を聞けばいい?
ただ、これは毎回 /ask を打つ必要があり、家族向けUXとしては弱いです。
そこで、セッション方式に変えました。
セッション方式
現在の流れはこうです。
リッチメニュー「AIに聞く」
→ /ask が送られる
→ FamilyCareSessions に mode = ask_ai を保存
→ Bot「次に聞きたいことをそのまま送ってね」
→ ユーザー「気管切開のあと先生に何を聞けばいい?」
→ OpenAI APIへ送信
→ 回答をLINEに返信
→ modeを削除
これで、ユーザーは /ask を手打ちする必要がありません。
AI用プロンプト
AIには以下のような制約を入れています。
あなたは家族ケアメモBotの情報整理アシスタントです。
医療判断は行わず、家族が医師・看護師・ST・PT・OTへ確認すべき質問の整理、説明内容の要約、ToDo化、文章化のみを行ってください。
禁止事項:
- 診断しない
- 治療方針を決めない
- 薬の判断をしない
- 食事可否を判断しない
- 嚥下可否を判断しない
- リハビリ可否を判断しない
- 緊急度を判定しない
- 予後を断定しない
- 不安を煽らない
必要に応じて「医師・看護師・リハビリ担当に確認してください」と添えてください。
回答は短く、LINEで読みやすい日本語にしてください。
このBotでは、AIを「答えを出すもの」ではなく、「質問を整理するもの」として使っています。
実装中に詰まったポイント
1. LINE Channel Access TokenにBearerを付けない
環境変数には Bearer を付けません。
LINE_CHANNEL_ACCESS_TOKEN=xxxxxxxx
コード側でこうします。
Authorization: `Bearer ${channelAccessToken}`
ここを間違えるとLINE Reply APIが401になります。
2. Channel SecretとAccess Tokenを間違えない
LINEには似たような値が複数あります。
- Channel Secret
- Channel Access Token
Reply APIに必要なのはChannel Access Tokenです。
署名検証に使うのがChannel Secretです。
3. Function URLはNONEでよいが署名検証は必要
LINEからLambda Function URLへ直接Webhookする場合、Function URLの認証タイプは NONE にしました。
ただし、公開URLになるので、Lambda内部で x-line-signature を検証する方針にしました。
4. Reply APIとPush APIは用途が違う
Webhookへの返信はReply APIです。
LINE Webhook
→ Lambda
→ Reply API
一方、定時リマインダーはWebhookのreplyTokenがないのでPush APIを使います。
EventBridge
→ Reminder Lambda
→ Push API
5. Reminder Lambdaには通知先IDが必要
Push APIで送るには to が必要です。
つまり、LINEの groupId / roomId / userId をどこかで把握しておく必要があります。
今回はMVPなので、DynamoDBやCloudWatch Logsで確認したIDを TARGET_ROOM_ID として環境変数に入れました。
将来的には /registerroom のようなコマンドで保存する方がよさそうです。
6. whenTextは人間には分かるがLambdaには分かりづらい
今日18時 や 明日午前 は人間には自然ですが、Lambdaで日付判定するには曖昧です。
今回は家族がゆるく使えることを優先し、まずは whenText に「今日」が含まれるかだけでリマインダー対象を判定しました。
将来的には dueDate を追加して正規化したいです。
7. /ask単体ではOpenAI APIは呼ばれない
最初、リッチメニューの「AIに聞く」を押してもOpenAI APIのダッシュボードに使用量が出ませんでした。
原因は、/ask 単体では案内だけを返していて、OpenAI APIを呼んでいなかったためです。
解決策として、/ask 単体を「AIに聞くモード開始」に変更しました。
/ask
→ FamilyCareSessions に mode = ask_ai を保存
→ 次の1通をOpenAI APIへ送る
8. OpenAI APIでLambdaがタイムアウトした
最後に一番ハマったのがこれです。
CloudWatch Logsに以下が出ました。
Calling OpenAI API
...
REPORT ... Duration: 3000.00 ms ... Status: timeout
原因はLambdaのタイムアウトが3秒だったことです。
OpenAI APIを呼ぶ処理で3秒は短すぎました。
Lambdaの設定を以下に変更して解決しました。
Timeout: 30 seconds
Memory: 256 MB
これでOpenAI APIのレスポンスを受け取ってLINEへ返せるようになりました。
9. モデル名は環境変数化する
モデル名はコードに直書きせず、環境変数にしました。
OPENAI_MODEL=gpt-5.4-mini
これにより、モデルを変えたいときにLambdaコードを触らずに済みます。
最終的にできたこと
最終的には、以下ができるようになりました。
普通のLINEメッセージ
→ メモ保存
/latest
→ 直近メモ表示
/todo
→ ToDo追加モード開始
ToDo追加モード中の次の1通
→ ToDo保存
/todos
→ 未完了ToDo一覧表示
→ Quick Replyで完了
/done 番号
→ ToDo完了
/cancel
→ 入力モード解除
毎日9:00 / 21:00
→ 今日の未完了ToDoをLINE Push通知
/ask
→ AIに聞くモード開始
AIに聞くモード中の次の1通
→ OpenAI APIへ送信
→ 質問整理・文章化をLINEへ返信
今後やりたいこと
今後やるなら、このあたりです。
Flex Message化
今はQuick ReplyでToDo完了しています。
次はFlex Messageを使って、ToDoごとに「完了する」ボタンを置きたいです。
そうすると、番号ズレの問題もなくなります。
質問リスト保存
AIが返した「医師に聞くこと」を、そのまま質問リストとして保存できるようにしたいです。
AI回答
→ 質問リストに保存する
→ /questions で一覧表示
dueDate正規化
whenText を自然入力にしているため、リマインダー精度には限界があります。
将来的には以下のような変換を入れたいです。
今日18時 → 2026-06-27
明日午前 → 2026-06-28
6/30 → 2026-06-30
病院行く前 → dueDateなし
まとめ
家族の入院や介護に関する情報は、普段慣れていない状況の中で量も多く、精神的にも重いことばかりです。
その中で「LINEに送るだけで残る」「ボタンで見返せる」「ToDoを忘れない」「医師に聞くことを整理できる」だけでも、かなり助けになると感じています。
今回の構成は、個人開発としてはかなりシンプルです。
LINE
+ AWS Lambda
+ DynamoDB
+ EventBridge Scheduler
+ OpenAI API
それでも、家族の実用にかなり近いものが作れました。
一番大事だったのは、技術的に凝ることよりも、以下でした。
- コマンドを覚えなくていい
- ボタンで使える
- 入力は短くていい
- 通知で忘れにくくする
- 家族の不安を煽らない
- あとで見返せる
家族のための小さいBotですが、こういう個人用の支援ツールこそ、LINE Botとサーバーレスの相性がかなり良いと感じました。
開発中に弟の容態が少し安定してきたとのしらせもあったので回復した際にはこのQiitaを見せつつ焼肉でもおごってもらおうかなと思います。



