Genkit JS Agent Branching 实战:基于不可变快照的分支会话(Beta)
2026/9/13 17:40:49 网站建设 项目流程

Genkit JS Agent Branching 实战:基于不可变快照的分支会话(Beta)

【免费下载链接】skillsAgent Skills for Google products and technologies项目地址: https://gitcode.com/GitHub_Trending/skills29/skills

本篇指南围绕 Genkit(Node.js/TypeScript)Agent Beta 版 API 的核心能力之一——分支(Branching)展开,讲解如何借助不可变快照(snapshotId)从任意历史会话节点分叉出多条相互独立的时间线,覆盖服务端分叉、浏览器端"多方案选择"、以及从快照恢复历史等完整实战场景。读完本文,你将掌握chat({ snapshotId })的分支语义、配套的会话存储选型与 HTTP 端点配置,能够直接在 Genkit 应用中实现"一次会话、多个候选分支、用户择优继续"的交互形态。

本文档是 Genkit JS Agent 参考文档系列中的一篇。建议按序阅读:agents.md(Agent 基础与 HTTP 服务)→ agents-sessions.md(会话存储与快照持久化)→ 本篇。

前置要求与适用范围

分支能力属于Beta / preview API,使用前需要明确以下前提(依据 agents.md 与 SKILL.md):

  • 必须使用genkit/beta导入路径。服务端 API 来自genkit/beta(如InMemorySessionStore),浏览器端客户端来自genkit/beta/client(如remoteAgent),而不是稳定的genkit入口;导入路径与函数签名后续可能变更。
  • 要求genkit>= 1.39.0,CLI 最低版本 1.29.0(可通过genkit --version校验)。
  • 分支必须配合会话存储(session store)。因为快照需要持久化,store是启用分支的前提;无 store 的纯无状态 Agent 虽然可以做多轮对话,但不具备快照分支能力。
  • 读者应已掌握 Agent 的基本用法(ai.defineAgentagent.chat()remoteAgent),本文不再重复基础定义。

核心概念:快照即不可变检查点

分支的基石是snapshotId。Genkit 把每一轮对话的会话状态(消息历史 + 自定义状态 + 工件)固化为一个不可变检查点(immutable checkpoint)——正如 agents.md 所述,chat.send()的返回结果中带有res.snapshotId,它就是这一轮结束时的快照 ID。

snapshotId的语义与 git commit 高度相似,理解这一点就理解了整个分支模型:

  • 不可变:快照一旦产生就不可修改,原快照在分支后保持不变;
  • 可派生:从同一个快照可以派生任意数量的独立时间线(independent timelines),它们互不影响;
  • 分叉即新快照:从快照S上开启的一轮新对话,会产生一个新的、独立的快照S',而S原封不动。

用代码表达就是一行调用:

const branchA = assistant.chat({ snapshotId: checkpoint });

打开一个附着在较早快照上的新chat,即完成一次分支;后续该chat的每一轮都会自动把状态沿分支链向前推进。

服务端分支:从同一快照分叉出独立时间线

在服务端代码中,分支操作非常直观(原文示例完整保留):

import { z } from 'genkit'; import { InMemorySessionStore } from 'genkit/beta'; import { ai } from './genkit.js'; export const assistant = ai.defineAgent({ name: 'assistant', system: 'You are a helpful assistant.', store: new InMemorySessionStore(), }); const root = assistant.chat(); const res1 = await root.send('Hello!'); const checkpoint = res1.snapshotId; // 分支点 // 分支 A —— 从 checkpoint 分叉。 const branchA = assistant.chat({ snapshotId: checkpoint }); await branchA.send('My name is Bob.'); const resA = await branchA.send('What is my name?'); // -> Bob // 分支 B —— 从同一个 checkpoint 分叉,完全独立。 const branchB = assistant.chat({ snapshotId: checkpoint }); await branchB.send('My name is John.'); const resB = await branchB.send('What is my name?'); // -> John

