跳转至

会话管理

你让 Agent 分析了一个认证模块,它花了三十秒读完所有相关文件、找出三个潜在问题。现在你想让它基于这些分析直接修复——如果重新来过,Agent 会再次读取所有文件、重新分析,浪费时间和 token。这就是会话管理要解决的问题:让 Agent 在多次交互之间保持上下文。

本文你会学到

  • 如何根据应用场景选择合适的会话管理方式
  • Continue、Resume、Fork 的区别与适用场景
  • 自动会话管理的使用方法
  • 如何跨主机恢复会话
  • 文件检查点(Checkpointing)的启用与回溯

会话管理方式对比

会话(Session)是 SDK 在 Agent 工作过程中积累的对话历史——包含你的提示、每个工具调用、每个工具结果和每次响应。SDK 会自动将对话历史写入磁盘,方便你之后恢复。

会话持久化的是**对话**,不是文件系统。如果你想快照和回退 Agent 对文件所做的更改,请参阅「文件检查点」部分。

根据你的应用场景,选择合适的会话管理方式:

场景 方案
一次性任务:单次提示,无后续 无需额外处理,一个 query() 搞定
同一进程内的多轮对话 ClaudeSDKClient(Python)或 continue: true(TypeScript)
进程重启后继续上次的对话 continue: true(TypeScript)/ continue_conversation=True(Python)
恢复某个特定的历史会话(而非最近的) 捕获 session ID,传给 resume
尝试不同方案而不丢失原始会话 使用 fork 分叉会话
无状态任务,不写磁盘(仅 TypeScript) 设置 persistSession: false

Continue、Resume、Fork 的区别

这三个选项都是 query() 的参数字段,都用于恢复已有会话,但行为不同:

Continue 和 Resume 都会找到已有会话并追加内容,区别在于如何定位那个会话:

  • Continue 自动找到当前目录下最近一次会话。你不需要跟踪任何 ID,适合一次只运行一个对话的应用
  • Resume 接受一个特定的 session ID。你需要自己跟踪 ID,适合多会话(比如多用户应用中每个用户一个会话)或需要恢复到非最近会话的场景

Fork 不同:它创建一个新会话,以原始会话历史的副本作为起点。原始会话保持不变。适合你想尝试不同方向但保留回退选项的场景。

自动会话管理

Python:ClaudeSDKClient

ClaudeSDKClient 在内部自动管理 session ID。每次调用 client.query() 都自动继续同一个会话,调用 client.receive_response() 来迭代当前查询的消息。Client 必须作为异步上下文管理器使用。

Python:ClaudeSDKClient 多轮对话
import asyncio
from claude_agent_sdk import (
    ClaudeSDKClient,
    ClaudeAgentOptions,
    AssistantMessage,
    ResultMessage,
    TextBlock,
)


def print_response(message):
    """只打印人类可读的消息部分"""
    if isinstance(message, AssistantMessage):
        for block in message.content:
            if isinstance(block, TextBlock):
                print(block.text)
    elif isinstance(message, ResultMessage):
        cost = (
            f"${message.total_cost_usd:.4f}"
            if message.total_cost_usd is not None
            else "N/A"
        )
        print(f"[完成: {message.subtype}, 花费: {cost}]")


async def main():
    options = ClaudeAgentOptions(
        allowed_tools=["Read", "Edit", "Glob", "Grep"],
    )

    async with ClaudeSDKClient(options=options) as client:
        # 第一次查询:Client 内部自动捕获 session ID
        await client.query("分析认证模块")
        async for message in client.receive_response():
            print_response(message)

        # 第二次查询:自动继续同一个会话,无需传 ID
        await client.query("现在把它重构为使用 JWT")
        async for message in client.receive_response():
            print_response(message)


asyncio.run(main())

两个查询通过同一个 client 实例运行,第二次查询自动拥有第一次的完整上下文。

TypeScript:continue: true

TypeScript 的稳定版 SDK(query() 函数)没有类似 Python ClaudeSDKClient 的会话持有对象。取而代之的是在每次后续 query() 调用中传入 continue: true,SDK 自动找到磁盘上最近的会话并恢复。

TypeScript:continue: true 多轮对话
import { query } from "@anthropic-ai/claude-agent-sdk";

// 第一次查询:创建新会话
for await (const message of query({
  prompt: "分析认证模块",
  options: { allowedTools: ["Read", "Glob", "Grep"] }
})) {
  if (message.type === "result" && message.subtype === "success") {
    console.log(message.result);
  }
}

// 第二次查询:continue: true 自动恢复最近的会话
for await (const message of query({
  prompt: "现在把它重构为使用 JWT",
  options: {
    continue: true,
    allowedTools: ["Read", "Edit", "Write", "Glob", "Grep"]
  }
})) {
  if (message.type === "result" && message.subtype === "success") {
    console.log(message.result);
  }
}

使用 query() 的会话选项

捕获 Session ID

Resume 和 Fork 都需要 session ID。从 ResultMessagesession_id 字段读取——无论会话成功还是失败,此字段都会存在。在 TypeScript 中,ID 还可以更早地从 SystemMessage 直接读取。

