AI Agent 教程

第8章:会话管理 —— 让 Agent 记住上下文

一句话:学会创建、恢复、分叉会话,让 Agent 能做长任务。

本章目标

前置知识


8.1 什么是会话(Session)?

先说个痛点

你有没有遇到过这种情况:

你让 Agent 帮你写一个项目,它写了一半,程序崩了,或者你关了终端。再次启动的时候,Agent 完全不记得之前做了什么。你得从头开始解释:"我之前让你做了个 XXX 项目,你已经写了 A 文件和 B 文件,现在请继续写 C 文件……"

这就像你跟一个同事合作,每天早上他都失忆了,你得重新介绍自己、重新说一遍昨天做了什么。太痛苦了!

会话(Session)就是解决这个问题的。

会话是什么?

简单来说:一次对话 = 一个会话

你跟 Agent 的每一次交互,SDK 都会自动创建一个"会话"。这个会话有一个唯一的 ID(就像身份证号),Agent 说过的每一句话、做过的每一件事,都记录在这个会话里。

把它想象成浏览器的标签页

没有会话会怎样?

如果没有会话管理,Agent 每次运行都是"全新的一次"。它不记得你之前说过什么,也不记得它之前做过什么。这意味着:

有了会话管理,这些问题全部解决。

会话的三大能力

能力 说明 类比
创建 每次 query() 自动创建新会话 开一个新的浏览器标签页
恢复(Resume) 用 session_id 继续上次的对话 重新打开之前关掉的标签页
分叉(Fork) 基于当前状态创建一个新分支 复制一个标签页,各走各的

8.2 获取 Session ID

从 init 消息中提取

还记得第7章学的消息类型吗?当你调用 query() 时,Agent 发回的第一条消息就是一条 system 类型的 init 消息。这条消息里面,就藏着 session_id

来看代码:

import { query } from "@anthropic-ai/claude-agent-sdk";

let sessionId: string | undefined;

const response = query({
  prompt: "帮我分析一下当前项目的结构",
  options: {
    model: "claude-sonnet-4-20250514",
    allowedTools: ["Read", "Glob", "Grep", "Bash"]
  }
});

for await (const message of response) {
  // 第一条消息就是 init 消息,包含 session_id
  if (message.type === "system" && message.subtype === "init") {
    sessionId = message.session_id;
    console.log(`会话已创建,ID: ${sessionId}`);
    // 把这个 ID 保存下来!后面恢复会话要用
  }

  // 处理其他消息...
  if (message.type === "assistant") {
    console.log("Agent 说:", message.content);
  }
}

// 到这里,sessionId 已经拿到了
console.log(`会话结束,保存的 ID: ${sessionId}`);

Python 版本:

from claude_code_sdk import query, ClaudeCodeOptions

session_id = None

async for message in query(
    prompt="帮我分析一下当前项目的结构",
    options=ClaudeCodeOptions(
        model="claude-sonnet-4-20250514",
        allowed_tools=["Read", "Glob", "Grep", "Bash"]
    )
):
    if message.type == "system" and message.subtype == "init":
        session_id = message.session_id
        print(f"会话已创建,ID: {session_id}")

    if message.type == "assistant":
        print("Agent 说:", message.content)

print(f"会话结束,保存的 ID: {session_id}")

Session ID 长什么样?

Session ID 是一个字符串,通常是一个 UUID 格式或者类似的唯一标识符,比如:

session-a1b2c3d4-e5f6-7890-abcd-ef1234567890

你不需要关心它的具体格式,只要把它当作一个不透明的字符串来用就行。

保存 Session ID 的几种方式

拿到 session_id 之后,你需要把它保存下来,这样下次才能恢复会话。保存方式取决于你的应用场景:

// 方式1:保存到文件(最简单)
import { writeFileSync, readFileSync, existsSync } from "fs";

function saveSessionId(id: string) {
  writeFileSync(".session_id", id, "utf-8");
}

function loadSessionId(): string | undefined {
  if (existsSync(".session_id")) {
    return readFileSync(".session_id", "utf-8").trim();
  }
  return undefined;
}

// 方式2:保存到 JSON 配置文件(更结构化)
function saveSession(sessionId: string, taskDescription: string) {
  const data = {
    sessionId,
    taskDescription,
    lastUpdated: new Date().toISOString()
  };
  writeFileSync(".agent_session.json", JSON.stringify(data, null, 2));
}

