
本記事は、以下の前提記事をもとに構成されています。まだ読んでいない場合は、先にご覧いただくことを推奨します。
大切な人の存在が日常から消えたとき、言葉や話し方だけでも手元に残せたらと願うことがあります。
面影AIは、その願いを実装という手段で形にする仕組みです。
本記事では、LINEとChatGPTを組み合わせて、個人の語彙と対話の記録を活用し、自分自身の心を支えるための対話システムを構築する方法を解説します。
Flask環境の構築手順や仮想環境の整備、VPS上での実行方法については、以下の記事で詳しく解説しています。
AI執事ボットのためのVPS環境構築マニュアル(LINE対応付き)
本記事で実現する“面影AI”とは
本記事では、ChatGPTとLINEを用いて「面影AI」と呼ばれる対話システムを構築する方法を解説します。
面影AIとは、故人や大切な存在の語彙や話し方を記録として保持し、それをもとに再現的な対話を生成する仕組みです。
一般的なチャットボットとは異なり、対話者の心に残る記憶を参照しながら、より個人的で意味のある対話を目指します。
本システムには倫理的な配慮が必要であり、設計上も「不特定多数への提供」ではなく、「自分自身が使用する」前提で設計を行っています。
記憶の再現と人格の模倣の違い
面影AIは、人格そのものを模倣することを目的としていません。目指すのは、会話の語彙や言い回しなどの記憶的要素を抽出し、それをもとに対話を再現するという仕組みです。
そのため、ChatGPTの出力に記憶データを適切に組み込むプロンプト設計と、対話履歴の蓄積・管理が重要な要素となります。これにより、表面的な模倣ではなく、「かつて交わした言葉に近い応答」を実現することを目指します。
想定ユースケースと前提条件
本システムの使用シーンは限定されており、以下のようなユースケースを想定しています。
- 亡くなった家族との思い出を日常的に振り返りたい場合
- 過去の会話を内省や感情整理の手段として利用したい場合
- 言葉に残る存在の記録を、一定の形式で再確認したい場合
また、導入にあたっては以下の環境と条件を満たす必要があります。
要件 | 内容 |
---|---|
開発環境 | VPS上でのPythonおよびFlask構成 |
LINE API | Messaging APIとWebhookが有効なLINEアカウント |
OpenAI | gpt-4oにアクセス可能なOpenAI APIキー |
記憶構造 | SQLiteを用いたローカル記憶データベース構成 |
これらの条件を前提とし、本記事では「どのように実装するか」ではなく、「なぜその構成が必要なのか」という背景も含めて解説を行います。
面影AIの構築に必要な準備
面影AIの実装には、あらかじめ整えておくべき環境やサービスがあります。このセクションでは、必要な準備項目を技術環境・サービス登録・使用制限という3つの観点から整理します。準備を怠ると実装工程でエラーが発生するため、事前にすべての条件を確認しておくことが重要です。
LINE Developersの登録とキー取得
面影AIでは、LINEを通じた対話インターフェースを構築するために、LINE DevelopersでBot用チャネルを作成する必要があります。以下は、登録から必要なキー取得までの手順です。
手順 | 操作内容 |
---|---|
1 | https://developers.line.biz/ にアクセスし、LINEアカウントでログインする |
2 | 「プロバイダー」を作成し、その下で「Messaging API」チャネルを作成する |
3 | チャネル名や業種、メールアドレスを入力し、チャネルを作成する |
4 | 作成後、「チャネル基本設定」から「チャネルシークレット」を取得する |
5 | 「Messaging API設定」タブから「チャネルアクセストークン(長期)」を発行し、保存する |
取得したシークレットとアクセストークンは、後のFlask実装で環境変数として使用されます。漏洩のないよう、安全な場所に記録してください。
OpenAI APIキーの発行手順
ChatGPTとの応答生成を行うには、OpenAIのAPIキーが必要です。以下は、その取得手順です。
手順 | 操作内容 |
---|---|
1 | https://platform.openai.com にアクセスする |
2 | Googleアカウントやメールアドレスでログインする |
3 | 右上のアカウントメニューから「API Keys」を開く |
4 | 「Create new secret key」ボタンを押し、新しいキーを発行する |
5 | 表示されたAPIキーをコピーし、安全な場所に保存する |
このAPIキーは一度しか表示されないため、再取得はできません。環境変数としてアプリに組み込む際に必要となるため、失わないように管理してください。
環境変数ファイル(.env)への登録方法
LINEやChatGPTのAPIキーを取得したら、それらをPythonアプリケーションから安全に参照するために、環境変数ファイル(.env)に登録する必要があります。これは、ソースコードに直接キーを書かずにすむようにするための基本的なセキュリティ対策です。
この .env ファイルは、開発者が自分で新規作成する設定ファイルです。プロジェクトのルートディレクトリ(app.py などがある場所)に、自分で .env という名前でファイルを作成し、必要な変数を記述してください。
.envファイルは、以下のように変数名と値を「キー=値」形式で記述します。
変数名 | 用途 |
---|---|
LINE_CHANNEL_SECRET | LINEで使用するチャネルシークレット |
LINE_CHANNEL_ACCESS_TOKEN | LINEで使用するチャネルアクセストークン(長期) |
OPENAI_API_KEY | OpenAIのAPIキー(ChatGPTと連携する際に使用) |
MEMORY_TARGET_USER_ID | 記憶対象となるLINEユーザーのID(Phase1での発言記録時に使用) |
PHASE_MODE | 動作モードを指定(learn:記憶モード、reply:応答モード) |
TARGET_ROLE | 役割という概念を明示的に持たせることで、関係性に基づいた対話が可能になる |
.envファイルに記載した内容は、Pythonアプリケーション内で「dotenv」ライブラリを通じて参照されます。実行前に必ず正しいパスに .env が存在していることを確認してください。
Flaskの handleMessage() 関数内に以下のようなコードを追加してください。
この値は、LINEから最初にメッセージを送ったユーザーに対して一度だけ user_id を取得することで確認できます。
print(f"Received message from user_id: {user_id}")
サーバー起動後にLINEからBotへ発言を送信すれば、ターミナルに user_id が出力されます。これを .env ファイルの MEMORY_TARGET_USER_ID= にコピーしてください。これにより、誰の発言を記憶対象とするかを明確に指定できます。
また、このファイルは機密情報を含むため、バージョン管理(Gitなど)には絶対に含めないよう .gitignore で除外設定を行ってください。
PHASE_MODEによる動作モードの切り替えについて
面影AIは、LINE上でのやり取りを「記憶フェーズ(Phase1)」と「応答フェーズ(Phase2)」に分けて運用します。
この切り替えは、.env ファイルに定義する PHASE_MODE という環境変数によって制御されます。 PHASE_MODEには、以下の2つの値を設定できます。
PHASE_MODEの値 | 動作内容 |
---|---|
learn | 記憶モード。すべての発言者のメッセージを対象として記録を行い、ChatGPTでの応答は一切行わない。各発言には発信元のユーザーIDが紐付けられ、後の応答フェーズで再現可能な形で保存される。 |
reply | 応答モード。任意のユーザーからのメッセージに対し、ChatGPTが指定された人格(MEMORY_TARGET_USER_ID)として応答を生成し、発話ログとともに記録する。誰が送信したかに関わらず、指定された人格で返答を行う。 |
$ cat .env
# --------------------------------------------
# Phase1: 記憶モード(PHASE_MODE=learn のとき有効)
# --------------------------------------------
PHASE_MODE=learn
LINE_CHANNEL_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
LINE_CHANNEL_ACCESS_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
# --------------------------------------------
# Phase2: 応答モード(PHASE_MODE=reply のとき有効)
# ※ 上記と入れ替える場合、以下の行を上へ移動 or コメント解除
# --------------------------------------------
# PHASE_MODE=reply
# LINE_CHANNEL_SECRET=yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
# LINE_CHANNEL_ACCESS_TOKEN=yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
# --------------------------------------------
# 共通設定
# --------------------------------------------
OPENAI_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
MEMORY_TARGET_USER_ID=Uxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
TARGET_ROLE=母
この形式で .env を記事中に差し込むことで、読者は明確に「どこを切り替えるか」が理解できるようになります。
このPHASE_MODEは、.env ファイルを書き換えてFlaskアプリを再起動することで切り替えます。 LINEチャネルは1つのみで済むため、ユーザー側の操作やアカウント切り替えは一切不要です。
注意:モード切り替えの誤操作にご注意ください
面影AIは、PHASE_MODEの設定によって処理内容が大きく変わる構成です。記憶と応答の両フェーズを同じLINEチャネルで実現するため、モード設定を誤ると以下のような深刻な問題が発生します。
PHASE_MODE | 記憶対象者の発言 | 他の人からの発言 |
---|---|---|
learn | 記録される | 完全に無視される(応答も保存もされない) |
reply | 無視される(応答なし・記憶なし) | 応答+記録される |
以下の点を必ず守ってください。
- PHASE_MODE の切り替えを行ったら、Flaskアプリを必ず再起動してください。
- 記憶対象者(MEMORY_TARGET_USER_ID)の user_id を .env に正しく記載してください。
- 誤って reply モードで本人が話してしまうと、意図しない記録や動作につながります。
このアプリケーションの本質は「記憶する相手が誰なのか」を正確に管理することです。設定ミスによって他の人の言葉が“面影”として記憶されないよう、細心の注意を払って運用してください。
開発環境の構成条件
VPS環境として国内の仮想専用サーバー(例:ConoHa VPSなど)を想定しています。
OSはUbuntu系で、root権限またはsudoが利用可能なユーザーを作成済みであることを前提とします。また、Python 3.11以上が使用可能な状態であり、Flaskや必要なパッケージをインストールできる仮想環境が構築されている必要があります。
仮想専用サーバー(VPS)の準備に関しては、以下の記事で初期設定の全手順をまとめています。ユーザー作成、SSH設定、UFWの構成、Pythonのインストールまでを一通り確認できます。
【関連記事】
【ConoHa】VPSを安全に使うための初期設定マニュアル
記憶の選定・保存・再現について具体的に解説します。全体像を整理した中編記事と合わせて読むと、理解がより深まります。
LINEおよびOpenAIとの連携には、サーバーサイドの処理環境が必要です。今回は軽量な構成として、Python+Flaskで構築可能なVPS環境を前提としています。以下の条件を満たすことが望ましいです。
項目 | 条件 |
---|---|
Pythonバージョン | 3.11以上 |
サーバー | 国内VPS(例:ConoHa VPSなど) |
通信 | https有効なSSL証明書設定済み |
OpenAI APIには月額制限やAPI使用回数の制限が設けられています。無料枠での実装も可能ですが、gpt-4oなどの高精度モデルを使用する場合は有料プランが前提となります。LINE側でも、無料プランでは一定回数を超えると通知制限がかかる場合があるため、使用量と費用を事前に確認しておく必要があります。
面影AIの記憶構造と再現アルゴリズム
面影AIが他のチャットボットと決定的に異なるのは、単なる一時的な対話ではなく「記憶を蓄積し、再現する」という機構を備えている点にあります。
この記憶の仕組みは、ただのログではなく、重要語や語調といった“面影を感じさせる情報”を選別しながら蓄積されていきます。ここでは、その記憶構造の全体像と、どのように再現に活用していくかを解説します。
memory_db(記憶DB)の構造設計
記憶はすべてmemory_dbと呼ばれるデータベースに格納されます。このDBは単なる発言ログではなく、「誰が」「どのような語調で」「どのような言葉を」使ったかを判定できるように設計されており、主に以下のような構造を持ちます。
カラム名 | 内容 | 型 |
---|---|---|
id | 主キー(発言ごとの識別子) | INTEGER |
timestamp | 発言された日時 | DATETIME |
speaker | ユーザーまたはAI | TEXT |
message | 発言内容 | TEXT |
importance | 記憶の重要度(重みスコア) | REAL |
この設計により、単なる発言の羅列ではなく、「重要な記憶」として残すべき情報と、それほどでもない情報を判別しながら蓄積していくことが可能になります。
対話履歴と記憶の統合方法
通常のチャットボットは、対話の履歴をそのままAPIに投げる形式を採用しますが、面影AIでは「過去の記憶から参照すべき内容を抽出し、プロンプトに挿入する」仕組みを取ります。
つまり、単純な履歴のコピーではなく、記憶DBから重要度の高い発言を選び出し、それらを「あなたは以前、このような会話をしていたAIです」という文脈に組み込みます。
この方式により、ChatGPTが出力する文章は、その場限りの応答ではなく「一貫性のある人格」としての返答に近づきます。記憶と履歴の統合は、面影の“らしさ”を形作る中核の部分です。
記憶の重み付けと忘却処理
全ての記憶を残し続ければ、精度は上がる一方で処理コストが肥大化し、応答時間や内容の混乱を招くことがあります。
そのため、面影AIでは記憶に対して重みスコア(importance)を付与し、定期的に低スコアの記憶を削除する「忘却処理」を実行します。 この重みスコアは、以下のような条件で変動します。
- ユーザーが手動で重要とマークした発言:スコア上昇
- 頻繁に参照されている語彙:スコア上昇
- 長期間使用されていない語彙:スコア低下
これにより、面影AIは常に“使われ続けている記憶”を残し、“役目を終えた記憶”を自然と手放す仕組みを保つことができます。こうした記憶管理によって、より人間的な“記憶の移ろい”を模倣する設計となっています。
LINE DevelopersでBotを準備する
LINEとの連携を実現するには、まずLINE Developers上でBotのチャネルを作成し、Webhook受信の設定を行う必要があります。このセクションでは、チャネル作成からMessaging APIの準備までの手順を整理します。
チャネルの作成と基本設定
LINE Developersの管理画面にログインし、Messaging API対応の新規チャネルを作成します。以下の情報を入力することで、面影AI専用のBotが準備されます。
設定項目 | 入力内容の例 |
---|---|
チャネル名 | 面影AI(学習用) または 面影AI(応答用) ※Flaskエンドポイントは固定:Phase1= /learn、Phase2= /reply |
プロバイダー名 | 個人名または屋号(BePro など) |
アプリタイプ | Messaging API |
業種 | 個人 または 教育・IT関連など |
メールアドレス | 開発用の連絡先メール |
この構成では、Flaskアプリ側のWebhookエンドポイントが /learn および /reply に固定されているため、チャネルも用途別に分けて作成してください。
たとえば、学習用には「面影AI(学習用)」、応答用には「面影AI(応答用)」といったように、LINE Developers側でも明確に区別できる名称にしておくと誤接続を防げます。
作成後は「チャネルアクセストークン(長期)」を発行し、`.env` ファイルに保存しておきます。
LINE Developersでのチャネル作成やWebhookの設定方法について不安がある方は、 以下の記事で画面付きで詳しく解説していますので、あわせてご確認ください。
→ AI執事ボットのためのVPS環境構築マニュアル(LINE対応付き)
Webhook URLの設定と有効化
Flaskアプリを公開した状態で、LINE Developersのチャネル設定画面から以下の設定を行います。
- Webhook URL: Flaskアプリのエンドポイントに応じて https://xxx/learn または https://xxx/reply を指定
- Webhookの利用: 有効化する
- 応答設定: Botモード(自動応答)に設定
注意: この面影AIでは、Flask側のWebhookエンドポイントは /learn(Phase1)または /reply(Phase2)です。 LINE公式ドキュメントでよく使われている /callback とは異なるため、設定時は注意してください。必ず Flask 側で定義されている /learn(Phase1)や /reply(Phase2)を選択してください。
Webhookが正しく受信できる状態になると、LINEからのメッセージがFlaskアプリに転送され、記憶処理または応答処理が実行されます。
FlaskでLINE Botを構築する手順
面影AIの根幹を担うのが、LINEとChatGPTを中継するFlaskアプリケーションです。
この構成を通じて、ユーザーがLINEで送信したメッセージをサーバーが受け取り、その内容をもとにChatGPTへ問い合わせ、得られた応答を再度LINEへ返すという一連の流れが実現されます。
本セクションでは、ディレクトリ構成から実装の要点までを段階的に整理します。
ディレクトリとファイルの構成
このセクションでは、Flaskを使ったLINE Bot構築の全体像を順を追って解説していきます。
面影AIのディレクトリ構成(Phase1)
以下は、Flask + LINE + ChatGPT + SQLite による記録型AIのディレクトリ構成です。
1 2 3 4 5 6 7 8 9 10 |
[crayon-6827e34a9cdc1402323340 inline="true" ]ai_omokage/ ├── app.py # Flaskのメインルーティング(/learn用) ├── .env # 環境変数ファイル(PHASE_MODE=learnなど) ├── requirements.txt # 必要パッケージ定義 ├── logic/ │ ├── __init__.py # パッケージ認識用の空ファイル(必須) │ ├── chatgpt_logic.py # ChatGPTへの問い合わせ処理 │ └── db_utils.py # DB操作(insert/selectなど) ├── memory.db # SQLiteデータベース(初回起動後に生成) └── .venv/ # 仮想環境(除外可) |
※注意: logic/ ディレクトリ内には必ず __init__.py を配置してください。これは Python に「このフォルダはパッケージですよ」と認識させるための必須ファイルです。中身は空でも問題ありません。
logic/__init__.py の内容
このファイルは logic ディレクトリを Python パッケージとして認識させるために必要です。内容は以下のように空で構いません。
# logic/init.py
# このファイルは空でOKです。
これで面影AIは、Flaskを用いたPythonアプリケーションとして構築します。
プロジェクトを構成する主要なアクターには、以下の5つがあります。
まず、LINEのWebhookを受信して応答処理を行う app.py がエントリーポイントになります。
記憶の保存や履歴の記録を管理する db_utils.py、保存された記憶をもとにプロンプトを生成してChatGPTと連携する chatgpt_logic.py、そして各種APIキーやシークレット情報を保持する .env ファイルが含まれます。
さらに、本システムではユーザーの記憶と対話履歴を永続的に保持するため、SQLite形式のデータベースファイル memory.db が使用されます。
このデータベース内には、記憶情報を管理する memories テーブルと、発話履歴を記録する dialogues テーブルの2つが含まれています。 以下は、各ファイル・生成物の具体的な役割を一覧にまとめたものです。
ファイル/ディレクトリ名 | 用途 |
---|---|
.env | LINEチャネル情報やOpenAIのAPIキーなど、環境依存の設定値を保持(実行時に最初に読み込まれる) |
memory.db | SQLiteで管理される記憶データベース。記憶(memories)と発話履歴(dialogues)の2テーブルを持つ |
db_utils.py | SQLiteの接続・初期化、記憶と発話の保存処理を統括。memory.dbへの操作を一元管理 |
chatgpt_logic.py | 保存された記憶から文脈を選定し、ChatGPTへのプロンプトを構成・送信する役割を担う |
app.py | Flaskアプリ本体。LINEからのWebhookを受信し、db_utilsやchatgpt_logicを呼び出す統括処理 |
すべてのファイルはFlaskアプリケーションのルートディレクトリに配置され、LINEからのメッセージ受信をトリガーとして処理が進行します。
APIキーやチャネルシークレットなどの秘匿情報は .env ファイルで一元管理し、コード本体との分離を徹底することで、セキュリティとメンテナンス性を両立させています。
認証情報とAPIキーを安全に管理する.envファイル
面影AIでは、LINEおよびOpenAIとの連携に必要な各種認証情報を、Flaskアプリケーション内に直接記述せず、.env ファイルにまとめて管理します。
この .env ファイルは実行時に読み込まれ、環境変数として各モジュールで使用されます。セキュリティ確保のため、.env はバージョン管理の対象外とし、.gitignore に明記しておく必要があります。
Flaskアプリケーション側では、Pythonの dotenv モジュールを用いてこの .env を読み込みます。コード内では以下のようにして環境変数を取得します。
from dotenv import load_dotenv
import os
load_dotenv()
channel_secret = os.getenv("LINE_CHANNEL_SECRET")
api_key = os.getenv("OPENAI_API_KEY")
これにより、コード本体に認証情報をハードコーディングせず、外部ファイルから安全に管理できる構成が実現されます。
記憶と対話を蓄積するSQLiteデータベース