Python:捕获 session ID
import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage

async def main():
    session_id = None

    async for message in query(
        prompt="分析认证模块并建议改进",
        options=ClaudeAgentOptions(
            allowed_tools=["Read", "Glob", "Grep"],
        ),
    ):
        if isinstance(message, ResultMessage):
            session_id = message.session_id
            if message.subtype == "success":
                print(message.result)

    print(f"Session ID: {session_id}")
    return session_id


session_id = asyncio.run(main())
TypeScript:捕获 session ID
import { query } from "@anthropic-ai/claude-agent-sdk";

let sessionId: string | undefined;

for await (const message of query({
  prompt: "分析认证模块并建议改进",
  options: { allowedTools: ["Read", "Glob", "Grep"] }
})) {
  if (message.type === "result") {
    sessionId = message.session_id;
    if (message.subtype === "success") {
      console.log(message.result);
    }
  }
}

console.log(`Session ID: ${sessionId}`);

按 ID 恢复会话

将 session ID 传给 resume 参数,Agent 会从该会话中断的地方继续,拥有完整的上下文。常见用途:

  • 跟进已完成任务:Agent 已分析完毕,现在基于分析结果直接行动,无需重新读取文件
  • 从限制中恢复:第一次运行因 error_max_turnserror_max_budget_usd 结束,用更高的限制恢复
  • 重启进程后恢复:关机前捕获了 ID,重启后恢复对话
Python:按 ID 恢复会话
# 之前的会话已经做了分析,现在直接基于分析结果行动
async for message in query(
    prompt="现在实现你建议的重构方案",
    options=ClaudeAgentOptions(
        resume=session_id,
        allowed_tools=["Read", "Edit", "Write", "Glob", "Grep"],
    ),
):
    if isinstance(message, ResultMessage) and message.subtype == "success":
        print(message.result)

