第8章:会话管理 —— 让 Agent 记住上下文
一句话:学会创建、恢复、分叉会话,让 Agent 能做长任务。
本章目标
- 理解什么是会话(Session),以及它为什么重要
- 学会从 init 消息中获取 session_id
- 掌握恢复会话(Resume)的用法,让 Agent 接着上次继续干
- 掌握分叉会话(Fork)的用法,让 Agent 走"平行宇宙"
- 理解会话的生命周期和存储机制
- 完成一个可中断、可恢复的长任务 Agent 实战
前置知识
- 需要先看完第4章(query() 函数的基本用法)
- 需要先看完第7章(流式输出,理解消息类型)
8.1 什么是会话(Session)?
先说个痛点
你有没有遇到过这种情况:
你让 Agent 帮你写一个项目,它写了一半,程序崩了,或者你关了终端。再次启动的时候,Agent 完全不记得之前做了什么。你得从头开始解释:"我之前让你做了个 XXX 项目,你已经写了 A 文件和 B 文件,现在请继续写 C 文件……"
这就像你跟一个同事合作,每天早上他都失忆了,你得重新介绍自己、重新说一遍昨天做了什么。太痛苦了!
会话(Session)就是解决这个问题的。
会话是什么?
简单来说:一次对话 = 一个会话。
你跟 Agent 的每一次交互,SDK 都会自动创建一个"会话"。这个会话有一个唯一的 ID(就像身份证号),Agent 说过的每一句话、做过的每一件事,都记录在这个会话里。
把它想象成浏览器的标签页:
- 每个标签页都是独立的,互不干扰
- 你可以关掉一个标签页,下次再打开(恢复会话)
- 你可以复制一个标签页,然后在两个标签页里分别操作(分叉会话)
- 每个标签页都有自己的浏览历史(会话记录)
没有会话会怎样?
如果没有会话管理,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,所以会直接开始写路由和控制器,不会重复劳动。
恢复会话的适用场景
- 长任务中断后继续 —— 写一个大项目,中途关了电脑,第二天接着做
- 多轮对话 —— 先让 Agent 分析问题,你看完分析后再给出下一步指令
- 渐进式开发 —— 今天做基础功能,明天在此基础上加新功能
- 任务暂停 —— 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();
这个例子里:
- 先创建一个初始会话,让 Agent 分析需求
- 然后从同一个起点分叉出两个会话
- 方案 A 让 Agent 手写实现
- 方案 B 让 Agent 用第三方库实现
- 最后你可以对比两个方案,选一个更好的
注意:两个分叉都是基于原始会话的同一个时间点,所以它们的起点是完全相同的。这就是分叉的威力。
8.5 会话的生命周期
一个会话的完整一生
创建(Create)
↓
使用(Use) ←→ 暂停(Pause)
↓
恢复(Resume) / 分叉(Fork)
↓
结束(End)
↓
清理(Clean up)
用大白话说:
- 创建:你调用
query(),SDK 自动创建一个新会话,分配一个 session_id - 使用:Agent 在会话中工作,所有消息自动记录到会话文件
- 暂停:
query()的 for-await 循环结束后,会话就"暂停"了。数据保存在磁盘上 - 恢复:你用
resume: sessionId调用query(),会话从暂停处继续 - 分叉:你用
resume: sessionId+forkSession: true,从当前状态开一个新分支 - 结束:你决定不再需要这个会话了
- 清理:删除会话文件,释放磁盘空间
会话数据存在哪里?
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"}
这种格式的好处:
- 追加写入:Agent 每做一步,追加一行就行,不用重写整个文件
- 方便读取:想恢复会话?逐行读出来就行
- 方便调试:你可以直接用文本编辑器打开看看 Agent 做了什么
如何清理旧会话?
会话文件会越来越多,时间长了会占不少磁盘空间。清理的方法:
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",它的任务是分析一个代码仓库,生成一份完整的技术报告。这个任务比较耗时,可能需要分多次完成。所以我们需要:
- Agent 能把当前进度保存下来
- 中断后能恢复,接着上次的进度继续
- 每次恢复时,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
这个例子的几个关键点
- 状态持久化:用一个 JSON 文件保存 session_id 和任务状态,程序重启后能读出来
- 智能恢复:恢复时告诉 Agent "检查之前的进度",让它自己判断做到哪了
- maxTurns 控制:每次最多跑 20 轮,防止 Agent 一口气跑太久、消耗太多 Token
- 完成检测:通过检查 Agent 的输出或文件是否存在,判断任务是否完成
- 优雅中断:即使被 Ctrl+C 中断,下次恢复也没问题,因为 SDK 已经把消息写入了会话文件
动手练习
练习1:实现一个可中断/恢复的长任务 Agent
任务:做一个"翻译 Agent",它需要把一个大的 Markdown 文件从英文翻译成中文。
要求:
- 文件可能很大,一次翻译不完
- Agent 每次翻译一个章节(或一定数量的段落)
- 翻译到一半可以中断,下次接着翻译
- 需要跟踪翻译进度(已翻译多少、还剩多少)
- 所有翻译结果保存在一个输出文件里
提示:
// 框架代码
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",让它用分叉会话来对比两种技术方案。
要求:
- 先创建一个初始会话,描述一个技术问题(比如"如何设计一个消息队列系统")
- 从同一个起点分叉出两个会话:
- 方案 A:用 Redis 实现
- 方案 B:用 Kafka 实现
- 两个方案都跑完后,创建第三个分叉会话,让 Agent 对比两个方案的优劣
- 最终输出一份对比报告
提示:
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 函数!
本章小结
这一章我们学了会话管理的三大核心能力:
- 获取 Session ID —— 从 init 消息中提取
session_id,保存下来备用 - 恢复会话(Resume) —— 用
resume: sessionId参数让 Agent 继续之前的对话,就像重新打开一个关掉的浏览器标签页 - 分叉会话(Fork) —— 用
resume: sessionId+forkSession: true创建平行分支,两个会话各走各的路
关键要点:
- 每次
query()调用都会自动创建会话,你只需要捕获 session_id - 恢复会话时,Agent 记得所有之前的对话内容和工具调用结果
- 分叉会话不影响原始会话,适合"试试看"的场景
- 会话数据以
.jsonl格式存储在磁盘上,自动管理 - 你的程序状态(变量、连接等)需要自己保存和恢复,SDK 只管对话上下文
- 用
maxTurns控制每次运行的长度,配合会话恢复实现"分步执行"
一句话总结:会话管理让 Agent 有了"记忆",不再是一条只有7秒记忆的金鱼。
下一章预告
下一章我们要学 结构化输出 —— 让 Agent 按你指定的格式返回数据。不再是一大段自然语言,而是规规矩矩的 JSON,方便你的程序直接解析和使用。想让 Agent 返回一个评分表?一份代码分析报告?一个包含特定字段的数据结构?第9章教你搞定。