メモリと文脈管理
LLM は基本的に「呼ばれるたびに記憶ゼロ」。連続性を持たせるには履歴を渡し続けるか、 外部に永続メモリを置く必要がある。
Context Window
LLM が一度に読める最大トークン数。これを超えるとエラー or 古い部分を忘れる。
- Claude Sonnet 4.6: 200K トークン
- Claude Opus 4.7 (1M): 1,000,000 トークン
- 1M = 日本語で約 50 万文字 ≒ 文庫本数冊
コンテキストが大きくても常に詰め込むのは非効率(コスト・遅延・精度低下)。 必要なものだけ選んで入れる。
メモリの 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)
]
}
圧縮のタイミング
- トークン上限の 80%に達したら
- 定期的(10 ターンごと)
- セッション境界(タスク完了時)
3. 永続メモリ(Long-term Memory)
DB やファイルに事実を保存して、必要なときに引き出す。
Claude Code の ~/.claude/memory/ や OpenAI Memory のような仕組み。
実装パターン
- メモリ書き込み用のツールを定義
- メモリ読み出し用のツールを定義
- System Prompt で「重要事実は memory_save を呼べ」と指示
- 会話開始時に直近メモリを 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)
セキュリティ
- 個人情報をそのまま記録しない(マスキング / 仮名化)
- ユーザーが「忘れて」と言ったら削除できる仕組みを用意(GDPR)
- メモリへの書き込み権限を絞る(注入攻撃対策)
長文を扱うコツ
- 章立てを保ったまま渡す(XML タグ / 見出し)
- 「Lost in the middle」現象 — 長文の中盤の情報は埋もれがち。冒頭と末尾に重要事項を置く
- 必要部分を抽出してから推論させる(要約 → 推論の 2 段階)
トークン削減のコツ
- tool_result の大きすぎる出力を要約してから履歴に入れる
- JSON よりYAML や Markdownの方がトークン効率が良いことも
- 不要な空白・コメント・タグ除去
- 古いツール呼び出しの結果を削除(要点だけ残す)
セッション復元
ブラウザを閉じて再開しても続きから話せるようにしたい場合:
- サーバー側に履歴保存
- session_id を URL/Cookie で持つ
- 再開時に履歴を loads → 必要なら圧縮 → 続行
メモリは「毎回送る短期」と「必要なときに引く長期」を分ける。 長期メモリも全部読み込むのではなく、関連分だけ選ぶ仕組みを用意する。