メモリと文脈管理

LLM は基本的に「呼ばれるたびに記憶ゼロ」。連続性を持たせるには履歴を渡し続けるか、 外部に永続メモリを置く必要がある。

Context Window

LLM が一度に読める最大トークン数。これを超えるとエラー or 古い部分を忘れる。

コンテキストが大きくても常に詰め込むのは非効率(コスト・遅延・精度低下)。 必要なものだけ選んで入れる。

メモリの 3 階層

階層意味
短期現在の会話履歴messages 配列
中期セッション内に必要なコンテキストシステム状態、現在のタスク状況
長期セッションを超えて永続する記憶ユーザープロファイル、過去の決定事項

1. 会話履歴の管理

素朴な実装

const history: Message[] = []

async function chat(userInput: string) {
  history.push({ role: "user", content: userInput })
  const res = await client.messages.create({
    model: "claude-sonnet-4-6",
    max_tokens: 1024,
    messages: history
  })
  history.push({ role: "assistant", content: res.content })
  return res.content
}

2. コンテキスト圧縮

履歴が長くなったら古い部分を要約して圧縮する。

async function compressIfNeeded(history: Message[]) {
  const tokens = await countTokens(history)
  if (tokens < 100_000) return history

  const oldHalf = history.slice(0, history.length / 2)
  const summary = await client.messages.create({
    model: "claude-haiku-4-5",
    max_tokens: 2000,
    system: "次の会話履歴を要点だけ箇条書きに要約",
    messages: [{ role: "user", content: JSON.stringify(oldHalf) }]
  })

  return [
    { role: "user", content: `これまでの要約:\n${summary.content[0].text}` },
    { role: "assistant", content: "理解しました。続けます。" },
    ...history.slice(history.length / 2)
  ]
}

圧縮のタイミング

3. 永続メモリ(Long-term Memory)

DB やファイルに事実を保存して、必要なときに引き出す。 Claude Code の ~/.claude/memory/ や OpenAI Memory のような仕組み。

実装パターン

  1. メモリ書き込み用のツールを定義
  2. メモリ読み出し用のツールを定義
  3. System Prompt で「重要事実は memory_save を呼べ」と指示
  4. 会話開始時に直近メモリを load
const tools = [
  {
    name: "memory_save",
    description: "今後の会話で使う事実を永続化",
    input_schema: {
      type: "object",
      properties: {
        key: { type: "string" },
        value: { type: "string" }
      },
      required: ["key", "value"]
    }
  },
  {
    name: "memory_recall",
    description: "保存済みの事実を取得",
    input_schema: {
      type: "object",
      properties: { key: { type: "string" } },
      required: ["key"]
    }
  }
]

4. ファイルベース・メモリ

Claude Code 方式: memory/MEMORY.md + 個別ファイルでカテゴリ分け。 テキストで読めるので人間が編集できる。

~/.claude/memory/
├── MEMORY.md           # インデックス
├── user_role.md        # ユーザーの役職
├── feedback_style.md   # フィードバック
├── project_xxx.md      # プロジェクト情報
└── reference_links.md  # 参照先
      

Prompt Caching を活かす

永続メモリ全部を System に積むならPrompt Cachingで再利用コストを下げる (→ LLM API の基礎)。

Sliding Window

単純に「直近 N ターン」だけ残す方式。実装が簡単だが、古い前提を忘れる。

選択的記憶

履歴全体ではなく、関連するメッセージだけを埋め込み検索で選んで context に入れる (→ RAG)。

状態管理(State Machine)

フォーム入力みたいに進捗が決まっているものは LLM に丸投げせずステートマシン化する。

state: "asking_name" → "asking_email" → "asking_phone" → "confirming" → "done"
      

各 state で LLM に求めるのは1 つの抽出だけに。安定性が桁違いに上がる。

マルチユーザー対応

サーバーで会話履歴を保持するなら、ユーザーごとにスコープを分離する。 Redis / Postgres / DurableObject など。

// 例: Redis にユーザー別履歴
const key = `chat:${userId}`
await redis.rpush(key, JSON.stringify(message))
await redis.expire(key, 60 * 60 * 24)
const history = (await redis.lrange(key, 0, -1)).map(JSON.parse)

セキュリティ

長文を扱うコツ

トークン削減のコツ

セッション復元

ブラウザを閉じて再開しても続きから話せるようにしたい場合:

原則

メモリは「毎回送る短期」と「必要なときに引く長期」を分ける。 長期メモリも全部読み込むのではなく、関連分だけ選ぶ仕組みを用意する。