第10章 メモリ — 会話をまたいで覚える
📖 この章のゴール:会話の中の記憶(第9章)とは別に、ユーザーの“事実”を別保存して毎回ちょっとだけ差し込む「メモリ」を作れるようになる。背骨①🧠記憶のゴールです。
📱 ChatGPTではこう見える
ChatGPT(チャットジーピーティー)を使っていると、まったく別の日に、別の話題で話しかけても、こんなことが起きます。
- 「ぼくの名前、覚えてる?」→「もちろん、◯◯さんですね」
- 「いつものおすすめ教えて」→ あなたの好みに合わせて答えてくれる
新しい会話を始めたのに、前に話したあなた自身のことを覚えている。これが「メモリ」です。今日はこれを自分のクローンに足します。
🤔 「会話の記憶」と「メモリ」はどう違う?(🟢 基礎)
第9章でやった記憶は、1つの会話の中の記憶でした。会話が変われば消えます。
メモリは、それより一段“長持ち”する記憶です。
- 🧠 会話の記憶(第9章)=1回のおしゃべりの間だけ覚える(その場のメモ)
- 🧠 メモリ(この章)=会話をまたいでずっと覚える=ユーザーの事実(名前・好み・前提)を別のテーブル(保存場所)に書いておき、毎回 system(システム=AIへの最初の指示)にちょっとだけ差し込む
たとえ話:いつものカフェの店員さんが、常連さんの好み(「ホットで、ミルク多め」)をカルテ(メモ帳)に書いておく。来店のたびに、そっとそれを見てから接客する。会話そのものは毎回ゼロから始まっても、カルテのおかげで“覚えている”ように見えるのです。
ここで大事な背骨①🧠:LLM(エルエルエム=大きな言語モデル)は、自分では何も覚えません(ステートレス=状態を持たない)。覚える仕組みを作るのは、わたしたち開発者です。カルテ(メモリ)も、開発者が用意します。
そして背骨②🛡守り:カルテは「自分のカルテだけ」見えなければいけません。Supabase(スーパベース)の RLS(アールエルエス=行ごとのアクセス制限) で、他人のメモリを絶対に読めないようにします。
🛠 こう作る(🟢 基礎)
まず、メモリをしまう箱(テーブル)を作ります。Supabaseの SQL(エスキューエル=データベースへの命令)です。
create table memories (
user_id uuid not null default auth.uid(),
fact text not null,
created_at timestamptz not null default now()
);
alter table memories enable row level security;
create policy "own memories" on memories
for all using (auth.uid() = user_id);
1行ずつ読むと:
create table memories… 「memories(思い出)」という名前の保存箱を新しく作るuser_id uuid ...… 誰のメモリか(持ち主のID)。auth.uid()は「いまログイン中の人」を自動で入れるfact text not null… 覚える事実そのもの(例「名前は太郎」)。空はダメcreated_at ...… いつ覚えたかの時刻。now()で自動で入るenable row level security… 🛡 RLSをオン(鍵をかける)create policy ... using (auth.uid() = user_id)… 「持ち主が自分のときだけ読み書きOK」というルール
次に、①会話から事実を抜き出して保存します。LLMに「覚えるべき事実だけ抜き出して」とお願いします。
const extract = await openai.chat.completions.create({
model: "gpt-4o-mini",
messages: [
{ role: "system", content: "次の発言から、長く覚えるべき事実だけ短く抜き出して。なければ空に。" },
{ role: "user", content: userText },
],
});
const fact = extract.choices[0].message.content?.trim();
if (fact) await supabase.from("memories").insert({ fact });
1行ずつ読むと:
openai.chat.completions.create… OpenAI(オープンエーアイ)のGPTに考えてもらう呼び出しmodel: "gpt-4o-mini"… 使うモデル名(小さく速くて安いGPT)systemの指示 … 「覚えるべき事実だけ短く出して。なければ空っぽで」とお願いuser: userText… ユーザーが今いった言葉を渡すconst fact = ...content?.trim()… 返ってきた事実の文を取り出し、前後の空白を消すif (fact)… 中身があるときだけ(空っぽなら何もしない)insert({ fact })… memoriesテーブルに1行追加。user_idはRLSのauth.uid()が自動で入れる
最後に、②送信前に自分のメモリを読んで、systemに少し足す。
const { data } = await supabase.from("memories").select("fact");
const memo = data?.map((m) => `- ${m.fact}`).join("\n") ?? "";
const messages = [
{ role: "system", content: `あなたは親切なアシスタント。\n【ユーザーについて】\n${memo}` },
...history,
];
1行ずつ読むと:
supabase.from("memories").select("fact")… 🛡 RLSのおかげで自分のメモリだけ取り出すdata?.map(...).join("\n")… 事実を1行ずつ「- ◯◯」の箇条書きにまとめるconst messages = [...]… LLMに送る会話リスト([{role, content}]の形)systemの【ユーザーについて】${memo}… 最初の指示に、カルテの中身をそっと差し込む...history… そのあとに今までの会話を続ける
これで、別の会話でも「あなたのこと」を覚えているクローンの完成です。
⚠️ ハマりどころ
- 何でも覚えてしまう:全部の発言を保存すると箱がパンパンに(肥大)。長く役立つ事実だけにしぼる
- PII(ピーアイアイ=個人を特定できる情報)に注意:住所・電話番号・カード番号などを勝手に保存しない。機微(きび=デリケート)な情報は、同意なしに残さない
- 古い事実とケンカする:「東京住み」のあとに「大阪に引っ越した」。古いメモが残ると矛盾する。上書き・整理の仕組みがいる(最初は件数を絞るだけでもOK)
- メモリもトークン(ことばのかたまり=料金とサイズの単位)を食う:systemに山ほど足すと毎回コストUP。少しだけ差し込むのがコツ
🤖 AIに頼むなら(Vibe codingのコツ)
🗣 プロンプト例:「TypeScript + Express で、会話から長く覚えるべき事実だけをGPT(gpt-4o-mini)で抽出して Supabase の
memoriesテーブルに保存し、次回以降は自分のメモリを select して system に『【ユーザーについて】…』として差し込む処理を書いて。memories には RLS(auth.uid() = user_id)を必ず付けて、PII は保存しすぎないで。」
チェックリスト:
memoriesテーブルに RLS が付いていて、自分の行しか読めない- 抽出は「覚えるべき事実だけ、なければ空」になっている
- systemに差し込むメモリは少量(入れすぎていない)
- 住所・電話番号などの PII を不用意に保存していない
- 別の会話を新しく始めても、名前や好みを覚えている
📝 ことばメモ
- メモリ:会話をまたいで覚える、ユーザーの事実の保存。第9章の「会話の記憶」より長持ち
- 事実抽出(じじつちゅうしゅつ):発言から「長く覚える価値のあること」だけ取り出すこと
- PII(ピーアイアイ):個人を特定できる情報(名前・住所・電話番号など)。残しすぎ注意
- RLS(アールエルエス):行ごとのアクセス制限。「自分のデータだけ」を守る鍵(🛡守り)
➡️ 次の章へ
これで背骨①🧠記憶はひと区切り。次は体験をリッチにします。ChatGPTのように、答えが1文字ずつながれて出てくる「ストリーミング」を作りましょう。