这段代码的关键行为:

  1. root.send('Hello!')完成后,checkpoint指向包含这条问候历史的快照;
  2. branchAcheckpoint继续,Agent 记住了"Bob"这个名字;
  3. branchB也从checkpoint继续,Agent 记住了"John"这个名字;
  4. 两条分支对彼此的状态一无所知resA回答 Bob、resB回答 John,互不干扰。

从源码结构看(见 agents-sessions.md),这里store承担了快照的读写职责:每个chat会持久化到存储并按快照链自动向前推进(chat.send()会沿用上一个快照),而chat({ snapshotId })则显式指定从哪个历史快照续接。

客户端分支:"多方案择优"模式

服务端分支是后端能力,而**客户端分支(client-side branching)**解决的是一个非常典型的交互需求:并行生成多个候选方案,让用户挑选一个,再从被选中的快照继续

原文给出了完整的浏览器/Node 客户端实现(genkit/beta/client中的remoteAgent):

import { remoteAgent } from 'genkit/beta/client'; const agent = remoteAgent({ url: '/api/branchingAgent' }); let snapshotId: string | undefined; // 当前分支点 async function twoVariants(text: string) { // 每个变体都从同一快照分叉出自己的 chat //(尚未有分支点时则开启全新会话)。 const makeChat = () => snapshotId ? agent.chat({ snapshotId }) : agent.chat(); const [a, b] = await Promise.all([ makeChat().send(text), makeChat().send(text), ]); // a.snapshotId !== b.snapshotId —— 两者从同一点分叉。 return { a, b }; } // 当用户选中某个变体时,其 snapshotId 成为新的分支点: function pick(chosenSnapshotId: string) { snapshotId = chosenSnapshotId; }

要点解析:

  • makeChat()是一个工厂函数:存在分支点时用agent.chat({ snapshotId })从旧快照分叉,否则agent.chat()开新会话;
  • 两个变体通过Promise.all并行发起,各自拿到独立的snapshotIda.snapshotId !== b.snapshotId);
  • pick(chosenSnapshotId)把用户的选择写入snapshotId变量,之后生成的任何新变体、或用户的后续对话,都从这条被选中的分支继续。

这就是"git 分支"思想在对话 UI 中的落地:用户可以比较多个续写方向,再决定合并进哪条主线。

从快照恢复历史:不发起回合读取状态

有时你不需要继续对话,只想读取某个快照里的状态——例如页面刷新后,根据 URL 中保存的snapshotId恢复聊天 UI。为此 Genkit 提供了agent.getSnapshot(snapshotId),它只读状态、不启动任何回合:

import type { Part } from 'genkit/beta'; import { remoteAgent } from 'genkit/beta/client'; const agent = remoteAgent({ url: '/api/branchingAgent' }); const snapshot = await agent.getSnapshot(snapshotId); const history = (snapshot?.state?.messages ?? []) .filter((m) => m.role === 'user' || m.role === 'model') .map((m) => ({ role: m.role, text: (m.content ?? []) .filter((p: Part) => p.text) .map((p: Part) => p.text) .join(''), }));

这段代码的实用价值在于:

  • 只保留usermodel两种角色的消息(过滤掉工具调用等内部消息);
  • 把消息内容中的文本片段(Part)拼接为纯文本,便于直接渲染到 UI;
  • getSnapshot返回的snapshot.state.messages与后台 Agent 内部的消息结构一致,因此这段"快照 → 可渲染历史"的转换逻辑也可复用到 agents-background.md 中轮询后台任务的场景(那里同样从snapshot.state.messages提取最终文本)。

注意:getSnapshot是一个远程调用,服务端必须暴露 Agent 的getSnapshotDataAction(对应POST /api/<name>/getSnapshot端点),详见下文 HTTP 配置一节。

分支背后的存储机制:选对 SessionStore

分支依赖快照持久化,而快照存在哪里由store决定。agents-sessions.md 给出了三类内置存储,理解它们的差异有助于为分支场景选型:

InMemorySessionStore —— 测试与本地开发

import { InMemorySessionStore } from 'genkit/beta'; const memStore = new InMemorySessionStore();

