跳转到主内容

Claude Memory Tool 实战:让 Agent 跨会话记住用户的完整指南

Claude memory tool 是客户端存储驱动的持久化记忆能力,6 个命令实现跨会话信息留存,配合 context editing 在长任务里实测 84% token 节省。本文给出原理、最小可运行 Python 实现、与 RAG 的区别、安全防护和国内接入方式。

开发指南memory记忆预计阅读15分钟
2026.05.22 发表
Claude Memory Tool 实战:让 Agent 跨会话记住用户的完整指南

Claude Memory Tool 实战:让 Agent 跨会话记住用户的完整指南

构建过 Agent 的人都遇到过这个问题:用户第一次说"我喜欢 dark mode,回答简短一点",下一次会话模型完全不记得。塞进 system prompt 治标不治本,自己搭 RAG 又是一坨工程量。

Anthropic 在 2025 年 9 月 29 日开放了 memory_20250818 工具(官方文档),用一种比"自己搓 RAG"轻得多的方式解决这件事——它让 Claude 把记忆当成本地文件来读写。配合 context editing,Anthropic 内部评测在 100 轮 web search 任务上拿到了 84% 的 token 节省39% 的效果提升

本文按"原理 → 命令 schema → 最小实现 → 与 RAG 对比 → 安全 → 国内接入"的顺序展开,文末给出可直接复制的完整代码。

一、Memory Tool 的核心机制:客户端存储 + 文件系统比喻

理解 memory tool 只要记住一句话:模型只发"读写指令",真正的存储在你这边

这个架构带来三个直接好处:

维度 含义
数据主权 用户数据从不离开你的服务器,存哪、加密不加密、保留多久全你说了算
可审计 所有 memory 操作都是可见的 tool_use/tool_result 块,可以直接 log 出来
存储自由 本地文件、PostgreSQL、Redis、S3、加密文件——继承一个抽象类就行

Anthropic 把记忆设计成"文件系统"而不是"key-value"是有讲究的——Claude 已经被训练过非常熟悉文件操作(view / create / str_replace),不需要额外指令。系统 prompt 里 Anthropic 还自动注入一句:“IMPORTANT: ALWAYS VIEW YOUR MEMORY DIRECTORY BEFORE DOING ANYTHING ELSE.”——这意味着每次会话开始模型都会主动查记忆。

二、6 个命令:完整的 Schema 与 Tool Result 约定

memory tool 的命令集很克制,只有 6 个,但够用。下面是模型会发给你的 JSON 结构与你必须返回的约定:

2.1 view — 查看目录或文件

{
  "command": "view",
  "path": "/memories",
  "view_range": [1, 10]
}
{
  "command": "view",
  "path": "/memories",
  "view_range": [1, 10]
}
  • 路径是目录 → 返回文件列表(含大小)
  • 路径是文件 → 返回内容;view_range 可选,1-based,闭区间
  • view_range: [-1, -1] 表示最后一行

2.2 create — 创建或覆盖文件

{
  "command": "create",
  "path": "/memories/preferences.md",
  "file_text": "## User Preferences\n- Theme: dark\n- Verbose: false\n"
}
{
  "command": "create",
  "path": "/memories/preferences.md",
  "file_text": "## User Preferences\n- Theme: dark\n- Verbose: false\n"
}

不存在则创建,存在则整体覆盖——所以 Claude 想"追加"时实际会先 view 再 create。

2.3 str_replace — 精确字符串替换

{
  "command": "str_replace",
  "path": "/memories/preferences.md",
  "old_str": "Theme: dark",
  "new_str": "Theme: light"
}
{
  "command": "str_replace",
  "path": "/memories/preferences.md",
  "old_str": "Theme: dark",
  "new_str": "Theme: light"
}

old_str 在文件里必须唯一,否则你应该返回错误:

No replacement was performed. Multiple occurrences of old_str ... Please ensure it is unique

不照这个文本格式返回,模型会重试不收敛。

2.4 insert — 在指定行插入

{
  "command": "insert",
  "path": "/memories/todo.md",
  "insert_line": 2,
  "insert_text": "- [ ] Review memory tool docs\n"
}
{
  "command": "insert",
  "path": "/memories/todo.md",
  "insert_line": 2,
  "insert_text": "- [ ] Review memory tool docs\n"
}

insert_line 是 0-based(在第 0 行前插入 = 文件开头)。越界返回:

Error: Invalid insert_line parameter: ... It should be within the range of lines of the file: [0, N]

2.5 delete — 递归删除

{ "command": "delete", "path": "/memories/old-notes" }
{ "command": "delete", "path": "/memories/old-notes" }