// 方式3:保存到数据库(适合生产环境)
// 用 SQLite、Redis 等,按你的技术栈来

会话数据存在哪里?

SDK 内部会把会话数据保存在本地磁盘上。默认位置是项目根目录下的 .claude/ 目录:

你的项目/
├── .claude/
│   └── projects/
│       └── <项目hash>/
│           └── sessions/
│               ├── <session-id-1>.jsonl   ← 第一个会话的记录
│               ├── <session-id-2>.jsonl   ← 第二个会话的记录
│               └── ...
├── src/
└── package.json

每个会话对应一个 .jsonl 文件(JSON Lines 格式),里面记录了这次会话的所有消息。这个文件是 SDK 自动管理的,你通常不需要手动操作它。

小知识.jsonl 格式就是每行一个 JSON 对象,方便追加写入,也方便逐行读取。Agent 每说一句话、每调一次工具,都会追加一行到这个文件里。


8.3 恢复会话(Resume)

基本用法

恢复会话非常简单,只需要在 options 里加一个 resume 参数,传入之前保存的 session_id:

import { query } from "@anthropic-ai/claude-agent-sdk";

// 上次保存的 session_id
const previousSessionId = "session-a1b2c3d4-e5f6-7890-abcd-ef1234567890";

const response = query({
  prompt: "继续上次没做完的工作",
  options: {
    resume: previousSessionId,  // 关键:传入之前的 session_id
    model: "claude-sonnet-4-20250514",
    allowedTools: ["Read", "Edit", "Write", "Glob", "Grep", "Bash"]
  }
});

for await (const message of response) {
  console.log(message);
}

就这么简单!SDK 会自动加载之前的对话历史和上下文,Agent 会记得之前说过什么、做过什么,然后接着你的新指令继续工作。

Python 版本:

from claude_code_sdk import query, ClaudeCodeOptions

previous_session_id = "session-a1b2c3d4-e5f6-7890-abcd-ef1234567890"

async for message in query(
    prompt="继续上次没做完的工作",
    options=ClaudeCodeOptions(
        resume=previous_session_id,  # 关键:传入之前的 session_id
        model="claude-sonnet-4-20250514",
        allowed_tools=["Read", "Edit", "Write", "Glob", "Grep", "Bash"]
    )
):
    print(message)

Agent 恢复后记得什么?

当你恢复一个会话时,Agent 能记住的东西:

记得的 不记得的
之前所有的对话内容 运行时的程序变量
之前调用了哪些工具 你代码里的局部状态
工具的执行结果 已关闭的网络连接
Agent 的思考过程 之前打开的文件句柄
文件的修改记录 -

换句话说: Agent 记得"对话层面"的所有内容,但你代码里的变量、状态之类的东西,需要你自己保存和恢复。

完整示例:开始 -> 保存 -> 恢复

来看一个完整的例子,模拟一个分两次完成的任务:

import { query } from "@anthropic-ai/claude-agent-sdk";
import { writeFileSync, readFileSync, existsSync } from "fs";

const SESSION_FILE = ".agent_session.json";

// ===== 工具函数 =====

function saveSession(sessionId: string, task: string) {
  writeFileSync(SESSION_FILE, JSON.stringify({ sessionId, task }, null, 2));
  console.log(`会话已保存: ${sessionId}`);
}

function loadSession(): { sessionId: string; task: string } | null {
  if (existsSync(SESSION_FILE)) {
    return JSON.parse(readFileSync(SESSION_FILE, "utf-8"));
  }
  return null;
}

// ===== 第一次运行:开始任务 =====

async function startNewTask() {
  let sessionId: string | undefined;

  const response = query({
    prompt: "请帮我创建一个 Express.js 项目,包含基本的用户注册和登录功能。先创建项目结构和 package.json。",
    options: {
      model: "claude-sonnet-4-20250514",
      allowedTools: ["Read", "Write", "Bash"]
    }
  });

  for await (const message of response) {
    if (message.type === "system" && message.subtype === "init") {
      sessionId = message.session_id;
      console.log(`新会话开始: ${sessionId}`);
    }

    if (message.type === "assistant") {
      // 显示 Agent 的输出
      for (const block of message.content) {
        if (block.type === "text") {
          console.log(block.text);
        }
      }
    }

    if (message.type === "result") {
      console.log(`\n第一阶段完成,状态: ${message.subtype}`);
    }
  }

  // 保存会话,下次继续
  if (sessionId) {
    saveSession(sessionId, "Express.js 用户认证项目");
  }
}

