一、先看退款助手为什么不能只靠 Agent
先想一个售后退款助手。用户怎么描述问题是开放的:他说“鞋码不合适”“包裹没收到”“想换颜色”,
都需要 agent 理解意图、追问信息、查订单。但退款流程又不能完全交给模型自由发挥:
必须先查订单,再判断是否符合政策,必要时人工审批,最后才写入退款系统。
这就是 ADK 把 Agent 和 Workflow 放在一起的原因。
如果只在 prompt 里写“先查订单,再查政策,最后退款”,模型也许这次照做,下次却可能省略一步, 或在重试时重复写入。反过来,如果所有情况都写成固定代码,用户换一种说法、信息不全或需要追问时, 流程又不会临场判断。退款助手需要的不是二选一,而是让自主判断和确定性控制在正确的位置交接。
二、先认识五个角色
- Agent:理解用户语言、追问信息、选择工具,并产出需要判断的结果。
- Workflow:保存流程图,决定哪些步骤并行、何时汇合、走哪条分支。
- Runner:启动一次运行,把 Agent 或 Workflow 接到用户、会话和服务上。
- Event:记录“某个节点开始、产出结果、等待审批或完成”等运行事实。
- Task API:让一个 Agent 把结构化子任务交给另一个 Agent,并明确收到完成结果。
最小退款流程只需要前四个角色;当“核对政策”要交给专门 Agent 时,Task API 才进入故事。 这样按需引入,比一开始背完整 API 更容易看清它们为什么存在。
三、一笔退款怎样走完
ADK 的 Runner 会在 root LlmAgent 与 root
BaseNode 之间选择入口,并不会先跑一个 chat Agent、再自动进入 Workflow。
为了让本例的确定性步骤可恢复,这里明确选择 root Workflow:Workflow 的第一个节点是负责理解用户语言的
LlmAgent(single_turn),后面才是查订单、查政策、审批和写入节点。另一个合法设计是以 chat Agent
为 root,再由应用显式把一个 Workflow 暴露成可调用能力;两种设计不能在图里画成未经说明的顺序管线。
- Runner 找到用户会话,把“鞋码不合适,想退款”交给 root Workflow。
- Workflow 的入口 Agent 节点理解意图并输出结构化订单号;资料不足时先补问,资料齐全后启动查订单与查政策。
- 两个步骤各自产生 Event;Workflow 等待它们都完成,而不是让模型自己记进度。
- 低风险且符合政策时走自动退款;高风险时进入人工审批分支并暂停。
- 如果核对政策由专门 Agent 完成,Task API 要求它用结构化结果明确结束任务。
- Runner 把过程事件和最终结果写入 session,并持续返回给调用方。
先记住这条主线:Agent 负责“怎么理解和判断”,Workflow 负责“哪些步骤必须怎样发生”,Runner 负责“整次运行怎样被看见和恢复”。
ADK Python 的 README 在 2.0 里把两件事放到最前面:Workflow Runtime 和 Task API。Workflow 处理 routing、fan-out/fan-in、loops、retry、state management、 human-in-the-loop 和 nested workflows;Task API 处理结构化 agent-to-agent delegation。 这不是功能堆叠,而是在说:开放决策交给 agent,确定性流程交给 workflow。 2.0 的新增重点 已经把这两类责任并列了。
Quick Start 也很直白:ADK 应用由两个主类构成,Agent 定义 instruction、tools 和 behavior,
Workflow 在 graph-based flow 里编排 agents 和 tasks。
这段说明在
README Quick Start。
顶层包导出也印证了这件事:google.adk 公开导出
Agent、Workflow、Runner、Event 和 Context。
源码在
__init__.py。
阅读契约。
这篇只抓一条线:ADK 不是让你在“全靠 agent 自主”和“全靠 workflow 写死”之间二选一。
Agent 负责开放语言和工具使用,Workflow 负责业务拓扑和恢复,
Runner 把两者接到 session event 上。
证据边界。
源码引用固定到 Google ADK Python 当前公开快照
62b97007378c4da749a1615d0881aba443a32325。本文只基于公开 README、源码和 samples,
不推断 Google 内部服务、托管平台或未公开控制台能力。
把退款助手压成一条形状级记录,ADK 的分工会更清楚。下面不是源码里的完整 schema,而是读源码时要追的 owner 交接:
Runner.run_async(root=refund_workflow, message="鞋码不合适,想退款")
-> intake_agent(single_turn): 输出 {order_id, reason} 或 RequestInput
-> Workflow state:
order = pending(check_order)
policy = pending(check_policy)
check_order node -> Event(output_for="check_order", output={order_id, status})
check_policy task agent -> finish_task(output={eligible: true, reason})
JoinNode waits for: check_order + check_policy
route:
eligible and low_risk -> issue_refund node
high_risk -> human_approval node
Runner event queue -> plugin callbacks -> session service -> caller stream
这段形状说明:ADK 不是让 agent 自己记住“我现在跑到退款第几步”。开放语言理解在 Agent,
拓扑和 join 在 Workflow,任务完成信号在 Task API,最终可观察、可恢复的事实由 Runner
变成 session event。
四、Agent 负责自主行为,但不独占整个应用
在退款助手里,agent 适合处理“用户到底想干什么”这种开放问题。LlmAgent 里确实有传统 agent 需要的东西:
model、instruction、tools、planner、callbacks、input/output schema、output_key 和 code executor。
但 ADK 更值得注意的是 mode:同样是 LLM agent,放在不同位置时,运行语义不一样。
源码在
LlmAgent 定义。
| mode | 像什么 | 适合放在哪里 |
|---|---|---|
chat |
可以和用户多轮对话的主助手。 | 退款助手的入口,负责理解用户诉求和追问缺失信息。 |
task |
可被委派任务,并用 finish_task 明确交付。 |
让另一个 agent 专门完成“核对退款政策”这类子任务。 |
single_turn |
不持续聊天,只做一次输入到输出。 | workflow 的某个节点,比如“总结订单风险”。 |
这说明 ADK 没有把所有“步骤”都包装成一个自主聊天 agent。它会根据位置给 agent 默认 mode:
root LLM agent 必须是 chat;workflow node 里的 LLM agent 默认是 single_turn;task mode agent 则会自动加入
finish_task 工具,让模型显式声明任务完成。
model_post_init
里还会把 single_turn/task 子 agent 包成工具。
FinishTaskTool 很能说明 Task API 的语义:它用 task agent 的 output schema 校验结果,
并把 finish_task 的调用当作“任务完成信号”。这比“让子 agent 最后说 DONE”稳得多。
相关源码在
工具声明
和
执行与校验。
五、Workflow 负责确定性 graph owner
Workflow 负责那些不能靠 prompt 含糊带过的业务路线。退款流程里,“查订单”和“查政策”可以并行, 但“发起退款”必须等它们都完成;高风险订单要进人工审批,低风险订单可以直接走自动退款。 这种拓扑应该由 graph runtime 拥有,而不是写进 agent 的一句 instruction。
Workflow 的文件头直接写了:它把用户可见的 graph definition 和 execution engine 结合起来。
Workflow(BaseNode) 的 _run_impl 就是 orchestration loop:
setup 构建 graph,loop 调度 ready nodes,finalize 收集 terminal output。
源码在
文件头
和
Workflow 定义。
这里的 graph 不是画图 DSL,而是执行责任。Graph.from_edge_items 可以把显式 Edge、
tuple chain、routing map、fan-out tuple 编译成内部 edge list;validate_graph 会检查重复节点名、
START、可达性、重复边、默认路由和无条件环。
关键入口在
edge item 类型、
编译与路由
和
graph 校验。
一个 sample 就能看清它的风格:fan-out/fan-in 里,三个函数从 START 并行触发,
再进入 JoinNode,最后交给 aggregate。
这不是让模型在 prompt 里“先大写、再计数、再反转”,而是让 graph runtime 拥有拓扑。
sample 在
fan_out_fan_in。
JoinNode 通过 _requires_all_predecessors 表示它必须等所有前驱完成,
源码在
JoinNode。
Agent owner
- instruction 与 static instruction
- tools、toolsets、planner、callbacks
chat/task/single_turnmode- output schema 与 output key
Workflow owner
- edge、route、fan-out/fan-in、join
- node input/output schema
- retry、timeout、wait_for_output
- resume、interrupt、terminal output
六、BaseNode 和 NodeRunner 是 workflow 的执行颗粒
Workflow 不是直接顺序调用函数,它把每一步都包成 node。BaseNode 负责这一步的名字、描述、
输入输出 schema、重试、超时、恢复时是否重跑、是否等待输出。这样一个“查订单”节点失败了,
workflow 知道它能不能重试、输出应该长什么样、恢复时要不要重新执行。
| BaseNode 负责的事 | 对业务流程的意义 |
|---|---|
input_schema / output_schema |
节点之间不是随便传字符串,而是有可校验的输入输出。 |
retry / timeout |
外部系统慢或失败时,恢复策略在 workflow 层可见。 |
rerun_on_resume |
中断恢复时,可以决定这个节点是否重新执行。 |
wait_for_output |
fan-in 或下游节点能明确等待什么结果。 |
BaseNode.run 是 public entry point:它调用 _run_impl,把 None 跳过,
把 Event 原样传出,把 RequestInput 转成 interrupt event,把任意其他值转成
Event(output=value)。
源码在
BaseNode 定义
和
BaseNode.run。
NodeRunner 是每个 node 的执行器。它创建 child context,驱动 BaseNode.run,
把事件放进 invocation event queue,把 output、route、interrupt_ids 写回 child context,再返回给 workflow loop。
它还会补 author、invocation_id、node_info.path、branch 和 isolation_scope。
这就是 workflow event 能恢复、能归因的原因。
源码在
NodeRunner 注释
和
事件 enrichment。
6.1 同一笔退款的状态到底归谁
“Workflow 有 state”不等于所有状态都归 Workflow。把退款 RF-204 放回各层,会看到四份用途不同的记录:
| 记录 | owner | 保存什么 | 不能证明什么 |
|---|---|---|---|
| child context | NodeRunner |
节点输入、输出、route、interrupt id | 不是外部退款已经发生 |
| invocation queue | Runner |
本次运行中待消费的 Event | 内存队列本身不是重启后的 durable record |
| session events | session service | 非 partial Event、节点归因和恢复材料 | 不等于模型下一轮看见完整事件历史 |
| 退款事实与回执 | 外部退款系统 | 幂等键、退款状态、业务回执 | 不能只靠 Workflow completed 推断 |
七、Runner 把 Agent 与 Workflow 接到 session event 上
到这里还差一个问题:Agent 和 Workflow 产生的事件,怎样变成一次可恢复的运行?这就是 Runner 的位置。
它用于运行 agents,并处理 session、event generation、artifact storage、session management 和 memory 等服务。
这在
Runner 属性。
它的 run 是方便测试的同步 wrapper,生产路径是 run_async。
run_async 会按 root 类型分流:root LlmAgent 走 chat mode,并被包装成 node;
root BaseNode 则走 node runtime。
源码在
run / run_async。
事件消费也很关键。_consume_event_queue 从 invocation queue 里拿事件,运行 plugin callback,
生成 output event,非 partial 事件写入 session service,然后 yield 给调用者。
这说明 ADK 的事件不是临时日志,而是 session 的持久化材料。
源码在
_consume_event_queue。
Event 本身也支持 workflow metadata:NodeInfo.path、output_for、
message_as_output、branch 和 isolation_scope。
这些 metadata 让 graph node、task delegation、branch history 和可恢复 session 有共同语言。
源码在
Event 与 NodeInfo。
这里还要把 pause、retry 和 resume 分开。它们都会让节点“没有一次跑完”,但 owner 和持久化语义不同:
| 运行情况 | runtime 做什么 | 恢复时依赖什么 | 容易误读的点 |
|---|---|---|---|
| Pause / interrupt | RequestInput 被规范化成带 interrupt id 的 Event;非 partial event 进入 session service。 |
调用方用 function response 回答对应 interrupt id。 | 不是节点失败,也不是把整张图从头再跑。 |
| Retry | NodeRunner 按 retry_config 和 timeout 在当前执行里重试节点。 |
当前 child context 与本地 attempt count;attempt count 不跨 resume 持久化。 | Retry 是当前 invocation 的失败处理,不等于中断后的恢复。 |
| Resume | Runner 解析 resume input,rehydration 扫描 session events,workflow 恢复已完成、等待中和中断节点。 |
invocation id、旧 event、interrupt response,以及节点的 rerun_on_resume。 |
rerun_on_resume=false 可以把恢复输入当节点输出;为 true 才从节点逻辑重新执行。 |
resume input 的提取与 context 重建在
Runner resume path,
旧 event 的节点状态恢复在
rehydration scan,
prior output 与 interrupt id 的回灌则在
NodeRunner child context。
但恢复 Workflow 不等于回滚外部系统。issue_refund 应携带稳定幂等键
refund:RF-204;退款系统返回业务回执后,节点才能把“已采用”写回事务记录。
如果网络在外部写入成功、Event 持久化之前断开,恢复时必须先按幂等键查询或重试,不能把“session 里没有完成 Event”
当成“退款一定没发生”。节点产生输出、规则校验通过、退款系统接受写入,是三个不同状态。
八、Task API 是 Agent 和 Workflow 之间的桥
_llm_agent_wrapper 把 LLM agent 作为 workflow node 运行。它给 single_turn agent 准备输入,
给 task agent 处理 delegated task 的 function call,把 output_schema 校验后的结果写成 node output。
run_llm_agent_as_node 还规定:LLM agent 作为 workflow node 时,默认 mode 是 single_turn;
task 和 single_turn 使用 isolation_scope 做上下文隔离,chat agent 则看完整对话。
相关源码在
输入输出处理
和
run_llm_agent_as_node。
这就是 ADK 与“纯 workflow”或“纯 agent loop”的不同处。Workflow 可以把 agent 当 node,
Agent 也可以把 task/single_turn 子 agent 当工具。两边不是互相取代,而是通过 Event、
node output、function call 和 isolation scope 互相接住。
九、读 ADK 时先记住这些 invariant
| Invariant | 源码事实 | 工程含义 |
|---|---|---|
| Agent 和 Workflow 是并列入口。 | 顶层导出同时公开 Agent、Workflow、Runner、Event。 |
ADK 不把业务流程全塞进 prompt,也不把 agent 降级成普通函数。 |
| Workflow 拥有 graph 执行。 | _run_impl 自己就是 setup、loop、finalize。 |
路由、并发、join、resume、terminal output 都有 runtime owner。 |
| NodeRunner 是事件归因颗粒。 | 每个 node 事件都会补 path、run_id、branch、isolation_scope。 | session 里能重建谁运行过、输出给谁、在哪里暂停。 |
| Task API 不是 prompt 约定。 | task mode 自动加 finish_task,并由 wrapper 处理 output。 |
多 agent delegation 可以有 schema、完成信号和上下文隔离。 |
| Runner 是服务边界。 | 事件进入 queue,经 plugin callback,写入 session service,再 yield。 | 应用层拿到的是可持久化、可观测、可恢复的执行流。 |
下一篇看 Agno。它的阅读角度会从“graph runtime”转到“agent platform”:当 agent 要进入产品, API、storage、tracing、scheduling、RBAC、control plane 这些平台 owner 应该由谁接住。