ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Genkit Go Agent Branching:用不可变 SnapshotID 实现会话分支与多路线 Agent 实战指南

Genkit Go Agent Branching:用不可变 SnapshotID 实现会话分支与多路线 Agent 实战指南 Genkit Go Agent Branching用不可变 SnapshotID 实现会话分支与多路线 Agent 实战指南【免费下载链接】skillsAgent Skills for Google products and technologies项目地址: https://gitcode.com/GitHub_Trending/skills29/skillsAgent 会话往往是一条道走到黑的线性对话但在真实应用中我们经常需要让同一段对话历史产生多种可能的走向——例如并行生成两版回复让用户挑选、基于某个历史检查点探索不同的回答策略或在 UI 刷新后精确恢复某一次对话现场。Genkit Go 的实验性 Agent API 通过引入不可变快照Snapshot这一机制为多轮会话提供了类似 git 的分支Branching能力任何一次对话回合都会产生一个新的检查点你可以从任意检查点 fork 出多个相互独立的时间线。本文将完整讲解 SnapshotID 的核心语义、分支代码范式、二选一变体模式、按会话与按快照恢复的区别以及如何通过 HTTP 暴露快照读取能力帮助你在 Genkit Go 中落地可回溯、可分支的多路线 Agent。前置要求分支能力属于Experimental / preview API。它依赖会话存储Session Store以保证快照持久化建议先阅读 agents.md 了解 Agent 的基础定义与运行方式。一、分支的核心思想SnapshotID 是不可变检查点要理解分支首先要理解 Genkit Go 会话模型中的一个关键概念——SnapshotID快照 ID。在多轮会话中Agent 的每一次回合turn都会产生一个不可变的检查点immutable checkpoint它就像 git 的一次 commit记录下那一刻的完整会话状态且一经产生就永远不会被修改。这意味着你可以从同一个快照出发fork 出任意多个独立的时间线——从某个快照继续的每一轮对话都会创建一个全新的、独立的快照而原快照始终保持不变。assistant : genkitx.DefineAgent(g, assistant, aix.InlinePrompt{ ai.WithModelName(googleai/gemini-flash-latest), ai.WithSystem(You are a helpful assistant.), }, aix.WithSessionStore(localstore.NewInMemorySessionStore[any]()), ) root, _ : assistant.RunText(ctx, Hello!) checkpoint : root.SnapshotID // branch point这段代码中genkitx.DefineAgent来自genkit/exp别名genkitx创建一个带会话存储的 Agent第一轮RunText(ctx, Hello!)产生了一个根快照rootroot.SnapshotID就是后续分支的分叉点branch point。每一次回合都会返回一个全新的SnapshotID因此分支之间在公共检查点之后永远不会共享历史——这正是分支与普通继续对话的本质区别。包与导入别名说明Agent API 分布在genkit/exp构造器别名genkitx与ai/exp类型与选项别名aix两个包中会话存储来自ai/exp/localstore。初始化时必须调用genkit.Init(ctx, genkit.WithExperimental(), ...)显式开启实验特性否则genkitx.DefineAgent等构造器会直接 panic。详见 agents.md。二、从检查点分支两个完全独立的对话线分支的语法非常轻量只需在运行回合时传入aix.WithSnapshotID(id)选项即可从指定快照继续。下面的完整示例展示了从同一个checkpointfork 出两条互不干扰的对话线// Branch A — forks from checkpoint. a1, _ : assistant.RunText(ctx, My name is Bob., aix.WithSnapshotID(checkpoint)) a2, _ : assistant.RunText(ctx, What is my name?, aix.WithSnapshotID(a1.SnapshotID)) // - Bob // Branch B — forks from the SAME checkpoint, fully independent. b1, _ : assistant.RunText(ctx, My name is John., aix.WithSnapshotID(checkpoint)) b2, _ : assistant.RunText(ctx, What is my name?, aix.WithSnapshotID(b1.SnapshotID)) // - John _ a2 _ b2执行过程可以这样理解回合起点快照结果快照对话内容root首次root.SnapshotIDHello!a1checkpointa1.SnapshotIDMy name is Bob.a2a1.SnapshotIDa2.SnapshotID回答 Bobb1checkpointb1.SnapshotIDMy name is John.b2b1.SnapshotIDb2.SnapshotID回答 John两条分支都从同一个checkpoint出发但各自的后续历史完全独立分支 A 认为用户叫 Bob分支 B 认为用户叫 John彼此不互相污染。这背后是会话存储localstore.NewInMemorySessionStore或localstore.NewFileSessionStore在起作用——每次回合自动将快照落盘/持久化从而让从任意历史点恢复成为可能。三、Pick a variant模式并行生成变体用户择优续接分支最有价值的实战场景之一是生成多个候选方案让用户挑选后继续。其思路是从同一检查点并行发起多个回合各自产生独立快照用户选中的那个快照成为新的分支点。以下示例用 Go 的sync.WaitGroup从同一checkpoint并行生成两个变体func twoVariants(ctx context.Context, agent *aix.Agent[any], checkpoint, text string) (a, b *aix.AgentOutput[any], err error) { var wg sync.WaitGroup var errA, errB error wg.Add(2) go func() { defer wg.Done(); a, errA agent.RunText(ctx, text, aix.WithSnapshotID(checkpoint)) }() go func() { defer wg.Done(); b, errB agent.RunText(ctx, text, aix.WithSnapshotID(checkpoint)) }() wg.Wait() if errA ! nil { return nil, nil, errA } if errB ! nil { return nil, nil, errB } return a, b, nil // a.SnapshotID ! b.SnapshotID; both branch from the same point } // When the user picks a variant, its SnapshotID becomes the new branch point: // checkpoint chosen.SnapshotID这段代码的关键点同一 checkpoint、同一文本、两次调用由于每次RunText都从快照生成新的独立快照两个 goroutine 的结果a.SnapshotID ! b.SnapshotID且互不影响并发安全快照的不可变性保证了并发 fork 不会发生写冲突——没有数据被覆盖也没有共享可变状态用户挑选后续接当用户选择某个变体后只需把chosen.SnapshotID作为新的分支点继续对话即可示例注释中的checkpoint chosen.SnapshotID。当还不存在分支点怎么办对于第一轮对话此时还没有任何历史快照直接省略调用选项开启一个全新会话然后将返回结果上的SnapshotID作为之后的分支点即可。这也是整个分支能力的种子来源。四、按会话恢复 vs 按快照恢复选对正确的 APIGenkit Go 提供了两个语义不同、容易混淆的恢复选项选项语义适用场景aix.WithSnapshotID(id)从精确的检查点 fork即分支需要回到某个特定历史时刻或进行分支aix.WithSessionID(id)继续该会话的最新快照只关心这段对话而不关心具体快照两者最重要的区别在于如果历史发生过分支forkWithSessionID会选择最近创建的那条分支继续。因此当你的业务需要锁定某一条特定分支时必须使用WithSnapshotID只有当会话从未分叉、或你可以接受沿最新分支继续时才使用WithSessionID。需要补充的两个边界恢复会被拒绝的情况如果最新快照是一个失败failed、被中止aborted或处于挂起死端pending dead end即还有后台任务在跑的状态恢复请求会被拒绝——此时应先等待或中止后台任务与客户端管理互斥aix.WithState客户端管理状态无存储与WithSessionID/WithSnapshotID服务端管理状态互斥不能混用。详见 agents-sessions.md。五、从快照恢复历史不运行回合也能读取现场分支不仅支持从快照继续对话还支持只读地查看某个快照的完整状态。使用Agent.GetSnapshot可以在不触发任何模型调用的前提下读取快照内容——这对页面刷新后恢复 UI这类场景非常有用例如浏览器 URL 中保存了一个SnapshotID前端刷新后即可用它还原整个对话历史snap, err : assistant.GetSnapshot(ctx, snapshotID) if err ! nil { log.Fatal(err) } for _, m : range snap.State.Messages { if m.Role ai.RoleUser || m.Role ai.RoleModel { fmt.Printf(%s: %s\n, m.Role, m.Text()) } }代码要点snap.State.Messages中是该快照时刻的全部历史消息遍历并过滤出ai.RoleUser/ai.RoleModel两类角色即可重建对话界面Agent.GetSnapshot会应用 Agent 上配置的aix.WithStateTransform例如 PII 脱敏如果 Agent 是客户端管理无 Session StoreGetSnapshot会返回FAILED_PRECONDITION错误。与之配套的还有Agent.GetLatestSnapshot(ctx, sessionID)用于按会话 ID 获取该会话最新的快照。通过 HTTP 暴露快照读取在 HTTP 部署场景下Agent 本身是api.BidiAction通过genkit.Handler(agent)即可按一次请求一个回合的方式对外服务。若要支持远程客户端读取快照例如前端应用按 URL 中的 SnapshotID 恢复会话需要额外暴露Agent.GetSnapshotAction()mux : http.NewServeMux() mux.HandleFunc(POST /api/weatherAgent, genkit.Handler(weatherAgent)) // Optional companions (needed for snapshot restore / branching / background): if snap : weatherAgent.GetSnapshotAction(); snap ! nil { mux.HandleFunc(POST /api/weatherAgent/getSnapshot, genkit.Handler(snap)) } log.Fatal(server.Start(ctx, 127.0.0.1:8080, mux))更推荐的做法是使用genkit/exp内置的默认路由布局genkitx.AllAgentRoutes(g)它会根据每个 Agent 的能力自动挂载/agents/name回合、/agents/name/getSnapshot快照读取仅存储型 Agent与/agents/name/abort中止后台任务仅可中止的存储免去手写路由的样板代码详见 agents-deployment.md。请求体的init字段携带会话来源服务端管理状态下传{sessionId: ...}或{snapshotId: ...}客户端管理状态下传{state: ...}省略init则开启全新会话。这与 Go API 中的WithSessionID/WithSnapshotID/WithState一一对应。六、分支的存储语义被遗弃的分支与链剪枝分支之所以能安全、廉价地存在源于快照的不可变设计。文档明确给出了两条存储语义被遗弃的分支会一直保留被丢弃的分支只是作为不可变快照静静地留在存储中分支发生时不会有任何数据被覆盖。这保证了 fork 的任意分支哪怕后来没人继续都可以在之后被重新拾起链剪枝不影响兄弟分支FileSessionStore配置了localstore.WithMaxPersistedChainLength(n)时只会沿着单条链的父级链接修剪最老的快照即只保留该链最新的 n 个兄弟分支会被独立保留不会被连带删除。这意味着你可以放心地限制每条链的长度来控制存储占用而不必担心误删并行分支。pruning, err : localstore.NewFileSessionStoreany, // 每条链只保留最新 3 个快照 )从实现上看localstore的两个单进程存储NewInMemorySessionStore、NewFileSessionStore都遵循aix.SessionStore接口约定SaveSnapshot以原子方式读取-应用-写入GetSnapshot/GetLatestSnapshot负责读取其中GetLatestSnapshot返回CreatedAt最大、平局按SnapshotID排序的行的语义也保证了按会话恢复 沿最新分支的行为是可预测的。多实例生产环境则应自行实现aix.SessionStore对接真实数据库。七、实战要点小结分支的前提是持久化Agent 必须配置 Session Storeaix.WithSessionStore(localstore.NewInMemorySessionStore[any]())或文件存储快照才能跨回合保留分支也才能成立分支三件套out.SnapshotID记录分支点、aix.WithSnapshotID(id)执行 fork、Agent.GetSnapshot(ctx, id)无副作用地查看检查点内容并发 fork 是安全的快照不可变从同一 checkpoint 并行发起多个回合不会互相覆盖这是Pick a variant模式的基石精确恢复优先用快照会话一旦分叉WithSessionID只会沿最新分支走锁定特定分支必须用WithSnapshotID结合其他 Agent 能力分支与会话持久化、人工介入interrupts、后台执行、多 Agent 编排等能力正交组合可构建出完整的可回溯、可干预、可并行的 Agent 应用Agent API 的整体概览与 CLI 调试方式如genkit start、flow:run包装单回合参见 SKILL.md 与 agents.md。最后提醒由于该 API 属于实验特性导入路径与函数签名可能在小版本更新中变化生产使用前请锁定依赖版本并始终通过genkit.WithExperimental()显式开启。【免费下载链接】skillsAgent Skills for Google products and technologies项目地址: https://gitcode.com/GitHub_Trending/skills29/skills创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进