// ===== 第二次运行:恢复任务 =====

async function resumeTask() {
  const saved = loadSession();
  if (!saved) {
    console.log("没有找到保存的会话,请先运行 startNewTask()");
    return;
  }

  console.log(`正在恢复会话: ${saved.sessionId}`);
  console.log(`任务描述: ${saved.task}`);

  const response = query({
    prompt: "继续上次的工作。现在请实现用户注册和登录的路由和控制器逻辑。",
    options: {
      resume: saved.sessionId,  // 恢复之前的会话!
      model: "claude-sonnet-4-20250514",
      allowedTools: ["Read", "Write", "Edit", "Bash"]
    }
  });

  for await (const message of response) {
    if (message.type === "assistant") {
      for (const block of message.content) {
        if (block.type === "text") {
          console.log(block.text);
        }
      }
    }

    if (message.type === "result") {
      console.log(`\n第二阶段完成,状态: ${message.subtype}`);
    }
  }
}

// 根据命令行参数决定是开始还是恢复
const action = process.argv[2];
if (action === "resume") {
  resumeTask();
} else {
  startNewTask();
}

运行方式:

# 第一次运行:开始任务
npx tsx session-demo.ts

# (关掉终端,喝杯咖啡,回来继续)

# 第二次运行:恢复任务
npx tsx session-demo.ts resume

Agent 恢复之后,它知道自己上次已经创建了项目结构和 package.json,所以会直接开始写路由和控制器,不会重复劳动。

恢复会话的适用场景

  1. 长任务中断后继续 —— 写一个大项目,中途关了电脑,第二天接着做
  2. 多轮对话 —— 先让 Agent 分析问题,你看完分析后再给出下一步指令
  3. 渐进式开发 —— 今天做基础功能,明天在此基础上加新功能
  4. 任务暂停 —— Agent 做到一半,你想先做别的事,回头再继续

8.4 分叉会话(Fork)

什么是分叉?

分叉会话就像 Git 的分支一样。你从某个时间点开始,创建一个"平行宇宙":

原始会话:  A → B → C → D(继续原来的路)
                    ↘
分叉会话:           C' → E → F(走另一条路)

基本用法

分叉会话用的是 resume + forkSession: true 两个参数的组合:

import { query } from "@anthropic-ai/claude-agent-sdk";

// 假设我们已经有了一个会话,讨论了"如何设计一个 API"
const originalSessionId = "session-original-id";

// 分叉出一个新会话,试试另一种方案
const forkedResponse = query({
  prompt: "咱们换个思路,把这个 REST API 改成 GraphQL 方案试试",
  options: {
    resume: originalSessionId,    // 基于原始会话
    forkSession: true,            // 关键:分叉出新会话!
    model: "claude-sonnet-4-20250514",
    allowedTools: ["Read", "Write", "Edit", "Bash"]
  }
});

let forkedSessionId: string | undefined;

for await (const message of forkedResponse) {
  if (message.type === "system" && message.subtype === "init") {
    forkedSessionId = message.session_id;
    // 注意:这个 ID 和 originalSessionId 是不同的!
    console.log(`分叉会话创建成功: ${forkedSessionId}`);
  }
  // 处理消息...
}

// 原始会话仍然可以继续
const originalContinued = query({
  prompt: "继续完善这个 REST API,加上认证功能",
  options: {
    resume: originalSessionId,     // 原始会话不受影响
    forkSession: false,            // 不分叉,继续原来的(默认就是 false)
    model: "claude-sonnet-4-20250514"
  }
});

Python 版本:

from claude_code_sdk import query, ClaudeCodeOptions

original_session_id = "session-original-id"