文件直接删,目录递归删——所以路径校验是安全防线(见第五节)。

2.6 rename — 重命名/移动

{ "command": "rename", "old_path": "/memories/draft.md", "new_path": "/memories/final.md" }
{ "command": "rename", "old_path": "/memories/draft.md", "new_path": "/memories/final.md" }

new_path 已存在必须报错,不能覆盖:

Error: The destination /memories/final.md already exists

三、最小可运行实现:30 行 Python

直接给一个可以跑起来的最小例子(用本地文件做存储)。生产环境建议用 SDK 提供的 BetaAbstractMemoryTool 抽象类,但理解原理先看朴素版:

import os
import json
from pathlib import Path
from anthropic import Anthropic

MEMORY_ROOT = Path("./agent_memory").resolve()
MEMORY_ROOT.mkdir(exist_ok=True)

def safe_path(p: str) -> Path:
    """防目录遍历:/memories/foo → ./agent_memory/foo,且必须在 ROOT 下"""
    rel = p.lstrip("/").removeprefix("memories/").removeprefix("memories")
    target = (MEMORY_ROOT / rel).resolve()
    target.relative_to(MEMORY_ROOT)  # 越界会抛 ValueError
    return target

def handle_memory(cmd: dict) -> str:
    op = cmd["command"]
    path = safe_path(cmd["path"])

    if op == "view":
        if path.is_dir():
            return "\n".join(f"{f.name} ({f.stat().st_size}B)" for f in path.iterdir())
        text = path.read_text(encoding="utf-8")
        if "view_range" in cmd:
            lines = text.splitlines()
            a, b = cmd["view_range"]
            text = "\n".join(lines[a-1:b])
        return text

    if op == "create":
        path.parent.mkdir(parents=True, exist_ok=True)
        path.write_text(cmd["file_text"], encoding="utf-8")
        return f"File created: {cmd['path']}"

    if op == "str_replace":
        text = path.read_text(encoding="utf-8")
        if text.count(cmd["old_str"]) != 1:
            return f"No replacement performed. Found {text.count(cmd['old_str'])} occurrences."
        path.write_text(text.replace(cmd["old_str"], cmd["new_str"]), encoding="utf-8")
        return "Replaced."

    if op == "insert":
        lines = path.read_text(encoding="utf-8").splitlines(keepends=True)
        i = cmd["insert_line"]
        if not 0 <= i <= len(lines):
            return f"Error: Invalid insert_line {i}. Range [0, {len(lines)}]."
        lines.insert(i, cmd["insert_text"])
        path.write_text("".join(lines), encoding="utf-8")
        return "Inserted."

    if op == "delete":
        if path.is_dir():
            import shutil; shutil.rmtree(path)
        else:
            path.unlink()
        return "Deleted."

    if op == "rename":
        new = safe_path(cmd["new_path"])
        if new.exists():
            return f"Error: The destination {cmd['new_path']} already exists"
        path.rename(new)
        return "Renamed."

    return f"Unknown command: {op}"


client = Anthropic(
    api_key=os.environ["CLAUDEAPI_KEY"],
    base_url="https://gw.claudeapi.com",
)

messages = [{"role": "user", "content": "记住:我喜欢简短的回答和 dark mode。"}]

while True:
    resp = client.beta.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=2048,
        betas=["context-management-2025-06-27"],
        tools=[{"type": "memory_20250818", "name": "memory"}],
        messages=messages,
    )

    if resp.stop_reason != "tool_use":
        print(resp.content[0].text)
        break

    # 执行模型发来的 memory 命令
    tool_results = []
    for block in resp.content:
        if block.type == "tool_use" and block.name == "memory":
            result = handle_memory(block.input)
            tool_results.append({
                "type": "tool_result",
                "tool_use_id": block.id,
                "content": result,
            })

    messages.append({"role": "assistant", "content": resp.content})
    messages.append({"role": "user", "content": tool_results})
import os
import json
from pathlib import Path
from anthropic import Anthropic

MEMORY_ROOT = Path("./agent_memory").resolve()
MEMORY_ROOT.mkdir(exist_ok=True)

def safe_path(p: str) -> Path:
    """防目录遍历:/memories/foo → ./agent_memory/foo,且必须在 ROOT 下"""
    rel = p.lstrip("/").removeprefix("memories/").removeprefix("memories")
    target = (MEMORY_ROOT / rel).resolve()
    target.relative_to(MEMORY_ROOT)  # 越界会抛 ValueError
    return target

