Dify Agent 插件工具层详解:如何把 Dify 插件工具以 `dify.plugin.tools` 层暴露给模型
本文围绕 Dify 的 Dify Agent 子项目中的插件工具层(plugin tool layer,类型 id 为 dify.plugin.tools)展开,讲清它的设计定位、完整配置字段、Dify API 侧的准备工作,以及运行时的参数校验、类型转换与插件 daemon 调用链路。读完本文,你可以独立为 Dify Agent 的 run 请求构建一个包含多插件工具的工具层,并理解每一条配置在 tools_layer.py 运行时中的实际作用。
一、插件工具层是什么:定位与设计边界
插件工具层负责把 Dify 插件体系中的工具(tools)暴露给 Agent 的模型。它的设计前提是:调用方(Dify API)在提交 run 请求之前,已经完成了用户工具选择的解析、插件 daemon 声明的读取、凭据的注入,以及手动/运行时输入(manual/runtime inputs)的确定。Dify Agent 本身不负责“发现”工具声明,它只是消费一份已经准备好的工具配置。
与插件 LLM 层有一个关键区别:
- 插件 LLM 层面向单一模型调用,绑定一个
plugin_id; - 插件工具层可以同时包含来自多个插件包的工具,每个
DifyPluginToolConfig各自携带自己的plugin_id。
而共享的执行上下文层(type id dify.execution_context)仍然只承载 tenant/user 级别的 daemon 上下文,不携带任何包身份(package-specific identity)。这一职责划分在 configs.py 的模块注释中有明确说明:
Tool configs also carry the API-side prepared parameter declarations and model-visible JSON schema so the agent runtime does not have to re-fetch and re-merge tool declarations at execution time.
也就是说,工具声明的“准备”工作前移到 API 侧,Agent 运行时不再回查 daemon 获取工具 schema,从而让 run 请求成为一次自包含(self-contained)的调用。
二、职责划分:API 侧准备,Agent 侧消费
Dify API 在提交 run 请求之前要做的事
- 解析用户选定的 provider 和工具名(resolve the selected provider and tool name);
- 合并声明参数与运行时参数(merge declared parameters with runtime parameters);
- 生成模型可见的 JSON schema;
- 提供隐藏的/手动的
runtime_parameters和凭据(credentials); - 选定发送给 daemon 的
credential_type。
在 Dify 主仓库中,API 侧的工具运行时解析由 ToolManager.get_agent_tool_runtime(...) 承担,其实现入口在 tool_manager.py,Agent v2 工作流节点的工具装配位于 dify_tools_builder.py。
Dify Agent 在运行时要做的事
拿到准备好的配置后,Dify Agent 在运行时完成:
- 校验必填的隐藏输入(missing hidden inputs 校验);
- 应用参数默认值(apply defaults);
- 对模型提供的调用值做类型转换(cast invocation values);
- 调用插件 daemon;
- 将工具响应流转换为模型可消费的 observation 文本。
源码中 DifyPluginToolsLayer 的模块注释(tools_layer.py#L1-L12)对此有完全一致的表述:API 侧负责解析 daemon 声明、应用运行时参数覆盖、生成干净的 LLM 可见 schema;运行时只做隐藏参数校验、调用参数装配,以及把 daemon 响应映射为 agent observation。
三、配置字段详解
3.1 层配置 DifyPluginToolsLayerConfig
插件工具层的类型 id 为 dify.plugin.tools(见 configs.py#L29-L30)。层配置本身很薄,只包含一个工具列表:
| 字段 | 类型 | 含义 |
|---|---|---|
tools |
list[DifyPluginToolConfig] |
准备暴露给模型的插件工具列表。 |
对应实现见 DifyPluginToolsLayerConfig。
3.2 工具配置 DifyPluginToolConfig
每个工具配置包含以下字段(与 configs.py#L126-L158 逐一对应):
| 字段 | 类型 | 含义 |
|---|---|---|
plugin_id |
str |
该工具所属插件包 id,例如 langgenius/wikipedia。 |
provider |
str |
插件内部的工具 provider 名。 |
tool_name |
str |
实际要调用的 daemon 工具名。 |
credential_type |
"api-key" | "oauth2" | "unauthorized" |
发送给插件 daemon 的凭据模式。 |
name |
str | None |
可选的模型可见工具名,默认回退为 tool_name。 |
description |
str | None |
可选的模型可见描述,默认回退为工具名。 |
credentials |
dict[str, str | int | float | bool | None] |
provider 专属的工具凭据。 |
runtime_parameters |
dict[str, JsonValue] |
隐藏的/手动的值:会合并进 daemon 调用,但不出现在模型可见 schema 中。 |
parameters |
list[DifyPluginToolParameter] |
API 准备好的有效参数声明,用于校验、默认值与类型转换。 |
parameters_json_schema |
dict[str, JsonValue] |
API 准备好的、展示给模型的 JSON schema。 |
两点值得特别注意(均出自 DifyPluginToolConfig 的 docstring,configs.py#L126-L143):
credential_type是调用方显式声明的传输选择,不是自动发现的属性。它必须与credentials的实际凭据模式匹配(例如"api-key"vs"oauth2");填错时配置校验可以通过,但调用会在运行时失败。parameters_json_schema的默认值是一个空对象 schema:{"type": "object", "properties": {}, "required": []},意味着不提供时模型将看不到任何参数。
3.3 参数声明 DifyPluginToolParameter
DifyPluginToolParameter(configs.py#L85-L102)的字段为:
| 字段 | 类型 | 说明 |
|---|---|---|
name |
str |
参数名。 |
type |
DifyPluginToolParameterType |
参数类型,见下表。 |
form |
DifyPluginToolParameterForm |
取值 schema / form / llm;form 与 schema 参数属于非 LLM 输入,需要由 runtime_parameters 供给。 |
required |
bool |
是否必填,默认 False。 |
default |
JsonValue |
默认值,默认 None。 |
llm_description |
str | None |
供 LLM 侧使用的参数描述。 |
input_schema |
dict[str, JsonValue] | None |
可选的嵌套输入 schema。 |
options |
list[DifyPluginToolOption] |
选项列表;DTO 会忽略 API 侧的 label、icon 等展示字段,只保留规范化后的 value。 |
type 枚举(DifyPluginToolParameterType,configs.py#L52-L67)包括:string、number、boolean、select、secret-input、file、files、app-selector、model-selector、any、dynamic-select、checkbox、system-files、array、object。其中 secret-input、select、checkbox 在 as_normal_type() 中会归一化为 string。
该 DTO 配置了 extra="ignore", from_attributes=True,因此它既能接受 API 侧 ToolParameter 属性对象,也能接受 ToolParameter.model_dump(mode="json") 序列化后的字典,并主动忽略 label、human_description 这类仅 API 内部使用的字段。这正是文档示例中 DifyPluginToolParameter.model_validate(parameter) 可以直接工作的原因。
四、完整示例:为模型准备一个 Wikipedia 工具
下面是用户手册给出的端到端示例(Dify API 侧把工具解析为 API 侧 Tool runtime,再在协议边界处适配为 Dify Agent 的 DTO):
from dify_agent.layers.execution_context import DIFY_EXECUTION_CONTEXT_LAYER_TYPE_ID, DifyExecutionContextLayerConfig
from dify_agent.layers.dify_plugin import (
DIFY_PLUGIN_TOOLS_LAYER_TYPE_ID,
DifyPluginToolConfig,
DifyPluginToolParameter,
DifyPluginToolsLayerConfig,
)
from dify_agent.protocol import RunComposition, RunLayerSpec
# Dify API side: resolve the selected tool into the API-side Tool runtime first,
# for example with ToolManager.get_agent_tool_runtime(...). Then adapt its
# effective ToolParameter objects at the protocol boundary. Dify Agent accepts
# both ToolParameter attribute objects and ToolParameter.model_dump(mode="json")
# dictionaries, ignoring API-only fields such as label and human_description.
tool_runtime = ToolManager.get_agent_tool_runtime(...)
effective_parameters = tool_runtime.get_merged_runtime_parameters()
prepared_parameters = [
DifyPluginToolParameter.model_validate(parameter)
# If the API serializes first, use:
# DifyPluginToolParameter.model_validate(parameter.model_dump(mode="json"))
for parameter in effective_parameters
]
parameters_json_schema = tool_runtime.get_llm_parameters_json_schema()
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",
invoke_from="workflow_run",
),
),
RunLayerSpec(
name="tools",
type=DIFY_PLUGIN_TOOLS_LAYER_TYPE_ID,
deps={"execution_context": "execution_context"},
config=DifyPluginToolsLayerConfig(
tools=[
DifyPluginToolConfig(
plugin_id="langgenius/wikipedia",
provider="wikipedia",
tool_name="wikipedia_search",
credential_type="unauthorized",
name="wikipedia_search",
description="Search Wikipedia for relevant pages.",
parameters=prepared_parameters,
runtime_parameters={"language": "en"},
parameters_json_schema=parameters_json_schema,
)
]
),
),
]
)
其中 deps={"execution_context": "execution_context"} 的语义是:把工具层的依赖字段 execution_context 绑定到组合中名为 execution_context 的层。工具层确实声明了这个依赖(见 DifyPluginToolsDeps:execution_context: DifyExecutionContextLayer 必填,shell: DifyShellLayer | None 可选,后者用于沙箱文件上传场景)。
一个容易忽略的前提:示例中的 DifyPluginToolsLayer 不能在客户端侧直接 from_config 构造——tools_layer.py#L167-L172 中 from_config 会抛出 TypeError,必须经由服务端 provider 工厂注入 inner_api_url / inner_api_key(以及 daemon 的 URL 与 API key,参见执行上下文层文档中的 DIFY_AGENT_PLUGIN_DAEMON_URL / DIFY_AGENT_PLUGIN_DAEMON_API_KEY 说明)。这样做的目的是把服务器凭据隔离在客户端提交的 layer config 与会话快照之外。
五、运行时机制:从配置到 daemon 调用
5.1 构建工具适配器与隐藏参数校验
DifyPluginToolsLayer.get_tools()(tools_layer.py#L188-L231)按顺序执行:
- 按
plugin_id缓存 daemon 客户端:同一插件的多个工具共享一个DifyPluginDaemonToolClient,客户端由执行上下文层的create_tool_client()创建(layer.py#L57-L72),注入 tenant_id、plugin_id、daemon URL/API key 和可选 user_id; - 对每个工具深拷贝一份
effective_parameters,随后调用_validate_required_hidden_parameters校验; - 通过
_build_pydantic_ai_tool把每个工具包装为 Pydantic AITool。
隐藏参数校验规则(_validate_required_hidden_parameters):任何 form 不是 llm、required 为真、没有 default、且名字不在 runtime_parameters 中的参数,都会触发 ValueError,错误消息形如 Tool 'xxx' requires non-LLM runtime_parameters for: a, b.。这保证了“隐藏必填输入”必须在配置阶段就补齐,而不是把责任推给模型。
工具注册时始终使用宽松模式(PLUGIN_TOOL_STRICT = False,tools_layer.py#L49-L53),以容忍插件 schema 差异和较老的 API 准备产物;模型可见 schema 则通过 prepare_tool_definition 回调强制替换为配置中的 parameters_json_schema(tools_layer.py#L283-L296)。
5.2 参数合并优先级
模型真正发起调用时,_prepare_tool_arguments(tools_layer.py#L307-L348)按如下优先级装配 daemon 调用参数,该顺序“有意对齐 Dify 旧工具运行时的契约”:
- 以配置中的
runtime_parameters(隐藏/手动输入)为基础; - 模型提供的同名字段覆盖基础值;
- 两者都没有时,回退到参数声明的
default; - 仍无值且参数为
required,抛出校验错误tool parameter xxx not found in tool config。
只有声明在 effective_parameters 中的参数会执行类型转换;合并后额外出现的键会被原样透传,以便兼容携带额外 daemon 输入的准备产物。
5.3 类型转换规则
_cast_tool_parameter_value(tools_layer.py#L431-L510)把模型输出的 JSON 值与隐藏输入统一转换为 daemon 期望的线上形态:
| 参数类型 | 转换行为 |
|---|---|
string / secret-input / select / checkbox / dynamic-select |
非字符串统一 str(),None 转为空串。 |
boolean |
字符串 true/yes/y/1 → True,false/no/n/0 → False;其余按布尔语义转换。 |
number |
含 . 的字符串解析为 float,否则解析为 int。 |
file |
单文件;若传入列表必须恰好一个元素。值可为 HTTP(S) URL、沙箱路径或 file mapping。 |
files / system-files |
列表化处理,逐项转为插件 file 参数。 |
app-selector / model-selector |
必须为 dict,否则报错。 |
any |
只接受 dict / list / str / int / float / bool。 |
array |
字符串会尝试 JSON 解析为列表,失败则包装为单元素列表。 |
object |
字符串会尝试 JSON 解析为 dict,失败则返回 {}。 |
文件类参数会经 _PluginToolFileContext 归一化为带 dify_model_identity: "__dify__file__" 标记的插件 file 参数(tools_layer.py#L562-L584):local_file / tool_file / datasource_file 类 mapping 会通过 Dify 内部 API /inner/api/download/file/request 换取带签名的下载 URL;沙箱路径则先经 dify-agent file upload 上传为 tool_file mapping 再换取 URL。
5.4 daemon 调用与响应转换
调用链路的传输细节在 tool_client.py:
- 端点:
POST /plugin/{tenant_id}/dispatch/tool/invoke,请求体包含provider、tool、credentials、credential_type、tool_parameters,嵌套在顶层data字段下;当共享执行上下文携带user_id时,会作为与data平级的顶层字段转发,供 daemon 侧审计与凭据逻辑归属到终端用户(tool_client.py#L207-L209); - 传输头:
X-Api-Key(daemon 服务端 key)、X-Plugin-ID(本工具的plugin_id)、Content-Type: application/json(tool_client.py#L254-L260); - 响应为 SSE 风格流:逐行解析
data:前缀,外层包裹code/message/data,非零code视为错误流项;blob_chunk消息会经merge_blob_chunks合并为完整 blob,默认上限 30MB 文件 / 8KB 单块(tool_client.py#L263-L268)。
daemon 的流式消息最终被 _convert_tool_response_to_text(tools_layer.py#L636-L678)折叠为模型 observation:text 直接追加;link 转为“result link: … please tell user to check it.”;image / image_link 提示用户结果已发送;json 消息在未被 suppress_output 抑制时序列化加入,并与已有文本去重;variable 消息保持内部使用不进入 observation。
错误处理则走 _tool_error_text(tools_layer.py#L619-L633):凭据类错误提示“Please check your tool provider credentials”;ToolNotFound / ProviderNotFound 提示工具不存在;HTTP 400 或参数校验类错误转为“tool parameters validation error: …”,其余归类为 tool invoke error。这些被“软化”成工具 observation 而非异常,让模型有机会自行修正参数后重试。
六、给 Dify API 调用方的注意事项
以下四条约束直接来自用户手册,并有源码对应依据:
- 不要让 Dify Agent 去发现工具声明。所有解析与准备都在创建 run 之前由 API 完成。
parameters必须包含全部有效参数,包括隐藏/手动参数——它们参与必填校验、默认值应用与类型转换(见 5.1、5.3)。parameters_json_schema只包含模型可见参数。隐藏/手动参数以及文件/系统文件参数,除非确实希望模型来填,否则应从 schema 中省略(schema 的默认空对象形态即印证了“少即是多”的设计)。runtime_parameters承载用户选定或工作流变量派生出来的隐藏/手动值,并且每个工具自己的plugin_id必须写在工具配置上——共享执行上下文层不携带包身份。
七、测试佐证与延伸阅读
层行为的契约在 test_layers.py 中有系统覆盖,包括:
- 隐藏必填参数缺失时的校验失败路径(
_missing_hidden_parameter_tools_config); form="form"的隐藏参数(如api_version、auth_scope)与 LLM 参数(如query、region)混合时的合并与转换;- file / system-files 参数从 URL、沙箱路径到插件 file 参数的归一化。
DTO 与协议 schema 的独立单测见 test_configs.py 与 test_protocol_schemas.py。
延伸阅读(相对仓库根目录):
- 层配置 DTO 与类型 id:dify-agent/src/dify_agent/layers/dify_plugin/configs.py
- 层运行时实现:dify-agent/src/dify_agent/layers/dify_plugin/tools_layer.py
- daemon 传输客户端:dify-agent/src/dify_agent/layers/dify_plugin/tool_client.py
- 执行上下文层文档:dify-agent/docs/dify-agent/user-manual/execution-context-layer/index.md
- 同系列文档中的插件 LLM 层,可与本文对照理解“单插件身份”与“多插件工具列表”的差异。
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 StartedRust0625
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