构建会话浏览器:使用Claude Agent SDK管理会话历史
技术背景
在将AI代理作为产品(如桌面应用、IDE扩展或聊天机器人)发布时,用户最迫切的需求之一就是会话历史管理。他们希望查看过去的对话、继续之前的讨论,或在不丢失原始上下文的情况下分支探索新方向。然而,代理循环本身只解决了单次交互的问题,而产品体验的另一半——会话管理——往往被忽视。Claude Agent SDK将每次对话以JSONL格式转录到磁盘,并提供了一组函数来读取和组织这些转录文件,使得开发者无需手动解析文件或遍历目录,即可构建功能完整的会话侧边栏。本教程旨在填补这一空白,帮助开发者掌握这些关键原语,从而在Agent SDK之上构建出用户友好的界面。
核心原理
Claude Agent SDK的会话管理基于本地文件系统存储。每次调用query()时,对话内容会以JSONL格式追加到~/.claude/projects/<encoded-cwd>/<session-id>.jsonl文件中。cwd选项决定了会话归属的项目目录,所有使用相同cwd的调用共享同一个存储桶。SDK提供的会话管理函数(如list_sessions、get_session_messages等)直接读取这些文件,不启动代理子进程,也不进行API调用,因此性能高效且独立于代理运行状态。核心设计决策包括:使用文件头部和尾部切片而非完整解析来快速获取元数据;通过追加操作实现重命名和标签,避免文件重写;分叉通过复制文件并重新映射消息ID实现,原始会话保持不变。
架构分析
本教程的架构围绕本地会话存储层展开,该层由Claude Agent SDK提供,位于~/.claude/projects/目录下。上层是会话管理API,包括list_sessions、get_session_info、get_session_messages、rename_session、tag_session、fork_session和delete_session。这些API封装了文件操作,为UI层提供数据。UI层可以是任何前端框架,通过调用这些API(直接或通过后端服务)来渲染会话列表、消息视图和操作按钮。数据流为:代理运行产生转录文件 -> 会话管理API读取文件返回结构化数据 -> UI展示给用户。关键设计决策包括:使用cwd隔离项目,便于按项目管理会话;分页支持(limit/offset)确保长列表性能;软删除模式(通过标签)避免误删数据。
完整实战教程
当你将代理作为产品(桌面应用、IDE扩展、内部聊天机器人)发布时,用户首先要求的就是侧边栏。他们想看到上周二的对话,跳回去继续,或者在不丢失原始对话的情况下分支到新方向。代理循环只是产品的一半;另一半是会话管理。
Claude Agent SDK 将每次对话写入磁盘上的 JSONL 转录文件。它还提供了一组函数,用于读取这些转录文件并组织它们,因此你可以构建侧边栏,而无需编写文件解析器或手动遍历 ~/.claude/projects/。
通过本教程,你将能够:
- 列出并渲染项目的过去会话,支持分页和元数据(如分支、标题和最后修改时间)
- 将存储的会话消息读回你的 UI,而无需启动代理
- 重命名、标记和过滤会话,以便用户组织他们的历史记录
- 在任意点分叉会话,并将分叉作为实时
query()调用恢复
这是 Claude Code Desktop 和 VS Code 扩展中会话侧边栏背后的模式。同样的原语适用于你希望在 Agent SDK 之上构建的任何 UI。
先决条件
在遵循本指南之前,请确保您具备:
所需知识
- Python 基础知识,包括
async/await - 熟悉 Agent SDK 的
query()函数(参见 Notebook 00 了解介绍)
所需工具
- Python 3.11 或更高版本
- Claude Code CLI(
npm install -g @anthropic-ai/claude-code) - Anthropic API 密钥(在此获取)
设置
安装所需的依赖项。会话管理功能在 claude-agent-sdk v0.1.51 中引入。
%%capture
%pip install -U "claude-agent-sdk>=0.1.51" python-dotenv pandas
从 .env 加载你的 API 密钥并配置模型。这里我们使用 Haiku,因为演示会话较短,我们希望它们便宜且快速;在实际产品中,你可以选择适合你的代理的任何模型。
import os
from pathlib import Path
from dotenv import load_dotenv
load_dotenv()
MODEL = "claude-haiku-4-5"
# All demo sessions live under this project directory. Using a dedicated
# cwd keeps the demo isolated from your real Claude Code sessions.
# Note: this path resolves relative to the kernel's working directory
# (claude_agent_sdk/ when launched per the README).
DEMO_DIR = str(Path("session_browser_demo").resolve())
os.makedirs(DEMO_DIR, exist_ok=True)
print(f"Demo project dir: {DEMO_DIR}")
第一部分:创建一些会话进行管理
会话管理函数读取query()(或Claude Code CLI)已经写入的转录。为了有内容可浏览,我们首先运行三个简短的对话并捕获它们的会话ID。
cwd选项告诉SDK这个对话属于哪个项目目录。转录文件存放在~/.claude/projects/<encoded-cwd>/<session-id>.jsonl下,因此所有使用相同cwd的调用最终都会归入同一个桶。这个桶就是之后list_sessions(directory=...)读取的内容。
我们禁用工具并将每次运行限制为一轮,以保持令牌使用量最小。
from claude_agent_sdk import ClaudeAgentOptions, ResultMessage, query
async def run_one_turn(prompt: str) -> str:
"""Run a single-turn conversation and return its session_id."""
opts = ClaudeAgentOptions(
model=MODEL,
cwd=DEMO_DIR,
max_turns=1,
allowed_tools=[], # text-only, no tool loop
)
session_id = None
async for msg in query(prompt=prompt, options=opts):
if isinstance(msg, ResultMessage):
session_id = msg.session_id
preview = (msg.result or "")[:80]
print(f"[{session_id[:8]}] {preview}...")
if session_id is None:
raise RuntimeError("No ResultMessage received; check API key and SDK version.")
return session_id
三个提示词,三个会话。在实际产品中,这些就是你的用户向智能体提出的任何请求。
prompts = [
"Give me three name ideas for a CLI tool that manages git worktrees.",
"Explain the difference between a mutex and a semaphore in one paragraph.",
"Write a haiku about merge conflicts.",
]
demo_session_ids = []
for p in prompts:
sid = await run_one_turn(p)
demo_session_ids.append(sid)
print(f"\nCreated {len(demo_session_ids)} sessions.")
第二部分:列出并检查会话
构建会话列表
list_sessions() 扫描项目的记录目录,并返回每个会话的元数据,按最新排序。它读取文件统计信息以及每个记录的开头和结尾,因此即使目录中有数百个文件也能保持快速。不会生成子进程,也不会进行 API 调用。
每个 SDKSessionInfo 都包含你在选择器中渲染一行所需的信息:显示摘要、最后修改时间戳、git 分支、工作目录,以及你设置的任何自定义标题或标签。
from datetime import datetime
import pandas as pd
from claude_agent_sdk import SDKSessionInfo, list_sessions
sessions = list_sessions(directory=DEMO_DIR)
# Render as a table. In a real UI this would be your sidebar component.
rows = []
for s in sessions:
rows.append(
{
"id": s.session_id[:8],
"summary": (s.summary[:50] + "...") if len(s.summary) > 50 else s.summary,
"modified": datetime.fromtimestamp(s.last_modified / 1000).strftime("%H:%M:%S"),
"branch": s.git_branch or "-",
"tag": s.tag or "-",
}
)
pd.DataFrame(rows)
对于较长的历史记录,可通过传递 limit 和 offset 参数进行分页。会话选择器通常先加载第一页,然后在用户滚动时获取更多内容:
page_2 = list_sessions(directory=DEMO_DIR, limit=20, offset=20)
如果你的应用已经存储了会话 ID(例如,在你自己数据库中与用户记录一起存储),并且你只需要那一行数据,那么 get_session_info() 比列出所有内容更节省资源。
from claude_agent_sdk import get_session_info
info = get_session_info(demo_session_ids[0], directory=DEMO_DIR)
print(f"Session: {info.session_id}")
print(f"Summary: {info.summary}")
print(f"First prompt: {info.first_prompt}")
print(f"Created: {datetime.fromtimestamp(info.created_at / 1000)}")
print(f"Size: {info.file_size:,} bytes")
读取会话消息
当用户在侧边栏点击某个会话时,你会将其对话加载到主视图中。get_session_messages() 从记录中重建消息链,并按顺序返回用户和助手的轮次。与列表函数一样,它也是纯文件读取:代理无需运行。
每个 SessionMessage 都有一个 type("user" 或 "assistant")、一个 uuid,以及一个与 Anthropic Messages API 格式相同的 message 字典(role、content)。
from claude_agent_sdk import get_session_messages
messages = get_session_messages(demo_session_ids[0], directory=DEMO_DIR)
for m in messages:
role = m.type
# content is a list of blocks; pull out the text ones
text_parts = [
b.get("text", "")
for b in m.message.get("content", [])
if isinstance(b, dict) and b.get("type") == "text"
]
text = " ".join(text_parts).strip()
print(f"[{role:>9}] {text[:100]}")
对于长时间的会话,limit 和 offset 允许你一次加载一个窗口。聊天视图可能在打开时加载最近 50 条消息,然后在用户向上滚动时获取更早的页面。偏移量按时间顺序(从最旧到最新)应用,因此第 0 页是对话的开始。
第3部分:使用标题和标签进行组织
重命名会话
自动生成的摘要适合快速浏览,但用户通常希望给会话起一个真正的名称。rename_session() 会在记录中追加一个标题条目;list_sessions() 在下次读取时会将其作为 custom_title 获取。
追加操作成本低且幂等:调用两次重命名只会让较新的标题生效。不会发生文件重写。
from claude_agent_sdk import rename_session
rename_session(demo_session_ids[0], "Worktree CLI naming brainstorm", directory=DEMO_DIR)
rename_session(demo_session_ids[2], "Haiku corner", directory=DEMO_DIR)
# Verify the titles stuck
for s in list_sessions(directory=DEMO_DIR):
label = s.custom_title or "(auto)"
print(f"{s.session_id[:8]} custom_title={label!r} summary={s.summary[:40]!r}")
标签与过滤
标签是附加到会话上的单个字符串。你可以根据产品需求使用它们进行任意分类:"archived"、"needs-review"、"favorite"。传入 None 可清除标签。
一个常见的模式是软删除:不删除转录文件,而是将其标记为 "__hidden",并在列表视图中过滤掉。数据仍然可以恢复。
from claude_agent_sdk import tag_session
# Mark two sessions as favorites, hide the other
tag_session(demo_session_ids[0], "favorite", directory=DEMO_DIR)
tag_session(demo_session_ids[2], "favorite", directory=DEMO_DIR)
tag_session(demo_session_ids[1], "__hidden", directory=DEMO_DIR)
def visible_sessions(directory: str, tag_filter: str | None = None) -> list[SDKSessionInfo]:
"""List sessions, hiding soft-deletes and optionally filtering by tag."""
results = []
for s in list_sessions(directory=directory):
if s.tag == "__hidden":
continue
if tag_filter is not None and s.tag != tag_filter:
continue
results.append(s)
return results
favorites = visible_sessions(DEMO_DIR, tag_filter="favorite")
print(f"Visible favorites: {len(favorites)}")
for s in favorites:
print(f" {s.session_id[:8]} [{s.tag}] {s.custom_title or s.summary}")
标签是单一值,不是列表。如果你需要多个维度(比如,一个状态加一个类别),可以将它们编码成一个字符串,如 "review:urgent",并在读取时解析,或者将更丰富的状态存储在你自己的数据库中,以 session_id 为键。
第4部分:分支与恢复
从现有对话分支
分支会将会话记录复制到一个新文件中,并重新映射消息ID。原始会话保持不变。这是“尝试不同方法”功能背后的原语:用户保留原始线程,并获得一个新线程进行实验。
fork_session() 写入新文件并返回其ID。它不会运行代理,因此分支会保留在磁盘上,直到您使用 query() 恢复它。
from claude_agent_sdk import fork_session
source = demo_session_ids[0]
fork = fork_session(
source,
directory=DEMO_DIR,
title="Worktree CLI names (round 2)",
)
print(f"Source: {source}")
print(f"Fork: {fork.session_id}")
# The fork starts with the same message history as the source
source_msgs = get_session_messages(source, directory=DEMO_DIR)
fork_msgs = get_session_messages(fork.session_id, directory=DEMO_DIR)
print(f"Source has {len(source_msgs)} messages, fork has {len(fork_msgs)}")
要从特定点而不是完整历史进行分支,请传入 up_to_message_id。分支将包含源对话记录,直到并包括该消息。你可以通过 get_session_messages()[i].uuid 获取消息 UUID。
将分支恢复为实时查询
分支只是一个对话记录文件。要将其恢复为正在进行的对话,请将其 ID 交给 ClaudeAgentOptions.resume。代理会加载分支历史并从中继续。
resume_opts = ClaudeAgentOptions(
model=MODEL,
cwd=DEMO_DIR,
max_turns=1,
allowed_tools=[],
resume=fork.session_id,
)
async for msg in query(
prompt="Those were okay. Give me three more names, but punnier.",
options=resume_opts,
):
if isinstance(msg, ResultMessage):
print(f"[fork {fork.session_id[:8]} resumed]")
print(msg.result)
原始会话仍然保持不变地留在那里。再次列出,你会看到源会话和分叉会话作为独立的行,各自拥有独立的历史记录。
for s in list_sessions(directory=DEMO_DIR):
marker = "(fork)" if s.session_id == fork.session_id else " "
print(f"{marker} {s.session_id[:8]} {s.custom_title or s.summary[:50]}")
清理
delete_session() 会删除一个转录文件。这是硬删除,这就是为什么第 3 部分中的软删除标签模式通常是面向用户界面的更安全默认选择。
这里我们用它来清理演示创建的所有内容。
from claude_agent_sdk import delete_session
# Clean up every session in the demo dir, including the fork
for s in list_sessions(directory=DEMO_DIR):
delete_session(s.session_id, directory=DEMO_DIR)
print(f"Deleted {s.session_id[:8]}")
remaining = list_sessions(directory=DEMO_DIR)
print(f"\n{len(remaining)} session(s) remaining.")
回顾
我们基于 Agent SDK 的本地会话记录存储构建了一个会话浏览器的核心功能:
- 列出会话:使用
list_sessions()可以获取渲染侧边栏所需的一切信息,并且由于它读取的是文件统计信息和头部/尾部切片,而不是解析整个记录,因此具有良好的扩展性。 - 读取会话:使用
get_session_messages()可以加载对话内容用于显示,而无需启动代理。 - 组织会话:使用
rename_session()和tag_session()可以追加元数据条目,这样成本低廉,且最近一次调用会覆盖之前的设置。 - 分叉会话:使用
fork_session()配合options.resume,用户可以分支对话并继续,而不会影响原始会话。
所有这些操作都是对 ~/.claude/projects/ 目录下的纯文件操作。无论代理子进程是否在运行,它们都能正常工作,并且能看到与 Claude Code CLI 写入的相同记录。
后续方向
- 接入 UI:这些函数与 UI 无关;你可以将它们放在 FastAPI 路由或 Electron IPC 处理器后面,即可作为会话侧边栏的后端。
- 跨主机会话:记录存储在本地磁盘上。要在多台机器之间共享会话,可以同步文件,或者将会话 ID 和消息索引到你自己的存储中。有关模式,请参阅在磁盘上管理会话。
- TypeScript:
@anthropic-ai/claude-agent-sdk中提供了相同的 API,使用驼峰命名(listSessions、forkSession等)。请参阅 TypeScript SDK 参考。 - 更广阔的视角:笔记本 00 到 03 涵盖了构建代理本身的内容。本笔记本则涵盖了管理代理所留下的数据。
常见问题
常见错误包括:1. 未安装正确版本的SDK(需>=0.1.51),导致函数不存在;2. cwd路径不一致,导致会话列表为空;3. 未设置API密钥,导致query()失败;4. 分页偏移量理解错误,偏移量按时间顺序从最旧开始;5. 标签是单一值,不能存储多个标签。排查方法:检查SDK版本,确认cwd路径正确,验证API密钥有效,打印list_sessions结果调试,阅读官方文档。生产环境优化:使用索引数据库(如SQLite)缓存会话元数据,避免频繁文件扫描;对大型会话使用流式读取;实现会话归档策略,定期清理旧文件;考虑跨设备同步,通过云存储或后端数据库。
企业应用扩展
本教程的会话管理功能可轻松集成到各种UI框架中,如FastAPI后端或Electron桌面应用。对于跨主机会话,可同步转录文件或将会话元数据存储到自己的数据库。TypeScript开发者可使用@anthropic-ai/claude-agent-sdk中的等价API(驼峰命名)。此外,可扩展功能包括:实现会话搜索、导出会话为其他格式、添加会话对比功能、集成用户认证和权限控制。更广阔的视角是,本教程是构建代理产品系列的一部分,其他笔记本(00-03)覆盖了代理本身的构建,而本教程专注于管理代理产生的数据,两者结合可构建完整的AI应用。
总结
本教程面向有编程基础的开发者,深入讲解如何利用Claude Agent SDK的本地会话管理功能,构建类似Claude Code Desktop的会话侧边栏。内容涵盖会话的创建、列出、读取、重命名、标签过滤、分叉与恢复,以及删除等核心操作。通过实际代码示例,展示如何高效管理代理产生的JSONL转录文件,并探讨了软删除、分页、跨主机会话等高级话题,帮助开发者掌握构建AI产品用户界面的关键工程实践。
- --
内容来源与验证
- 内容等级:PREMIUM_A
- 质量等级:A
- 难度:中级
- 验证等级:V1
- 原文作者:qing-ant
- 原文日期:2026-03-30
- 来源许可:MIT
- 来源版本:
1b64f67b0f5a6319e7e215c840fe980a2a8f3350 - 查看原文
站内还有更多完整实战教程
加入会员,解锁全部教程、完整代码与持续更新内容。
加入会员