ADK for Go 2.0: 以图形式构建Agent工作流
坦率地说,在实际生产环境中运行Agent应用,仅靠一个简单的prompt远远不够。现实世界的Agent需要具备分类、条件分支、并行处理、等待人工审批、失败重试甚至循环执行的能力。如果将这些复杂的控制逻辑全部硬编码到业务代码中,代码很快就会变得脆弱不堪,维护成本居高不下。
从ADK for Go 1.0开始,这个开发工具包就致力于帮助Go开发者构建生产级的Agent,它提供了干净、符合语言习惯(idiomatic)的API——强类型、iter.Seq2事件流,以及一个能自然融入现有Go服务的运行时。这一坚实的基础正是迈向下一步升级的关键。
今天,我们正式推出ADK for Go 2.0。此次更新的核心是一个全新的、头等公民的Agent组合方式:基于图的workflow引擎。同时引入的还有人类在环(HITL)作为内置原语、纯Go编写的动态编排、LLM Agent模式,以及一个统一的节点运行时——它将这一切整合在一起,使单个Agent和图现在运行在同一个执行模型之上。
如果你关注过Python ADK 2.0,那么对这个方向不会陌生:同样的“图优先”路线,只是这次完全按照Go的语言习惯从零设计。
为什么选择图?
真实世界的Agent应用远不止发送一个prompt那么简单。它们需要分类、分支、分发给不同的专家Agent、收集结果、请求人类审批、失败重试以及循环执行。如果用ad-hoc控制流来表达这种复杂的编排,代码很快就会变得脆弱不堪。
ADK 2.0允许你将应用的形状描述为一个由节点和边连接而成的图,然后将执行交给一个调度器——它知道如何并发运行、持久化状态、暂停等待人类介入,并在后续恢复,甚至跨进程重启也不成问题。来看看将节点串联起来有多简单:
import "google.golang.org/adk/v2/workflow"
upper:= workflow.NewFunctionNode("upper",upperFn,cfg)
suffix := workflow.NewFunctionNode("suffix", suffixFn, cfg)
edges := workflow.Chain(workflow.Start, upper, suffix)
wf, _ := workflowagent.New(workflowagent.Config{
Name:"simple_sequence_workflow",
Edges: edges,
})
这个wf就是一个标准的agent.Agent。它可以直接运行在现有的runner、launcher和console上——不需要特殊的框架,也不需要开启新服务器。一个图就是一个Agent。
构建基石
万物皆节点
节点是任何工作单元,只需实现Node接口即可。但你很少需要手动编写那个接口——ADK已经为常见场景内置了类型化的节点构造函数:
- Function nodes:封装一个普通的带类型的Go函数。泛型会自动推导输入/输出的schemas:
workflow.NewFunctionNode("classify",
func(ctx agent.Context, in string) (Category, error) { ... }, cfg)
- Emitting function nodes:在Function node的基础上增加了一个
emit回调,这样单个函数就能流式输出事件或暂停等待人类介入,而不必退化为动态节点:
workflow.NewEmittingFunctionNode("progress",
func(ctx agent.Context, in Job, emit func(*session.Event) error) (Result, error) { ... }, cfg)
- Agent nodes:将任何
agent.Agent(比如LlmAgent)直接放入图里。 - Tool nodes:将一个
tool.Tool变成图中的一个步骤。 - Join nodes:这是一个fan-in的屏障——它会等待所有前驱节点完成,然后交给你一个包含它们输出的map。
- Dynamic nodes:让你在代码中自由编排(下面会细说)。
- Workflow nodes:可以将一个完整的子workflow嵌入成一个节点——图是可以组合的。
- Parallel workers:将一个节点并发地应用于列表中的每个元素,然后汇总结果。
- State-bound nodes(
NewFunctionNodeFromState):通过state:"标签,从会话状态中选取值,直接注入到一个类型化的Params结构体中——再也不用手动编写状态传递的样板代码了。"
边、路由和所需的各类形状
边用来连接节点,并且可以携带路由条件。节点会发出一个路由值,匹配的边就会触发。仅仅这一个概念,就能实现你需要的所有控制流形状:
b := workflow.NewEdgeBuilder()
b.AddRoutes(router, map[string]workflow.Node{
"question":answerNode,
"statement": commentNode,
"exclamation": reactNode,
})
b.AddFanOut(planner, researchA, researchB, researchC) // 并行分支
b.AddFanIn(join, researchA, researchB, researchC) // 收集结果
顺序链、条件路由器、fan-out/fan-in、嵌套子图,甚至循环(一个完成的节点可以重新触发,所以循环是头等公民)——所有这些,都只靠边和路由来实现。标准路由有StringRoute、IntRoute、BoolRoute、MultiRoute,以及一个当没有任何其他路由匹配时触发的Default。如果需要更深度的配置,实现Route接口即可。
让LLM充当图的“大脑”
一个非常有用的模式是,让模型作为路由器的大脑。LlmAgent先对用户的消息进行分类,然后一个简单的函数发射出匹配的路由值,图就会把请求分发给正确的处理器:
用户 -> 现在几点了?
Agent -> question 提问问题...
用户 -> 你好世界!
Agent -> exclamation 对感叹句做出反应...
用户 -> 天空是蓝色的。
Agent -> statement 对陈述句发表评论...
模型来做决策,而图则让整个过程变得可靠、可观测、可恢复。(参见 examples/workflow/routing/llm/。)
有时候,执行顺序要到运行时才能确定——它取决于数据、循环次数、或者模型刚刚说了什么。针对这种情况,ADK 2.0提供了动态节点,编排体本身就是一个普通的Go函数,通过调用RunNode(...)来执行每个子节点:
greeter := workflow.NewDynamicNode("greeter_workflow",
func(nc agent.Context, in string, emit func(*session.Event) error) (string, error) {
return workflow.RunNode[string](nc, greeterNode, in)
},
workflow.NodeConfig{},
)
循环、条件判断、累加、动态列表的并行分发——所有这些,都用你已经熟悉的Go来表达。WithRunID、WithUseSubBranch、WithUseAsOutput、WithIsolationScope这些选项可以让你精确控制子节点的身份、历史隔离和输出委托。这相当于Python ADK动态图的Go版本实现。
人类在环,原生内置
生产级的Agent经常需要人类在运行过程中来批准、纠正或提供信息。在ADK 2.0中,任何节点都可以暂停图,并向人类提出一个问题——workflow会持久地等待答案:
event := workflow.NewRequestInputEvent(ctx, session.RequestInput{
InterruptID:"approve_refund",
Message:"Approve a $200 refund? (yes/no)",
ResponseSchema: schema,
})
// 发射这个事件;节点进入 "waiting" 状态
当人类在后续的交互中回复时,workflow就会恢复。你有两种选择:
- Handoff——答案直接流向下一个节点。
- Re-entry——暂停的节点重新运行,人类响应可以通过
ctx.ResumedInput(...)获取。
而且恢复是持久化的。运行状态保存在会话中,ADK甚至可以通过扫描会话历史来重建一个暂停的workflow——所以一个workflow可以在进程重启后恢复,甚至跨不同的运行时,因为中断格式与Python ADK是共享的。响应对schema进行验证,恢复是幂等的,当出现问题时你会得到清晰的错误(ErrInvalidResumeResponse、ErrNothingToResume)。
console launcher和Web UI都原生支持HITL,可以同时显示工具确认提示和workflow输入请求。
弹性,无需样板代码
每个节点都可以携带一个带有指数退避和抖动的重试策略——不需要任何外部依赖:
cfg := workflow.NodeConfig{ RetryConfig: workflow.DefaultRetryConfig() }
// 5次尝试,初始间隔1秒,上限60秒,2倍退避,全抖动
给每个节点加上Timeout,用WithMaxConcurrency(n)限制图级别的并发,然后隔离并行分支,这样一条分支的杂乱信息就不会泄露到另一条的LLM prompt历史中。调度器会帮你处理好goroutine、channel、背压和取消操作。
Agent模式和一个统一的运行时
ADK 2.0为LLM Agent引入了模式——Chat、Task和SingleTurn——这样,一个协调者Agent可以与用户聊天,同时子Agent安静地完成任务或运行单次操作。正确的辅助工具(finish_task、single_turn、task)会根据每个Agent的角色自动安装。
在底层,现在的runner通过与workflow一样的节点运行时来驱动一个普通的LlmAgent。好处是:单Agent应用和完整的图共享同一个执行模型,而且人类在环现在对普通的LLM Agent也同样生效——不仅仅是workflow内部才有。
我们还对编程模型进行了平滑统一:ToolContext和CallbackContext现在合并成了统一的agent.Context——不管你写的是工具、回调还是图节点,都只需要学习这一个类型。而且节点和Agent的执行现在出现在同一个一致的telemetry span树里,所以你可以清晰地看到你的图到底做了什么。
从1.0升级
ADK 2.0是高度增量式的——整个workflow引擎都是你选择加入的新包。但伴随着运行时的统一,也有一些新增和破坏性变更;每个变更都有简单、机械的修复方式:
- 节点和节点函数的签名现在接收
agent.Context。如果你写了节点或节点函数,把第一个参数从agent.InvocationContext改成agent.Context(它内嵌了InvocationContext,所以你之前用的所有方法都还能用):
// 之前:
func(ctx agent.InvocationContext, in string) (string, error)
// 之后:
func(ctx agent.Context, in string) (string, error)
- 一个统一的上下文。
ToolContext、CallbackContext都不存在了——工具、回调和workflow节点现在都直接接收agent.Context。如果你在测试中mock了上下文,agent/context_mock.go仍然保留;使用那个文件里的StrictContextMock作为测试替身。 - 自定义
InvocationContext的实现需要两个方法:IsolationScope()和ResumedInput(id string)。大多数代码内嵌了提供的实现,所以可以免费获得这些方法。 - 事件流更丰富了。事件现在带有节点字段(
IsolationScope、Output、Routes、RequestedInput)和一个元数据字段(NodeInfo)。如果你在测试中使用了严格的session.Event相等性断言,需要注意新字段;自定义的session存储也需要持久化它们。 llmagent.New可能安装模式专属的工具。如果你设置了子Agent的模式,有效的工具集会反映这些模式;task模式的Agent不能用作静态图节点。- session.NewEvent现在接收一个context。签名现在是
NewEvent(ctx context.Context, invocationID string)。迁移时,把已经在你作用域内的context.Context作为第一个参数传入。
整个列表就这些。runner.Run/RunLive、agenttool和llmagent回调的公开签名没有变化。如需一步步的迁移前后说明,请参阅ADK Go 2.0迁移指南。
试试看
最快感受ADK 2.0的方式是看看新的workflow示例:
go run ./examples/workflow/basic/
go run ./examples/workflow/routing/llm/ # LLM 作为路由器
go run ./examples/workflow/dynamic/hitl/ # 动态 + 人类在环
go run ./examples/workflow/hitl_rerun/ # HITL 带 re-entry 恢复
go run ./examples/workflow/complex/ # 一个更大的、多形状的图
ADK 1.0证明了,在Go中构建严肃的Agent可以很干净、很高效。ADK 2.0则迈出了下一步:把这些Agent组合成可靠、可观测、可恢复的workflow——以图的形式,用地道的Go,并在需要时让人类参与进来。
非常期待看到你们会用它来做什么。
