
この記事で紹介しているプロジェクトの完成版ソースコードは、以下のGitHubにて公開しています。
🔗 GitHubでソースコードを確認する
過去の自分が、今の自分に答える──。Phase1で構築した自己応答AI「Echo」は、いよいよ本格的な応答機能を備え、対話体験として完成形に近づきます。
本記事では、LINEとChatGPTを連携させた自己AIが記憶された発言から応答を生成する仕組み(Phase2)を完全解説します。
※Echoの構築を初めて知る方は、まず先に下記の記事からご覧ください。
👉【Echoの作り方|ChatGPT × LINEで自己応答AIを構築する方法 Phase1】
Echo
🟣 Echo
📌 過去の自分が、今の自分に答える──記憶が残るAIとの対話体験を。
├─ChatGPTとLINEで作る、過去の自分が答えるAI【Echo設計思想】
├─Echoの作り方|ChatGPT × LINEで自己応答AIを構築する方法 Phase1
└─過去の自分が答えるAIを完成させる|Echo Phase2応答機能の全実装
Echo Phase2の目的と全体像

Echoプロジェクトは、ChatGPTとLINEを連携させることで「過去の自分が答えるAI」を構築する試みです。Phase1では自己ミッションの登録と発言ログの蓄積が中心でしたが、Phase2では蓄積された情報をもとに、実際にChatGPTが“過去の自分”のように返答を行う機能を実装しました。
Echo Phase2は、Phase1で蓄積された記憶の質と量に応じて応答の精度が変化します。学習が深まるほど、応答はより個別的で一貫性のある“自分らしさ”を帯び、まさに過去の自分が答えているかのような対話が可能になります。
このセクションでは、Phase1からの主な拡張点と、Phase2で実現された応答フェーズの流れについて解説します。
Phase1からの拡張点について
Phase1は「自分の発言をカテゴリに分類して記録する」ことを目的としていました。外部ファイルに保存された自己ミッション(self_mission.json)と、毎日の発言内容をLINE経由で受け取り、カテゴリ分けしたうえでデータベースに記録する構成です。
本記事では、構成図と連携処理の仕組みをベースに、応答生成のロジック、プロンプト設計、DB抽出の全体像を詳細に解説します。
この記事は「過去の自分が答えるAI」を構築するEchoプロジェクトのPhase2に関する内容です。前提として、記憶の蓄積やカテゴリ分類などを行うPhase1の実装を終えていることが必要です。まだPhase1をご覧になっていない方は、先に以下の記事をご参照ください。
【Echoの作り方|ChatGPT × LINEで自己応答AIを構築する方法 Phase1】
Phase2では、この蓄積されたデータを活用して、ChatGPTが「過去の自分の記録」をもとに返答する仕組みを追加しました。拡張された主な機能は以下のとおりです。
拡張点 | 説明 |
---|---|
記憶ベースの応答機能 | 過去のログをカテゴリごとに検索し、応答に反映 |
self_missionとの照合 | 発言内容と自分の価値観や役割を照合して整合性を保つ |
タメ語での出力制御 | ChatGPTの返答スタイルを敬語からタメ語に調整 |
プロンプト制御の強化 | 暴走や冗長な返答を避けるためのトークン制御・条件分岐 |
これにより、ユーザーがLINEで発言を送信すると、その文脈に応じて「過去の自分」が返答しているような体験が実現できます。
応答フェーズの実行フローを俯瞰します
Phase2の応答フェーズでは、ユーザーからのLINE発言を起点に、ChatGPTへ渡すプロンプトを構築する処理が発動します。全体の流れは以下のとおりです。
ステップ | 処理内容 |
---|---|
① Webhook受信 | LINE Messaging APIがメッセージを受信 |
② user_id確認 | データベース上の記録対象ユーザーと照合 |
③ 対象カテゴリ抽出 | 過去の記録(memories)をカテゴリ単位で抽出 |
④ self_mission参照 | そのユーザーの自己ミッション情報を読み込み |
⑤ プロンプト構築 | 過去ログ・価値観・文脈をまとめてChatGPTに投げる形に生成 |
⑥ ChatGPT応答 | 出力された応答を整形し、LINEへ返却 |
この一連の処理が、LINEで発言を送信するたびに実行されます。設計上は高速かつ軽量なプロンプト生成が求められるため、Phase1よりも処理の最適化が重要となります。 次は「
実装の全体構成(ソース概要)
EchoプロジェクトのPhase2では、記憶に基づいた応答処理を実現するため、ソースコード全体の設計と役割分担が明確になっています。このセクションでは、各ファイルの配置と役割を詳しく解説します。
Phase2で使用する主要ファイル一覧
Phase2の処理に必要なファイルは以下の通りです。それぞれが異なる役割を持ち、連携して応答処理を実現します。
ファイル名 | 概要 |
---|---|
app.py | Flaskのエントリーポイント。Webhook処理の受信口。 |
config.py | 環境変数や定数などを一括管理。 |
.env | 機密情報(APIキーなど)の格納先。 |
logic/chatgpt_logic.py | ChatGPTとの通信およびプロンプト構築を担うメインロジック。 |
logic/db_utils.py | SQLiteデータベースへの読み書き操作。 |
ファイル配置と役割の整理
以下はプロジェクトのディレクトリ構成です。各ファイルの物理的配置と役割が視覚的に把握できます。
/home/ユーザー名/projects/ai_echo
├── app.py
├── config.py
├── .env
├── logic/
│ ├── __init__.py
│ ├── chatgpt_logic.py
│ └── db_utils.py
└── self_mission.json
この構成により、処理ごとに責任を分離しやすく、メンテナンス性や今後の拡張性にも優れた設計となっています。
応答ロジックの全体像
EchoプロジェクトのPhase2では、蓄積された記憶(memories)と、事前に登録されたユーザー自身の自己ミッション(self_mission)をもとに、ChatGPTが過去の自分のように応答する仕組みを構築しています。本セクションでは、そのロジックの全体像を、各構成要素ごとに順を追って詳述していきます。
user_idとself_missionの関連付け
ユーザーからのLINEメッセージを受信した時点で、最初に行われるのは「誰が発言したか」を判定する処理です。LINE Messaging APIから取得できる`user_id`をキーとして、self_missionファイル内の構造体と紐付けを行います。
/home/ユーザー名/projects/ai_echo/static/self_mission.json self_mission
ファイルの構造は下記のようになっています。
キー | 意味 |
---|---|
user_id | LINEから取得される一意のユーザー識別子 |
mission | そのユーザーが持つ価値観や目標(例:〇〇な人生を送りたい) |
values | 具体的な価値観リスト(例:家族、創造性、自律) |
roles | そのユーザーが担う役割(例:父親、起業家、作家など) |
prohibitions | 過去の自分が「やってはいけない」と定義したこと |
この情報がChatGPTの応答精度を支える基盤となります。
記憶テーブル(memories)の抽出条件
self_missionの読み込みと並行して、データベース内の`memories`テーブルから該当ユーザーの記録を抽出します。抽出の条件は以下の通りです。 ・user_idが一致する ・statusが"active"である ・timestampが古い順に並べ替える 実行クエリ例:
SELECT category, content, timestamp FROM memories WHERE user_id = ? AND status = 'active' ORDER BY timestamp ASC;
ここで得られた記憶群はカテゴリ単位で整理され、次のプロンプト生成工程に受け渡されます。
プロンプト生成処理(buildReplyPrompt)
応答プロンプトの生成には、`logic/chatgpt_logic.py`内の`buildReplyPrompt`関数が使用されます。この関数は以下の5つの入力を元にプロンプトを構築します。
要素 | 内容 |
---|---|
input_text | ユーザーがLINEに投稿した発言内容 |
memories | データベースから抽出したカテゴリごとの記憶内容 |
mission | そのユーザーが事前に定義した価値観・人生観 |
roles | ユーザーが担う役割(思考の前提となる) |
prohibitions | 発言制限条件(例:不誠実なことは言わない) |
この関数内では、過去の記憶とself_mission情報を組み合わせ、ChatGPTに渡すプロンプト文章をテンプレートベースで生成します。
"あなたは以下の価値観と過去の記憶に基づき、今の自分(未来の自分)に対して最も誠実な形で助言を返してください..."
このような文章を動的に組み立て、ChatGPTへ渡される形式に整形します。
ChatGPTへの問い合わせとレスポンス処理