面影AIでは、ユーザーから送信されたメッセージと、それに基づいて保存される「記憶情報」の両方を永続的に管理するため、SQLite形式のデータベースファイル memory.db を使用します。
このファイルはアプリケーションのルートディレクトリに配置され、Flaskアプリケーション初回起動時に自動で生成されます。 データベースには、次の2つのテーブルが定義されています。
テーブル名 | 用途 |
---|---|
memories | 面影AIの根幹となる記憶本文を格納し、カテゴリ分類や登録日時などの基本情報を保持 |
weights | 各記憶に対する重みスコア(importance)や忘却フラグなど、意味づけや状態変化を管理 |
dialogues | ユーザーとの対話履歴を保存し、使用された記憶との対応関係を記録する |
[テーブル構成設計]
テーブル名 | カラム名 | 型 | 用途 |
---|---|---|---|
memories | memory_id | INTEGER | 記憶データの主キー(自動採番) |
content | TEXT | 記憶の本文(対象者の発言) | |
category | TEXT | 話題カテゴリ(例:家族、仕事など) | |
weight | INTEGER | 初期重み(使用頻度の初期値として1) | |
target_user_id | TEXT | 人格対象のユーザーID(ChatGPTが模倣する対象) | |
is_forgotten | INTEGER | 忘却フラグ(0=記憶中、1=除外) | |
created_at | TIMESTAMP | 記憶の登録日時 | |
weights | weight_id | INTEGER | 重み履歴の主キー(自動採番) |
memory_id | INTEGER | 対象記憶のID(memories.memory_id を参照) | |
interact | TEXT | 変更理由や操作記録(例:再評価) | |
created_at | TIMESTAMP | 履歴の登録日時 | |
dialogues | dialogue_id | INTEGER | 発話ログの主キー(自動採番) |
target_user_id | TEXT | 対象人物(ChatGPTが模倣する相手) | |
sender_user_id | TEXT | 発言者のLINEユーザーID(自分・姉・AIなど) | |
message_type | TEXT | 発話の種類(input または reply) | |
is_ai_generated | BOOLEAN | AIが生成した発言かどうか | |
text | TEXT | 実際に送信された発話内容 | |
memory_refs | TEXT | 参照された記憶IDのリスト(例:"[2,4]") | |
prompt_version | TEXT | 使用されたプロンプトのバージョン識別子 | |
temperature | REAL | ChatGPT応答時の温度設定値 | |
created_at | TIMESTAMP | 発話の記録日時 |
以下は、これら3つのテーブルを作成するための初期化コードです。これは db_utils.py に定義され、アプリケーション起動時に自動で呼び出されます。
import sqlite3
import os
def initDatabase():
if not os.path.exists("memory.db"):
conn = sqlite3.connect("memory.db")
c = conn.cursor()
c.execute("""
CREATE TABLE memories (
memory_id INTEGER PRIMARY KEY AUTOINCREMENT,
content TEXT NOT NULL,
category TEXT DEFAULT NULL,
weight INTEGER DEFAULT 1,
target_user_id TEXT NOT NULL,
is_forgotten INTEGER DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
c.execute("""
CREATE TABLE dialogues (
dialogue_id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT NOT NULL,
sender_user_id TEXT NOT NULL,
message_type TEXT NOT NULL,
is_ai_generated BOOLEAN NOT NULL,
message TEXT NOT NULL,
memory_refs TEXT,
prompt_version TEXT,
temperature REAL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
c.execute("""
CREATE TABLE weights (
weight_id INTEGER PRIMARY KEY AUTOINCREMENT,
memory_id INTEGER NOT NULL,
importance REAL DEFAULT 0.5,
is_forgotten INTEGER DEFAULT 0,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (memory_id) REFERENCES memories(memory_id)
)
""")
conn.commit()
conn.close()
この構造により、記憶と発言を別テーブルで管理しながら、両者を紐付けて活用できる柔軟な記憶再現ロジックを実現しています。
記憶と発話を登録・取得するdb_utils.pyの役割
Flaskアプリケーションにおいて、db_utils.py はSQLiteデータベース memory.db に対するすべての操作を一元管理します。
記憶(memories)および発話履歴(dialogues)の登録、データベースの初期化、そして後に記憶を再利用するための取得処理を担います。 このファイルでは以下の3種類の処理を関数として定義します。
関数名 | 分類 | 用途 |
---|---|---|
initDatabase() | 初期化 | アプリ起動時に呼び出され、テーブルが存在しない場合は作成する |
registerMemoryAndDialogue(user_id, message, content) | 登録 | 1つのトランザクションで記憶と発話ログを同時に保存する |
getAllMemories() | 取得 | 登録済みの記憶(is_forgotten=0)をすべて取得する |
db_utils.pyの完成ソース
このファイルは、記憶(memories)と発話履歴(dialogues)をSQLiteに保存・取得するためのロジックを担います。
FlaskやChatGPTロジックからは独立させ、データベース処理に特化した構成としています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 |
import sqlite3 import os import json # 使用するSQLiteデータベースのファイル名 DB_NAME = "memory.db" # データベースが存在しない場合、初期化処理を行う def initDatabase(): if not os.path.exists(DB_NAME): conn = sqlite3.connect(DB_NAME) c = conn.cursor() # memories テーブルに target_user_id を追加 c.execute(""" CREATE TABLE memories ( memory_id INTEGER PRIMARY KEY AUTOINCREMENT, content TEXT NOT NULL, category TEXT DEFAULT 'uncategorized', weight INTEGER DEFAULT 1, target_user_id TEXT NOT NULL, is_forgotten INTEGER DEFAULT 0, created_at TEXT DEFAULT CURRENT_TIMESTAMP ) """) # 発話ログテーブル(Phase2用) c.execute(""" CREATE TABLE dialogues ( dialogue_id INTEGER PRIMARY KEY AUTOINCREMENT, target_user_id TEXT NOT NULL, sender_user_id TEXT NOT NULL, message_type TEXT NOT NULL, is_ai_generated BOOLEAN NOT NULL, text TEXT NOT NULL, memory_refs TEXT, prompt_version TEXT, temperature REAL, created_at TEXT DEFAULT CURRENT_TIMESTAMP ) """) # 重み履歴テーブル c.execute(""" CREATE TABLE weights ( weight_id INTEGER PRIMARY KEY AUTOINCREMENT, memory_id INTEGER NOT NULL, interact TEXT, created_at TEXT DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (memory_id) REFERENCES memories(memory_id) ) """) conn.commit() conn.close() print("Database initialized with target_user_id column.") else: print("Database already exists.") # ✅ 記憶と発話ログを1つのトランザクションで同時に保存(カテゴリ+weight=1) def registerMemoryAndDialogue( user_id, message, content, category, memory_refs=None, is_ai_generated=False, sender_user_id="self", message_type="input" ): conn = sqlite3.connect(DB_NAME) c = conn.cursor() try: # ✅ 記憶を保存(初期weight=1) c.execute( """ INSERT INTO memories (content, category, weight, target_user_id) VALUES (?, ?, ?, ?) """, (content, category, 1, user_id) ) memory_id = c.lastrowid # ✅ 発話ログ(dialogues)へ保存 c.execute( """ INSERT INTO dialogues ( target_user_id, sender_user_id, message_type, is_ai_generated, text, memory_refs, prompt_version, temperature ) VALUES (?, ?, ?, ?, ?, ?, ?, ?) """, ( user_id, sender_user_id, message_type, is_ai_generated, message, json.dumps(memory_refs) if memory_refs else None, None, None ) ) # ✅ 重み初期ログ c.execute( "INSERT INTO weights (memory_id, interact) VALUES (?, ?)", (memory_id, "初期登録(weight=1)") ) conn.commit() print("Memory and dialogue registered.") except Exception as e: conn.rollback() raise e finally: conn.close() # ✅ 全記憶(忘却フラグなし) def getAllMemories(): conn = sqlite3.connect(DB_NAME) c = conn.cursor() c.execute("SELECT memory_id, content, category, weight FROM memories WHERE is_forgotten = 0") results = c.fetchall() conn.close() return results # ✅ 重み履歴を追加 def insertWeightLog(memory_id, interact): conn = sqlite3.connect(DB_NAME) c = conn.cursor() try: c.execute( "INSERT INTO weights (memory_id, interact) VALUES (?, ?)", (memory_id, interact) ) conn.commit() print(f"Weight log inserted for memory_id={memory_id}") except Exception as e: conn.rollback() raise e finally: conn.close() # ✅ 特定memory_idに紐づく重み履歴を取得 def getWeightLogsByMemoryId(memory_id): conn = sqlite3.connect(DB_NAME) c = conn.cursor() c.execute( "SELECT interact, created_at FROM weights WHERE memory_id = ? ORDER BY created_at DESC", (memory_id,) ) logs = c.fetchall() conn.close() return logs # ✅ 全weight履歴を取得(管理・表示用) def getAllWeightLogs(): conn = sqlite3.connect(DB_NAME) c = conn.cursor() c.execute("SELECT weight_id, memory_id, interact, created_at FROM weights ORDER BY created_at DESC") logs = c.fetchall() conn.close() return logs # ✅ 単体実行でのテスト実行 if __name__ == "__main__": initDatabase() # 任意テスト用サンプル # registerMemoryAndDialogue("U123", "これはテスト発言です", "これは記憶です", "感情") # print(getAllMemories()) # insertWeightLog(1, "再評価:強調対象としてweight変更候補") # print(getWeightLogsByMemoryId(1)) |
理解すべきポイント1:registerMemoryAndDialogue の処理構造と設計意図
以下に、registerMemoryAndDialogue 関数の実装例を示します。
import sqlite3
def registerMemoryAndDialogue(user_id, message, content):
conn = sqlite3.connect("memory.db")
c = conn.cursor()
try:
c.execute("INSERT INTO memories (content) VALUES (?)", (content,))
memory_id = c.lastrowid
c.execute("INSERT INTO dialogues (user_id, message, memory_id) VALUES (?, ?, ?)", (user_id, message, memory_id))
conn.commit()
except Exception as e:
conn.rollback()
raise e
finally:
conn.close()
この関数では、ユーザーの入力内容を記憶テーブルへ保存し、その記憶に基づく対話ログをdialoguesテーブルへ紐づけて保存します。
両者を1トランザクション内で実行することにより、「記憶と発話は必ずセットで存在する」という一貫性を保証します。
例外処理では rollback() を行い、DBの整合性を守る構成としています。
理解すべきポイント2:関数内完結のローカル接続と安全な閉処理
関数は sqlite3.connect() を内部で実行し、finally ブロックで必ず conn.close() を呼び出します。
これにより、Flaskアプリケーション本体からは DB接続やカーソル処理の実装詳細を気にせず、シンプルな関数呼び出しのみでデータ登録が行えます。
この設計は、疎結合・再利用性・単体テストのいずれにも有効なアプローチです。
理解すべきポイント3:initDatabase の役割と初期化の安全性
この関数は、面影AIで使用する3つのデータベーステーブル( memories・ dialogues・ weights)を初回起動時に作成します。 記憶の保存、発話ログ、記憶の評価履歴という3つの役割を、それぞれ独立したテーブルで担う構成です。
import sqlite3
import os
def initDatabase():
if not os.path.exists("memory.db"):
conn = sqlite3.connect("memory.db")
c = conn.cursor()
c.execute("""
CREATE TABLE memories (
memory_id INTEGER PRIMARY KEY AUTOINCREMENT,
content TEXT NOT NULL,
category TEXT DEFAULT NULL,
weight INTEGER DEFAULT 1,
target_user_id TEXT NOT NULL,
is_forgotten INTEGER DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
c.execute("""
CREATE TABLE dialogues (
dialogue_id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT NOT NULL,
sender_user_id TEXT NOT NULL,
message_type TEXT NOT NULL,
is_ai_generated BOOLEAN NOT NULL,
message TEXT NOT NULL,
memory_refs TEXT,
prompt_version TEXT,
temperature REAL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
c.execute("""
CREATE TABLE weights (
weight_id INTEGER PRIMARY KEY AUTOINCREMENT,
memory_id INTEGER NOT NULL,
importance REAL DEFAULT 0.5,
is_forgotten INTEGER DEFAULT 0,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (memory_id) REFERENCES memories(memory_id)
)
""")
conn.commit()
conn.close()
memories テーブルには「発言内容」「カテゴリ」「重み」などの本体記憶が、 dialogues テーブルにはユーザーごとの発話ログが、 weights テーブルには記憶ごとの評価・操作履歴(初期登録、強調、再評価など)が記録されるように設計されています。
なお、この関数は**Phase1 / Phase2 を問わず毎回実行されますが、既存の memory.db が存在すれば何も変更を加えません。** そのため、デプロイや検証で繰り返し再起動しても、登録済みの記憶データが消えることはありません。
理解すべきポイント4:getAllMemories の取得条件と記憶選定の前提
この関数は is_forgotten = 0 の記憶を全件取得します。 今後は category による抽出処理が getValidMemories() 側で実装される予定であり、より精度の高い記憶選定が可能になる基盤処理となります。
def getAllMemories():
conn = sqlite3.connect("memory.db")
c = conn.cursor()
c.execute("SELECT memory_id, content, category, weight FROM memories WHERE is_forgotten = 0")
results = c.fetchall()
conn.close()
return results
戻り値としては [(memory_id, content, category, weight), …] の形でリストが返されます。 現時点では一部カラムしか利用していませんが、今後の応答処理において category によるフィルタや weight による優先制御を行うための基盤設計となっています。
本関数は読み取り専用に設計されており、他関数と同様に conn.close() を明示してDB接続が確実に解放される構造となっています。
記憶をもとにChatGPTの返答を生成するchatgpt_logic.pyの役割
このファイルは、保存された記憶をもとにChatGPTへ送信するプロンプトを組み立て、対話に必要な自然な応答を生成する役割を担います。
chatgpt_logic.pyの完成ソース
Flaskアプリケーションからはこのファイルの関数を通じて、記憶ベースの返答処理が実行されます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 |
from openai import OpenAI import os from dotenv import load_dotenv import sqlite3 # .envファイルから環境変数を読み込む load_dotenv() # OpenAIクライアント初期化 client = OpenAI(api_key=os.getenv("OPENAI_API_KEY")) # ✅ 指定カテゴリの記憶を取得(忘却されていないもの) def getMemoriesByCategory(category, target_user_id, limit=10): conn = sqlite3.connect("memory.db") c = conn.cursor() c.execute(""" SELECT memory_id, content FROM memories WHERE is_forgotten = 0 AND category = ? AND target_user_id = ? ORDER BY created_at DESC LIMIT ? """, (category, target_user_id, limit)) results = c.fetchall() conn.close() return results # ✅ ChatGPTに与えるプロンプトを構築する(記憶と発話を組み合わせる) def buildPrompt(memories, user_message, role_label): memory_section = "\n".join(f"- {m}" for m in memories) print(f"🔍 役割: {role_label}") # ✅ 安全性確保のための制限命令を追加 restriction = """ あなたは記憶再現AIです。 性的な内容、疑似恋人としての振る舞い、または性的なロールプレイは一切行ってはいけません。 そのような話題が含まれる場合は「この話題には応答できません」と返答してください。 """ prompt = f""" {restriction} あなたは過去の記憶をもとに、人間らしく返答するAIです。 今からあなたは「{role_label}」として返答してください。 以下は過去に記録された重要な記憶です: {memory_section} この記憶をもとに、以下の発言に自然に返答してください: 「{user_message}」 """ return prompt.strip() # ✅ ChatGPTで自然な応答を得る(カテゴリごとに記憶を絞る) def getChatGptReply(user_message, target_user_id): # ① カテゴリ判定 category = getCategoryByGpt(user_message) print(f"🔍 判定カテゴリ: {category}") # ② 指定カテゴリ × ユーザーIDの記憶を取得 memory_items = getMemoriesByCategory(category, target_user_id) memory_ids = [m[0] for m in memory_items] memory_texts = [m[1] for m in memory_items] # ③ プロンプト生成 role_label = os.getenv("TARGET_ROLE") prompt = buildPrompt(memory_texts, user_message, role_label) # ④ ChatGPT API呼び出し response = client.chat.completions.create( model="gpt-4o", messages=[ {"role": "system", "content": "あなたは過去の記憶を踏まえて人間らしく返答するAIです。"}, {"role": "user", "content": prompt} ] ) reply_text = response.choices[0].message.content.strip() return { "reply_text": reply_text, "used_memory_ids": memory_ids } # ✅ ユーザー発言をカテゴリに分類(Phase1と共通) def getCategoryByGpt(message): system_prompt = ( "以下のユーザー発言に対して、最も適切なカテゴリを1単語で返してください。\n" "候補カテゴリには「家族」「仕事」「感情」「趣味」「健康」「その他」があります。\n" "出力はカテゴリ名のみで、他の説明を含めないでください。" ) try: response = client.chat.completions.create( model="gpt-4o", messages=[ {"role": "system", "content": system_prompt}, {"role": "user", "content": message} ] ) category = response.choices[0].message.content.strip() return category if category else "uncategorized" except Exception as e: print("[ChatGPT Error]", e) return "uncategorized" |
事前に os.path.exists() によってDBの存在をチェックしているため、すでに存在する環境で誤って上書きするリスクがありません。
さらに、記憶の重み(weight)や忘却フラグ(is_forgotten)など、後続のChatGPT連携で活用するメタ情報もあらかじめテーブル設計に組み込まれており、
設計の初期段階から拡張性と運用を見越した構成となっています。
理解すべきポイント1:ChatGPTへの送信処理 getChatGptReply()
def getChatGptReply(user_message):
memories = getValidMemories()
prompt = buildPrompt(memories, user_message)
response = openai.ChatCompletion.create(
model="gpt-4",
messages=[
{"role": "system", "content": "あなたは過去の記憶を踏まえて人間らしく返答するAIです。"},
{"role": "user", "content": prompt}
]
)
return response["choices"][0]["message"]["content"]
この関数では、まず getValidMemories() により忘却されていない記憶を取得し、それとユーザーの発言内容を buildPrompt() に渡してプロンプトを構築しています。
その後、OpenAIの ChatCompletion.create() に system ロールと組み合わせて送信し、最終的な応答メッセージ(content)だけを抽出して返しています。
この構造により、外部のFlaskアプリケーションはこの関数を呼び出すだけで、記憶ベースの応答生成を一括して実行できる設計になっています。
理解すべきポイント2:プロンプト構築処理 buildPrompt()
def buildPrompt(memories, user_message):
memory_section = "\n".join(f"- {m}" for m in memories)
prompt = f"""
以下は過去に記録された重要な記憶です:
{memory_section}
この記憶をもとに、以下の発言に自然に返答してください:
「{user_message}」
"""
return prompt.strip()
この関数では、記憶リストを1行ずつ箇条書きとして整形し、ユーザーの発話を文末に付加する形でプロンプト全体を構成しています。
構造的には「前提(記憶)→指示(自然に返答せよ)→入力(ユーザー発言)」という順序で構築されており、ChatGPTが文脈を理解しやすいよう配慮されたレイアウトになっています。
また、記憶リストはすべて - 記号付きで列挙されるため、ChatGPTにとっては「文脈的な知識群」として処理されやすく、システムとしての応答精度にも寄与する構造です。
理解すべきポイント3:ChatGPTによるカテゴリ分類処理 getCategoryByGpt()
def getCategoryByGpt(message):
system_prompt = (
"以下のユーザー発言に対して、最も適切なカテゴリを1単語で返してください。\n"
"候補カテゴリには「家族」「仕事」「感情」「趣味」「健康」「その他」があります。\n"
"出力はカテゴリ名のみで、他の説明を含めないでください。"
)
response = openai.ChatCompletion.create(
model="gpt-4",
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": message}
]
)
category = response["choices"][0]["message"]["content"].strip()
return category if category else "uncategorized"
この関数では、ユーザーが発言したメッセージを ChatGPT に送信し、 その内容がどのカテゴリ(家族・仕事・感情・趣味・健康・その他)に該当するかを1単語で判定させています。
構造的には「対象発言 → systemロールの制約付きプロンプト → 1単語の返答」という流れになっており、 ChatGPTが余計な説明や文脈を付加せず、明確に分類することを意図した設計です。
また、万一ChatGPTが空文字などの無効な返答をした場合に備えて、uncategorized をフォールバックとして返す処理が組み込まれています。
この関数により、Phase1で登録されるすべての記憶にカテゴリが付与されるため、 Phase2での記憶抽出の最適化(プロンプトトークンの節約・応答精度の向上)につながる仕組みとなっています。
Webhookを起点に記憶と応答を統括するapp.pyの役割
このファイルは、LINEのメッセージを受け取り、記憶データの登録とChatGPTによる返答を統括的に制御するエントリーポイントです。 FlaskによるWebhookルーティングを中心に、各処理を役割ごとに外部ファイルへ分離した構成になっています。
app.pyの完成ソース
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 |
from flask import Flask, request, abort from linebot.v3.messaging import Configuration, ApiClient, MessagingApi from linebot.v3.webhook import WebhookHandler from linebot.v3.webhooks import MessageEvent, TextMessageContent from linebot.v3.messaging.models import ReplyMessageRequest, TextMessage from dotenv import load_dotenv import os import json from logic.db_utils import initDatabase, registerMemoryAndDialogue from logic.chatgpt_logic import getChatGptReply, getCategoryByGpt # 環境変数の読み込み load_dotenv() # 各種設定値を取得 channel_secret = os.getenv("LINE_CHANNEL_SECRET") access_token = os.getenv("LINE_CHANNEL_ACCESS_TOKEN") openai_api_key = os.getenv("OPENAI_API_KEY") memory_target_user_id = os.getenv("MEMORY_TARGET_USER_ID") phase_mode = os.getenv("PHASE_MODE") # learn または reply # 致命的な設定ミスの検出 if not memory_target_user_id: raise ValueError("MEMORY_TARGET_USER_ID is not set. Startup aborted.") if phase_mode not in ["learn", "reply"]: raise ValueError("PHASE_MODE must be 'learn' or 'reply'. Startup aborted.") # FlaskとLINE初期化 app = Flask(__name__) handler = WebhookHandler(channel_secret) messaging_api = MessagingApi(ApiClient(Configuration(access_token=access_token))) # DB初期化 initDatabase() @app.route("/callback", methods=["POST"]) def callback(): signature = request.headers["X-Line-Signature"] body_text = request.get_data(as_text=True) body_json = request.get_json(force=True) events = body_json.get("events", []) if not events: print("⚠️ Warning: No events in body.") return "NO EVENT", 200 user_id = events[0]["source"]["userId"] print("user_id:", user_id) try: handler.handle(body_text, signature) except Exception as e: print(f"[{phase_mode.upper()}] Webhook Error: {e}") abort(400) return "OK" @handler.add(MessageEvent, message=TextMessageContent) def handleMessage(event): try: user_id = event.source.user_id message = event.message.text NG_WORDS = ["セフレ", "エロ", "性欲", "キスして", "付き合って", "いやらしい"] if any(ng in message.lower() for ng in NG_WORDS): reply_text = "この話題には応答できません。" reply = ReplyMessageRequest( reply_token=event.reply_token, messages=[TextMessage(text=reply_text)] ) messaging_api.reply_message(reply) return print(f"[{phase_mode.upper()}] Received message from user_id: {user_id}") print(f"[{phase_mode.upper()}] MEMORY_TARGET_USER_ID: {memory_target_user_id}") if phase_mode == "learn": if user_id == memory_target_user_id: # ✅ カテゴリをChatGPTに判定させて登録 category = getCategoryByGpt(message) registerMemoryAndDialogue( user_id=user_id, message=message, content=message, category=category, memory_refs=None, is_ai_generated=False, sender_user_id="self", message_type="input" ) print(f"Memory recorded with category: {category}") else: print("Ignored: Not memory target (LEARN mode)") elif phase_mode == "reply": # if user_id == memory_target_user_id: # print("Ignored: memory_target_user_id should not speak in REPLY mode") # return gpt_result = getChatGptReply(message, memory_target_user_id) reply_text = gpt_result["reply_text"] memory_refs = json.dumps(gpt_result["used_memory_ids"]) registerMemoryAndDialogue( user_id=memory_target_user_id, message=message, content=reply_text, category="応答", memory_refs=memory_refs, is_ai_generated=True, sender_user_id=user_id, message_type="reply" ) reply = ReplyMessageRequest( reply_token=event.reply_token, messages=[TextMessage(text=reply_text)] ) messaging_api.reply_message(reply) print("Reply sent and recorded (REPLY mode)") except Exception as e: print(f"[{phase_mode.upper()}] Handler Error: {e}") if __name__ == '__main__': initDatabase() app.run(debug=False, host='0.0.0.0', port=5000) |
理解すべきポイント1:FlaskアプリケーションとLINE連携の初期化構造
app = Flask(__name__)
handler = WebhookHandler(channel_secret)
configuration = Configuration(access_token=access_token)
messaging_api = MessagingApi(ApiClient(configuration))
initDatabase() この構成では、Flaskアプリのインスタンスを生成した後、 LINEのWebhookハンドラーとメッセージAPIクライアントを初期化しています。
channel_secret および access_token は .env ファイルから取得されており、 コード上にハードコードせず、秘匿性を保った設計になっています。
initDatabase() はアプリ起動時に1度だけ呼び出され、必要なテーブルが存在しない場合に初期化処理を行います。 これにより、サーバー起動後すぐにWebhook受信処理が開始できる状態が整います。
理解すべきポイント2:ユーザーの種別による処理分岐とChatGPT応答の制御
この処理では、フェーズ(learn or reply)に応じて動作が完全に分岐されます。
- Phase1(learn)では対象ユーザーからの発言を記憶し、ChatGPTでカテゴリを自動分類したうえでDBへ格納
- Phase2(reply)では対象者以外からの発言にのみ反応し、ChatGPTによる応答生成 → DBへ保存 → LINEで返信
@handler.add(MessageEvent, message=TextMessageContent)
def handleMessage(event):
user_id = event.source.user_id
message = event.message.text
if phase_mode == "learn":
if user_id == memory_target_user_id:
category = getCategoryByGpt(message)
registerMemoryAndDialogue(user_id, message, message, category)
print(f"Memory recorded with category: {category}")
else:
print("Ignored: Not memory target (LEARN mode)")
elif phase_mode == "reply":
if user_id == memory_target_user_id:
print("Ignored: memory_target_user_id should not speak in REPLY mode")
return
reply_text = getChatGptReply(message)
registerMemoryAndDialogue(user_id, message, reply_text, "応答")
reply = ReplyMessageRequest(
reply_token=event.reply_token,
messages=[TextMessage(text=reply_text)]
)
messaging_api.reply_message(reply)
print("Reply sent and recorded (REPLY mode)")
この構造により、面影AIの根幹方針「記憶される本人には応答しない」がコード上でも明確に反映されるだけでなく、カテゴリによる記憶分類という設計思想が、実装全体に一貫して統合されています。
記憶対象者の user_id は .env ファイルで定義されており、アプリケーションの設定を変更するだけで、誰を記憶対象にするかを切り替えることが可能です。
この方式により、環境に応じて対象ユーザーを動的に変更できる柔軟な設計が実現されています。
Webhook受信とメッセージ処理の実装
LINEから送信されたメッセージは、LINE Developersで指定したWebhook URLにPOSTリクエストとして届きます。Flaskのルーティング機能を利用してこのリクエストを受け取り、署名検証を行ったうえで、ユーザーのテキストメッセージを抽出します。
この段階では、以下の要素が最低限必要となります。
・Python用 LINE SDK v3(linebot-sdk-python)の導入とインポート確認
・.env からアクセストークンとチャネルシークレットの読み込み
・テキストメッセージであることの判定(@handler.addによる条件登録)
@handler.add(MessageEvent, message=TextMessageContent) により、画像・スタンプなどは除外され、
テキストメッセージのみがChatGPT連携処理へと進みます。
ChatGPT連携部分の詳細実装
ChatGPTとの連携部分では、記憶DBに格納された情報を取得し、プロンプトに組み込んでChatGPT APIへリクエストを送信します。得られた応答はLINEへ返信されるだけでなく、ユーザーの発言とともに「新たな記憶」としてデータベースに保存されます。
記憶の選定ロジック
現時点の実装では、is_forgotten = 0 の記憶をすべて対象にChatGPTへプロンプトを構築しています。 将来的には以下のような条件に基づく選定ロジック(重要度や関連性の重み付け)を導入することで、 より精度の高い返答が可能になります。
これにより、面影AIの応答は常に関連性の高い情報をもとに生成されるようになります。
ChatGPTとの基本的な接続方法やAPI設定の流れは、過去記事に詳しくまとめています。すでに設定済みの読者は、このセクションからプロンプト設計に進んでください。
基準項目 | 選定条件 |
---|---|
importanceスコア | 閾値以上の記憶のみを対象とする |
発言の新しさ | 直近の対話履歴を優先して抽出 |
話題の一致 | 今回の入力文に含まれるキーワードと一致する記憶を抽出 |
【関連記事】
AI執事ボットのためのVPS環境構築マニュアル(LINE対応付き)
再現応答の生成プロンプト設計
プロンプトには記憶された情報だけでなく、「あなたは◯◯という人物の話し方を再現するAIです」といった文言を含めることで、ChatGPTの出力を特定の人格に寄せていきます。プロンプト設計では以下の要素が重要になります。
- 記憶DBからの抽出結果(複数行にまとめて提示)
- 応答のトーン(丁寧・フレンドリーなど)の指示
- ユーザーの過去発言に合わせた話題の流れ
こうしたプロンプトを構成することで、ChatGPTの出力が一貫性を持ち、かつ“あの人らしさ”を感じさせる再現性を実現できます。
面影AIを支える記憶中心のデータ構造とは?
面影AIでは、「記憶」を中心にすべてのデータが管理される構成を採用しています。 ユーザーの発言、記憶の評価、履歴の追跡まですべてが一貫した設計で記録され、Phase2以降の機能拡張にも柔軟に対応できる仕組みとなっています。
3つのテーブルはすべて memory_id でつながっている
面影AIで使用されるデータベースには、以下の3つのテーブルが存在します。
1. memories(記憶の本体)
- 主キー: memory_id
- 記憶そのもののテキスト、カテゴリ、重み(weight)、忘却フラグなどを含みます
- 全ての情報の中心に位置する、データ構造の核です
2. dialogues(LINEからの発言ログ)
- 外部キー: memory_id → memories.memory_id
- LINEで受信したユーザーの発言を記録します
- その発言が「どの記憶として保存されたか」を追跡するために紐づけます
3. weights(記憶の評価・履歴)
- 外部キー: memory_id → memories.memory_id
- 記憶に対する評価・変更・人間による再解釈の履歴などを記録します
- 将来的に記憶の再評価や優先度制御に活用される拡張テーブルです
テーブル同士の関係を視覚的に整理すると
記憶(memories)を中心に、発言(dialogues)と評価履歴(weights)が周囲に紐づいている構造です。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
+----------------+ | memories | |----------------| | memory_id (PK) | +----------------+ ▲ ┌──────────┴──────────┐ │ │ +----------------+ +----------------+ | dialogues | | weights | |----------------| |----------------| | memory_id (FK) | | memory_id (FK) | +----------------+ +----------------+ |
なぜこの構成が重要なのか?
面影AIの特徴は、「記録されたテキスト」だけでなく、それがどんな文脈で、どんな価値で保存されたのかまで含めて記録することにあります。 このテーブル設計により、以下がすべて可能になります:
- どの発言が、どの記憶に変換されたか追跡
- 記憶がいつ・どのように評価され直したか確認
- Phase2以降で重みや忘却フラグを活用し、柔軟な記憶の制御ができる
ただ記録するだけでなく、「意味を持って残す」ための構造。それがこの記憶中心のDB設計です。
Phase1テスト:記憶の登録とデータ構造の確認

ここでは、実際に面影AIの記録モード(Phase1)を稼働させ、LINEから送信した発言がどのように「記憶」として登録され、 またそれがどのようにデータベースに保存されたかをテストしました。 本機能が正しく機能しているかを確認する、重要な検証です。
テスト実行時のLINE送信とFlaskログ出力
開発サーバ上で面影AIを起動し、LINEから複数の自然文メッセージを送信しました。 Flaskのログ出力には、以下のようにユーザーIDの検出・カテゴリ分類・記憶登録が一連の流れとして出力されています。
1 2 3 4 5 6 7 8 9 10 11 |
user_id: Ueba19f7869e5320fac1292760f9b8bfa [LEARN] Received message from user_id: Ueba19f7869e5320fac1292760f9b8bfa [LEARN] MEMORY_TARGET_USER_ID: Ueba19f7869e5320fac1292760f9b8bfa Memory and dialogue registered. Memory recorded with category: 健康 Memory recorded with category: 仕事 Memory recorded with category: 感情 Memory recorded with category: 趣味 Memory recorded with category: 健康 Memory recorded with category: その他 |
データベースに保存された記憶の内容
SQLiteのメモリデータベース( memory.db)に対して、CLIから実際に中身を確認しました。 3つのテーブル memories, dialogues, weights の内容は以下の通りです。
📄 memories テーブル(記憶の本体)
1 2 3 4 5 6 7 |
memory_id content category weight 6 明日の天気ってどうなるんだろう。 その他 1 5 最近、夜なかなか眠れなくて困ってる。 健康 1 4 週末は久しぶりに釣りに行こうと思ってるんだ。 趣味 1 3 なんだか今日は気分が落ち込んでる。 感情 1 2 明日までに仕事の資料をまとめなきゃいけない。 仕事 1 1 最近、体の調子があまり良くないみたいなんだ。 健康 1 |
🗣️ dialogues テーブル(発言ログ)
1 2 3 4 5 6 7 |
dialogue_id user_id message memory_id 6 Ueba19f7869e5320fac1292760f9b8bfa 明日の天気ってどうなるんだろう。 6 5 Ueba19f7869e5320fac1292760f9b8bfa 最近、夜なかなか眠れなくて困ってる。 5 4 Ueba19f7869e5320fac1292760f9b8bfa 週末は久しぶりに釣りに行こうと思ってるんだ。 4 3 Ueba19f7869e5320fac1292760f9b8bfa なんだか今日は気分が落ち込んでる。 3 2 Ueba19f7869e5320fac1292760f9b8bfa 明日までに仕事の資料をまとめなきゃいけない。 2 1 Ueba19f7869e5320fac1292760f9b8bfa 最近、体の調子があまり良くないみたいなんだ。 1 |
⚖️ weights テーブル(記憶の重み履歴)
1 2 3 4 5 6 7 |
weight_id memory_id interact created_at 6 6 初期登録(weight=1) 2025-05-12 08:16:50 5 5 初期登録(weight=1) 2025-05-12 08:15:56 4 4 初期登録(weight=1) 2025-05-12 08:15:40 3 3 初期登録(weight=1) 2025-05-12 08:15:25 2 2 初期登録(weight=1) 2025-05-12 08:15:15 1 1 初期登録(weight=1) 2025-05-12 08:15:00 |
weights テーブルは、各記憶に対して「どのような評価・扱いをされたか」を履歴として記録するための補助テーブルです。 ユーザーによる強調指示、再評価、フィルタリング対象などの操作を記録しておくことで、将来的な記憶の再評価や抽出精度向上に活用できます。
テーブル構成(weights)
1 2 3 4 5 6 7 |
CREATE TABLE weights ( weight_id INTEGER PRIMARY KEY AUTOINCREMENT, memory_id INTEGER NOT NULL, interact TEXT, created_at TEXT DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (memory_id) REFERENCES memories(memory_id) ); |
記録される主な情報
- memory_id: 対象となる記憶のID
- interact: 操作・評価の内容(例:初期登録、再評価、優先付与など)
- created_at: その記録が保存された日時
なお、 importance や updated_at などのスコア要素は今後の設計次第で導入予定ですが、現時点ではテーブルに含まれていません。
このように、記憶DBに保存された過去の会話や登録フレーズが、ChatGPTのプロンプトに反映されることで“面影”としての一貫性が生まれます。
よくあるエラーと対処方法
面影AIの構築では、主に環境設定やAPI連携部分においてエラーが発生しやすい傾向があります。以下は、代表的なエラーとその対処法をまとめたものです。
エラー内容 | 原因 | 対処方法 |
---|---|---|
LINEから応答が返ってこない | Webhook URLが無効または未登録 | LINE DevelopersでURLを再設定し、正常なHTTPレスポンスを確認する |
ChatGPTからエラーが返る | APIキーの誤設定または制限超過 | .envファイルを確認し、gpt-4oのAPI使用条件を満たしているか確認する |
記憶が反映されない | memory_dbへの書き込み処理が正常に行われていない | DB書き込み関数と読み出しロジックのログを確認し、保存条件を見直す |
接続不良やWebhookの設定ミスについては、下記の構築マニュアルでも具体例を挙げて対処法を紹介しています。必要に応じて確認してください。
【関連記事】
AI執事ボットのためのVPS環境構築マニュアル(LINE対応付き)
これらのエラーは、事前に構成ミスを防ぐことで回避できるものが多いため、動作確認時にはサーバーログやAPIレスポンスのステータスコードを丁寧に追跡することが求められます。
次はいよいよ、記録した“面影”が語り出す──Phase2として蓄積された記憶をもとに応答を返すAIを実装していきます。
▶︎【Pythonの基礎知識】面影AIが応答する|Phase2:記憶をもとに返す言葉
【倫理的制限に関する注意】
本ソースコードには、性的・恋愛的・ロールプレイ的な応答を抑制するための明示的な制御処理が組み込まれています。
これらの制限を解除・改変した上で利用された場合、その結果生じるすべての責任は、改変および利用を行った個人に帰属するものとします。
本プロジェクト開発者は、意図的な制限解除による悪用・誤用・不適切な利用に対し、一切の責任を負いません。