使用 ADK Go 2.0 构建可靠的多智能体应用:图工作流引擎、人机协同与动态编排
英文标题:Build reliable multi-agent applications with ADK Go 2.0. Discover our new graph-based workflow engine, built-in human-in-the-loop, and dynamic orchestration
Agent Development Kit (ADK) for Go 2.0 正式发布,引入一流的图工作流引擎,帮助开发者构建复杂的多智能体应用。该版本内置人机协同(HITL)原语,支持用纯 Go 代码进行动态编排,提供 LLM 智能体模式,并将单智能体与完整图统一到同一运行时。开发者现在可以用地道的 Go 来描述应用拓扑,从而获得并发执行、状态持久化和跨进程恢复等能力,显著提升生产级智能体应用的可靠性与可维护性。
406.6 KB
87.9 KB
64.8 KB
12.8 KB
496.8 KB
88.0 KB
中文

ADK for Go 2.0:将智能体工作流构建为图
构建真实的智能体应用很少只是发送单一提示那么简单。生产级智能体必须进行分类、分支、扇出、请求人工批准、失败重试以及循环直到完成。将这种复杂的编排表达为临时控制流会很快变得脆弱。
自 1.0 版本发布以来,Agent Development Kit (ADK) for Go 一直帮助 Go 开发者使用简洁、地道的 API 构建生产级智能体——强类型、iter.Seq2 事件流,以及能自然融入现有 Go 服务的运行时。这个基础非常成功,正是它让下一步成为可能。
今天,我们激动地分享 ADK for Go 2.0。核心亮点是一种全新的、第一等的组合多智能体应用的方式:基于图的工作流引擎。与之相伴的还有作为内置原语的人机协同(HITL)、用纯 Go 编写的动态编排、LLM 智能体模式,以及一个统一的节点运行时,将所有这些整合在一起——单个智能体与完整图现在运行在相同的执行模型上。
如果你关注过 Python ADK 2.0,这会让你感到熟悉:这是相同的图优先方向,从底层开始就为 Go 而生。
为什么选择图?
真实的智能体应用很少是单一提示。它们进行分类、分支、扇出到专家、收集结果、请求人工批准、失败重试以及循环直到完成。将这种逻辑表达为临时控制流会迅速变得脆弱。
ADK 2.0 允许你把应用的形状描述为一个由节点和边连接的图,并把执行交给一个调度器,它知道如何并发运行、持久化状态、为人工干预暂停并在之后恢复——甚至可以跨进程重启。下面是将节点串联起来的简单示例:
go 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 和控制台中——无需特殊的测试工具,无需新的服务器。图就是智能体。
构建块
适用于一切的节点
节点是实现 Node 接口 的任何工作单元。你很少需要手动编写该接口——ADK 为常见情况提供了类型化的节点构造函数:
- 函数节点封装一个普通的类型化 Go 函数。泛型会为你推断输入/输出模式:
go workflow.NewFunctionNode("classify", func(ctx agent.Context, in string) (Category, error) { ... }, cfg)
- 发射函数节点在函数节点基础上还获得一个
emit回调,因此单个函数就可以流式发送事件或为人工干预暂停,而无需降级为动态节点:
go workflow.NewEmittingFunctionNode("progress", func(ctx agent.Context, in Job, emit func(*session.Event) error) (Result, error) { ... }, cfg)
- 智能体节点将任何
agent.Agent(例如LlmAgent)放入图中。 - 工具节点将
tool.Tool变为图中的一个步骤。 - 连接节点是扇入屏障:它们等待所有前驱节点,然后将它们的输出映射交给你。
- 动态节点让你可以用代码进行编排(下面详述)。
- 工作流节点将整个子工作流作为一个节点嵌入——图可以进行组合。
- 并行工作者对列表中的每个元素并发运行一个节点,并聚合结果。
- 状态绑定节点 (
NewFunctionNodeFromState) 通过state:"<key>"标签将选定的会话状态值直接拉入类型化的 Params 结构体——无需手动处理状态。
边、路由以及你需要的所有形态
边连接节点,并且可以携带路由条件。一个节点发出路由值;匹配的边被触发。仅凭这个概念你就能实现所有控制流形态:
go 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) // 收集结果
顺序链、条件路由、扇出/扇入、嵌套子图,甚至循环(已完成的节点可以被重新触发,因此循环是一等公民)——所有这些都来自边和路由。标准路由包括 StringRoute、IntRoute、BoolRoute、MultiRoute,以及一个当没有其他匹配时触发的 Default。如需更深入的配置,可使用 Route 接口。
让 LLM 驾驭图
最有用的模式之一是使用模型作为路由器的大脑。一个 LlmAgent 对用户消息进行分类;一个简单的函数发出匹配的路由;图将请求分发到正确的处理程序:
text User -> What time is it? Agent -> question 正在回答问题... User -> Hello world! Agent -> exclamation 正在对感叹做出反应... User -> The sky is blue. Agent -> statement 正在对陈述进行评论...
模型做出决策;图使其可靠、可观测且可恢复。(参见 [examples/workflow/routing/llm/](https://github.com/google/adk-go/tree/main/examples/workflow/routing/llm)。)
动态编排——用纯 Go 代码
有时执行顺序直到运行时才知道:它取决于数据、循环次数或模型刚刚输出的内容。为此,ADK 2.0 提供了动态节点,其中编排主体是普通 Go 代码,它为每个子节点调用 RunNode(...):
go 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 中的对应实现。
人机协同(HITL),内置
生产级智能体经常需要人工在运行中批准、纠正或提供某些信息。在 ADK 2.0 中,任何节点都可以暂停图并向人类提问——并且工作流会持久地等待答案:
go event := workflow.NewRequestInputEvent(ctx, session.RequestInput{ InterruptID: "approve_refund", Message: "批准 200 美元退款吗?(是/否)", ResponseSchema: schema, }) // 让出事件;节点进入“等待”状态
当人类在之后的轮次中回复时,工作流恢复。你可以选择恢复方式:
- 移交——答案直接流向下一节点。
- 重新进入——暂停的节点以可通过
ctx.ResumedInput(...)获取的人类响应重新运行。
并且恢复是持久的。运行状态存储在会话中,ADK 甚至可以通过扫描会话历史来重建暂停的工作流——因此工作流可以在进程重启后恢复,甚至可以跨不同运行时恢复,因为中断格式与 Python ADK 共享。响应会根据模式进行验证,恢复是幂等的,当出现不一致时会得到清晰的错误(ErrInvalidResumeResponse、ErrNothingToResume)。
控制台启动器和 Web UI 都原生支持 HITL,能同时展示工具确认提示和工作流输入请求。
无需样板代码的弹性能力
每个节点都可以携带包含指数退避和抖动的重试策略——无需外部依赖:
go cfg := workflow.NodeConfig{ RetryConfig: workflow.DefaultRetryConfig() } // 5 次尝试,初始延迟 1s,上限 60s,2 倍退避,全抖动
添加每个节点的 Timeout,使用 WithMaxConcurrency(n) 限制图级别的并发,并隔离并行分支,这样某个分支的对话就不会泄漏到另一个分支的 LLM 提示历史中。调度器为你处理 goroutine、channel、背压和取消。
智能体模式和运行一切的统一运行时
ADK 2.0 为 LLM 智能体引入了模式——Chat、Task 和 SingleTurn——这样协调器可以与用户聊天,而子智能体则安静地完成任务或单次运行。正确的辅助工具(finish_task、single_turn、task)会根据每个智能体的角色自动安装。
在底层,runner 现在通过与工作流相同的节点运行时来驱动纯 LlmAgent。其收获是:单智能体应用和完整图共享一个执行模型,并且人机协同现在也适用于纯 LLM 智能体——不仅限于工作流内部。
我们还平滑了编程模型:ToolContext 和 CallbackContext 现在统一为 agent.Context——无论你是在编写工具、回调还是图节点,都只需学习一个类型——节点/智能体的执行会显示在一致的遥测跨度树中,因此你可以确切地看到图做了什么。
从 1.0 升级
ADK 2.0 是高度附加的——整个工作流引擎都是你可选择加入的新包。统一运行时的过程中有少量新增和破坏性变更;每个都有简单、机械的修复方法:
- 节点和节点函数签名的参数使用
agent.Context。如果你编写节点或节点函数,请将第一个参数从agent.InvocationContext改为agent.Context(它内嵌了InvocationContext,因此你使用的每个方法仍然有效):
go // 之前: func(ctx agent.InvocationContext, in string) (string, error) // 之后: func(ctx agent.Context, in string) (string, error)
- 一个统一的上下文。
ToolContext、CallbackContext已移除——工具、回调和图节点都直接接收agent.Context。如果你在测试中模拟了上下文,agent/context_mock.go仍然保留;使用该文件中的StrictContextMock作为你的测试替身。 - 自定义
InvocationContext实现需要两个方法:IsolationScope()和ResumedInput(id string)。大多数代码内嵌了提供的实现并免费获得这些方法。 - 事件流更丰富。 事件现在携带节点字段(
IsolationScope、Output、Routes、RequestedInput)和一个元数据字段(NodeInfo)。如果你在测试中断言session.Event的精确相等性,请预期这些新字段;自定义会话存储应持久化它们。 llmagent.New可能会安装模式特定的工具。 如果你设置了子智能体模式,有效的工具集会反映它们;task模式下的智能体不能用作静态图节点。- session.NewEvent 接受一个上下文。签名现在是
NewEvent(ctx context.Context, invocationID string)。迁移调用点,将作用域中已存在的context.Context作为第一个参数传入。
以上就是完整的列表。runner.Run/RunLive、agenttool 和 llmagent 回调的公共签名保持不变。有关逐步的前后对比说明,请参见 ADK Go 2.0 迁移指南。
立即尝试
感受 ADK 2.0 的最快方式是查看新的工作流示例:
shell 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 go run ./examples/workflow/complex/ # 更大的多形态图
ADK 1.0 证明了用 Go 构建严肃的智能体可以是干净且高效的。ADK 2.0 迈出了下一步:将这些智能体组合成可靠、可观测、可恢复的工作流——以图的形式,用地道的 Go,并在需要时将人类置于回路中。
我们迫不及待想看到你们的作品。
—— ADK for Go 团队
English / 原文

ADK for Go 2.0: build agent workflows as a graph
Building real-world agent applications is rarely as simple as sending a single prompt. Production agents must classify, branch, fan out, ask a human to approve something, retry on failure, and loop until done. Expressing that complex orchestration as ad-hoc control flow gets brittle fast.
Since its 1.0 release, Agent Development Kit (ADK) for Go has helped Go developers build production agents with a clean, idiomatic API — strong typing, iter.Seq2 event streams, and a runtime that fits naturally into existing Go services. That foundation has been a real success, and it's exactly what made the next step possible.
Today we're excited to share ADK for Go 2.0. The headline is a brand-new, first-class way to compose multi-agent applications: a graph-based workflow engine. Alongside it come human-in-the-loop (HITL) as a built-in primitive, dynamic orchestration written in plain Go , LLM agent modes , and a unified node runtime that brings all of this together — single agents and full graphs now run on the same execution model.
If you've followed Python ADK 2.0, this will feel familiar: it's the same graph-first direction, designed from the ground up to feel like Go.
Why a graph?
Real agent applications are rarely a single prompt. They classify, branch, fan out to specialists, gather results, ask a human to approve something, retry on failure, and loop until done. Expressing that as ad-hoc control flow gets brittle fast.
ADK 2.0 lets you describe the shape of your application as a graph of nodes connected by edges , and hands execution to a scheduler that knows how to run it concurrently, persist its state, pause for a human, and resume later — even across process restarts. Here is how simple it is to chain nodes together:
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,
})
Go
That wf is just an agent.Agent. It runs in the same runner, launcher, and console you already use — no special harness, no new server. A graph is an agent.
The building blocks
Nodes for everything
A node is any unit of work that implements the Node interface. You rarely write that interface by hand — ADK ships typed node constructors for the common cases:
-
Function nodes wrap a plain typed Go function. Generics infer the input/output schemas for you:
workflow.NewFunctionNode("classify", func(ctx agent.Context, in string) (Category, error) { ... }, cfg)
Go
-
Emitting function nodes are function nodes that also get an
emitcallback, so a single function can stream events or pause for a human without dropping down to a dynamic node:workflow.NewEmittingFunctionNode("progress", func(ctx agent.Context, in Job, emit func(*session.Event) error) (Result, error) { ... }, cfg)
Go
- Agent nodes drop any
agent.Agent(like anLlmAgent) into the graph. - Tool nodes turn a
tool.Toolinto a graph step. - Join nodes are fan-in barriers: they wait for all predecessors and hand you a map of their outputs.
- Dynamic nodes let you orchestrate in code (more on this below).
- Workflow nodes embed an entire sub-workflow as a single node — graphs compose.
- Parallel workers run a node concurrently across every item in a list and aggregate the results.
- State-bound nodes (
NewFunctionNodeFromState) pull selected session-state values straight into a typed Params struct viastate:"<key>"tags — no manual state plumbing.
Edges, routing, and the shapes you need
Edges connect nodes, and they can carry routing conditions. A node emits a routing value; matching edges fire. That single idea gives you every control-flow shape you need:
b := workflow.NewEdgeBuilder()
b.AddRoutes(router, map[string]workflow.Node{
"question": answerNode,
"statement": commentNode,
"exclamation": reactNode,
})
b.AddFanOut(planner, researchA, researchB, researchC) // parallel branches
b.AddFanIn(join, researchA, researchB, researchC) // gather results
Go
Sequential chains, conditional routers, fan-out/fan-in, nested sub-graphs, and even loops (a completed node can be re-triggered, so cycles are first-class) — all from edges and routes. Standard routes come in StringRoute, IntRoute, BoolRoute, MultiRoute, and a Default that fires when nothing else matches. For deeper configuration, leverage the Route interface.
Let an LLM steer the graph
One of the most useful patterns is using a model as the brain of a router. An LlmAgent classifies the user's message; a trivial function emits the matching route; the graph dispatches to the right handler:
User -> What time is it? Agent -> question answering question...
User -> Hello world! Agent -> exclamation reacting to exclamation...
User -> The sky is blue. Agent -> statement commenting on statement...
Plain text
The model makes the decision; the graph makes it reliable, observable, and resumable. (See examples/workflow/routing/llm/.)
Dynamic orchestration — in plain Go
Sometimes the execution order isn't known until runtime: it depends on data, on a loop count, on what the model just said. For that, ADK 2.0 gives you dynamic nodes , where the orchestration body is ordinary Go code that calls RunNode(...) for each child:
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
Loops, conditionals, accumulation, fan-out across a dynamic list — all expressed with the Go you already know. Options like WithRunID, WithUseSubBranch, WithUseAsOutput, and WithIsolationScope give you precise control over child identity, history isolation, and output delegation. This is the Go counterpart to Python ADK's dynamic graphs.
Human-in-the-loop, built in
Production agents often need a human to approve, correct, or supply something mid-run. In ADK 2.0, any node can pause the graph and ask a human a question — and the workflow durably waits for the answer:
event := workflow.NewRequestInputEvent(ctx, session.RequestInput{
InterruptID: "approve_refund",
Message: "Approve a $200 refund? (yes/no)",
ResponseSchema: schema,
})
// yield the event; the node moves to "waiting"
Go
When the human replies on a later turn, the workflow resumes. You choose how:
- Handoff — the answer flows straight to the next node.
- Re-entry — the paused node re-runs with the human's response available via
ctx.ResumedInput(...).
And resume is durable. The run state lives in the session, and ADK can even reconstruct a paused workflow by scanning session history — so a workflow can resume after a process restart, or even across different runtimes, because the interrupt format is shared with Python ADK. Responses are validated against a schema, resume is idempotent, and you get clear errors (ErrInvalidResumeResponse, ErrNothingToResume) when something doesn't line up.
Both the console launcher and the Web UI understands HITL out of the box, surfacing both tool-confirmation prompts and workflow input requests.
Resilience without the boilerplate
Every node can carry a retry policy with exponential backoff and jitter — no external dependency required:
cfg := workflow.NodeConfig{ RetryConfig: workflow.DefaultRetryConfig() }
// 5 attempts, 1s initial delay, 60s cap, 2x backoff, full jitter
Go
Add a per-node Timeout, cap graph-wide concurrency with WithMaxConcurrency(n), and isolate parallel branches so one branch's chatter never leaks into another's LLM prompt history. The scheduler handles the goroutines, channels, backpressure, and cancellation for you.
Agent modes and one runtime to run them all
ADK 2.0 introduces modes for LLM agents — Chat, Task, and SingleTurn — so a coordinator can chat with the user while sub-agents quietly complete tasks or run single-shot. The right helper tools (finish_task, single_turn, task) are installed automatically based on each agent's role.
Under the hood, the runner now drives a plain LlmAgent through the same node runtime that powers workflows. The payoff: single-agent apps and full graphs share one execution model, and human-in-the-loop now works for a plain LLM agent too — not just inside a workflow.
We also smoothed the programming model: ToolContext and CallbackContext are now a single unified to agent.Context — one type to learn, whether you're writing a tool, a callback, or a graph node — and node/agent execution shows up in one consistent telemetry span tree, so you can see exactly what your graph did.
Upgrading from 1.0
ADK 2.0 is highly additive — the entire workflow engine is new packages you opt into. There are a few new and breaking changes that come with unifying the runtime; each has a simple, mechanical fix:
-
Node and node-function signatures take
agent.Context****. If you write nodes or node functions, change the first parameter fromagent.InvocationContexttoagent.Context(it embedsInvocationContext, so every method you used still works):// before: func(ctx agent.InvocationContext, in string) (string, error) // after: func(ctx agent.Context, in string) (string, error)
Go
- One unified context.
ToolContext,CallbackContextare gone – tools, callbacks, and workflow nodes all receiveagent.Contextdirectly. If you mocked a context in tests,agent/context_mock.gois retained; useStrictContextMockfrom that file as your test double. - Custom **
InvocationContext**implementations need two methods:IsolationScope()andResumedInput(id string). Most code embeds the provided implementation and gets these for free. - Event streams are richer. Events now carry node fields (
IsolationScope,Output,Routes,RequestedInput) and a metadata field (NodeInfo). If you assert on exactsession.Eventequality in tests, expect the new fields; custom session stores should persist them. - **
llmagent.New**may install mode-specific tools. If you set sub-agent modes, the effective tool set reflects them;task-mode agents can't be used as static graph nodes. - session.NewEvent takes a context. The signature is now
NewEvent(ctx context.Context, invocationID string). Migrate call sites by passing thecontext.Contextalready in scope as the first argument.
That's the whole list. Public signatures for runner.Run/RunLive, agenttool, and the llmagent callbacks are unchanged. For step-by-step before/after instructions, see the ADK Go 2.0 migration guide.
Try it
The fastest way to get a feel for ADK 2.0 is the new workflow examples:
go run ./examples/workflow/basic/
go run ./examples/workflow/routing/llm/ # LLM-as-router
go run ./examples/workflow/dynamic/hitl/ # dynamic + human-in-the-loop
go run ./examples/workflow/hitl_rerun/ # HITL with re-entry resume
go run ./examples/workflow/complex/ # a larger, multi-shape graph
Shell
ADK 1.0 proved that building serious agents in Go could be clean and productive. ADK 2.0 takes the next step: compose those agents into reliable, observable, resumable workflows — as a graph, in idiomatic Go, with humans in the loop when it matters.
We can't wait to see what you build.
— The ADK for Go team