快照保存在内存中,进程重启即丢失。分支语义完整可用,适合验证逻辑、跑测试;生产环境不建议。

FileSessionStore —— 本地文件持久化

import { FileSessionStore } from 'genkit/beta'; // 快照持久化在 <dir>/global/<snapshotId>.json 下 const fileStore = new FileSessionStore('./.snapshots'); // 带链修剪:每条链只保留最近 N 个快照 const pruning = new FileSessionStore('./.snapshots', { maxPersistedChainLength: 3, });
  • 每个快照对应磁盘上的一个 JSON 文件(global/<snapshotId>.json);
  • maxPersistedChainLength可以限制单条分支链上保留的快照数量,避免分支泛滥时磁盘无限增长——注意它修剪的是链上旧快照,不影响"被多个分支共享的分叉点",因为分叉点的快照属于每条分支链。

FirestoreSessionStore —— 生产级可扩展存储

import { genkit } from 'genkit/beta'; import { FirestoreSessionStore } from '@genkit-ai/google-cloud/beta'; const myAgent = ai.defineAgent({ name: 'myAgent', system: 'You are a helpful assistant.', store: new FirestoreSessionStore(), });

针对长会话(聊天/编码 Agent)设计:每轮以JSON Patch 增量 diff形式写入,并锚定周期性分片检查点,避免单个文档逼近 Firestore 1 MiB 限制。可选参数:

  • db:显式传入 Firestore 实例(默认新建Firestore(),遵循FIRESTORE_EMULATOR_HOST);
  • collection:快照集合名(默认"genkit-sessions"),配套集合"<collection>-pointers""<collection>-shards"自动派生;
  • checkpointInterval:全量检查点间隔轮数(默认25),状态小且读多可调低,单轮状态大则调高;
  • shardSize:单个分片/diff 文档的最大字节数(默认512 KiB)。

自定义 SessionStore

若内置存储不满足需求,可自行实现SessionStore<S>接口(见 agents-sessions.md),核心是两个方法:

import type { SessionStore } from 'genkit/beta'; // S 为自定义状态类型。 const store: SessionStore<MyState> = { // 按 snapshotId 或 sessionId(二选一)加载快照。 async getSnapshot(opts) { /* ... */ return undefined; }, // 原子地 读取 → 变更 → 持久化。返回用到的 snapshotId, // 当 mutator 返回 null 时返回 null。 async saveSnapshot(snapshotId, mutator, options) { /* ... */ return snapshotId ?? 'new-id'; }, // 可选:订阅快照状态变更(后台 Agent 使用)。 onSnapshotStateChange(snapshotId, callback, options) { return () => {}; // 取消订阅 }, };

实现自定义存储时需保证saveSnapshot原子性——分支场景下多个 chat 可能同时基于同一快照写入,读改写必须串行化,否则会出现分支覆盖。

HTTP 端点配置:让客户端能够分支与恢复

客户端分支(remoteAgent)和快照恢复(getSnapshot)都依赖服务端暴露的 HTTP 端点。agents.md 与 agents-deployment.md 给出了标准做法。

用 expressHandler 暴露 Agent 及其伴随动作

import { expressHandler } from '@genkit-ai/express'; import express from 'express'; import { weatherAgent } from './weather-agent.js'; const app = express(); app.use(express.json()); // 主回合端点: app.post('/api/weatherAgent', expressHandler(weatherAgent)); // 伴随动作(快照恢复 / 分支 / 后台任务需要): app.post( '/api/weatherAgent/getSnapshot', expressHandler(weatherAgent.getSnapshotDataAction) ); app.post( '/api/weatherAgent/abort', expressHandler(weatherAgent.abortAgentAction) ); app.listen(8080);

getSnapshotDataAction正是remoteAgent.getSnapshot()在服务端的对应实现,因此分支与恢复功能的服务端配置就靠这一个额外端点remoteAgent的默认路径约定是${url}/getSnapshot${url}/abort,与上述注册路径天然匹配,客户端只需提供基础url

伴随动作的选择矩阵

agents-deployment.md 提供了一个可复用的exposeAgent辅助函数,并给出不同能力所需的伴随动作对照表:

