Dify Agenton 用户指南:用 Layer 图组合可复用 Agent 计划与可恢复会话
【免费下载链接】difyBuild Agentic workflows, RAG pipelines, with rich AI model and tool support on one collaborative workspace. Deploy on cloud, VPC, or self-hosted, so teams move from prototype to production without rebuilding the stack.项目地址: https://gitcode.com/GitHub_Trending/di/dify
Agenton 是 Dify 仓库dify-agent包中的核心框架:它以LayerNode和LayerProvider组成可复用的"图层图计划",把系统提示词、用户提示词和工具的定义从 Agent 主循环中解耦出来。读完本篇,你将掌握如何在 Dify 的 Agenton 中定义带配置校验的 Layer、构建Compositor并在运行时注入配置、通过deps建立图层间直接依赖、聚合 prompt/tool 素材,以及用会话快照(session snapshot)实现跨调用的状态挂起与恢复——这些能力正是 dify-agent 运行时 驱动 Agent 执行所依赖的底层机制。
Agenton 的核心设计原则是state-only(仅状态):Compositor本身不保存任何存活的 layer 实例、客户端、清理栈或运行状态。每次Compositor.enter(...)调用都会创建一个全新的CompositorRun,包含新的 layer 实例、直接的依赖绑定、生命周期状态,以及一个可选的已水合(hydrated)会话快照。这一设计可以从 compositor 核心实现 的模块 docstring 中得到印证:Compositor 只存储不可变的图节点和可选的聚合 transformer,因此它可以被安全地反复甚至并发地 enter,每一次进入都产生独立的CompositorRun。
配置与运行时状态的四类划分
Agenton 严格区分四种数据,这是理解整个框架的钥匙,也是它"state-only"承诺的来源:
- Graph config(图配置):可序列化的拓扑描述,包括节点
name、providertype、依赖映射(deps)和元数据(metadata)。对应的 DTO 是 LayerNodeConfig,它故意不包含任何 per-run 的 layer 配置——节点只描述"这个节点是什么类型、依赖谁",而不描述"这一次怎么构造"。 - Per-run layer config(每次运行的层配置):通过
Compositor.enter(configs=...)传入,以节点名为键的映射。Provider 会在任何工厂函数运行之前,用该 layer 的config_type逐一校验这些值(见 _validate_layer_configs,它注释明确说明"在任何 provider 工厂被调用之前校验所有节点配置")。 - Runtime state(运行时状态):挂在
layer.runtime_state上的可序列化、逐层的调用状态。会话快照只持久化生命周期状态和这份 JSON 安全的数据。 - Live Python resources(存活 Python 资源):客户端、文件句柄、socket、进程句柄等,一律留在 Agenton 核心之外,由应用代码或包裹 compositor 进入逻辑的集成层 context manager 持有。
LayerNodeConfig、CompositorConfig、CompositorSessionSnapshot等 DTO 均启用了extra="forbid",且图配置与快照是两个刻意分离的边界:前者只描述可复用的组合状态(schema 版本、有序节点名、provider type id、依赖映射、元数据),后者只携带有序的生命周期状态与可序列化 runtime_state。外部 DTO 即使已经是构造好的 Pydantic 模型实例也会被重新 dump 再校验(见 _validate_config_model_input),防止构造后的变更绕过 compositor 入口校验器。
定义一个配置驱动的 Layer
为 per-run 配置使用LayerConfig模型,并继承一个带类型的 layer 家族(如PlainLayer),这样Layer.__init_subclass__就能从泛型基类推断出 schema。base 层抽象 中,__init_subclass__会依次推断deps_type、config_type与runtime_state_type;无法推断时才需要子类显式声明。一个完整的例子如下:
from dataclasses import dataclass from pydantic import ConfigDict from typing_extensions import Self, override from agenton.layers import LayerConfig, NoLayerDeps, PlainLayer class GreetingConfig(LayerConfig): prefix: str model_config = ConfigDict(extra="forbid") @dataclass(slots=True) class GreetingLayer(PlainLayer[NoLayerDeps, GreetingConfig]): type_id = "example.greeting" prefix: str @classmethod @override def from_config(cls, config: GreetingConfig) -> Self: return cls(prefix=config.prefix) @property @override def prefix_prompts(self) -> list[str]: return [self.prefix]要点解析:
type_id是可序列化图配置中引用该 layer 的注册标识。Compositor.from_config通过它把LayerNodeConfig.type解析为 provider(见 _build_provider_type_map),未声明type_id的 provider 会直接报错。from_config钩子:LayerProvider.from_layer_type会用config_type校验原始配置,然后调用这个类方法构造实例(见 Layer.from_config)。没有配置的 layer 走默认的无参构造路径;有具体配置 schema 的 layer 必须覆写它来消费带类型的 Pydantic 模型,否则默认实现会抛出TypeError。- 省略的 schema 槽位:如果 layer 未指定配置或运行时状态,则默认落到
EmptyLayerConfig和EmptyRuntimeState(见 base 层中的默认定义)。 - 生命周期钩子:
on_context_create / on_context_resume / on_context_suspend / on_context_delete都是 layer 实例上的无参方法,应通过self.deps读取依赖、通过self.runtime_state读写可序列化的可变状态。
存活资源:Agenton 不替你清理,但你可以在边界处确定性地清理
Agenton 不拥有资源清理。把存活资源留在外围应用中,显式地传给能力方法即可:
@dataclass(slots=True) class ClientUserLayer(PlainLayer[NoLayerDeps]): def make_client_user(self, *, http_client: httpx.AsyncClient) -> ClientUser: return ClientUser(http_client) compositor = Compositor([LayerNode("client_user", ClientUserLayer)]) async with httpx.AsyncClient() as http_client: async with compositor.enter() as run: layer = run.get_layer("client_user", ClientUserLayer) user = layer.make_client_user(http_client=http_client)这样做的好处是把确定性清理保留在集成边界,同时让 Agenton 的快照只包含可序列化的 runtime state。值得一提的是,Agenton 并非完全没有"资源作用域"概念:Layer.resource_context 提供了一个对称的 active-scope 异步上下文,Agenton 会在on_context_create/on_context_resume之前进入它,在on_context_suspend/on_context_delete之后退出它,即使后续钩子或 run 主体失败也会保证确定性地拆除。但正如 run 模块 docstring 所强调的,resource_context()中获取的资源只属于 active 作用域,永远不会出现在任何快照 DTO 中。
构建 Compositor:从可序列化图配置出发
对配置驱动的 layer,使用 provider,并在进入时传入 per-run 配置:
from agenton.compositor import Compositor, CompositorConfig, LayerNodeConfig, LayerProvider from agenton_collections.layers.plain import PromptLayer, PromptLayerConfig providers = ( LayerProvider.from_layer_type(PromptLayer), LayerProvider.from_layer_type(GreetingLayer), ) compositor = Compositor.from_config( CompositorConfig( layers=[ LayerNodeConfig(name="prompt", type="plain.prompt"), LayerNodeConfig(name="greeting", type="example.greeting"), ] ), providers=providers, ) async with compositor.enter( configs={ "prompt": PromptLayerConfig(user="Answer with examples."), "greeting": GreetingConfig(prefix="Hi"), } ) as run: prompts = run.prompts其中PromptLayer是agenton_collections提供的现成 layer,位于 plain basic 实现,接受prefix、user、suffix三个配置字段,可直接产出三段式系统提示词素材。
构建 API 的几条规则(均可在 compositor core 源码 中验证):
CompositorConfig只支持schema_version == 1,其他版本在进入时直接抛错。providers按 type id 解析:_build_provider_type_map会拒绝未声明type_id或重复注册type_id的 provider;节点type找不到对应 provider 时抛出带 type id 的KeyError。LayerProvider.from_factory(...):当构造需要 Python 对象或可调用对象时使用。Provider 工厂只能拿到"已校验的配置",拿不到图节点数据,并且必须为每次调用返回全新的 layer 实例——providers 模块 通过一个基于弱引用的全局注册表(_claim_fresh_layer_instance)强制这一点,复用旧实例会在依赖绑定和生命周期钩子运行之前被拒绝。node_providers={"node_name": provider}:配合Compositor.from_config使用,按节点名覆盖 type id 选出的 provider,实现节点级定制构造而不必把节点数据塞进工厂;传入未知节点名会在构建期报错。- 依赖方向有约束:
_validate_nodes会检查节点名唯一、依赖键必须声明在 layer 的 deps schema 中、依赖目标必须存在,且依赖目标必须指向图序中更早的节点(dependencies must target preceding layer nodes in compositor order),这样才能让资源作用域按依赖顺序嵌套。
图层依赖:直接把上游 layer 实例绑到 self.deps
图层依赖把直接的 layer 实例绑定到self.deps,作用域仅限一次 run。依赖映射以"依赖字段名"为键、以"compositor 节点名"为值:
class ModelDeps(LayerDeps): plugin: PluginLayer @dataclass(slots=True) class ModelLayer(PlainLayer[ModelDeps]): def make_model(self) -> Model: return self.deps.plugin.make_provider()底层机制见 LayerDeps 与 bind_deps:deps_type子类中每个带注解的成员必须是一个具体的Layer子类,或形如SomeLayer | None的现代可选依赖。绑定时的三类失败全部发生在生命周期钩子运行之前:
- 缺失必需依赖(
Missing layer dependencies); - 未知的依赖键(
Unknown layer dependencies); - 依赖目标的 layer 类型不匹配(抛出
TypeError,提示期望类型与实际类型)。
可选依赖(LayerSubclass | None)在缺席时会被显式赋值为None,而不是缺失属性。Compositor侧的绑定入口是 _bind_deps:把每个节点的deps映射解析为节点名到 layer 实例的直连映射,再逐个调用layer.bind_deps(...)。
系统提示词、用户提示词与工具的四个创作面
每个 layer 暴露四个 authoring surface(定义于 Layer 基类的四个 property):
| 创作面 | 含义 | 聚合顺序 |
|---|---|---|
prefix_prompts | 系统提示词片段 | 按层序(先声明者在前) |
suffix_prompts | 系统提示词片段 | 按逆层序(先声明者在后) |
user_prompts | 用户消息片段 | 按层序 |
tools | 工具条目 | 按层序 |
聚合结果可以在活动CompositorRun上通过run.prompts、run.user_prompts、run.tools读取。聚合顺序在 run 的 prompts 属性实现 中有精确体现:先正序收集所有prefix_prompts,再逆序收集所有suffix_prompts,每个 item 都经过 layer 的wrap_prompt包装;user_prompts与tools同理按图序收集。
接入 pydantic-ai 时,导入agenton_collections.transformers.pydantic_ai.PYDANTIC_AI_TRANSFORMERS并传给Compositor(...)或Compositor.from_config(...),让带 tag 的 layer 条目被转换为 Pydantic AI 的 prompt、user prompt 和 tool 值。这个常量定义在 transformers 模块,它作为prompt_transformer/user_prompt_transformer/tool_transformer三个后聚合钩子工作——run 模块 docstring 说明它们只在 layer 级包装和 run 级聚合完成之后运行,未安装 transformer 时包装后的条目原样返回。PlainLayer与PydanticAILayer两个 typed 家族分别把原生值包装为PlainPromptType、PydanticAIPromptType等带 tag 的类型(见 layers/types.py),使不同家族的条目可以在同一图中共存而不互相污染。
会话快照与恢复:显式的跨调用状态
核心 Agenton 的 run 槽位默认是delete-on-exit:run 退出时调用on_context_delete,槽位进入CLOSED。当希望下一次快照可恢复时,在活动上下文中调用run.suspend_on_exit()或run.suspend_layer_on_exit(name):
async with compositor.enter(configs=configs) as run: run.suspend_on_exit() snapshot = run.session_snapshot async with compositor.enter(configs=configs, session_snapshot=snapshot) as restored_run: restored_layer = restored_run.get_layer("stateful", StatefulLayer)这套机制对应的源码事实:
- 生命周期状态机:LifecycleState 有
NEW、ACTIVE、SUSPENDED、CLOSED四态,其中ACTIVE是内部专用状态,LayerSessionSnapshot 的校验器 会拒绝它出现在外部快照中("LifecycleState.ACTIVE is internal-only")。 - 退出意图:
ExitIntent.DELETE/ExitIntent.SUSPEND控制退出时调用on_context_delete还是on_context_suspend;意图只能在槽位处于ACTIVE时修改(见 _set_layer_exit_intent)。 - 快照生成时机:
run.session_snapshot在上下文退出之后才被填充(见 _exit_layers),内容为"有序 layer 名 + 非 ACTIVE 生命周期状态 + 各层 JSON 化 runtime_state"。快照中不含配置、依赖、prompt、工具或任何存活资源。 - 恢复约束:恢复时必须把快照传给一个具有相同 layer 名称和顺序的后续
Compositor.enter(...)调用——_validate_session_snapshot 会逐项比对名称序列;同时 _ensure_layers_can_enter 保证ACTIVE状态无法进入、CLOSED的 layer 无法再次进入。水合时 runtime state 通过runtime_state_type.model_validate(...)重新校验(见 _create_run)。 - 失败语义:若
on_context_create/on_context_resume抛错,该层永远不会成为ACTIVE,且那次失败的进入尝试不会运行正常的on_context_suspend/on_context_delete钩子——enter 钩子自己负责对部分副作用做业务补偿或幂等处理,Agenton 只保证resource_context()的清理,不保证钩子回滚。
延伸阅读与源码索引
- 可运行示例(对应原文档的 See also):
- basics.py——基础组合用法;
- pydantic_ai_bridge.py——pydantic-ai 桥接;
- session_snapshot.py——会话快照与恢复。
- 核心实现:
- compositor core:图计划构建、配置校验、快照水合与 enter 上下文;
- DTO 与边界校验:
CompositorConfig、CompositorSessionSnapshot及 ACTIVE 拒绝逻辑; - Layer 基类:
LayerConfig、LayerDeps、生命周期钩子与 schema 推断; - CompositorRun:槽位生命周期、退出意图、快照与 prompt 聚合;
- LayerProvider:配置校验、工厂调用与"全新实例"强制;
- typed 家族:
PlainLayer与PydanticAILayer的 prompt/tool 包装契约。
【免费下载链接】difyBuild Agentic workflows, RAG pipelines, with rich AI model and tool support on one collaborative workspace. Deploy on cloud, VPC, or self-hosted, so teams move from prototype to production without rebuilding the stack.项目地址: https://gitcode.com/GitHub_Trending/di/dify
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考