# 分叉出一个新会话
async for message in query(
    prompt="咱们换个思路,把这个 REST API 改成 GraphQL 方案试试",
    options=ClaudeCodeOptions(
        resume=original_session_id,
        fork_session=True,  # Python 用下划线命名
        model="claude-sonnet-4-20250514",
        allowed_tools=["Read", "Write", "Edit", "Bash"]
    )
):
    if message.type == "system" and message.subtype == "init":
        forked_session_id = message.session_id
        print(f"分叉会话创建成功: {forked_session_id}")

分叉 vs 继续:有什么区别?

行为 forkSession: false(默认) forkSession: true
Session ID 保持和原来一样 生成一个全新的 ID
对话历史 追加到原始会话里 从分叉点创建新分支
原始会话 被修改了 保持不变
适用场景 继续线性对话 探索不同的方案

用一个更形象的比喻:

实战:方案对比

这是分叉最常见的用法 —— 让 Agent 用两种不同的方法解决同一个问题,然后你来对比哪个更好:

import { query } from "@anthropic-ai/claude-agent-sdk";

async function compareSolutions() {
  // ===== 第一步:创建初始会话,描述问题 =====

  let sessionId: string | undefined;

  const initial = query({
    prompt: `我有一个 Node.js 项目,需要实现一个缓存系统。
要求:
- 支持 TTL(过期时间)
- 支持 LRU 淘汰策略
- 内存使用不超过 100MB
请先分析一下这个需求。`,
    options: {
      model: "claude-sonnet-4-20250514",
      allowedTools: ["Read", "Glob", "Grep"]
    }
  });

  for await (const message of initial) {
    if (message.type === "system" && message.subtype === "init") {
      sessionId = message.session_id;
    }
    // 收集分析结果...
  }

  console.log("需求分析完成,现在分叉两个方案来对比。\n");

  // ===== 第二步:分叉方案 A —— 自己实现 =====

  console.log("========== 方案 A:手写实现 ==========");

  const planA = query({
    prompt: "请用纯 TypeScript 手写实现这个缓存系统,不用任何第三方库。",
    options: {
      resume: sessionId,
      forkSession: true,  // 分叉!不影响原始会话
      model: "claude-sonnet-4-20250514",
      allowedTools: ["Write", "Bash"]
    }
  });

  let planAResult = "";
  for await (const message of planA) {
    if (message.type === "assistant") {
      for (const block of message.content) {
        if (block.type === "text") {
          planAResult += block.text;
        }
      }
    }
  }

  // ===== 第三步:分叉方案 B —— 用第三方库 =====

  console.log("\n========== 方案 B:使用第三方库 ==========");

  const planB = query({
    prompt: "请使用 lru-cache 这个 npm 包来实现这个缓存系统。",
    options: {
      resume: sessionId,   // 同样基于原始会话分叉
      forkSession: true,   // 分叉!
      model: "claude-sonnet-4-20250514",
      allowedTools: ["Write", "Bash"]
    }
  });

  let planBResult = "";
  for await (const message of planB) {
    if (message.type === "assistant") {
      for (const block of message.content) {
        if (block.type === "text") {
          planBResult += block.text;
        }
      }
    }
  }

  // ===== 第四步:对比两个方案 =====

  console.log("\n========== 方案对比 ==========");
  console.log("方案 A(手写):", planAResult.slice(0, 200) + "...");
  console.log("方案 B(第三方库):", planBResult.slice(0, 200) + "...");
}

compareSolutions();

这个例子里:

  1. 先创建一个初始会话,让 Agent 分析需求
  2. 然后从同一个起点分叉出两个会话
  3. 方案 A 让 Agent 手写实现
  4. 方案 B 让 Agent 用第三方库实现
  5. 最后你可以对比两个方案,选一个更好的

注意:两个分叉都是基于原始会话的同一个时间点,所以它们的起点是完全相同的。这就是分叉的威力。


8.5 会话的生命周期

一个会话的完整一生

创建(Create)
   ↓
使用(Use) ←→ 暂停(Pause)
   ↓
恢复(Resume) / 分叉(Fork)
   ↓
结束(End)
   ↓
清理(Clean up)

用大白话说:

  1. 创建:你调用 query(),SDK 自动创建一个新会话,分配一个 session_id
  2. 使用:Agent 在会话中工作,所有消息自动记录到会话文件
  3. 暂停query() 的 for-await 循环结束后,会话就"暂停"了。数据保存在磁盘上
  4. 恢复:你用 resume: sessionId 调用 query(),会话从暂停处继续
  5. 分叉:你用 resume: sessionId + forkSession: true,从当前状态开一个新分支
  6. 结束:你决定不再需要这个会话了
  7. 清理:删除会话文件,释放磁盘空间

