第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文字ずつながれて出てくる「ストリーミング」を作りましょう。

第11章 ストリーミング — 1文字ずつ返す(SSE)へすすむ →

← 目次・はじめにへもどる