第9章 溢れさせない — 要約(summary)+ログのハイブリッド

📖 この章のゴール:第8章で出た コンテキストの上限(入れられる量の天井) への王道の対処——古い会話をLLM自身に要約させて短くし、毎回渡すのは「設定+これまでの要約+直近のN件」だけにする方法を、実装できるようになる。 ← 目次・はじめにへもどる


📱 ChatGPTではこう見える

ChatGPTと 何時間も、何十往復も 話し続けても、あるところで急に「もう無理です」と止まったりはしません。会話がどれだけ伸びても、破綻せずに話が続いていきます。

でも第8章で学んだとおり、LLMに一度に入れられる量には 天井(コンテキスト上限) があります。会話をぜんぶ積み続ければ、いつか必ずあふれるはずです。なのに、なぜ続くのでしょう? 答えは「裏でうまく圧縮している」からです。


🤔 全部は送れない。だから「要点メモ+最近の発言」だけ送る(🟢 基礎)

第8章の結論は「会話が伸びると、毎回の messages がコンテキスト上限を超えてあふれる」でした。第4章でやった「毎回ぜんぶ送る」を、いつまでも続けられないのです。

そこで王道の対処はこうです。

古いやりとりは、LLM自身に「要約」させて短くまとめ、毎回渡すのは「設定(system)+これまでの要約+直近のN件」だけにする。

要約(summary)とは、長い会話を「ここまでの要点だけ」に縮めた短い文章のこと。LLMは文章を縮めるのが得意なので、要約づくりも LLMにお願い します。

🗂 たとえ話:分厚い議事録を持ち歩かない 100ページの会議録を毎回まるごと持ち歩く人はいません。ふつうは「ここまでの要点メモ(A4一枚)+ 最近の数発言」だけ手元に置きます。要点メモさえあれば話の流れは分かるし、最近の発言があれば「いま何の話か」も分かる。LLMに渡すのも、これと同じ「要点メモ+最近のやりとり」です。

大事なのは、会話の全文(ログ)は捨てないことです。

  • 要約:LLMに毎回渡す、短い「要点メモ」。あふれないための圧縮版。
  • ログ(全文):DBにそのまま残す、消さない記録。画面で会話を表示するのに使う。

この 「要約はLLMへ/全文はDBへ」を両方持つやり方 を、ハイブリッド(hybrid=組み合わせ)と呼びます。

🧠 背骨①そのもの:会話が続いて見えるのは、やはりLLMが覚えているからではありません。開発者が「要約+直近」という形で記憶を作り直して渡しているから。じつはこの教材を動かしている MulmoClaude 自身 も、まさにこの「要約+直近ログ」方式で長い会話を覚えています。


🛠 こう作る — messagesを「要約+直近N件」で組み立てる(🟢 基礎)

これまで(第5章)は、DBの発言を 古い順に全部 並べて messages を作っていました。これを 「要約を1個 + 直近N件だけ」 に差し替えます。

ステップ1:要約をconversationに保存できるようにする

-- conversations 表に「これまでの要約」を入れる列を足す
alter table conversations add column summary text not null default '';

1行ずつ読むと:

  • alter table conversations add column summary text:会話の表(第5章のお弁当箱)に summary(要約)の列 を1つ追加する。
  • not null default '':最初はまだ要約が無いので、空っぽ(空文字) を初期値にしておく。会話が伸びてから中身が入る。

ステップ2:渡すmessagesを「要約+直近N件」で組み立てる

const RECENT_COUNT = 10; // 直近で何件そのまま渡すか

// 1) 直近N件だけ、新しい順にとって、古い順に並べ直す
const { data: recent } = await supabase
  .from("messages")
  .select("role, content")
  .eq("conversation_id", conversationId)
  .order("created_at", { ascending: false })
  .limit(RECENT_COUNT);
const recentMessages = recent.reverse();

// 2) 要約を「system発言」として、直近の前に差し込む
const messages = [
  { role: "system", content: "あなたは親切なアシスタントです。" },
  { role: "system", content: `これまでの会話の要約:\n${summary}` },
  ...recentMessages,
  { role: "user", content: userText },
];

1行ずつ読むと:

  • const RECENT_COUNT = 10:直近で 何件そのまま渡すか を名前付きの定数にする(あとで調整する数。マジックナンバーにしない)。
  • .order("created_at", { ascending: false }).limit(RECENT_COUNT)新しい順 に並べて先頭から RECENT_COUNT 件——つまり 直近のN件だけ を取る(全部は取らない=あふれ対策)。
  • recent.reverse():取った直近N件を 古い順に並べ直す。LLMには会話の進んだ順で渡す必要があるため。
  • { role: "system", content: ... }(1個目):第4章と同じ、いちばん最初の 設定(性格・役割)
  • { role: "system", content: 'これまでの会話の要約:\n${summary}' }これまでの要約を system 発言として差し込む。「ここまでの話はこうだった」とLLMに前提を渡す役割。
  • ...recentMessages:その後ろに 直近N件 を並べる(要点メモの後に最近の発言、という順)。
  • { role: "user", content: userText }:最後に今回の新しい発言。これでLLMは「要約+直近+今の質問」を見て答えられる。