会话数据存在哪里?

Claude Agent SDK 使用 .jsonl(JSON Lines)格式保存会话数据。每个会话一个文件。

默认情况下,会话文件存放在:

~/.claude/projects/<项目路径的hash>/sessions/

其中 <项目路径的hash> 是你项目目录路径的哈希值,确保不同项目的会话不会混在一起。

会话文件的内容

每个 .jsonl 文件里,每一行都是一条 JSON 记录,记录了会话中的每一条消息:

{"type":"system","subtype":"init","session_id":"abc123","timestamp":"2026-02-25T10:00:00Z"}
{"type":"assistant","content":[{"type":"text","text":"好的,我来帮你分析这个项目..."}],"timestamp":"2026-02-25T10:00:01Z"}
{"type":"assistant","content":[{"type":"tool_use","name":"Glob","input":{"pattern":"**/*.ts"}}],"timestamp":"2026-02-25T10:00:02Z"}
{"type":"assistant","content":[{"type":"text","text":"项目中有15个TypeScript文件..."}],"timestamp":"2026-02-25T10:00:05Z"}
{"type":"result","subtype":"success","timestamp":"2026-02-25T10:00:06Z"}

这种格式的好处:

如何清理旧会话?

会话文件会越来越多,时间长了会占不少磁盘空间。清理的方法:

import { readdirSync, unlinkSync, statSync } from "fs";
import { join } from "path";
import { homedir } from "os";

function cleanOldSessions(maxAgeDays: number = 30) {
  // 会话目录路径(根据实际情况调整)
  const sessionsDir = join(homedir(), ".claude", "projects");

  // 遍历所有项目目录
  const projects = readdirSync(sessionsDir);

  let cleaned = 0;
  const now = Date.now();
  const maxAgeMs = maxAgeDays * 24 * 60 * 60 * 1000;

  for (const project of projects) {
    const sessionPath = join(sessionsDir, project, "sessions");
    try {
      const files = readdirSync(sessionPath);
      for (const file of files) {
        if (file.endsWith(".jsonl")) {
          const filePath = join(sessionPath, file);
          const stat = statSync(filePath);
          if (now - stat.mtimeMs > maxAgeMs) {
            unlinkSync(filePath);
            cleaned++;
          }
        }
      }
    } catch {
      // 目录不存在就跳过
    }
  }

  console.log(`清理了 ${cleaned} 个旧会话文件`);
}

// 清理30天前的会话
cleanOldSessions(30);

当然,你也可以简单粗暴地手动删除:

# 查看会话文件占了多少空间
du -sh ~/.claude/projects/

# 删除所有会话文件(谨慎操作!)
find ~/.claude/projects -name "*.jsonl" -mtime +30 -delete

8.6 实战:可中断的长任务

场景说明

我们来做一个"研究助手 Agent",它的任务是分析一个代码仓库,生成一份完整的技术报告。这个任务比较耗时,可能需要分多次完成。所以我们需要:

  1. Agent 能把当前进度保存下来
  2. 中断后能恢复,接着上次的进度继续
  3. 每次恢复时,Agent 知道自己已经做了哪些、还剩哪些

完整代码

import { query } from "@anthropic-ai/claude-agent-sdk";
import { writeFileSync, readFileSync, existsSync } from "fs";

// ===== 类型定义 =====

interface TaskState {
  sessionId: string;
  taskName: string;
  startedAt: string;
  lastResumedAt: string;
  resumeCount: number;
  status: "in_progress" | "completed";
}

// ===== 状态管理 =====

const STATE_FILE = ".research_task.json";

function saveState(state: TaskState) {
  writeFileSync(STATE_FILE, JSON.stringify(state, null, 2));
}

function loadState(): TaskState | null {
  if (existsSync(STATE_FILE)) {
    return JSON.parse(readFileSync(STATE_FILE, "utf-8"));
  }
  return null;
}

function clearState() {
  if (existsSync(STATE_FILE)) {
    const { unlinkSync } = require("fs");
    unlinkSync(STATE_FILE);
  }
}