如果 resume 返回了一个全新的会话而非预期的历史上下文,最常见的原因是 cwd 不匹配。会话文件存储在 ~/.claude/projects/<encoded-cwd>/*.jsonl 下,其中 <encoded-cwd> 是绝对工作目录中所有非字母数字字符被替换为 - 的结果。如果恢复调用运行在不同的目录,SDK 会在错误的位置查找。

Fork 探索不同方案

Fork 创建一个新会话,以原始会话历史的副本作为起点,之后独立发展。Fork 获得自己的 session ID,原始会话的 ID 和历史保持不变。你最终拥有两个独立的会话,可以分别恢复。

Python:Fork 探索不同方案
# Fork:从 session_id 分叉出新会话
forked_id = None
async for message in query(
    prompt="不用 JWT,改用 OAuth2 实现认证模块",
    options=ClaudeAgentOptions(
        resume=session_id,
        fork_session=True,
    ),
):
    if isinstance(message, ResultMessage):
        forked_id = message.session_id  # Fork 的新 ID,与 session_id 不同
        if message.subtype == "success":
            print(message.result)

print(f"分叉会话: {forked_id}")

# 原始会话完好无损,恢复它继续 JWT 方案
async for message in query(
    prompt="继续 JWT 方案",
    options=ClaudeAgentOptions(resume=session_id),
):
    if isinstance(message, ResultMessage) and message.subtype == "success":
        print(message.result)

Fork 分叉的是**对话历史**,不是文件系统。如果分叉后的 Agent 编辑了文件,这些更改是真实的,同一目录下的所有会话都能看到。如果要同时分叉和回退文件更改,使用「文件检查点」。

跨主机恢复

会话文件是本机的。要在不同主机之间恢复会话(CI Worker、临时容器、Serverless),有两种方式:

  • 迁移会话文件:将 ~/.claude/projects/<encoded-cwd>/<session-id>.jsonl 从第一次运行中持久化,并在新主机上恢复到相同路径后再调用 resumecwd 必须匹配
  • 不依赖会话恢复:将需要的结果(分析输出、决策、文件 diff)捕获为应用状态,传入新会话的 prompt。这通常比到处搬运会话文件更健壮

两个 SDK 都提供了在磁盘上枚举会话和读取消息的函数:

Python TypeScript 用途
list_sessions() listSessions() 枚举会话列表
get_session_messages() getSessionMessages() 读取会话消息
get_session_info() getSessionInfo() 查看会话元信息
rename_session() renameSession() 重命名会话
tag_session() tagSession() 为会话添加标签

这些函数适合构建自定义的会话选择器、清理逻辑或对话查看器。

文件检查点(Checkpointing)

为什么需要文件检查点?

Agent 在会话中会频繁修改文件——创建新文件、编辑已有文件、修改 Notebook 单元格。如果 Agent 的某次修改出了问题,你想回退到之前的状态怎么办?普通的 git checkout 可能不够——Agent 可能还没有提交,或者你只是想临时回退到某个中间状态看看。文件检查点就是为此设计的:它自动跟踪 Agent 通过 WriteEditNotebookEdit 工具所做的文件修改,让你可以回退到任意一个检查点。

只有通过 WriteEditNotebookEdit 工具的修改会被跟踪。通过 Bash 命令(如 echo > file.txtsed -i)的修改不会被捕获。

启用检查点

两个关键配置:

配置项 Python TypeScript 说明
启用检查点 enable_file_checkpointing=True enableFileCheckpointing: true 跟踪文件变更以支持回退
接收检查点 UUID extra_args={"replay-user-messages": None} extraArgs: { 'replay-user-messages': null } 在响应流中接收用户消息 UUID
Python:启用检查点
from claude_agent_sdk import ClaudeSDKClient, ClaudeAgentOptions

options = ClaudeAgentOptions(
    enable_file_checkpointing=True,
    permission_mode="acceptEdits",
    extra_args={"replay-user-messages": None},
)

async with ClaudeSDKClient(options) as client:
    await client.query("重构认证模块")

捕获检查点 UUID

启用 replay-user-messages 后,响应流中的每条用户消息都有一个 UUID,作为检查点标识。通常捕获第一个用户消息的 UUID 即可——回退到它将所有文件恢复到原始状态。

Python:捕获检查点 UUID 和 session ID
checkpoint_id = None
session_id = None

async for message in client.receive_response():
    # 每条用户消息更新检查点(只保留最新的)
    if isinstance(message, UserMessage) and message.uuid:
        checkpoint_id = message.uuid
    # 从 ResultMessage 捕获 session ID
    if isinstance(message, ResultMessage):
        session_id = message.session_id
TypeScript:捕获检查点 UUID 和 session ID
let checkpointId: string | undefined;
let sessionId: string | undefined;

for await (const message of response) {
  if (message.type === "user" && message.uuid) {
    checkpointId = message.uuid;
  }
  if ("session_id" in message) {
    sessionId = message.session_id;
  }
}

回退文件

回退分两种情况:在流处理中立即回退,以及**流结束后回退**。

流结束后回退需要先恢复会话,再用空 prompt 建立连接,然后调用 rewind_files() / rewindFiles()

Python:流结束后回退文件
1
2
3
4
5
6
7
async with ClaudeSDKClient(
    ClaudeAgentOptions(enable_file_checkpointing=True, resume=session_id)
) as client:
    await client.query("")  # 空 prompt 建立连接
    async for message in client.receive_response():
        await client.rewind_files(checkpoint_id)
        break
TypeScript:流结束后回退文件
1
2
3
4
5
6
7
8
9
const rewindQuery = query({
  prompt: "",  // 空 prompt 建立连接
  options: { ...opts, resume: sessionId }
});

for await (const msg of rewindQuery) {
  await rewindQuery.rewindFiles(checkpointId);
  break;
}

如果你捕获了 session ID 和 checkpoint UUID,也可以通过 CLI 回退:

claude -p --resume <session-id> --rewind-files <checkpoint-uuid>

常见模式

在 risky 操作前保存检查点

在每次 Agent 回合前更新检查点,覆盖前一个。如果出问题立即回退并退出循环:

Python:在 risky 操作前保存检查点
safe_checkpoint = None

async with ClaudeSDKClient(options) as client:
    await client.query("重构认证模块")

    async for message in client.receive_response():
        # 每次回合前更新检查点(只保留最新的)
        if isinstance(message, UserMessage) and message.uuid:
            safe_checkpoint = message.uuid

        # 根据你的逻辑决定是否回退
        # 例如:错误检测、验证失败、用户输入等
        if your_revert_condition and safe_checkpoint:
            await client.rewind_files(safe_checkpoint)
            break

多个恢复点

如果 Agent 跨多个回合修改文件,你可能想回退到某个中间状态而非全部回退。将所有检查点 UUID 存入数组,之后选择任意一个恢复:

Python:多个恢复点
from dataclasses import dataclass
from datetime import datetime


@dataclass
class Checkpoint:
    id: str
    description: str
    timestamp: datetime


checkpoints = []
session_id = None

async with ClaudeSDKClient(options) as client:
    await client.query("重构认证模块")

    async for message in client.receive_response():
        if isinstance(message, UserMessage) and message.uuid:
            checkpoints.append(
                Checkpoint(
                    id=message.uuid,
                    description=f"第 {len(checkpoints) + 1} 轮之后",
                    timestamp=datetime.now(),
                )
            )
        if isinstance(message, ResultMessage) and not session_id:
            session_id = message.session_id

# 之后:恢复到任意检查点
if checkpoints and session_id:
    target = checkpoints[0]  # 选择任意检查点
    async with ClaudeSDKClient(
        ClaudeAgentOptions(enable_file_checkpointing=True, resume=session_id)
    ) as client:
        await client.query("")
        async for message in client.receive_response():
            await client.rewind_files(target.id)
            break
    print(f"已回退到: {target.description}")

检查点的局限性

局限性 说明
仅限 Write/Edit/NotebookEdit Bash 命令的文件修改不会被跟踪
绑定会话 检查点与创建它的会话绑定
仅文件内容 创建、移动、删除目录不会被回退操作撤销
仅本地文件 远程或网络文件不会被跟踪