ステップ3:会話が伸びたら、要約を「差分だけ」更新する

要約は毎回作る必要はありません。会話が一定以上たまったときだけ、しかも 前回の要約+今回あふれた分だけ をまとめ直します。

// 「前回の要約」+「直近から外れて古くなった分」だけを、LLMにまとめ直させる
async function updateSummary(oldSummary: string, overflowed: Message[]): Promise<string> {
  const completion = await openai.chat.completions.create({
    model: "gpt-4o-mini",
    messages: [
      { role: "system", content: "次の要約と新しい発言を、短い要約にまとめ直して。固有名詞・決定事項は必ず残して。" },
      { role: "user", content: `これまでの要約:\n${oldSummary}\n\n新しい発言:\n${formatLog(overflowed)}` },
    ],
  });
  return completion.choices[0].message.content;
}

1行ずつ読むと:

  • async function updateSummary(oldSummary, overflowed)前回の要約と、直近N件から押し出されて古くなった発言の2つを受け取る関数。
  • model: "gpt-4o-mini":要約づくりも、いつもの安いモデルでLLMに頼む。
  • { role: "system", content: "...短い要約にまとめ直して。固有名詞・決定事項は必ず残して。" }消えると困る情報(名前・決めたこと)は残す よう指示する(後述のハマりどころ対策)。
  • これまでの要約:...\n\n新しい発言:...ゼロからではなく「前回要約+あふれた分」だけ を渡す。会話全体を毎回要約しないので 安く済む(差分だけ)。
  • return completion.choices[0].message.content:まとめ直した新しい要約の文章を返す。これを次のステップでDBに保存する。
// 返ってきた要約を conversation に保存し直す
await supabase
  .from("conversations")
  .update({ summary: newSummary })
  .eq("id", conversationId);

1行ずつ読むと:

  • .update({ summary: newSummary }):会話の summary 列を、まとめ直した新しい要約で 上書き保存 する。
  • .eq("id", conversationId)その会話の行だけ を更新する(取り違え防止)。次回ステップ2は、この更新後の要約を読んで使う。

⚠️ ハマりどころ

  • 要約しすぎて大事な情報が消える → 名前・日付・決めたこと(決定事項) まで縮めると、後の会話がちぐはぐに。要約のプロンプトで「固有名詞・決定事項は必ず残す」と明示する。
  • 要約のコストも課金される → 要約づくりも LLMの呼び出し=お金がかかる(第8章・付録D)。毎回ゼロから全文を要約すると高いので、前回要約+あふれた分の差分だけ にする。
  • 全文(ログ)まで消してしまう → 要約は「LLMに渡す圧縮版」であって、会話の記録ではありませんmessages の全文はDBに残し、画面表示はそちらを使う。
  • 🔧 直近を何件残すか・いつ要約するかの調整RECENT_COUNT を増やすと文脈は濃いがあふれやすく・高くなる。要約のタイミング(何件たまったらか)も含め、会話の長さと予算を見て決める チューニングどころ(応用)。

🤖 AIに頼むなら(Vibe codingのコツ)

🗣 プロンプト例: 「いまの会話を、古い会話を要約して conversations.summary に保存し、LLMに毎回渡すのは『system設定+これまでの要約+直近10件+今回の発言』だけにする形へ変えて。会話の全文(messages)はDBに残したままにして。要約の更新は 前回の要約+直近から外れた分の差分だけ をLLMにまとめ直させ、固有名詞・決定事項は残す ように指示して」

出てきたものの確認チェックリスト:

  • LLMに渡すのが 「要約+直近N件」だけ になっているか(古い発言を全部は積んでいないか)
  • 会話の 全文(messages)はDBに残っている か(要約のために消していないか)
  • 要約の更新が 差分だけ(前回要約+あふれた分)で、毎回ゼロから全文を要約していないか
  • 要約プロンプトに 「固有名詞・決定事項は残す」 が入っているか
  • 直近の件数が RECENT_COUNT のような 名前付き定数 で、後から調整できるか

📝 ことばメモ

  • 要約(summary):長い会話を「ここまでの要点」に縮めた短い文章。LLMに作らせ、毎回渡す圧縮版
  • ハイブリッド(hybrid):「要約はLLMへ/全文はDBへ」を両方持つやり方。あふれずに記録も残せる
  • 直近ウィンドウ(recent window):そのまま渡す「最近のN件」のかたまり。RECENT_COUNT で大きさを決める
  • 差分要約:会話全体ではなく「前回の要約+新しくあふれた分」だけをまとめ直すこと。安く済む

➡️ 次の章へ

これで、会話がどれだけ長くなっても あふれずに続けられる ようになりました。でもこの要約は、あくまで 「その1本の会話の中」 の記憶です。別の会話を開けば、また何も知らない状態に戻ります。

第10章では 会話をまたいで覚える=メモリ を扱います。「前に名前を伝えたら、別の会話でも覚えていてくれる」——そんな、本物のアシスタントらしい記憶のしくみです。

第10章 メモリ — 会話をまたいで覚える

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