// ===== Agent 运行逻辑 =====

async function runResearchAgent() {
  const existingState = loadState();

  if (existingState && existingState.status === "completed") {
    console.log("任务已完成!如需重新开始,请删除 .research_task.json");
    return;
  }

  // 决定是开始新任务还是恢复旧任务
  const isResume = existingState !== null;

  let prompt: string;
  let options: any;

  if (isResume) {
    console.log("=== 恢复之前的研究任务 ===");
    console.log(`任务: ${existingState.taskName}`);
    console.log(`开始时间: ${existingState.startedAt}`);
    console.log(`已恢复次数: ${existingState.resumeCount}`);
    console.log("");

    prompt = `请继续上次的工作。检查一下之前的进度,然后继续完成剩余的分析。
如果所有分析都已完成,请生成最终报告。`;

    options = {
      resume: existingState.sessionId,  // 恢复之前的会话
      model: "claude-sonnet-4-20250514",
      allowedTools: ["Read", "Glob", "Grep", "Write", "Bash"],
      maxTurns: 20  // 每次最多跑 20 轮,避免跑太久
    };
  } else {
    console.log("=== 开始新的研究任务 ===");
    console.log("");

    prompt = `请对当前项目进行全面的技术分析,生成一份研究报告。
报告需要包含以下部分:

1. 项目概述(目录结构、使用的技术栈)
2. 代码质量分析(代码风格、注释覆盖率、复杂度)
3. 依赖分析(用了哪些第三方库,版本是否过时)
4. 潜在问题(安全隐患、性能瓶颈、代码异味)
5. 改进建议

请一步一步来,先做第1部分。每完成一部分就告诉我。
最终把报告写入 RESEARCH_REPORT.md 文件。`;

    options = {
      model: "claude-sonnet-4-20250514",
      allowedTools: ["Read", "Glob", "Grep", "Write", "Bash"],
      maxTurns: 20
    };
  }

  // 运行 Agent
  let sessionId: string | undefined;
  let lastMessage = "";

  const response = query({ prompt, options });

  for await (const message of response) {
    // 捕获 session_id
    if (message.type === "system" && message.subtype === "init") {
      sessionId = message.session_id;
      if (!isResume) {
        console.log(`会话已创建: ${sessionId}\n`);
      } else {
        console.log(`会话已恢复: ${sessionId}\n`);
      }
    }

    // 显示 Agent 的输出
    if (message.type === "assistant") {
      for (const block of message.content) {
        if (block.type === "text") {
          process.stdout.write(block.text);
          lastMessage = block.text;
        }
        if (block.type === "tool_use") {
          console.log(`\n[调用工具: ${block.name}]`);
        }
      }
    }

    // 处理结果
    if (message.type === "result") {
      console.log(`\n\n--- Agent 停止,原因: ${message.subtype} ---`);
    }
  }

  // 保存状态
  if (sessionId) {
    const isCompleted = lastMessage.includes("报告已完成") ||
                        lastMessage.includes("全部完成") ||
                        existsSync("RESEARCH_REPORT.md");

    const state: TaskState = {
      sessionId,
      taskName: "项目技术分析报告",
      startedAt: existingState?.startedAt || new Date().toISOString(),
      lastResumedAt: new Date().toISOString(),
      resumeCount: (existingState?.resumeCount || 0) + (isResume ? 1 : 0),
      status: isCompleted ? "completed" : "in_progress"
    };

    saveState(state);

    if (isCompleted) {
      console.log("\n任务完成!报告已生成。");
    } else {
      console.log("\n任务已暂停,进度已保存。");
      console.log("再次运行此脚本即可继续。");
    }
  }
}

// ===== 入口 =====

// 支持 --clean 参数来重置任务
if (process.argv.includes("--clean")) {
  clearState();
  console.log("任务状态已清除。");
} else {
  runResearchAgent().catch(console.error);
}

怎么用?

# 第一次运行:开始新任务
npx tsx research-agent.ts

# Agent 跑了一会儿,你按 Ctrl+C 中断,或者它跑满了 maxTurns 自动停止
# 进度自动保存

# 第二次运行:恢复任务
npx tsx research-agent.ts

