· Agent SDK 会话体系与跨轮记忆

Agent SDK 会话体系与跨轮记忆

2336 字
12 分钟
Agent SDK 会话体系与跨轮记忆
StarryLin/Geek · 摘要

Agent SDK 四层会话体系按需选型:一次性查询适合单次交互,进程内会话适用于同任务多轮,可恢复会话通过 Session API 跨进程重建历史,应用层存储则自主管理持久化、容量与成本。

示例版本说明:本文对照 CodeBuddy 官方示例仓库 提交 25953e3 核对。下文代码为结构摘录,省略了部分导入、类型和实现;博客仓库不包含这些示例目录。unstable_v2_* 属于不稳定接口,运行时请锁定依赖并对照当前文档。

先看要点#

  • Agent SDK 的会话体系分四层:一次性查询 → 进程内会话 → 可恢复会话 → 应用层存储,四者不是替代关系,而是由「记忆存多久、存哪里」决定取舍。
  • 核心 API 一句话:unstable_v2_createSession() 建会话(发 send 收 stream),流里那条 system/init 消息会吐出 session_id,把它存起来,下次用 unstable_v2_resumeSession(sessionId) 可在既有会话记录仍可访问时恢复对话,不能承诺保留全部原始历史。
  • chat-demo 的教训很有代表性:应用层把消息存了 Map、也渲染成 UI,但模型每轮用的是未携带历史或恢复参数的新 query() 调用——「界面有历史」不等于「模型记得」。要跨轮记忆,就得让历史真正进入模型上下文。
  • 工程上只需盯住三件事:拿稳 session_id、想清记忆边界、设好过期与清理。

一、先分清四个层次#

“会话”这个词在不同的代码里往往指不同东西。先建立一张总表:

层次谁来存生命周期典型 API / 代码代表示例
① 一次性查询无单轮query() / unstable_v2_prompt()quick-start
② 进程内会话SDK(内存)一个进程实例内、多轮createSession() + send()/stream()multi-turn-session 的 multi-turn
③ 可恢复会话SDK / CLI 可访问的会话记录在记录仍可访问时跨进程、跨时间createSession() 拿 session_id → resumeSession(id)multi-turn-session 的 resume
④ 应用层存储你自己想存多久存多久Map / SQLite / Redischat-demo、mail-assistant

关键心智模型:

模型“记得”什么 = 每次请求带进上下文的对话历史
① 每次请求 = 白纸
② 同一 session 连续 send = SDK 替你拼接历史
③ resume 旧 session = SDK 找回历史
④ 你自己拼历史再 query = 完全可控但全要自己管

二、三分钟看完四个示例#

multi-turn-session/examples.ts 一次给了四个可运行的例子,正好是四个层次的递进。运行方式:

Terminal window
cd multi-turn-session
npx tsx examples.ts basic # ① 基础会话(发送/接收)
npx tsx examples.ts multi-turn # ② 多轮对话(核心价值)
npx tsx examples.ts one-shot # ③ 单次查询便捷函数
npx tsx examples.ts resume # ④ 会话恢复(跨会话保持上下文)

② 多轮对话:Session 的核心价值#

// multi-turn-session/examples.ts(摘录)
await using session = unstable_v2_createSession({ }); // 不指定模型,使用默认配置
// 第一轮
await session.send('今年是哪一年?只回答年份数字。');
for await (const msg of session.stream()) {
if (msg.type === 'assistant') {
const text = msg.message.content.find((c): c is { type: 'text'; text: string } => c.type === 'text');
console.log(`第一轮:${text?.text}`);
}
}
// 第二轮 - 智能体记住上下文
await session.send('再往后推10年是哪一年?只回答数字。');
for await (const msg of session.stream()) {
/* …同样地消费 stream… */
}

两个语法点:

  • await using:TypeScript 的显式资源管理。会话是“有生命周期”的资源,作用域结束自动释放,不用手动 close,也不用担心忘记清理。
  • send() / stream() 分离:send() 只是把问题发出去,答案通过异步流 stream() 逐条取——后面接流式 UI、接广播,都是同一个模式。

④ 会话恢复:把记忆“冻起来,再解冻”#

// multi-turn-session/examples.ts(摘录)
let sessionId: string | undefined;
// 第一个会话 - 建立记忆
{
await using session = unstable_v2_createSession({ });
await session.send('我的幸运数字是7,请帮我记住这个信息!');
for await (const msg of session.stream()) {
if (msg.type === 'system' && msg.subtype === 'init') {
sessionId = msg.session_id; // ← 关键!从这里拿会话 ID
}
/* …消费 assistant 消息… */
}
}
console.log('--- 会话已关闭,时间流逝... ---\n');
// 第二个会话 - 恢复并验证记忆
{
await using session = unstable_v2_resumeSession(sessionId!, { }); // 用 ID 复活
await session.send('我之前告诉你的幸运数字是多少?'); // 它还记得 7
/* …消费 assistant 消息… */
}

注意 本例的 session_id 从第一轮 stream() 中的 system/init 消息提取。应以锁定版本的 API 为准,不要假定仅保存一个 ID 就同时备份了会话文件和外部工作区。

③ 单次查询:不想开会话时的捷径#

