エージェントにとって「記憶」とはなにか
AI エージェントを開発するとき、最初の壁になりやすいのが**「セッションを超えた記憶の維持」**です。会話型の LLM はコンテキストウィンドウの範囲内でしか情報を保持できません。ユーザーが翌日また起動したとき、前の会話内容は失われてしまいます。
Antigravity はエージェントアーキテクチャを柔軟に組めるプラットフォームですが、メモリ設計を正しく行わないと、せっかく高度なエージェントを作っても「毎回ゼロから始まる」という体験をユーザーに与えてしまいます。
3つのメモリパターンと、それを Antigravity で実装する具体的な方法を順番にご紹介します。私自身、2014年から個人開発を続ける中で複数サイトの運用補助エージェントに「前回までの判断」を引き継がせる仕組みを育ててきました。記憶の持たせ方を1段階整理するだけで、エージェントの使い勝手は見違えます。
メモリの3つのレイヤー
エージェントのメモリは用途に応じて3つのレイヤーに分けて考えると整理しやすくなります:
1. ワーキングメモリ(短期) 現在のセッション内での会話履歴。コンテキストウィンドウがこれに相当します。Antigravity の標準的な会話履歴管理で対応できます。
2. エピソード記憶(中期) 「先週このユーザーが〇〇と言った」「このプロジェクトでは△△を選択した」という具体的な過去のやりとり。セッションをまたいで保持する必要があります。
3. セマンティック記憶(長期) 「このユーザーはReactを好む」「このプロジェクトはTypeScript製」といった抽象化された事実・知識。最も長期に渡って参照されます。
パターン 1: エピソード記憶 — 過去の会話を構造化保存
最もシンプルなアプローチは、各セッションの要点を構造化されたデータとしてDBに保存し、次回起動時に読み込む方式です。
実装例:Supabase を使ったエピソード記憶
// memory/episode-store.ts
import { createClient } from '@supabase/supabase-js';
interface Episode {
id: string;
userId: string;
sessionId: string;
summary: string;
keyFacts: string[];
timestamp: string;
embedding?: number[];
}
const supabase = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_KEY!
);
export async function saveEpisode(episode: Omit<Episode, 'id'>): Promise<void> {
await supabase.from('episodes').insert(episode);
}
export async function recallRecentEpisodes(
userId: string,
limit = 5
): Promise<Episode[]> {
const { data } = await supabase
.from('episodes')
.select('*')
.eq('userId', userId)
.order('timestamp', { ascending: false })
.limit(limit);
return data || [];
}セッション終了時に要約を保存するエージェントスキル
// skills/summarize-and-save.ts
export async function summarizeAndSave(
conversationHistory: Message[],
userId: string,
sessionId: string
): Promise<void> {
// Claude に要約させる
const summary = await callClaude(`
以下の会話を200字以内で要約し、重要な事実をJSON配列として抽出してください。
会話履歴:
${conversationHistory.map(m => `${m.role}: ${m.content}`).join('\n')}
レスポンス形式:
{
"summary": "...",
"keyFacts": ["...", "..."]
}
`);
const parsed = JSON.parse(summary);
await saveEpisode({
userId,
sessionId,
summary: parsed.summary,
keyFacts: parsed.keyFacts,
timestamp: new Date().toISOString(),
});
}パターン 2: セマンティック記憶 — ベクトルDBで意味検索
エピソード記憶が「いつ何があったか」を保存するのに対し、セマンティック記憶は**「ユーザーについて何を知っているか」**を保存します。テキストをベクトル化して保存し、関連する記憶を意味的に検索します。
pgvector を使った実装
// memory/semantic-store.ts
interface SemanticMemory {
id: string;
userId: string;
content: string;
embedding: number[];
createdAt: string;
}
export async function embedAndStore(
userId: string,
fact: string
): Promise<void> {
// OpenAI Embeddings API でベクトル化
const embedding = await getEmbedding(fact);
await supabase.from('semantic_memories').insert({
userId,
content: fact,
embedding,
createdAt: new Date().toISOString(),
});
}
export async function recallRelevant(
userId: string,
query: string,
topK = 5
): Promise<SemanticMemory[]> {
const queryEmbedding = await getEmbedding(query);
// pgvector の <=> 演算子でコサイン類似度検索
const { data } = await supabase.rpc('match_memories', {
query_embedding: queryEmbedding,
match_user_id: userId,
match_count: topK,
});
return data || [];
}Supabase のカスタム関数(SQL)
CREATE OR REPLACE FUNCTION match_memories(
query_embedding vector(1536),
match_user_id text,
match_count int DEFAULT 5
)
RETURNS TABLE(
id uuid,
content text,
similarity float
)
LANGUAGE sql
AS $$
SELECT
id,
content,
1 - (embedding <=> query_embedding) AS similarity
FROM semantic_memories
WHERE user_id = match_user_id
ORDER BY embedding <=> query_embedding
LIMIT match_count;
$$;パターン 3: メモリを使ったプロンプト拡張
記憶を持っていても、エージェントのプロンプトに適切に組み込まなければ意味がありません。Antigravity のエージェント定義でこれを実現する方法:
// agent/memory-augmented-agent.ts
export async function buildSystemPrompt(userId: string, currentQuery: string): Promise<string> {
// 並列でメモリを取得
const [recentEpisodes, relevantMemories] = await Promise.all([
recallRecentEpisodes(userId, 3),
recallRelevant(userId, currentQuery, 5),
]);
const episodeContext = recentEpisodes.length > 0
? `## 最近のセッション記録\n${recentEpisodes.map(e => `- ${e.summary}`).join('\n')}`
: '';
const memoryContext = relevantMemories.length > 0
? `## 関連する記憶\n${relevantMemories.map(m => `- ${m.content}`).join('\n')}`
: '';
return `あなたはユーザーのパーソナルAIアシスタントです。
${episodeContext}
${memoryContext}
上記の記憶を参考にしながら、ユーザーを一貫して支援してください。
新しい重要な事実が出てきた場合は、それをメモとして記録することを提案してください。`;
}メモリの自動更新戦略
記憶は「書き込む」だけでなく「更新」「削除」「重みづけ」も必要です。古い情報が邪魔になることを防ぐための戦略を3つ紹介します:
1. TTL(有効期限)付きエピソード
// 30日経過したエピソードは自動削除
const EPISODE_TTL_DAYS = 30;
export async function cleanOldEpisodes(userId: string): Promise<void> {
const cutoff = new Date();
cutoff.setDate(cutoff.getDate() - EPISODE_TTL_DAYS);
await supabase
.from('episodes')
.delete()
.eq('userId', userId)
.lt('timestamp', cutoff.toISOString());
}2. 重複排除(Deduplication)
新しい記憶を保存する前に、既存の類似した記憶がないかチェックします:
export async function storeIfNovelFact(userId: string, fact: string): Promise<boolean> {
const similar = await recallRelevant(userId, fact, 1);
if (similar.length > 0 && similar[0].similarity > 0.92) {
console.log('重複する記憶をスキップ:', fact);
return false;
}
await embedAndStore(userId, fact);
return true;
}3. 重要度スコアによる優先度管理
interface MemoryWithScore extends SemanticMemory {
importanceScore: number;
accessCount: number;
lastAccessedAt: string;
}
// アクセスされるたびに重要度を更新
export async function accessMemory(memoryId: string): Promise<void> {
await supabase.rpc('increment_importance', { memory_id: memoryId });
}Antigravity での統合: SKILL.md によるメモリスキル定義
Antigravity のスキルシステムを使えば、メモリ管理をエージェントに自律的に行わせることができます:
# Memory Manager Skill
## Description
ユーザーとのやりとりから重要な情報を抽出し、永続的なメモリに保存・参照するスキル。
## Triggers
- ユーザーが「覚えておいて」「メモして」と言ったとき
- 会話の中で新しいユーザープリファレンスが判明したとき
- セッション開始時・終了時
## Actions
1. **recall**: 現在のクエリに関連する記憶を取得
2. **store**: 新しい事実をセマンティック記憶に保存
3. **summarize**: セッション終了時にエピソード記憶を保存
4. **forget**: ユーザーが「忘れて」と言ったときに記憶を削除どこから実装を始めるか
3層を一度に作る必要はありません。体感的に効果が大きい順は「エピソード記憶 → プロンプト拡張 → セマンティック記憶」です。セッション終了時の要約保存(パターン1)と、その読み込み(パターン3)だけで「毎回ゼロから」の摩擦はほぼ消えます。ベクトルDBによる意味検索は、記憶が数百件を超えて「直近5件では足りない」と感じてからで遅くありません。
運用面で一つだけ先回りしておくと、記憶の「削除」導線(ユーザーが「忘れて」と言える仕組み)は後付けしづらいので、最初のスキーマ設計時に入れておくことをお勧めします。実装の参考になれば幸いです。