def handle_memory(cmd: dict) -> str:
    op = cmd["command"]
    path = safe_path(cmd["path"])

    if op == "view":
        if path.is_dir():
            return "\n".join(f"{f.name} ({f.stat().st_size}B)" for f in path.iterdir())
        text = path.read_text(encoding="utf-8")
        if "view_range" in cmd:
            lines = text.splitlines()
            a, b = cmd["view_range"]
            text = "\n".join(lines[a-1:b])
        return text

    if op == "create":
        path.parent.mkdir(parents=True, exist_ok=True)
        path.write_text(cmd["file_text"], encoding="utf-8")
        return f"File created: {cmd['path']}"

    if op == "str_replace":
        text = path.read_text(encoding="utf-8")
        if text.count(cmd["old_str"]) != 1:
            return f"No replacement performed. Found {text.count(cmd['old_str'])} occurrences."
        path.write_text(text.replace(cmd["old_str"], cmd["new_str"]), encoding="utf-8")
        return "Replaced."

    if op == "insert":
        lines = path.read_text(encoding="utf-8").splitlines(keepends=True)
        i = cmd["insert_line"]
        if not 0 <= i <= len(lines):
            return f"Error: Invalid insert_line {i}. Range [0, {len(lines)}]."
        lines.insert(i, cmd["insert_text"])
        path.write_text("".join(lines), encoding="utf-8")
        return "Inserted."

    if op == "delete":
        if path.is_dir():
            import shutil; shutil.rmtree(path)
        else:
            path.unlink()
        return "Deleted."

    if op == "rename":
        new = safe_path(cmd["new_path"])
        if new.exists():
            return f"Error: The destination {cmd['new_path']} already exists"
        path.rename(new)
        return "Renamed."

    return f"Unknown command: {op}"


client = Anthropic(
    api_key=os.environ["CLAUDEAPI_KEY"],
    base_url="https://gw.claudeapi.com",
)

messages = [{"role": "user", "content": "记住:我喜欢简短的回答和 dark mode。"}]

while True:
    resp = client.beta.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=2048,
        betas=["context-management-2025-06-27"],
        tools=[{"type": "memory_20250818", "name": "memory"}],
        messages=messages,
    )

    if resp.stop_reason != "tool_use":
        print(resp.content[0].text)
        break

    # 执行模型发来的 memory 命令
    tool_results = []
    for block in resp.content:
        if block.type == "tool_use" and block.name == "memory":
            result = handle_memory(block.input)
            tool_results.append({
                "type": "tool_result",
                "tool_use_id": block.id,
                "content": result,
            })

    messages.append({"role": "assistant", "content": resp.content})
    messages.append({"role": "user", "content": tool_results})

跑一次你会在 ./agent_memory/ 下看到 preferences.md(或类似命名)被创建出来。下次启动同样的脚本,模型会自动 view 这个目录,读到偏好后基于它回答。

注意:betas=["context-management-2025-06-27"] 必须带,否则会报 beta 未启用。模型必须用支持列表里的:Sonnet 4.5/4.6、Haiku 4.5、Opus 4.1/4.6/4.7。

四、和 Context Editing 配合:84% token 节省的来源

memory tool 单独用已经有用,但和 context editing 一起开才是 Anthropic 给的"39% 提升 / 84% token 节省"那个数字的来源:

resp = client.beta.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=2048,
    betas=["context-management-2025-06-27"],
    tools=[{"type": "memory_20250818", "name": "memory"}],
    context_management={
        "edits": [{
            "type": "clear_tool_uses_20250919",
            "trigger": {"type": "input_tokens", "value": 100000},
            "keep": {"type": "tool_uses", "value": 3},
            "exclude_tools": ["memory"],  # 关键:memory 操作不清
        }]
    },
    messages=messages,
)
resp = client.beta.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=2048,
    betas=["context-management-2025-06-27"],
    tools=[{"type": "memory_20250818", "name": "memory"}],
    context_management={
        "edits": [{
            "type": "clear_tool_uses_20250919",
            "trigger": {"type": "input_tokens", "value": 100000},
            "keep": {"type": "tool_uses", "value": 3},
            "exclude_tools": ["memory"],  # 关键:memory 操作不清
        }]
    },
    messages=messages,
)

逻辑很简单:长任务里把搜索/网页/文件这些"一次性的工具结果"自动清掉,只保留最近 3 次 + memory 操作。模型靠 memory 把"重要发现"沉淀到文件,下次需要时再 view,而不是让上下文窗口被几十次 tool_use 撑爆。

这套组合让 Agent 第一次有了"边干边学"的能力——在 100 轮的搜索任务里,传统做法早就因 context 爆掉失败了。

五、Memory Tool vs RAG vs Managed Agents Memory:怎么选

