Dify Agent 执行上下文层(Execution Context Layer):为插件守护进程调用传递租户与执行身份
Dify Agent 采用分层(Layer)架构组织一次 Agent 运行的全部资源,其中「执行上下文层」(execution-context layer)承担着共享 Dify 运行标识与租户/用户身份的职责,是插件 LLM 层、插件工具层等所有需要访问插件守护进程(plugin daemon)的业务层的共同底座。读完本文,你将理解该层在 Dify Agent 分层组合中的定位、全部配置字段的取值规则与服务端凭证的注入机制,并能结合源码与测试用例掌握其“无状态、不持有 HTTP 客户端”的设计约束,从而正确编写包含执行上下文层的运行组合(RunComposition)。
一、执行上下文层在分层架构中的定位
执行上下文层携带三类信息:
- 共享的 Dify 运行标识(如
app_id、workflow_run_id、node_id等); - 调用插件守护进程所需的租户 ID 与可选的终端用户 ID;
- 用于可观测性与链路关联的调用来源分类(
invoke_from)等身份信息。
根据官方文档 execution-context-layer 的说明,该层需要与 plugin LLM layer 配合使用;当调用方希望把 Dify 工具暴露给模型时,还需叠加 plugin tool layer。这两个业务层都依赖执行上下文层来触达插件守护进程——例如插件 LLM 层通过 deps={"execution_context": "execution_context"} 显式声明这种绑定关系,因为 API 网关在解析模型凭证时需要调用方身份。
该层的类型 ID 为 dify.execution_context,对应源码常量 DIFY_EXECUTION_CONTEXT_LAYER_TYPE_ID,定义在 configs.py:
DIFY_EXECUTION_CONTEXT_LAYER_TYPE_ID: Final[str] = "dify.execution_context"
二、配置字段全解
官方文档给出的字段表如下(原文档骨架):
| 字段 | 类型 | 含义 |
|---|---|---|
tenant_id |
str |
调用插件守护进程时使用的 Dify 租户/工作区 ID。 |
user_id |
str | None |
可选的终端用户 ID,透传给插件守护进程。 |
invoke_from |
Literal[...] |
记录用于可观测性与关联的 Dify 调用方类别。 |
app_id / workflow_id / workflow_run_id / node_id / node_execution_id / conversation_id / agent_id / agent_config_version_id / trace_id |
str | None |
随运行转发、由 Dify 管理的可选执行标识。 |
对照当前仓库的实现 DifyExecutionContextLayerConfig,该配置类在上述字段之外还包含几组必填或可选的身份字段,编写组合时必须注意:
| 字段 | 类型 | 含义 |
|---|---|---|
agent_mode |
Literal["workflow_run", "single_step", "agent_app", "babysit", "fasten"] |
必填,Agent 后端运行模式,标识本次运行属于哪类 Dify 产品场景。 |
invoke_from |
Literal["service-api", "openapi", "web-app", "trigger", "explore", "debugger", "published", "validation"] |
必填,Dify 调用方类别,用于可观测性与关联。 |
user_from |
Literal["account", "end-user"] | None |
可选,调用者身份模型。知识库层(knowledge layer)会读取该字段,让 Dify 内部 API 区分「平台用户」与「终端用户」的检索,且该身份判定不受模型控制。 |
agent_config_version_kind |
Literal["snapshot", "draft", "build_draft"] | None |
可选,Agent 配置版本类别。 |
此外,配置类声明了 ConfigDict(extra="forbid", ...),即拒绝任何未知字段。测试 test_configs.py 明确验证了两点:
- 传入
daemon_url这类运行时设置会触发ValidationError——守护进程传输配置不属于客户端提交的层配置; - 传入任意未知字段(如
unknown)同样被拒绝。
另一个容易踩坑的点:测试 test_execution_context_rejects_legacy_agent_mode_in_invoke_from 表明,把旧的 agent_mode 取值(如 workflow_run)塞进 invoke_from 会被视为非法值。invoke_from 只接受上文列出的 8 个调用方类别,运行模式必须写入独立的 agent_mode 字段。
三、基本用法
官方文档给出的最小示例如下(原文档骨架,注意当前版本还需补充必填的 agent_mode 字段):
from dify_agent.layers.execution_context import (
DIFY_EXECUTION_CONTEXT_LAYER_TYPE_ID,
DifyExecutionContextLayerConfig,
)
from dify_agent.protocol import RunLayerSpec
execution_context_layer = RunLayerSpec(
name="execution_context",
type=DIFY_EXECUTION_CONTEXT_LAYER_TYPE_ID,
config=DifyExecutionContextLayerConfig(
tenant_id="replace-with-tenant-id",
user_id="replace-with-user-id",
invoke_from="workflow_run",
),
)
如果不需要终端用户 ID,省略 user_id 或传 None 即可;其余大部分可选执行标识(app_id、workflow_run_id、node_id 等)在不可获得时同样可以省略——只有 tenant_id、agent_mode、invoke_from 是必填项。
结合仓库内 plugin-llm-layer 文档 展示的完整最小模型组合,一份与当前源码取值严格对齐的执行上下文层写法是:
from dify_agent.layers.execution_context import (
DIFY_EXECUTION_CONTEXT_LAYER_TYPE_ID,
DifyExecutionContextLayerConfig,
)
from dify_agent.protocol import DIFY_AGENT_MODEL_LAYER_ID, RunComposition, RunLayerSpec
composition = RunComposition(
layers=[
RunLayerSpec(
name="execution_context",
type=DIFY_EXECUTION_CONTEXT_LAYER_TYPE_ID,
config=DifyExecutionContextLayerConfig(
tenant_id="replace-with-tenant-id",
user_id="replace-with-user-id",
user_from="account",
app_id="replace-with-app-id",
agent_mode="single_step",
invoke_from="debugger",
),
),
# ……以及依赖它的 llm 层(deps={"execution_context": "execution_context"})
]
)
四、服务端配置:守护进程凭证不进层配置
官方文档强调:执行上下文层的配置不包含守护进程传输设置,这些凭证配置在 Dify Agent 服务端:
DIFY_AGENT_PLUGIN_DAEMON_URL=http://localhost:5002
DIFY_AGENT_PLUGIN_DAEMON_API_KEY=replace-with-plugin-daemon-server-key
这样做的目的是让服务端凭证既不进入客户端提交的层配置,也不进入会话快照(session snapshot),避免敏感信息随快照流转。
服务端对应的读取位置在 server/settings.py,两个设置项均有默认值:
plugin_daemon_url: str = "http://localhost:5002"
plugin_daemon_api_key: str = ""
即守护进程 URL 默认为本地 http://localhost:5002,API Key 默认为空(生产环境必须显式设置)。这些设置的用途说明可参考 get-started 文档:DIFY_AGENT_PLUGIN_DAEMON_API_KEY 是服务端发给插件守护进程的 API Key。
五、源码级实现:凭证如何注入、客户端如何创建
5.1 层实例只能由 Provider 工厂创建
从源码结构看,DifyExecutionContextLayer 是一个刻意保持“纯配置/纯设置”的 PlainLayer:
- 直接调用
from_config(config)会抛出TypeError,强制要求经由 Provider 工厂构造; - 服务端专用的构造入口是
from_config_with_settings(config, *, daemon_url, daemon_api_key),把服务端注入的daemon_url与daemon_api_key作为实例字段保存。
而工厂注入发生在运行时的 compositor_factory.py:
layer_type=DifyExecutionContextLayer,
create=lambda config: DifyExecutionContextLayer.from_config_with_settings(
DifyExecutionContextLayerConfig.model_validate(config),
daemon_url=plugin_daemon_url,
...
)
也就是说,客户端提交的只是 DifyExecutionContextLayerConfig,守护进程 URL 与 API Key 由服务端在组装 Compositor 时补齐——这正是第四节环境变量设计的底层依据。
5.2 create_tool_client:业务层共用同一个共享 HTTP 客户端
执行上下文层为下游业务层提供的核心能力是 create_tool_client:
def create_tool_client(self, *, plugin_id: str, http_client: httpx.AsyncClient) -> DifyPluginDaemonToolClient:
if http_client.is_closed:
raise RuntimeError("DifyExecutionContextLayer.create_tool_client() requires an open shared HTTP client.")
return DifyPluginDaemonToolClient(
tenant_id=self.config.tenant_id,
plugin_id=plugin_id,
plugin_daemon_url=self.daemon_url,
plugin_daemon_api_key=self.daemon_api_key,
user_id=self.config.user_id,
http_client=http_client,
)
要点有三:
- 层本身不创建、不缓存、不关闭 HTTP 客户端。运行时把由 FastAPI lifespan 持有的共享
httpx.AsyncClient在每次工具调用时传入,层只是把它装配进DifyPluginDaemonToolClient; plugin_id由调用方(业务层)传入——具体是哪个插件包,属于业务调用细节,执行上下文层只负责身份与传输上下文;- 传入的共享客户端若已关闭,会抛出
RuntimeError快速失败。
5.3 测试用例验证的三条约束
unit 测试 用三个用例钉死了上述行为:
- test_execution_context_layer_creates_tool_client_from_shared_http_client:用
httpx.MockTransport构造共享客户端,验证生成的工具客户端逐字段继承tenant_id、user_id、plugin_id、守护进程 URL 与 API Key,且客户端不会被层关闭; - test_execution_context_layer_rejects_closed_shared_http_client:已关闭的共享客户端会触发
RuntimeError; - test_execution_context_layer_lifecycle_does_not_manage_http_client:进入完整 Compositor 生命周期(含
session_snapshot与suspend_layer_on_exit)后,共享客户端依然保持打开——证明该层不参与资源生命周期管理。
六、注意事项汇总
结合官方文档 Notes 小节与源码实现,使用时需注意:
- 不管理 HTTP 客户端生命周期:执行上下文层不会打开、缓存、关闭或快照 HTTP 客户端,其生命周期钩子保持继承的空实现,资源管理交给运行时;
plugin_id归属业务层:模型调用属于插件 LLM 层的plugin_id,工具调用属于每个插件工具配置的plugin_id,执行上下文层只携带共享的 Dify 调用上下文;- 约定层名为
execution_context:如果使用别的名字,必须同步把 LLM 层与工具层deps中的依赖指向改为该名字; extra="forbid"严格校验:不要在层配置里塞daemon_url等传输字段,也不要把运行模式误填进invoke_from,二者都会导致ValidationError。
至此,执行上下文层的完整链路是:客户端在 RunLayerSpec 中提交租户/用户/执行标识 → 服务端 Provider 工厂注入守护进程 URL 与 API Key → 业务层通过 create_tool_client 复用共享 HTTP 客户端访问插件守护进程。这一设计把敏感凭证与无状态身份配置彻底分离,使会话快照与客户端请求都不携带服务端密钥。
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 StartedRust0627
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