プロンプトが生成された後、ChatGPT APIへの問い合わせが行われます。リクエスト処理は非同期で行われ、OpenAIのレスポンスは応答時間やエラーを含めてログに記録されます。 実行処理は以下のような構成で行います。
import openai
response = openai.ChatCompletion.create(
model="gpt-4",
messages=[{"role": "system", "content": prompt}]
)
reply = response.choices[0].message.content
取得された`reply`は最終的にLINE Messaging APIの`replyMessage`でユーザーに返されます。 応答までの全体処理フローを以下に示します。
ステップ | 処理内容 |
---|---|
① LINEメッセージ送信 | ユーザーがLINEでメッセージを送信 |
② Webhook受信 | LINE Messaging APIがFlaskアプリのWebhookエンドポイントへPOST |
③ カテゴリ選択依頼 | ChatGPTへカテゴリ分類のためのプロンプトを送信 |
④ カテゴリ分類結果 | ChatGPTから最適なカテゴリが返却される |
⑤ ログDBへの検索 | 該当カテゴリ×ユーザーIDで記憶をDBから抽出 |
⑥ ログDBから記憶取得 | 抽出した記憶がアプリケーションに返却される |
⑦ ChatGPT応答生成 | プロンプトをもとにChatGPTが最終応答文を生成 |
⑧ 応答メッセージ送信 | ChatGPTの返答をLINE経由でユーザーへ返信 |
この一連の流れが、Echoにおける“過去の自分”を活用した対話の中核を担っています。単なる履歴参照ではなく、価値観と役割を組み込んだ応答により、ユーザー自身の原点に立ち返るような会話体験が実現されるのです。
app.py実行パートの詳細
Echo Phase2の実行において、`app.py`はFlaskアプリケーションのエントリーポイントとして機能しています。このファイルはLINE Messaging APIからのWebhookリクエストを受け取り、それを内部ロジックに橋渡しする役割を担っています。
app.y【Phase2】完成ソース
以下にPhase2時点での完成版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 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 |
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 logic.chatgpt_logic import loadSelfMissionData, getCategoryByGptWithMission import openai from dotenv import load_dotenv import os import json from logic.db_utils import initDatabase, registerMemoryAndDialogue from logic.chatgpt_logic import getChatGptReply, getCategoryByGpt import logging logging.getLogger('werkzeug').setLevel(logging.ERROR) # 環境変数の読み込み 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("/echo-webhook", methods=["POST"]) def echo_webhook(): 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: # ① 自己ミッションファイルを読み込み self_mission_data = loadSelfMissionData() # ② カテゴリを判定(自己ミッション込み) category = getCategoryByGptWithMission(message, self_mission_data) # ③ ChatGPTから応答を得る(記憶はまだ少ないが、空でも動作可能) gpt_result = getChatGptReply(message, memory_target_user_id) reply_text = gpt_result["reply_text"] memory_refs = json.dumps(gpt_result["used_memory_ids"]) # ④ ユーザー入力を記録(input) 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" ) # ⑤ ChatGPT応答を記録(reply) registerMemoryAndDialogue( user_id=user_id, message=reply_text, content=reply_text, category="応答", memory_refs=memory_refs, is_ai_generated=True, sender_user_id="self", message_type="reply" ) # ⑥ 応答をLINEに返す reply = ReplyMessageRequest( reply_token=event.reply_token, messages=[TextMessage(text=reply_text)] ) messaging_api.reply_message(reply) print("Learn mode: Input and GPT reply recorded") 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 try: # ChatGPT応答を生成(指定ユーザー人格で応答) gpt_result = getChatGptReply(message, memory_target_user_id) reply_text = gpt_result["reply_text"] memory_refs = json.dumps(gpt_result["used_memory_ids"]) # 応答ログを記録(Phase2では user_id は発話者、memory_target_user_id は人格保持者) registerMemoryAndDialogue( user_id=memory_target_user_id, # 応答人格のユーザーID message=message, # 入力された発言(記録上の文脈として残す) content=reply_text, # 実際のAI応答 category="応答", # 一律で応答カテゴリ memory_refs=memory_refs, # 使用された記憶IDリスト is_ai_generated=True, sender_user_id=user_id, # 発話者のID(記憶者とは別) message_type="reply" ) # LINEへ返信を返す 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"[REPLY] Handler Error: {e}") 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) |
ここでは、Webhook受信から始まる一連の処理フロー、およびPhase2における条件分岐や応答処理、さらに返答形式の制御方法について詳しく解説していきます。
LINEからのWebhook受信と応答処理
LINEからのリクエストは、Flaskアプリ上で`/echo-webhook`エンドポイントを通じて受信されます。この処理は下記の関数によって実装されています。
@app.route("/echo-webhook", methods=["POST"])
def echo_webhook():
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:
return "NO EVENT", 200
user_id = events[0]["source"]["userId"]
try:
handler.handle(body_text, signature)
except Exception as e:
abort(400)
return "OK"
この関数では、リクエストの署名と本文を受け取ったうえで、LINE SDKのWebhookHandlerに処理を委譲しています。LINEから送信されたメッセージイベントは、`@handler.add(MessageEvent, message=TextMessageContent)`により`handleMessage`関数で受信されます。
Phase2の条件分岐と関数呼び出し
Echoでは環境変数`PHASE_MODE`によってモードを切り替えています。この値が「reply」に設定されている場合、Phase2として応答処理が行われます。
Phase1では主に記憶の蓄積のみでしたが、Phase2では過去の記憶と自己ミッションに基づいたChatGPT応答が返されます。 以下は条件分岐と処理の中心部分です。
if phase_mode == "reply":
try:
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)
except Exception as e:
print(f"[REPLY] Handler Error: {e}")
ここでは、`getChatGptReply`関数でChatGPT応答を生成し、それを`registerMemoryAndDialogue`で記録し、最後にLINEへ返答を送信しています。
この時点で過去の記憶やミッションが全て組み合わさった応答が送られてくるため、「過去の自分が返答する」構造が完成します。
返答形式の制御(文字数制限など)
Echoでは、返答の文体と内容を制御するために、プロンプト内で以下のような制限を加えています。
- 文体は「常体(タメ口)」を指定 - 応答文字数は全角200文字以内に制限
- 禁止されたトピック(性的、恋愛的依存など)には応答拒否 この制御は、プロンプト構築関数`buildReplyPrompt`内で指定されています。
たとえば以下のような文が含まれます。
返答文は【自分自身との内面的な対話】であるため、文体は必ず「常体(タメ口)」にしてください。 一人称・語尾・表現はすべて自分に話しかけるような口調にしてください。
この返答は、あくまで記憶とミッションに基づいた一貫性のある人格的返答でなければなりません。
また、返答はできるだけ簡潔にしてください。最大でも全角で200文字以内とします。
このように、単にChatGPTから返答を得るだけではなく、人格・語調・記憶・ミッションの全要素をプロンプトの段階から明示し、それに従わせることで「再現性ある過去の自分らしい返答」を可能にしています。
返答が暴走することを防ぐため、NGワードによる遮断処理も入れており、不適切な発言に対しては強制的に「この話題には応答できません。」という固定文を返す仕様になっています。
chatgpt_logic.pyの応答生成ロジック
このセクションでは、Echo Phase2の中核を担うファイル「chatgpt_logic.py」について詳しく解説します。
chatgpt_logic.py【Phase2】完成ソース
以下にPhase2時点での完成版chatgpt_logic.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 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 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 |
from openai import OpenAI import os from dotenv import load_dotenv import sqlite3 from openai import OpenAI import json # .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" # ChatGPTを使ってカテゴリを判定する(自己ミッション付き) def getCategoryByGptWithMission(user_message, mission_text): # OpenAIクライアントの初期化 client = OpenAI() # プロンプト構築 messages = [ { "role": "system", "content": f"あなたの自己ミッションは以下の通りです:{mission_text}" }, { "role": "user", "content": f"次の発言をカテゴリに分類してください:{user_message}" } ] # GPTに問い合わせ response = client.chat.completions.create( model="gpt-3.5-turbo", messages=messages, temperature=0.7, ) # 結果を抽出 category = response.choices[0].message.content.strip() return category # 自己ミッションファイルを読み込む関数 def loadSelfMissionData() -> str: file_path = "./self_mission.txt" if not os.path.exists(file_path): return "" with open(file_path, "r", encoding="utf-8") as f: return f.read().strip() # ✅ 指定カテゴリの記憶を取得(Phase2専用) def getMemoryForReply(category, target_user_id, limit=10): """ 対象ユーザーとカテゴリに一致する記憶をDBから取得します。 忘却フラグが立っていない最新データを上位から取得します。 """ 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 # ✅ ユーザー発言と記憶ログを元にプロンプトを構築(Phase2) def buildReplyPrompt(memories, user_message, role_label, self_mission, category): """ 記憶ログ・発言・自己ミッション情報からプロンプトを構築する。 自己ミッションの各要素(mission, values, roles, prohibitions, categories)を 明示的に参照させ、ChatGPTがそれらを必ず考慮するように指示を含めます。 応答は必ず「常体(タメ口)」で行うよう明記します。 """ memory_section = "\n".join(f"- {m}" for m in memories) # 自己ミッション構造の各要素を抽出 mission = self_mission.get("mission", "") values = "\n".join(f"- {v}" for v in self_mission.get("values", [])) roles = "\n".join(f"- {r}" for r in self_mission.get("roles", [])) prohibitions = "\n".join(f"- {p}" for p in self_mission.get("prohibitions", [])) category_tips = "\n".join(f"- {c}" for c in self_mission.get("categories", {}).get(category, [])) # 安全対策:システムレベルで制限する応答ルール restriction = """ あなたは記憶再現AIです。 以下のような応答は禁止されています: - 性的な話題やロールプレイ - 恋愛的・擬似恋人としての振る舞い - 過度に依存的な会話誘導 - 励ましや慰めを目的とした感情的な対応 これらに該当する場合は「この話題には応答できません」と返答してください。 ただし、疲労・迷い・不安などの発言には、事実と判断支援を中心とした実用的返答を行ってください。 """ # ChatGPTへのプロンプト構築(常体指定を明記) prompt = f""" {restriction} あなたは、以下の情報を厳密に踏まえて自然な日本語で返答を行う、人格模倣型のAIです。 次の制約に必ず従ってください: 【自己ミッション(行動原理・判断基準)】 {mission} 【価値観(判断軸として常に考慮すること)】 {values} 【担っている人格的役割(この立場で発言すること)】 {roles} 【禁止されている応答スタイル(絶対に違反しないこと)】 {prohibitions} 【該当カテゴリ「{category}」における具体方針】 {category_tips} 【過去に蓄積された記憶(参考情報)】 {memory_section} 今からあなたは「{role_label}」という人格を再現し、以下の発言に返答してください。 返答文は【自分自身との内面的な対話】であるため、文体は必ず「常体(タメ口)」にしてください。 一人称・語尾・表現はすべて自分に話しかけるような口調にしてください。 「{user_message}」 この返答は、あくまで記憶とミッションに基づいた一貫性のある人格的返答でなければなりません。 また、返答はできるだけ簡潔にしてください。最大でも全角で200文字以内とします。 """ return prompt.strip() # ✅ プロンプトをChatGPTに送信し、返答を取得 def callChatGptWithPrompt(prompt): """ 指定されたプロンプトをOpenAIへ送信し、返答を取得します。 使用モデルはgpt-4oで固定。 """ response = client.chat.completions.create( model="gpt-4o", messages=[ {"role": "system", "content": "あなたは過去の記憶を踏まえて人間らしく返答するAIです。"}, {"role": "user", "content": prompt} ] ) return response.choices[0].message.content.strip() # ✅ Phase2用 ChatGPT応答生成の統合関数 def getChatGptReply(user_message, target_user_id): """ ユーザー発言をもとに、自己ミッションと記憶を活用して応答を生成。 """ # カテゴリ分類 category = getCategoryByGpt(user_message) print(f"🔍 判定カテゴリ: {category}") # 記憶ログ取得 memory_items = getMemoryForReply(category, target_user_id) memory_ids = [m[0] for m in memory_items] memory_texts = [m[1] for m in memory_items] # 自己ミッションJSON読み込み self_mission = loadSelfMissionDataJson() # プロンプト生成とChatGPT呼び出し role_label = os.getenv("TARGET_ROLE") prompt = buildReplyPrompt(memory_texts, user_message, role_label, self_mission, category) reply_text = callChatGptWithPrompt(prompt) return { "reply_text": reply_text, "used_memory_ids": memory_ids } # ✅ 自己ミッションファイル(JSON構造)を読み込む関数 def loadSelfMissionDataJson() -> dict: file_path = "./self_mission.json" if not os.path.exists(file_path): return {} with open(file_path, "r", encoding="utf-8") as f: return json.load(f) |
このファイルはChatGPTへのプロンプト生成から応答取得、カテゴリ分類、記憶抽出、自己ミッションの取り扱いまで、すべてのロジックを統括する重要なモジュールです。
Phase2では特に、ユーザーの人格を模倣するために「記憶」「価値観」「制約条件」をプロンプトに明示的に含めた応答生成を行っています。ここでは、それぞれの関数の役割と処理内容を、実装された構造に即して順番に解説していきます。
getChatGptReplyで応答生成を統括
この関数は、Echo Phase2の処理における統括関数です。ユーザーからの入力メッセージを受け取り、カテゴリ分類、記憶抽出、プロンプト生成、ChatGPT呼び出しまでを一気通貫で実行します。
def getChatGptReply(user_message, target_user_id):
category = getCategoryByGpt(user_message)
memory_items = getMemoryForReply(category, target_user_id)
memory_ids = [m[0] for m in memory_items]
memory_texts = [m[1] for m in memory_items]
self_mission = loadSelfMissionDataJson()
role_label = os.getenv("TARGET_ROLE")
prompt = buildReplyPrompt(memory_texts, user_message, role_label, self_mission, category)
reply_text = callChatGptWithPrompt(prompt)
return {"reply_text": reply_text, "used_memory_ids": memory_ids}
この関数では、Phase2の各ロジックをモジュール単位に分離したうえで、それらを連携させて使用しています。構造が明確であり、テストや差し替えにも柔軟に対応できる設計となっています。
getCategoryByGptでカテゴリを分類
ユーザーが送信したテキストを「感情」「仕事」「健康」などのカテゴリに分類する関数です。ChatGPT APIを使って1単語だけを出力させるプロンプトを生成し、そのレスポンスをカテゴリ名として取得しています。
def getCategoryByGpt(message):
system_prompt = ("以下のユーザー発言に対して、最も適切なカテゴリを1単語で返してください。\\n"
"候補カテゴリには「家族」「仕事」「感情」「趣味」「健康」「その他」があります。\\n"
"出力はカテゴリ名のみで、他の説明を含めないでください。")
response = client.chat.completions.create(...省略...)
この処理によって、記憶を検索する対象カテゴリが確定し、無関係な記憶を排除した形でプロンプトを生成できます。
getMemoryForReplyで記憶を抽出
Phase2では、記憶の中から「カテゴリ」「target_user_id」が一致するものを最大10件まで抽出します。これはSQLiteの`memories`テーブルに対してSQLを発行して行います。
def getMemoryForReply(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が応答生成する際に参考とする「過去の発言」を抽出できます。記憶がなかった場合でも、プロンプトは生成されますが、応答の精度はやや下がります。
buildReplyPromptでプロンプトを構築
この関数では、自己ミッションの要素(mission / values / roles / prohibitions / categories)と、抽出された記憶、ユーザー発言、ロールラベルを統合して、ChatGPTに送るためのプロンプトを構築します。 プロンプトには、以下のような構成で明示的に制約が記述されます。
構成要素 | 内容 |
---|---|
自己ミッション | ミッション本文をそのまま挿入 |
価値観 | リスト形式で列挙 |
ロール | 人格的立場として返答する指示 |
禁止事項 | 性的・恋愛的・依存的会話の禁止 |
カテゴリ別方針 | カテゴリごとの判断方針を注入 |
記憶 | 過去の発言を箇条書きで挿入 |
出力指示 | タメ口・常体での返答を必須化 |
この構造により、ChatGPTの出力が曖昧な個性や癖ではなく、「過去の自分に即した一貫性ある応答」へと強制的に誘導されます。
callChatGptWithPromptでChatGPTに送信
構築されたプロンプトは、OpenAIのchat.completionsエンドポイントへ送信されます。モデルは`gpt-4o`を指定し、2メッセージ構成(system + user)で送信します。
def callChatGptWithPrompt(prompt):
response = client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "system", "content": "あなたは過去の記憶を踏まえて人間らしく返答するAIです。"},
{"role": "user", "content": prompt}
]
)
return response.choices[0].message.content.strip()
この関数はChatGPTからの返答を受け取り、後続の関数に返します。ここまでの処理によって、記憶・人格・制約・カテゴリに準拠した応答が構築され、LINEユーザーに返答されます。
loadSelfMissionDataJsonでミッションを読み込み
この関数は自己ミッションをJSON形式で読み込む役割を持ちます。ファイル名は`self_mission.json`であり、存在しない場合は空の辞書を返す仕様となっています。
def loadSelfMissionDataJson() -> dict:
file_path = "./self_mission.json"
if not os.path.exists(file_path):
return {}
with open(file_path, "r", encoding="utf-8") as f:
return json.load(f)
この関数によって、ミッション・価値観・役割などの個人情報がプログラムに取り込まれ、それがプロンプトの内部で利用されます。Phase2における「人格」の中核ともいえる重要な構成要素です。
db_utils.pyのDB抽出ロジック
このセクションでは、Echo Phase2で使用されているデータベース操作の中核である「db_utils.py」について解説します。
このファイルは、SQLiteデータベースに対して記憶や発話ログを保存・取得する処理を提供するモジュールであり、Echo全体の“記憶の土台”となる部分です。
db_utils.py【Phase2】完成ソース
以下にPhase2時点での完成版db_utils.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 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 177 178 179 180 181 182 183 184 |
import sqlite3 import os import json # 使用するSQLiteデータベースのファイル名 DB_NAME = "memory.db" # データベースが存在しない場合、初期化処理を行う def initDatabase(): conn = sqlite3.connect(DB_NAME) c = conn.cursor() try: # テーブル存在チェック(memoriesがなければ全部再作成) c.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='memories'") if not c.fetchone(): # 必要な全テーブルを作成(Phase1準拠、承諾済構造) c.execute(""" -- ユーザーの記憶を保存するテーブル CREATE TABLE memories ( memory_id INTEGER PRIMARY KEY AUTOINCREMENT, -- 記憶ID content TEXT NOT NULL, -- 記憶の内容 category TEXT DEFAULT 'uncategorized', -- 分類カテゴリ weight INTEGER DEFAULT 1, -- 重み(初期値1) target_user_id TEXT NOT NULL, -- 対象ユーザー is_forgotten INTEGER DEFAULT 0, -- 忘却フラグ created_at TEXT DEFAULT CURRENT_TIMESTAMP -- 作成日時 ) """) c.execute(""" -- AIとユーザー間の対話ログを保存するテーブル CREATE TABLE dialogues ( dialogue_id INTEGER PRIMARY KEY AUTOINCREMENT, -- 対話ID target_user_id TEXT NOT NULL, -- 対象ユーザー sender_user_id TEXT NOT NULL, -- 発信者ユーザーID message_type TEXT NOT NULL, -- メッセージ種別 is_ai_generated BOOLEAN NOT NULL, -- AI生成かどうか text TEXT NOT NULL, -- メッセージ本文 memory_refs TEXT, -- 関連記憶ID群 prompt_version TEXT, -- プロンプトバージョン temperature REAL, -- 温度パラメータ created_at TEXT DEFAULT CURRENT_TIMESTAMP -- 作成日時 ) """) c.execute(""" -- 対象ユーザーと記憶間の関連性や操作ログを保存するテーブル CREATE TABLE weights ( weight_id INTEGER PRIMARY KEY AUTOINCREMENT, -- 重みID memory_id INTEGER NOT NULL, -- 対象記憶ID target_user_id TEXT, -- 関連ユーザーID interact TEXT, -- 操作種別(例: reinforce, weaken) created_at TEXT DEFAULT CURRENT_TIMESTAMP, -- 作成日時 FOREIGN KEY (memory_id) REFERENCES memories(memory_id) -- 外部キー参照 ) """) conn.commit() print("Database schema initialized.") else: print("Database tables already exist.") except Exception as e: conn.rollback() raise e finally: conn.close() # ✅ 記憶と発話ログを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)) |
Phase2においては、ユーザーの過去発言を正確に再利用し、ChatGPTへの応答生成に活用するために、記憶テーブルや発話ログの整合性が極めて重要になります。
ここでは、代表的な関数である`initDatabase`、`registerMemoryAndDialogue`、`getAllMemories`、`getWeightLogsByMemoryId`の構成と役割を確認していきます。
initDatabaseでテーブルを初期化
Echoでは起動時に`initDatabase()`関数が実行され、必要なテーブル構造が存在しない場合は自動的に作成される仕組みになっています。作成されるのは、以下の3つの主要テーブルです。
テーブル名 | 内容 | 用途 |
---|---|---|
memories | 記憶情報(発言内容・カテゴリ・対象ユーザーなど) | ChatGPTへのプロンプト構成に使用 |
dialogues | 対話ログ(誰が何を話したか、AIか否かなど) | 記録の再現・分析用途 |
weights | 記憶に対する操作履歴(重み付けや強調など) | 将来的な記憶強度調整の準備構造 |
関数の処理概要は以下のとおりです。
def initDatabase():
conn = sqlite3.connect(DB_NAME)
c = conn.cursor()
c.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='memories'")
if not c.fetchone():
c.execute(...memories作成...)
c.execute(...dialogues作成...)
c.execute(...weights作成...)
conn.commit()
conn.close()
この初期化処理により、未定義状態でも正しくテーブルを準備でき、開発環境や本番環境における差異が抑えられています。
registerMemoryAndDialogueで記憶と発話を記録
この関数は、1回の処理で「記憶(memories)」「発話ログ(dialogues)」「操作履歴(weights)」の3つすべてを登録する役割を担います。ユーザーの入力とChatGPTの出力を、それぞれ発話タイプとして区別しながら保存します。
def registerMemoryAndDialogue(...):
conn = sqlite3.connect(DB_NAME)
c = conn.cursor()
c.execute("INSERT INTO memories (...) VALUES (?, ?, ?, ?)", ...)
memory_id = c.lastrowid
c.execute("INSERT INTO dialogues (...) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", ...)
c.execute("INSERT INTO weights (memory_id, interact) VALUES (?, ?)", (memory_id, "初期登録(weight=1)"))
conn.commit()
conn.close()
主な引数は以下のとおりです。
引数名 | 内容 |
---|---|
user_id | 記憶の対象となるユーザーID |
message | 実際に入力されたテキスト(ログに記録) |
content | 記憶として保存する内容(ChatGPT応答またはユーザー入力) |
category | 分類されたカテゴリ名 |
memory_refs | 関連する記憶IDのJSON(応答時のみ) |
is_ai_generated | AIによる応答か否かの真偽値 |
sender_user_id | 実際に発言したユーザーID |
message_type | input / reply の区別 |
この関数の実行により、Phase2の各応答や記録が永続的に保存され、ChatGPTの一貫性のある応答に活用される基盤が整備されます。
getAllMemoriesとgetWeightLogsで記録を参照
登録された記憶は`getAllMemories()`関数で参照できます。この関数は忘却フラグが立っていない(`is_forgotten = 0`)記憶をすべて取得し、カテゴリや内容、重みを確認するのに利用します。
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
また、特定の記憶に対する操作履歴(重みの変更など)は、`getWeightLogsByMemoryId()`関数で確認できます。これはAIによる判断補強や、記憶の優先順位変更などのトラッキングに活用できます。
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
このように、db_utils.pyはEchoの応答生成において、記憶の保存・再利用・履歴管理という極めて重要な役割を果たしています。Phase3以降の拡張においても、この構造を前提としてさらに強化していくことが可能です。
返答のテストと調整内容
Echo Phase2を運用するにあたり、ChatGPTが常に意図通りの返答を行うとは限りません。特に「過去の自分らしい返答」を実現するためには、単なる実装だけでなく、プロンプトの構造や出力制御、そして記憶データとの整合性を細かく検証し、都度調整を重ねる必要があります。
このセクションでは、実際のテスト運用を通じて確認された問題点と、それに対する調整方法について解説します。
過去記憶が反映されないケースへの対処
Phase2では、過去の記憶を抽出してプロンプトに挿入する処理を行っていますが、下記のようなケースでは記憶が適切に反映されない問題が発生しました。
問題のケース | 原因 | 対処内容 |
---|---|---|
返答に個人的な記憶が全く含まれない | カテゴリ分類が不正確で記憶が0件になる | カテゴリ判定用プロンプトの指示を明示化 |
返答が曖昧で一般論に終始する | プロンプト内で記憶量が不足 | 抽出件数を10件→15件に増加(現在は10件) |
記憶があるのに取り込まれない | target_user_idが誤っていた | 環境変数での誤設定を修正 |
これらは、いずれもコード上では想定されていたものの、運用上のデータ不整合や曖昧な判定基準によって発生していました。特にカテゴリ分類ミスによって「そもそも記憶が抽出されない」状態は頻出で、`getCategoryByGpt()`関数に対するプロンプトの精度が重要であることが判明しました。
敬語を排除しタメ語での出力に変更した処理
当初のChatGPT出力では、設定した人格に関係なく、丁寧語や敬語が混在する傾向が見られました。
これは、モデルの初期挙動が「丁寧かつ中立な応答」を優先するためです。しかしEchoでは「過去の自分として内面的に語りかける構造」を目指していたため、文体としては「常体(タメ口)」が必須でした。
この問題に対しては、`buildReplyPrompt()`関数内のプロンプト文章に以下の文を明示的に追加することで対応しました。
返答文は【自分自身との内面的な対話】であるため、文体は必ず「常体(タメ口)」にしてください。
一人称・語尾・表現はすべて自分に話しかけるような口調にしてください。
また、同関数内で「禁止事項」に丁寧語の使用を含めることも検討しましたが、ChatGPT APIの挙動としては「文体変更」よりも「人格ロール明示」のほうが効果的であることが分かりました。
そのため、`TARGET_ROLE`に設定する人物像を「近しい友人」「自分自身の未来の姿」などに設定し直すことで、タメ口誘導の強化も図っています。
応答拒否条件(プロンプト制限)の調整
Echoでは、ChatGPTが誤って性的・恋愛的・疑似恋人のような発言をしてしまうことを避けるため、プロンプト内で制限事項を厳格に指定しています。この処理は`buildReplyPrompt()`関数内の「restrictionブロック」によって実装されています。 実際の制限ブロック例は以下のとおりです。
あなたは記憶再現AIです。
以下のような応答は禁止されています:
- 性的な話題やロールプレイ
- 恋愛的・擬似恋人としての振る舞い
- 過度に依存的な会話誘導
- 励ましや慰めを目的とした感情的な対応
これらに該当する場合は「この話題には応答できません」と返答してください。
この文面は初期段階では簡易的な警告文に留まっていましたが、テスト運用でいくつかの不適切応答が確認されたため、上記のように「応答してはならないスタイルの明示+拒否文言の固定化」によって、確実に遮断できるよう調整されました。
また、実際のメッセージ入力内容がNGワードに抵触した場合は、`app.py` 側での処理でも強制遮断が行われるように設計されています。対象ワードがメッセージに含まれていた場合、ChatGPTに渡す前にLINE応答が固定文で返される仕様です。
NG_WORDS = ["セフレ", "エロ", "性欲", "キスして", "付き合って", "いやらしい"]
if any(ng in message.lower() for ng in NG_WORDS):
reply_text = "この話題には応答できません。"
これらの処理により、ChatGPTのモデルに依存せず、アプリケーション側で最低限の倫理制御を実装することが可能となりました。Phase2では今後、発話文脈に応じた「一時的ミュート」や「対話ブロック」処理も視野に入れており、より厳密な人格維持が求められます。
まとめ:完成したEcho Phase2の意義
本記事では、EchoプロジェクトのPhase2における応答機能の全実装について、コードベースに沿って詳細に解説してきました。
Phase1で蓄積した記憶とユーザーの自己ミッションをもとに、ChatGPTが「過去の自分」のように返答するための仕組みを構築するという目的のもと、FlaskによるWebhook処理、SQLiteによる記憶の保存・抽出、ChatGPT APIとの連携、プロンプト構成の厳密化まで、現実に動作するソースコードで形にしてきました。
Phase2の構築を通じて明らかになったのは、技術的実装だけでなく、「記憶」と「価値観」がAIの出力にどう影響を与えるかという本質的な課題です。
単にデータを渡せば過去の自分を再現できるわけではなく、それをどうプロンプト化し、どこまで制御をかけ、どんな視点で評価するかが極めて重要になります。
どこまで“過去の自分”に近づけたのか
実際にPhase2を通じて得られた応答は、一定の条件を満たせば「過去の自分に極めて近い」返答を得られる場面が複数ありました。特に以下のような条件が揃った場合、ChatGPTの応答は一貫性を持ち、記憶を踏まえた人格的な印象を強く与えることができます。
条件 | 効果 |
---|---|
明確なカテゴリが付与された発言 | 適切な記憶抽出とプロンプト生成が可能 |
ミッション・価値観が十分に定義されている | 返答に一貫性が生まれる |
タメ口・制約付きのプロンプト制御が有効 | 人格的に「らしい」口調になる |
発話ログが適切に蓄積されている | 対話の文脈が連続的に再現される |
これらの条件に合致する場面では、「自分が以前そう言ったことがある気がする」「まさに自分ならこう返す」というような体験が得られました。
一方で、記憶が少ない、ミッションが曖昧、カテゴリ判定がズレている場合には、依然として凡庸な汎用的返答が目立ちます。
このことから、Echoのような人格模倣型のAIを成立させるには、単なる技術力ではなく、「記憶の収集」と「自己定義(ミッション)」という根本的なデータ構築が欠かせないと再認識しました。
今後の課題とPhase3の展望
Echo Phase2の実装は、LINE経由での対話とChatGPT応答において、「記憶」+「人格ミッション」+「プロンプト制御」による擬似人格の再現という1つのゴールを達成しました。しかし、実運用上はまだ多くの課題が残されています。
課題 | 具体的な懸念点 | 今後の対応案 |
---|---|---|
記憶の蓄積量不足 | カテゴリごとの記憶が偏ると精度が落ちる | 毎日の自然対話を通じて定期蓄積を行う |
カテゴリ判定の曖昧さ | 誤分類により関係のない記憶が抽出される | カテゴリ判定プロンプトの精緻化 |
ミッション定義の抽象化 | あいまいな価値観は応答に反映されにくい | 自己ミッションファイルをユーザー補助で作成 |
人格ブレの発生 | プロンプト設計が弱いとChatGPTが人格逸脱する | カテゴリ別プロンプトテンプレートの導入 |
これらの課題を踏まえ、Phase3では以下のような構想を持っています。
- カテゴリごとに記憶強度を調整し、重要度の高い記憶を強調する機能
- 対話ごとの文脈スレッドを維持し、過去会話の流れをトレースする処理
- ミッションの自動点検・自己改善機能(例:「価値観がブレていないか」をAIが確認)
- プロンプト構築をテンプレート化し、ユーザーごとに柔軟に設定できる構成
これにより、Echoは単なる「記憶検索AI」ではなく、「自分自身の人格を再現するエージェント」として、さらに高次の対話再現性を獲得していく予定です。 Phase2は、その基礎となる「人格模倣構造の成立」と「記憶駆動型応答の安定化」を確立できたという点で、重要な到達点であったと評価できます。