Agent 能力snapshotabort
普通对话(客户端或服务端状态)
快照恢复 /分支
后台任务 / detach

即:纯分支场景只需getSnapshot一个伴随动作;如果同时支持后台执行(见 agents-background.md),才需要额外暴露abort。对应注册代码:

// 快照恢复 / 分支 —— 需要 getSnapshot: exposeAgent('branchingAgent', branchingAgent, { snapshot: true }); // 后台 Agent —— 需要 getSnapshot(轮询)与 abort: exposeAgent('backgroundAgent', backgroundAgent, { snapshot: true, abort: true });

若使用 Next.js,可通过@genkit-ai/nextappRoute将伴随动作注册为独立路由文件(如app/api/weatherAgent/getSnapshot/route.ts);Hono/Bun/Deno 等 Fetch 运行时可用@genkit-ai/fetchfetchHandler实现同样效果。

快照状态与分支的边界行为

分支产生的每个快照都有生命周期状态。虽然分支本身不涉及后台执行,但理解快照状态有助于正确判断"能否从这个快照继续":

  • pending—— 仍在处理中(后台任务场景);
  • completed—— 成功完成,只有completed快照可被恢复/续接
  • failed/aborted/expired—— 终态,保留供检查但不可续接(依据 agents-human-in-the-loop.md 与 agents-background.md)。

对于分支而言,被放弃的分支会作为不可变快照继续留在存储中,分支不会覆盖任何已有数据(原文明确说明:nothing is overwritten when you branch)。这意味着:

  • 你可以放心地反复从同一个checkpoint分叉,产生 N 条实验性分支;
  • 被放弃的分支不会"污染"主线,也不会因为新分支的产生而丢失;
  • 配合getSnapshot(snapshotId),用户随时可以"回到过去"重新选择另一条分支继续。

典型应用场景与最佳实践

综合以上能力,分支在 Genkit Agent 应用中最常见的落地方案是:

  1. 写作/创作助手的多方案生成:用户输入一个主题,服务端(或客户端并行)从同一快照生成 2~3 个不同风格的续写,用户择优后,pick(chosenSnapshotId)锚定选中分支继续打磨;
  2. 对话历史恢复:把snapshotId存入 URL,页面刷新后用agent.getSnapshot(snapshotId)重建 UI,无需重新发起回合;
  3. A/B 实验与回溯:同一会话上下文下测试不同的提示策略或工具编排,被放弃的分支以快照形式留档,便于事后审计。

实战中还需注意:

  • 分支点必须在会话存储启用后才有意义:无store的 Agent 虽然可用 客户端托管状态 完成多轮对话,但chat({ snapshotId })依赖服务端快照检索,因此分支场景务必配置store
  • 保存分支点:客户端需要在内存(或 URL)中维护"当前分支点snapshotId",并在每次pick后更新,否则后续生成的分支会从错误的节点分叉;
  • 生产环境存储选型:本地开发用InMemorySessionStore/FileSessionStore,长会话或高并发生产环境优先FirestoreSessionStore,并可按需配置checkpointIntervalshardSize
  • 客户端过滤消息:渲染历史时过滤非user/model角色并拼接Part.text(如本文恢复历史示例),避免把工具调用等内部消息暴露给用户。

小结

Genkit JS 的 Agent 分支能力用极简的 API 表达了一套强大的会话语义:快照不可变、分支可并行、历史可恢复。服务端一行assistant.chat({ snapshotId: checkpoint })即可分叉,客户端通过remoteAgentgetSnapshotDataAction端点把"多方案择优"变成现实。其底层依赖 agents-sessions.md 中介绍的会话存储体系,选型得当即可支撑从开发调试到生产级长会话的完整分支场景。想要深入掌握 Agent 全貌,可继续阅读本系列其他参考文档:agents.md、agents-state.md、agents-multi-agent.md 与 agents-deployment.md。

【免费下载链接】skillsAgent Skills for Google products and technologies项目地址: https://gitcode.com/GitHub_Trending/skills29/skills

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询