2026.06.16
Claudeのメモリ機能を調査してみた
こんにちは。次世代システム研究室のL.W.です。 最近 Claude のアプリを触っていて、過去の会話を勝手に踏まえて返答してくることに気づきました。いわゆる「メモリ機能」です。さらに調べると、Claude には消費者向けのメモリと、API の memory ツールという二系統があり、仕組みがまったく違うことが分かりました。 今回はこの二系統を整理し、API の memory ツールを Node.js で実装して挙動を追ってみます。
1.Claudeのメモリ機能を使ってみた
1.1 二系統のメモリ
「メモリ」とひとことで言っても、Claude には実装の異なる二つの仕組みがあります。最初にここを切り分けておかないと話が混ざります。
| 消費者向けメモリ(claude.ai / アプリ) | API memory ツール | |
|---|---|---|
| 対象 | エンドユーザー | 開発者(エージェント実装者) |
| 保存実体 | Anthropic 側(サーバー) | 自分のインフラ(クライアント側) |
| 中身 | 過去チャットの要約・抽出された事実 | /memories 配下のファイル群 |
| 更新 | 約24時間ごとにバックグラウンドで合成 | タスク実行中にリアルタイムで読み書き |
1.2 消費者向けメモリ(claude.ai / アプリ)
こちらは設定 → Capabilities にトグルがあるおなじみの機能です。挙動を調べた限り、ポイントは以下の通りです。- 会話のトランスクリプトを丸ごと持っているわけではない。過去チャットから抽出した事実・好みを構造化して保持し、約24時間ごとに合成(synthesis)を更新します。
- トグルは二つに分かれています。「Generate memory from chat history(チャット履歴からメモリを生成)」と「Search and reference past chats(過去チャットを検索・参照)」です。
- メモリを参照したときは引用(citation)で元のチャットにリンクが貼られ、その会話を個別に削除できます。透明性が高いのが特徴です。
- Project 内のチャットはこの合成の対象外で、スコープが分離されています。
- Incognito / Temporary Chat ではメモリは生成されません。
1.3 API memory ツール
2025年9月29日にリリースされたmemory_20250818 ツールです。発想がシンプルで、「モデルは読み書きの指示(tool_use)を出すだけ。実際の保存は自分のインフラ側で行う」というクライアントサイド設計になっています。RAG のようにベクトル DB を組む必要がなく、Claude が /memories というディレクトリをファイルシステムのように操作します。 ベータヘッダー context-management-2025-06-27 を付けることで有効化でき、Claude API・Amazon Bedrock・Google Cloud Vertex AI で使えます。ZDR(Zero Data Retention)対象でもあります。 2. memoryツールの仕組み
2.1 6つのコマンド
memory ツールでモデルが発行できるコマンドは6つです。これは text editor ツールとよく似たファイル操作のセットで、Claude はこの種の操作で十分に訓練されているため、追加の細かい指示なしで自然に使いこなします。
| command | 役割 |
|---|---|
view |
ディレクトリ一覧 or ファイル内容(行番号付き)を表示 |
create |
新規ファイル作成 |
str_replace |
ファイル内の文字列を置換 |
insert |
指定行に挿入 |
delete |
ファイル / ディレクトリを削除 |
rename |
リネーム / 移動 |
2.2 やり取りの流れ
memory ツールを有効にすると、システムプロンプトに「何かする前に必ずメモリディレクトリを view せよ」という指示が自動で挿入されます。実際の往復は公式ドキュメントによると以下のようになります。タスク開始 → view /memories → 関連ファイルを view → 中身を踏まえて回答、という流れです。
# 1. ユーザー
"Help me respond to this customer service ticket."
# 2. Claude が memory を view(自動でディレクトリを確認)
{
"type": "tool_use",
"name": "memory",
"input": { "command": "view", "path": "/memories" }
}
# 3. アプリ側がディレクトリ内容を返す(tool_result)
{
"type": "tool_result",
"content": "Here're the files ... \n4.0K\t/memories\n1.5K\t/memories/customer_service_guidelines.xml"
}
# 4. Claude が関連ファイルを view
{
"type": "tool_use",
"name": "memory",
"input": { "command": "view", "path": "/memories/customer_service_guidelines.xml" }
}
# 5. アプリ側がファイル内容を返す → 6. その内容を踏まえて回答
重要なのは、3 と 5 の「返す」処理を実装するのは自分だという点です。保存先がローカルファイルでも DB でも暗号化ストレージでも、tool_result の文字列さえ仕様通り返せば動きます。ここを Node.js で書いてみます。
3. Node.jsで実装してみた
3.1 コード
3.1.1 memory ストア(クライアント側ハンドラ)
まずは /memories を実ディスクの ./memory_store にマッピングするストアを作ります。パストラバーサル対策は必須なので、/memories 配下に解決されないパスは弾きます。
import fs from "fs";
import path from "path";
// /memories を実ディスクの ./memory_store にマッピング
const MEMORY_ROOT = path.resolve("./memory_store");
// パストラバーサル対策:/memories 配下に解決されないパスは弾く
function resolveMemoryPath(memPath) {
if (typeof memPath !== "string" || !memPath.startsWith("/memories")) {
throw new Error(`Invalid path: ${memPath} (must start with /memories)`);
}
const rel = memPath.replace(/^\/memories/, "");
const abs = path.resolve(MEMORY_ROOT, "." + rel);
// 正規化後に MEMORY_ROOT の外を指していたら拒否(../ や %2e%2e%2f 対策)
if (abs !== MEMORY_ROOT && !abs.startsWith(MEMORY_ROOT + path.sep)) {
throw new Error(`Path traversal detected: ${memPath}`);
}
return abs;
}
// 6コマンドの実装。返す文字列は公式仕様に合わせる
export function handleMemoryCommand(input) {
switch (input.command) {
case "view": {
const abs = resolveMemoryPath(input.path);
if (!fs.existsSync(abs)) {
return `The path ${input.path} does not exist. Please provide a valid path.`;
}
if (fs.statSync(abs).isDirectory()) {
const lines = fs.readdirSync(abs).map((name) => {
const size = fs.statSync(path.join(abs, name)).size;
return `${size}\t${input.path}/${name}`;
});
return `Here're the files and directories in ${input.path}:\n${lines.join("\n")}`;
}
const numbered = fs
.readFileSync(abs, "utf8")
.split("\n")
.map((line, i) => `${String(i + 1).padStart(6)}\t${line}`)
.join("\n");
return `Here's the content of ${input.path} with line numbers:\n${numbered}`;
}
case "create": {
const abs = resolveMemoryPath(input.path);
fs.mkdirSync(path.dirname(abs), { recursive: true });
fs.writeFileSync(abs, input.file_text, "utf8");
return `File created successfully at: ${input.path}`;
}
case "str_replace": {
const abs = resolveMemoryPath(input.path);
const text = fs.readFileSync(abs, "utf8");
if (!text.includes(input.old_str)) {
return `No replacement was performed, old_str \`${input.old_str}\` did not appear verbatim in ${input.path}.`;
}
fs.writeFileSync(abs, text.replace(input.old_str, input.new_str), "utf8");
return "The memory file has been edited.";
}
case "insert": {
const abs = resolveMemoryPath(input.path);
const lines = fs.readFileSync(abs, "utf8").split("\n");
lines.splice(input.insert_line, 0, input.insert_text.replace(/\n$/, ""));
fs.writeFileSync(abs, lines.join("\n"), "utf8");
return `The file ${input.path} has been edited.`;
}
case "delete": {
const abs = resolveMemoryPath(input.path);
fs.rmSync(abs, { recursive: true, force: true });
return `Successfully deleted ${input.path}`;
}
case "rename": {
const from = resolveMemoryPath(input.old_path);
const to = resolveMemoryPath(input.new_path);
if (fs.existsSync(to)) return `Error: The destination ${input.new_path} already exists`;
fs.mkdirSync(path.dirname(to), { recursive: true });
fs.renameSync(from, to);
return `Successfully renamed ${input.old_path} to ${input.new_path}`;
}
default:
return `Unknown command: ${input.command}`;
}
}
3.1.2 エージェントループ
あとは、Claude の応答に tool_use(name: “memory”)が含まれる限りハンドラを呼んで結果を返す、というループを回すだけです。betas にベータヘッダーを、tools に memory_20250818 を渡します。
import Anthropic from "@anthropic-ai/sdk";
import { handleMemoryCommand } from "./memory-store.js";
const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
async function runWithMemory(userInput) {
const messages = [{ role: "user", content: userInput }];
while (true) {
const resp = await client.beta.messages.create({
model: "claude-opus-4-8",
max_tokens: 2048,
messages,
tools: [{ type: "memory_20250818", name: "memory" }],
betas: ["context-management-2025-06-27"],
});
messages.push({ role: "assistant", content: resp.content });
// tool_use が無ければ最終回答
if (resp.stop_reason !== "tool_use") {
const text = resp.content
.filter((b) => b.type === "text")
.map((b) => b.text)
.join("");
console.log("Assistant:", text);
return text;
}
// memory ツール呼び出しを処理して tool_result を返す
const toolResults = [];
for (const block of resp.content) {
if (block.type === "tool_use" && block.name === "memory") {
console.log(`[memory] ${block.input.command} ${block.input.path ?? block.input.old_path ?? ""}`);
let content;
try {
content = handleMemoryCommand(block.input);
} catch (e) {
content = `Error: ${e.message}`;
}
toolResults.push({ type: "tool_result", tool_use_id: block.id, content });
}
}
messages.push({ role: "user", content: toolResults });
}
}
// セッション1:好みを伝える(別プロセスでも ./memory_store が残る)
await runWithMemory(
"僕の名前はlwです。AWSのECS Blue/Greenデプロイをよく扱います。覚えておいてください。"
);
// セッション2:会話履歴を渡さず、メモリだけで思い出せるか確認
await runWithMemory("僕の名前と、よく扱うデプロイ方式は何だっけ?");
3.1.3 ポイント
messages配列は会話履歴そのものです。セッション2 ではセッション1 の会話を渡していません。にもかかわらず思い出せるなら、それはディスク上の/memoriesから復元できているということです。- ベクトル DB も埋め込みも出てきません。ファイル I/O だけです。
3.1.4 実行トレース
上記を実行すると、Claude はまず必ず view /memories を打ち、その後に create でメモリを書き込みます。セッション2 では view でファイルを読み戻してから回答します。コマンドのトレースは以下のようになります。
===== セッション1 ===== [memory] view /memories [memory] create /memories/user_profile.xml Assistant: 覚えました。lwさん、AWSのECS Blue/Greenデプロイをよく扱う、と記録しました。 ===== セッション2(会話履歴は渡していない)===== [memory] view /memories [memory] view /memories/user_profile.xml Assistant: お名前はlwさん、よく扱うのはAWS ECSのBlue/Greenデプロイですね。
このとき ./memory_store/user_profile.xml には、概ね次のような構造化メモが書き込まれます(Claude は XML やマークダウンの箇条書きで整理する傾向があります)。
<user_profile>
<name>lw</name>
<expertise>
<item>AWS ECS Blue/Green デプロイ</item>
</expertise>
</user_profile>
3.1.5 分析
保存はあくまで自分のディスク
セッション2 が会話履歴なしで成立するのは、合成や要約ではなく自分が書いたファイルをそのまま読み戻しているからです。消費者向けメモリ(約24時間ごとの合成・サーバー側)とは、ここが決定的に違います。リアルタイムかつ可逆で、中身は完全に自分の管理下にあります。
セキュリティが実装者責任になる
クライアントサイドである代償として、安全性は実装側に降りてきます。最低限おさえるべきは以下です。
- パストラバーサル:
../や URL エンコード(%2e%2e%2f)で/memoriesの外に出られないよう、正規化後のパスを必ず検証する(3.1.1 のresolveMemoryPath)。 - 機微情報:Claude は通常パスワードやカード番号をメモリに書くのを避けますが、過信せず書き込み前に自前でフィルタするのが安全です。
- ファイル肥大化:長時間タスクで Claude が追記し続けると膨らみます。読み取り上限を設けてページングさせる、定期的に古いファイルを掃除する、などの対策を。
- マルチテナント:ユーザーごとに
MEMORY_ROOTをBASE / user_idへ切り替え、ユーザー間でファイルが見えないようにする。
context editing / compaction との相性
memory ツールは context editing(古い tool_result のクリア)や compaction(サーバー側で会話を要約)と組み合わせる前提で設計されています。長い会話でコンテキストが詰まっても、重要な情報はメモリに退避しておけば失われません。Anthropic の社内評価では、context editing との併用で100ターンの Web 検索タスクにおいてトークン約84%削減・性能39%改善という数字が出ています。ただしこれは自己申告のベンチマークなので、実際の効果は自分のワークロードで検証すべきです。
4.まとめ
「Claudeのメモリ」は一枚岩ではなく、サーバー側で合成される消費者向けメモリと、クライアント側のファイルとして自分が管理する API の memory ツールという、目的も実装も異なる二系統でした。
1. 二系統の整理
| 消費者向けメモリ | API memory ツール | |
|---|---|---|
| 保存場所 | Anthropic 側 | 自分のインフラ |
| 中身 | 合成された事実・好み | /memories のファイル |
| 更新タイミング | 約24時間ごと(非同期) | タスク中にリアルタイム |
| 制御主体 | 設定トグル / 引用・削除 | 実装側(コードで完全制御) |
| 有効化 | Settings → Capabilities | ベータヘッダー + tools 指定 |
2. RAG ではなく「ファイルシステム」を選んだ理由
memory ツールが key-value ストアやベクトル DB ではなくファイル操作として設計されているのは意図的です。Claude は view / create / str_replace といったファイル操作で大量に訓練済みで、追加の指示なしに「いつ・何を・どう保存するか」を判断できます。だから実装側はストレージのバックエンドさえ用意すれば、メモリ運用のロジックはモデルに任せられます。
3. 実装者にとっての勘所
クライアントサイドは「自由」と「責任」の表裏です。パストラバーサル検証・機微情報フィルタ・ファイル肥大化対策・マルチテナント分離はこちらの実装に降りてくるので、PoC の段階から組み込んでおくのが安全です。逆にそこさえ押さえれば、暗号化ストレージや既存 DB にそのまま載せられる柔軟性があります。
4. どちらを使うか
エンドユーザーとして claude.ai を使うなら、設定トグルをオンにして引用・削除で透明性を保ちながら使うのが手軽です。一方、自前のエージェントに「セッションをまたいで学習・継続する」能力を持たせたいなら API の memory ツール一択で、context editing / compaction と組み合わせると長時間タスクで効いてきます。 これから Claude Code やエージェント用途での /memory 運用パターン(初期化セッション + 進捗ログ)も掘ってみたいです。 皆さんもぜひ、自分のエージェントに永続メモリを足してみてください。
「会話履歴を渡さなくても思い出せる」状態を一度作ると、設計の引き出しが一気に増えます。
5.最後に
次世代システム研究室では、グループ全体のインテグレーションを支援してくれるアーキテクトを募集しています。アプリケーション開発者の方、次世代システム研究室にご興味を持って頂ける方がいらっしゃいましたら、ぜひ募集職種一覧からご応募をお願いします。 皆さんのご応募をお待ちしています。
グループ研究開発本部の最新情報をTwitterで配信中です。ぜひフォローください。
Follow @GMO_RD


