Claude Code 的持久记忆:跨会话上下文究竟如何运作
周五,你合上笔记本电脑,结束了一场持续三小时的会话。在这期间,Claude Code 重构了你的身份验证模块,了解到你的团队使用自定义的 Result<T> 包装器,而不是抛出异常,并终于理解了为什么 UserService 不能从以下模块导入: billing/。周一,你打开一个新会话,Claude 自信地用 try/catch 包裹了一个新函数,还导入了那个禁止使用的模块。它学到的一切都消失了。
这是智能体编程工具最典型的使用体验问题:模型没有情景记忆。每次会话都从零开始,只能加上你手动记录的内容。让我们用工程方法解决这个问题,并如实说明,包括那些行不通的部分。
核心问题:会话在设计上是无状态的
Claude Code 会话是一段对话。会话结束时,对话记录会被归档,而工作上下文——通过数十次工具调用逐步积累的理解——则会消失。下一次会话会从以下三个来源重建上下文:
- 你的
CLAUDE.mdfiles (project and user scope) - Claude 在会话期间读取的文件
- 你输入的内容
这些来源都无法记录 推导出的 知识:你已经探索过的死胡同、某个奇怪的变通方案存在的原因,以及你曾尝试方案 A 却导致 CI 失败的事实。每次会话重新推导这些知识,都会消耗 token 和实际时间。
直觉上的做法是构建一个记忆层:记录发生过的事情,将其压缩,并在启动时重新注入相关部分。开源 claude-mem 项目正是这一模式的简洁参考实现,无论你是否采用它,都值得研究。
Claude Code 持久记忆背后的架构模式
所有严肃的 Claude Code 持久记忆方案都遵循同一个三阶段循环。分别理解各个阶段,能让你决定自行构建、购买现成方案,还是跳过其中某个阶段。
阶段 1:捕获
你需要观察会话中发生了什么。Claude Code 提供了生命周期 钩子 ——即在事件发生时触发的 shell 命令。相关钩子如下:
| 钩子 | 触发时机 | 用途 |
|---|---|---|
SessionStart | 会话开始时 | 注入记忆 |
SessionEnd | 会话终止时 | 触发捕获 |
PostToolUse | 每次工具调用之后 | 流式记录细粒度活动 |
Stop | 智能体完成响应时 | 创建检查点 |
在以下位置配置的最简捕获钩子: .claude/settings.json:
{
"hooks": {
"SessionEnd": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/mem/capture.sh"
}
]
}
]
}
}
钩子通过标准输入接收描述事件的 JSON。对于 SessionEnd ,你会获得会话 ID 和会话记录路径,因此 capture.sh 可以读取完整的对话日志:
#!/usr/bin/env bash
INPUT=$(cat)
TRANSCRIPT=$(echo "$INPUT" | jq -r '.transcript_path')
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id')
# Hand the raw transcript to the compression stage
python3 "$CLAUDE_PROJECT_DIR/.claude/mem/compress.py" \
--transcript "$TRANSCRIPT" \
--session "$SESSION_ID"
一个常见陷阱是:钩子有执行超时限制,而将大型会话记录同步传给 LLM 会超过这一限制。实际使用中,捕获操作应当触发后就返回——将会话记录路径写入队列,再由后台工作进程处理,而不是直接在钩子内处理。
阶段 2:压缩
原始会话记录无法直接用作记忆——它们体积庞大,充斥着噪声(文件内容转储、冗余工具输出、你的拼写错误)。压缩阶段使用 LLM 将会话提炼为可长期保留的事实。这里应该调用 Haiku 4.5,而不是 Opus——摘要正是 Haiku 的定价所适合的高调用量、低风险任务。
import anthropic
client = anthropic.Anthropic()
PROMPT = """You are a memory compressor. From this coding session
transcript, extract ONLY durable facts worth remembering next session:
- Architectural decisions and their rationale
- Conventions discovered (naming, error handling, imports)
- Dead ends already explored (so we don't repeat them)
- Unresolved TODOs
Output JSON: a list of {fact, category, files[], confidence}.
Omit anything transient. If nothing durable happened, output []."""
resp = client.messages.create(
model="claude-haiku-4-5",
max_tokens=1500,
system=PROMPT,
messages=[{"role": "user", "content": transcript_text}],
)
memories = resp.content[0].text
请注意 confidence 字段,以及明确要求输出 []的指令。这两者都是为了减轻后文将讨论的失效问题。将输出存储在可查询的位置——例如带有嵌入向量列的 SQLite,或以项目和文件路径为键的向量存储。
阶段 3:注入
会话开始时,你检索相关记忆并将其注入。 SessionStart 钩子可以向标准输出打印内容,这些输出会被加入 Claude 的上下文:
{
"hooks": {
"SessionStart": [
{
"matcher": "*",
"hooks": [
{ "type": "command", "command": "$CLAUDE_PROJECT_DIR/.claude/mem/inject.sh" }
]
}
]
}
}
关键的工程决策是 相关性过滤。注入所有记忆是一种幼稚且错误的做法——它会让上下文膨胀,淹没有用的事实。更好的做法是根据会话可能涉及的内容进行检索。如果暂时无法判断,就只注入高置信度、适用于整个项目的事实,并通过一个 MCP 服务器 提供其余记忆,让 Claude 可以按需查询:
# A tiny MCP tool the agent calls when it wants history
@mcp.tool()
def recall(query: str, files: list[str] = []) -> list[dict]:
"""Retrieve relevant memories about this codebase."""
return memory_store.search(query, files=files, min_confidence=0.6)
在实践中,这种拉取模式优于推送模式。你无需在启动时猜测哪些内容相关,而是让智能体在确实需要时询问:“我们以前处理过这段计费代码吗?”钩子提供一份始终保留在上下文中的简短摘要;MCP 服务器负责定向检索记忆。
哪些地方会出问题——而且确实会出问题
长期使用记忆层的经历让我对它们持怀疑态度。这些失效问题真实存在,而且会相互叠加。
AI 压缩的记忆会产生幻觉。 压缩器本身也是语言模型。它偶尔会把一个实际已被放弃的想法记录为“决策”,或者把某个选择的理由颠倒过来。这样一来,会话启动时就会注入听起来很权威的错误记忆,而智能体对它的信任超过了应有的程度。这比没有记忆更糟,因为它错得信心十足。 confidence 字段和对记忆存储的人工审核,是仅有的真正缓解措施。
过时的上下文是无声的杀手。 你在周二重构了 UserService 。周一的记忆却仍然写着“认证逻辑位于 UserService”。没有任何机制会让这条记忆失效。代码一旦变化,记忆的准确性就开始下降,而又没有明确的信号来判定它们何时过期。将记忆与文件哈希或提交 SHA 关联起来,并在底层文件发生移动时,将相关记忆视为存疑。
每次会话都会增加 token 成本。 注入的记忆属于上下文,无论是否用到,你都要在每一轮为它付费。如果记忆层在启动时注入 4K 个 token,那么整个会话的输入成本都会增加相应的用量。基于拉取的 MCP 模式有所帮助,但捕获和压缩本身也会产生持续费用。
下面是实事求是的比较:
| 方式 | 配置成本 | 持续 token 用量 | 过时风险 | 最适合 |
|---|---|---|---|---|
CLAUDE.md 仅使用 | 数分钟 | 低且可确定 | 由你掌控 | 大多数项目 |
| 限定范围的 MCP 工具 | 数小时 | 仅按需消耗 | 低 | 实时系统状态 |
| 完整记忆层 | 数天 | 较高,每次会话都会消耗 | 高 | 长期探索性工作 |
令人不太舒服的事实:你可能并不需要它
在构建这些机制之前,先写好一份 CLAUDE.md。一份人工整理的文件,只要写明“我们使用 Result<T>,绝不抛出异常; UserService 不得导入 billing/;认证逻辑已迁移至 auth/ ,迁移时间为第二季度”,就能解决 80% 的记忆问题,而且没有幻觉风险,成本也可确定。它由人工编写,因此从源头上就值得信赖。
记忆层真正物有所值的适用范围,比宣传中所说的更窄:在长期探索性工作中, 推导出的 知识(走不通的路、尚未完成的调查)确实无法预先写好,因为你还不知道这些内容。对于常规功能开发, CLAUDE.md 加上几个限定范围的 MCP 服务器,比自动化记忆流水线能覆盖更多需求,而且绝不会骗你。
如果你确实要构建这条流水线,压缩阶段最需要关注成本——频繁使用 Haiku 生成摘要的费用并不高,但前提是你的每 token 定价本来就合理。这也是团队将此类高调用量后台工作交给 AI Prime Tech 等服务提供商的原因之一。
实用要点
- 会话是无状态的。 真正丢失的是推导出的知识——走不通的路、决策理由、约定——而重新推导这些知识的代价很高。
- 基本模式是捕获 → 压缩 → 注入。 通过
SessionEnd/SessionStart钩子连接捕获与注入流程;异步执行捕获,避免钩子超时。 - 优先采用拉取,而非推送。 启动时提供一份极简摘要,然后开放一个
recallMCP 工具,让智能体仅在相关时获取记忆。 - 使用 Haiku 4.5 进行压缩,为每条记忆标注置信度和源文件哈希,并在文件变化时将相关记忆视为存疑。
- 自动化记忆可能说谎。 产生幻觉或已经过时的上下文比没有上下文更糟,因为它显得很权威。让人工持续参与记忆存储的审核。
- 先试试
CLAUDE.md。 对于大多数项目,手写上下文文件加上限定范围的工具,在成本、可信度和维护方面都优于完整记忆层——只有当你的工作确实长期且具有探索性时,才考虑持久化。
一个 API 密钥即可使用 Claude Opus 5.5、Sonnet 5、Haiku 4.5 和 Fable 5.1,以及 GPT-6 个模型。按量付费,无需订阅。
获取 API 密钥 →