新功能容易让人困惑——和已有的方案到底什么关系?给一张对比表:

维度 memory tool(本文主题) 自建 RAG Managed Agents Memory
触发方式 模型主动 view/create 应用层先 retrieve 注入 自动挂载到 session 目录
存储介质 客户端,你决定 向量库(Chroma/PG/Pinecone) Anthropic 托管
检索粒度 文件级 chunk + 相似度 文件级(带版本)
用户隔离 你自己分目录 你自己加 metadata workspace-scoped 内置
复杂度 ★★(继承一个类) ★★★★(embed + retrieve + rerank) ★(API 调用)
适用场景 Agent 偏好/笔记/项目状态 大规模知识检索 不想自己运维存储

简单结论:

  • 个性化、状态、笔记 → memory tool
  • 大语料知识检索 → RAG(memory tool 不是为这个设计的,几百 KB 以下还行,几个 GB 就别用了)
  • 不想运维存储 + 用 Managed Agents 平台 → Managed Agents Memory(文档

memory tool 和 RAG 不是替代关系,可以叠加:RAG 负责"知识",memory tool 负责"个性化和过程沉淀"。

六、四个必须做的安全防护

memory tool 让模型直接调用文件操作,安全比功能更重要。至少做这四件事

1. 强制路径校验

最重要的一条。看上面 safe_path() 函数——用 Path.resolve() + relative_to(MEMORY_ROOT),恶意路径 /memories/../../../etc/passwd 会被 relative_to 抛异常。永远不要拼字符串校验

2. 大小限额

单文件给上限(建议 ≤ 64KB),总目录给上限(建议 ≤ 10MB / 用户)。Claude 可能在长任务里疯狂追加,没限额就是定时炸弹。

3. 敏感信息过滤

写入前过滤明显的敏感字段(信用卡号、密码、邮箱可选),或要求用户显式同意。memory 是"持久化"的,意味着 GDPR / 个保法都在管。

4. 用户隔离

user_id 给每个用户分独立的根目录,绝不能让模型跨用户访问。多租户场景把 MEMORY_ROOT 改成 BASE / user_id 即可。

七、国内接入:用 claudeapi.com 一行配置

国内直连 Anthropic 官方做 Agent 开发会遇到两个问题:网络不稳定 + 账号风控。memory tool 这种长会话场景对稳定性要求更高。

claudeapi.com 提供国内可达的网关,完整兼容 Anthropic SDK 包括 beta 头和 memory_20250818 工具。把上面代码的 base_url 指过去即可:

client = Anthropic(
    api_key="sk-你的key",
    base_url="https://gw.claudeapi.com",
)
client = Anthropic(
    api_key="sk-你的key",
    base_url="https://gw.claudeapi.com",
)

支持的模型(memory tool 适配的全在这里)与定价:

模型 ID 输入 输出 适合做 memory Agent 吗
claude-opus-4-7 ¥20/M ¥100/M ✅ 复杂决策 Agent
claude-opus-4-6 ¥20/M ¥100/M ✅ 同上,备选
claude-sonnet-4-6 ¥4/M ¥20/M ✅✅ 默认选这个,性价比最佳
claude-haiku-4-5-20251001 ¥1/M ¥5/M ✅ 高频简单 memory 读写

国内 Agent 开发的实际成本:以一个客服 Agent 为例,平均每会话 8 轮、每轮约 3000 输入 token + 500 输出 token + 2 次 memory 操作(约 1500 token),用 Sonnet 4.6 单次会话成本约 ¥0.13,加上 prompt caching 还能再降 50%。

新用户注册有体验额度:console.claudeapi.com 5 分钟拿 key,把本文第三节那 30 行代码跑通,你就有了一个会记住用户偏好的 Agent。

八、生产化建议清单

最后给一份打勾清单,做生产前过一遍:

  • [ ] 路径校验用 Path.resolve() + relative_to(),不要字符串拼接
  • [ ] 单文件 + 总目录都有大小限额
  • [ ] 多租户场景按 user_id 隔离根目录
  • [ ] 敏感字段写入前过滤或加密
  • [ ] 配合 clear_tool_uses_20250919 + exclude_tools: ["memory"]
  • [ ] 记录 audit log:每次 memory 操作的命令 + 命中文件 + 用户
  • [ ] 定期清理超过 90 天未访问的文件
  • [ ] 优先用 SDK 的 BetaAbstractMemoryTool 抽象,而不是裸 dict

memory tool 把"让 Agent 有长期记忆"这件事从"自建 RAG 工程"压到了"实现一个文件系统接口"。对中小团队来说,这可能是 2026 年最值得花一下午摸清楚的能力。

相关文章