// 不需要维护状态、只想要“问一次拿答案”
const result = await unstable_v2_prompt('长江的源头在哪个省份?只回答省份名称。', { });
if (result.subtype === 'success') {
console.log(`回答:${result.result}`);
console.log(`费用:$${result.total_cost_usd.toFixed(4)}`); // 连账单都带回来
}

三、一个容易被忽略的坑:界面有历史 ≠ 模型记得#

chat-demo 会在界面里保留聊天记录,刷新后也能回放,但这不代表下一次模型调用已经带上了历史。这里需要分别检查应用存储和模型上下文。

不。看它的数据流:

// chat-demo/server/agent.ts(摘录)
async sendMessage(content: string) {
this.currentStream = query({
prompt: content, // ← 每次只把“当前这句”发给模型
options: { systemPrompt: SYSTEM_PROMPT, /* … */ },
})[Symbol.asyncIterator]();
}

chat-demo 的历史存在两个地方:

数据存在哪谁看得见
用户/助手消息(带时间戳)store.ts 的内存 Map前端 UI、REST 接口——模型看不见
固定系统提示词 SYSTEM_PROMPTagent.ts 常量模型能看见,但它是常量,不是对话历史

所以真相是:聊到第二轮时,模型依然是一张白纸,它只看到 SYSTEM_PROMPT + 用户当前这句话。历史消息存在 Map 里纯粹是为了 UI 展示与回放。

把这条结论说透,就是本系列第 3 篇那句「SDK 只干一件事」的补充版:

当前示例没有恢复会话,也没有传入历史,所以每轮是新上下文;这不是 query() API 永远无状态。官方 SDK 还提供 options.resume 等会话恢复配置,也可使用 Session API 或显式组织历史。

对照:三种把“记忆”喂给模型的做法#

做法代码量可控性适用
每次手动拼历史:prompt = 拼接(历史 + 新问题)中最高(截断策略、消息改写全自己定)要精细控制 token 与上下文
Session API(send/stream 循环)低高(记忆边界由 SDK 管理)进程内长对话
resumeSession(sessionId)最低中(依赖已有会话记录可用性)跨进程/跨时段的“续聊”

四、从存储位置理解 Session 的设计#

把几个示例拼起来看,Session 机制背后其实是一套通用工程推演:

用户要“连续对话”
│ 如果每轮都无状态 → 模型每轮失忆
▼
需要“进程内会话” → 建 session、连续 send/stream (②)
│ 进程一重启,记忆就没了
▼
需要“会话持久化/恢复” → 提取 session_id、resumeSession (③)
│ 但 session_id 放哪?进程内存显然不行
▼
需要“应用层状态管理” → Map / SQLite / Redis 存会话与历史 (④)
│ 还要解决:多客户端共享、断线重连、过期清理
▼
落地为一个可运行的会话服务(如 chat-demo 的 sessions Map)

这也是为什么 chat-demo 的 ChatSession 用 Map<chatId, ChatSession> 管理:它模拟的正是「④ 应用层」——用 chatId 当业务键,把 SDK 会话(或其替代品)挂在其下。生产实现还要补上持久化、用户与会话隔离、并发控制、恢复失败处理和过期清理,不能只把 Map 替换成数据库。


五、实现 Session 时的检查项#

  1. 拿稳 session_id 从第一轮 system/init 消息中提取后,立刻持久化(DB 列 / Redis),别等“需要时再取”。

  2. 想清“记忆边界” 一次会话共享一份记忆,但主题往往漂移。长对话要自己定截断/摘要策略——SDK 替你拼历史,SDK / CLI 可能有自己的上下文压缩机制,应用仍需决定哪些业务信息必须保留。

  3. 生命周期与清理 无界 session = 无界账单与存储。参考 chat-demo 的数字:单会话最多 100 条、全局最多 50 个会话、LRU 淘汰;再结合 result 消息里的 total_cost_usd 做成本监控。

  4. 幂等与重试 客户端网络抖动时,重发 send 可能造成重复;用消息 ID 幂等,并区分续聊与重试;resumeSession 本身不提供业务操作幂等保证。

  5. 无状态接口做兜底 不需要记忆的查询(如“翻译这句话”)永远走 unstable_v2_prompt()/query(),别让它们污染长会话上下文——这也是成本优化。


六、小结:一张决策表收尾#

你的场景推荐做法
工具类单次调用(翻译、摘要、改写)query() / unstable_v2_prompt(),别开会话
进程内的连续对话(CLI/REPL/单机流程)createSession() + send()/stream()
对话要“隔夜续聊”、多端同步持久化 session_id → resumeSession()
要自己掌控历史、做 RAG/摘要注入手动拼历史 + query()
多用户 Web 服务应用层(④)按 chatId 建会话,配过期清理;生产用 DB 存 session_id 与历史

记住最反直觉的那一条:“界面记得”和“模型记得”是两件事。前者靠你的存储,后者靠你把历史真正送进模型的上下文——要么靠 Session API,要么靠自己的 prompt 拼接。

核对资料#

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

Agent SDK 会话体系与跨轮记忆
https://mjzy.tech/posts/ai-engineering/04-会话体系与跨轮记忆/
作者
JIA
发布于
2026-09-04
许可协议
CC BY-NC-SA 4.0

评论区

正在加载 GitHub 评论…

Tips · 随手记
仅自己可见 · 每篇文章单独保存

文章目录