Dify Agent 运行生命周期详解:agent run 与 workflow run 的关系与退出信号控制机制
本文围绕 Dify 仓库中 dify-agent 子项目的官方概念文档 Agent Run Lifecycle 展开,讲清两件事:一是从调用方(caller)视角看 agent run 与 workflow run 的对应关系,以及 session_snapshot 如何在多次 Agent 执行之间传递状态;二是如何通过 CreateRunRequest.on_exit 的退出信号(exit signals)精确控制每个 layer 在运行结束时的生命周期去向(suspend 或 delete)。读完后,你将能够正确编排 Agent 节点内的人机交互(HITL)续跑流程,并为一次完整的 workflow 运行设计正确的快照保留与清理策略。
agent run 与 workflow run 的关系
在 Dify 的工作流体系中,两者的语义边界是:
workflow run:一次完整的工作流执行;agent run:工作流运行过程中,由 Agent 节点启动的一次 Agent 执行。
二者不是一一映射关系:一个 workflow run 中往往会包含多个 agent run。理解这一点是后续所有快照管理、HITL 处理策略的前提。
首次进入 Agent 节点
当某次 workflow run 首次到达一个 Agent 节点时,调用方为该节点启动第一个 agent run。该 agent run 会依次进入其 composition(层组合)中定义的各个 layer:
- 如果请求中不带
session_snapshot,每个 layer 都以全新状态进入,并各自初始化运行时状态; - 如果请求携带了上一次
agent run返回的session_snapshot,每个 layer 会从快照中恢复运行时状态并从中断处继续。
进入各层之后,Agent 开始执行 LLM 调用与工具调用,直到当前 agent run 达到一个终态结果。需要特别注意:agent run 结束不代表外层工作流结束——它只是该次 Agent 执行告一段落。
以 final_output 工具调用结束
当 Agent 以 final_output 工具调用收尾时,表示该 Agent 节点在当前这一次执行中产出了最终输出。调用方应当:
- 读取当前
agent run的终态输出; - 让
workflow run继续流向下游节点。
此时当前 agent run 已经结束,但返回的 session_snapshot 仍然可以保存下来。如果同一次 workflow run 后续可能再次进入同一个 Agent 会话,调用方应当继续复用这份快照,而不是丢弃。
以 human 工具调用结束:Agent run 没有暂停态
这是该文档强调的一个常见误区。当 Agent 以 human 工具调用收尾时,意味着业务流程需要人类输入才能继续。很多人会下意识地把它理解为"agent run 被暂停了",但事实是:
Agent runs do not have a pause state(agent run 不存在暂停状态)。 在 human 工具场景下,当前
agent run已经结束;需要被暂停的是外层的workflow run。
调用方应当按以下五步处理该流程:
- 读取当前
agent run的结果,在终态run_succeeded事件上检测deferred_tool_call; - 进入工作流的 HITL(Human-in-the-Loop)处理逻辑,暂停 graphon(工作流图执行);
- 等待人类输入完成;
- 恢复工作流时,基于上一个
session_snapshot、相同的 composition,以及以原始工具调用 id 为键的deferred_tool_results,在同一个 Agent 节点上启动第二个agent run; - 保持 history layer 处于激活状态,这样 Dify Agent 才能把人类返回的结果匹配到上一次运行的消息历史中存储的那个待处理(pending)工具调用上。
换句话说,human 工具的含义不是"把这个 agent run 挂起直到恢复",而是"这个 agent run 以了一个需要人类输入的结果而结束"。调用方完成 HITL 处理后,应当基于同一份历史/会话快照创建一个全新的 agent run 来继续。
这一设计在协议层有明确支撑。CreateRunRequest 的文档注释指出:ask-human 续跑场景要求匹配的那个 pending 工具调用仍存在于先前的历史状态中,因此调用方应当跨多次 run 保持 history layer 激活,让 deferred_tool_results 能与原始模型响应进行匹配,而不是开启一个全新的 user-prompt 轮次。
进入另一个 Agent 节点
当同一次 workflow run 继续执行并到达另一个 Agent 节点时,会启动另一个 agent run。下一个 Agent 节点可能是完全不同的 Agent,也可能是由 router(文档原文写作 roaster)复用的同一个 Agent。
由此得出一个关键实践原则:调用方应当按 Agent 会话(Agent session)来保存和传递 session_snapshot,而不是假设一次 workflow run 里只有一个 agent run。
Agent run 退出信号(exit signals)
当一个 agent run 结束时,Dify Agent 会退出当前 run 所进入的各层。调用方通过 CreateRunRequest.on_exit 控制每个 layer 是被挂起(suspend)还是被删除(delete)。
必须分清一个语义边界:
退出信号控制的是层的生命周期状态(layer lifecycle state),而不是
agent run的执行状态。
默认策略是 suspend,因此任何进入了 compositor 上下文并正常退出的 run——包括失败或被取消的 run——都可以返回一份可复用的 session_snapshot。只有在进入 compositor 上下文之前就失败或被取消的 run,才不会有新的快照。这一点与协议注释中的行为一致:成功 run 始终在终态 run_succeeded 事件上发布可恢复的会话快照;失败和被取消的 run 则在其 compositor 上下文确实进入并退出时发布(见 schemas.py 模块注释)。
默认策略:挂起(suspend)各层
如果请求没有显式设置 on_exit,等价于:
{
"on_exit": {
"default": "suspend",
"layers": {}
}
}
其含义是:所有已进入的 layer 都以 suspended 状态退出,并被写入返回的 session_snapshot。调用方可以在下一次 agent run 中提交这份快照来恢复这些 layer。
对于工作流中的常规 Agent 执行——无论以 final_output 还是 human 工具结束——除非你确定该 Agent 会话永远不会再被恢复,否则调用方都应保持默认的 suspend 策略。
工作流结束时删除各层:清理 run
当整个 workflow run 结束,调用方应当再启动一次"清理型" agent run:
- 复用最后可用的
session_snapshot; - 省略 LLM layer——因为这次 run 只是用来进入并清理现有状态,不需要再次调用模型;
- 以
delete信号退出各层。
清理请求的退出信号应写成:
{
"on_exit": {
"default": "delete",
"layers": {}
}
}
这次 run 之后,对应各层走 delete 路径退出。删除之后返回的快照不应再被用于恢复该 Agent 会话。
仅覆盖部分 layer
调用方也可以在默认 suspend 的同时只删除指定的层:
{
"on_exit": {
"default": "suspend",
"layers": {
"temporary_context": "delete"
}
}
}
此时只有 temporary_context 以 delete 退出,其余所有活跃 layer 都按默认的 suspend 行为退出。适合"会话整体保留、但某一临时上下文不再需要"的场景。
退出信号 API 参考
CreateRunRequest 中与退出控制相关的字段:
| 字段 | 类型 | 必填 | 含义 |
|---|---|---|---|
session_snapshot |
CompositorSessionSnapshot | None |
否 | 上一次 agent run 返回的会话快照。用于恢复同一个 Agent 会话。 |
on_exit |
LayerExitSignals |
否 | 本次 agent run 退出各层时使用的退出策略。省略时所有活跃 layer 默认挂起。 |
LayerExitSignals 的结构:
| 字段 | 类型 | 默认值 | 含义 |
|---|---|---|---|
default |
"suspend" | "delete" |
"suspend" |
未在 layers 中显式列出的 layer 的退出意图。 |
layers |
dict[str, "suspend" | "delete"] |
{} |
按 layer 名称的逐层退出意图覆盖。每个键必须对应当前 composition 中的一个 layer 名。 |
退出意图的语义:
| 退出意图 | Layer 退出状态 | 效果 |
|---|---|---|
suspend |
suspended |
保留 layer 运行时状态,使返回的 session_snapshot 可被后续 agent run 使用。 |
delete |
closed |
删除/关闭 layer 上下文。对应 layer 的快照不应再被恢复。 |
Python DTO 示例(对应 protocol/schemas.py 中的类型定义):
from agenton.layers import ExitIntent
from dify_agent.protocol import CreateRunRequest, LayerExitSignals
request = CreateRunRequest(
composition=composition,
session_snapshot=previous_snapshot,
on_exit=LayerExitSignals(
default=ExitIntent.SUSPEND,
layers={
"temporary_context": ExitIntent.DELETE,
},
),
)
注意事项:
on_exit只控制 layer 的退出行为,不会取消agent run本身(取消是独立的 cancel 协议操作);- Agent run 没有暂停状态,human 工具的等待由外层 workflow/HITL 流程负责;
on_exit.layers中的键必须引用当前 composition 中存在的 layer 名;- 同一个 Agent 会话后续需要继续时,使用
suspend并保存返回的session_snapshot; - 整个
workflow run结束后,启动一次不带 LLM layer 的清理 run,并使用delete。
源码实现印证:退出信号如何被执行
下面从源码层面印证上述语义,方便读者核对文档描述与实际行为的一致性。
LayerExitSignals 的定义
LayerExitSignals 定义于 dify-agent/src/dify_agent/protocol/schemas.py:
class LayerExitSignals(BaseModel):
"""Requested per-layer lifecycle behavior for the top-level ``on_exit`` field."""
default: ExitIntent = ExitIntent.SUSPEND
layers: dict[str, ExitIntent] = Field(default_factory=dict)
model_config: ClassVar[ConfigDict] = ConfigDict(extra="forbid")
两个要点与文档一致:默认值即 ExitIntent.SUSPEND;extra="forbid" 意味着多写字段会直接校验失败,防止拼错的 layer 名静默通过。CreateRunRequest 中对应字段为 on_exit: LayerExitSignals = Field(default_factory=LayerExitSignals)(schemas.py#L154),省略时即文档所说的默认 suspend 策略。
先校验、后应用的两段式流程
退出信号的处理集中在 dify-agent/src/dify_agent/runtime/layer_exit_signals.py 的两个函数中,并在 runner 的调用链里前后各出现一次:
-
进入 run 之前校验。AgentRunRunner._run_agent 在构建 compositor 后立即调用
validate_layer_exit_signals(compositor, self.request.on_exit)(runner.py#L310)。该校验会取 compositor 中所有节点名作为已知 layer 集合,on_exit.layers里任何不存在的键都会抛出ValueError(提示形如on_exit.layers references unknown layer ids: ...),并被归一化为AgentRunValidationError——即文档所说"每个键必须对应当前 composition 中的 layer 名"在实现上是强约束的。 -
进入 run 之后应用。在 runner.py#L333-L335,runtime 先
compositor.enter(configs=..., session_snapshot=...)进入层组合,拿到活跃的CompositorRun,再调用apply_layer_exit_signals(run, self.request.on_exit)。该函数遍历 run 当前激活的每个 slot,按layers.get(layer_id, default)解析出每层的意图,再分别调用run.suspend_layer_on_exit(layer_id)或run.delete_layer_on_exit(layer_id)(见 layer_exit_signals.py#L31-L43)。
这里有一个值得注意的实现细节:layer_exit_signals.py 的模块注释 说明 Agenton 在初始化每个 run slot 时默认是 delete-on-exit 意图,因此 runtime 必须在进入(enter)之后才显式应用请求级的 suspend/delete 意图——这正是"默认 suspend 策略由 Dify Agent 请求层提供"与"Agenton 底层默认 delete"能够共存的原因。
意图的最终落点:CompositorRun 与 ExitIntent
suspend_layer_on_exit / delete_layer_on_exit 的底层实现位于 dify-agent/src/agenton/compositor/run.py#L92-L108,它们只是为指定 slot 设置退出意图;此外还提供 suspend_on_exit() / delete_on_exit() 两个批量方法。意图枚举本身定义在 dify-agent/src/agenton/layers/base.py#L166-L174:
class ExitIntent(StrEnum):
DELETE = "delete"
SUSPEND = "suspend"
同一文件中还存在 LifecycleState.SUSPENDED = "suspended" 等生命周期状态,layer 在 slot 退出时会根据最终意图触发对应的 on_exit_suspended / on_exit_deleted 钩子(base.py#L304-L316),分别对应文档中 suspend 意图下"保留运行时状态并写入 session_snapshot"与 delete 意图下"关闭上下文、快照不再可恢复"两种语义。而快照本身由 CompositorRun.snapshot_session 在退出后生成,且明确禁止对仍处于 ACTIVE 状态的 layer 做快照——这保证了快照只包含已按退出意图收口的层状态。
实践要点小结
- 按 Agent 会话管理快照:一次 workflow run 可能包含多个 agent run,
session_snapshot的存取单位是 Agent 会话而非 workflow run。 - 不要把 human 工具当暂停:agent run 以
deferred_tool_call结束即为终态;暂停的是 workflow,恢复时要用旧快照 + 相同 composition +deferred_tool_results发起新的 agent run,并保持 history layer 激活。 - 常规执行保持默认 suspend:失败/取消但已进入 compositor 的 run 也返回可恢复快照;仅在确定会话不会续用时才显式 delete。
- workflow 收尾做一次清理 run:复用最后快照、省略 LLM layer、
on_exit.default = delete,且删除后的快照不再复用。 - 逐层覆盖要精确:
on_exit.layers的键必须是当前 composition 中真实存在的 layer 名,否则请求在校验阶段即被拒绝(AgentRunValidationError)。
以上行为的协议定义见 dify-agent/src/dify_agent/protocol/schemas.py,运行时处理见 dify-agent/src/dify_agent/runtime/runner.py 与 dify-agent/src/dify_agent/runtime/layer_exit_signals.py,层生命周期机制见 dify-agent/src/agenton/compositor/run.py;配套的概念文档位于 dify-agent/docs/dify-agent/concepts/run-lifecycle/index.md,各 layer(history、plugin-llm、plugin-tool、ask-human 等)的用法可继续参考 dify-agent/docs/dify-agent/user-manual 下的对应章节。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00