一、先看退款助手为什么不能只靠 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 暴露成可调用能力;两种设计不能在图里画成未经说明的顺序管线。

  1. Runner 找到用户会话,把“鞋码不合适,想退款”交给 root Workflow。
  2. Workflow 的入口 Agent 节点理解意图并输出结构化订单号;资料不足时先补问,资料齐全后启动查订单与查政策。
  3. 两个步骤各自产生 Event;Workflow 等待它们都完成,而不是让模型自己记进度。
  4. 低风险且符合政策时走自动退款;高风险时进入人工审批分支并暂停。
  5. 如果核对政策由专门 Agent 完成,Task API 要求它用结构化结果明确结束任务。
  6. 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_turn mode
  • 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 应该由谁接住。

参考源码与文档