# 第三次运行:继续恢复(可以恢复无限多次)
npx tsx research-agent.ts

# 清除状态,重新开始
npx tsx research-agent.ts --clean

这个例子的几个关键点

  1. 状态持久化:用一个 JSON 文件保存 session_id 和任务状态,程序重启后能读出来
  2. 智能恢复:恢复时告诉 Agent "检查之前的进度",让它自己判断做到哪了
  3. maxTurns 控制:每次最多跑 20 轮,防止 Agent 一口气跑太久、消耗太多 Token
  4. 完成检测:通过检查 Agent 的输出或文件是否存在,判断任务是否完成
  5. 优雅中断:即使被 Ctrl+C 中断,下次恢复也没问题,因为 SDK 已经把消息写入了会话文件

动手练习

练习1:实现一个可中断/恢复的长任务 Agent

任务:做一个"翻译 Agent",它需要把一个大的 Markdown 文件从英文翻译成中文。

要求

  1. 文件可能很大,一次翻译不完
  2. Agent 每次翻译一个章节(或一定数量的段落)
  3. 翻译到一半可以中断,下次接着翻译
  4. 需要跟踪翻译进度(已翻译多少、还剩多少)
  5. 所有翻译结果保存在一个输出文件里

提示

// 框架代码
import { query } from "@anthropic-ai/claude-agent-sdk";
import { readFileSync, writeFileSync, existsSync } from "fs";

interface TranslationState {
  sessionId: string;
  sourceFile: string;
  outputFile: string;
  totalSections: number;
  completedSections: number;
}

async function translateDocument(sourceFile: string) {
  const state = loadState();

  if (state) {
    // 恢复之前的翻译
    console.log(`恢复翻译: ${state.completedSections}/${state.totalSections} 已完成`);
    // ... 用 resume 继续
  } else {
    // 开始新的翻译
    // ... 让 Agent 先分析文档结构,然后逐章翻译
  }
}

// 请你来实现完整的逻辑!

练习2:实现"方案对比"功能

任务:做一个"架构顾问 Agent",让它用分叉会话来对比两种技术方案。

要求

  1. 先创建一个初始会话,描述一个技术问题(比如"如何设计一个消息队列系统")
  2. 从同一个起点分叉出两个会话:
    • 方案 A:用 Redis 实现
    • 方案 B:用 Kafka 实现
  3. 两个方案都跑完后,创建第三个分叉会话,让 Agent 对比两个方案的优劣
  4. 最终输出一份对比报告

提示

async function compareArchitectures() {
  // 第1步:创建初始会话,分析需求
  let baseSessionId = await analyzeRequirement(
    "设计一个日处理1000万条消息的消息队列系统"
  );

  // 第2步:分叉方案 A
  const planAResult = await fork(baseSessionId,
    "请使用 Redis Streams 来实现这个消息队列系统,给出详细设计"
  );

  // 第3步:分叉方案 B
  const planBResult = await fork(baseSessionId,
    "请使用 Kafka 来实现这个消息队列系统,给出详细设计"
  );

  // 第4步:分叉对比
  const comparison = await fork(baseSessionId,
    `请对比以下两个方案并给出推荐:
    方案A(Redis)的要点: ${planAResult}
    方案B(Kafka)的要点: ${planBResult}`
  );

  console.log("对比报告:", comparison);
}

// 请你来实现 analyzeRequirement 和 fork 函数!

本章小结

这一章我们学了会话管理的三大核心能力:

  1. 获取 Session ID —— 从 init 消息中提取 session_id,保存下来备用
  2. 恢复会话(Resume) —— 用 resume: sessionId 参数让 Agent 继续之前的对话,就像重新打开一个关掉的浏览器标签页
  3. 分叉会话(Fork) —— 用 resume: sessionId + forkSession: true 创建平行分支,两个会话各走各的路

关键要点:

一句话总结:会话管理让 Agent 有了"记忆",不再是一条只有7秒记忆的金鱼。


下一章预告

下一章我们要学 结构化输出 —— 让 Agent 按你指定的格式返回数据。不再是一大段自然语言,而是规规矩矩的 JSON,方便你的程序直接解析和使用。想让 Agent 返回一个评分表?一份代码分析报告?一个包含特定字段的数据结构?第9章教你搞定。

← 上一章7. 流式输出