一、先看退款助手为什么不能只靠 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 当前公开快照
d57c84f13baf53cfd910c2155449f8c4254e01c5。本文只基于公开 README、源码和 samples,
不推断 Google 内部服务、托管平台或未公开控制台能力。
把退款助手压成一条运行记录,ADK 的分工会更清楚。下面不是源码里的完整 Schema,而是读源码时要追的模块交接:
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,也支持显式 task;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
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 负责什么
- instruction 与 static instruction
- tools、toolsets、planner、callbacks
chat/task/single_turnmode- output schema 与 output key
Workflow 负责什么
- 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 |
Python 类型与 Pydantic 模型可校验并转换输入输出;原始 JSON Schema 字典和 genai Schema 在此层仅作描述,不执行校验。 |
retry_config / timeout |
外部系统慢或失败时,恢复策略在 workflow 层可见。 |
rerun_on_resume |
中断恢复时,可以决定这个节点是否重新执行。 |
wait_for_output |
fan-in 或下游节点能明确等待什么结果。 |
BaseNode.run 是 public entry point:它调用 _run_impl,把 None 跳过,
校验 Event.output 后传出事件,把 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。
如果失败的是嵌套 Workflow,配置在它上面的 retry_config 会重试整个子流程,但不会简单重跑全部子节点。
已产生输出或状态变更的子节点从事件中重放;没有留下这两类记录的子节点才重新执行。
重试配置
与 NodeRunner 重试循环
共同实现这个边界。因此退款工具仍需幂等:没有可重放记录,不能证明外部副作用没有发生。
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,也可显式使用 task 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 互相接住。
对退款协调者而言,委派政策核对后还可能需要委派风险核对。chat wrapper 在每次子任务结果写成 function response 后,
会再次进入父 Agent 的模型循环;只有这轮不再委派或发生 agent transfer 才结束,而不是第一次子任务返回就退出。
根 Agent 也可直接配置 mode="task",调用者从终止 Event 的 output 读取结构化结果。
前者是 连续委派循环,
后者由 Runner 的根 task 支持保证。
九、读 ADK 时先记住这些运行约束
| 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 应该由谁接住。