首页
/ Dify Agent 插件工具层详解:如何把 Dify 插件工具以 `dify.plugin.tools` 层暴露给模型

Dify Agent 插件工具层详解:如何把 Dify 插件工具以 `dify.plugin.tools` 层暴露给模型

2026-09-06 14:39:44作者:冯梦姬Eddie

本文围绕 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 请求之前要做的事

  1. 解析用户选定的 provider 和工具名(resolve the selected provider and tool name);
  2. 合并声明参数与运行时参数(merge declared parameters with runtime parameters);
  3. 生成模型可见的 JSON schema;
  4. 提供隐藏的/手动的 runtime_parameters 和凭据(credentials);
  5. 选定发送给 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

DifyPluginToolParameterconfigs.py#L85-L102)的字段为:

字段 类型 说明
name str 参数名。
type DifyPluginToolParameterType 参数类型,见下表。
form DifyPluginToolParameterForm 取值 schema / form / llmformschema 参数属于非 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 侧的 labelicon 等展示字段,只保留规范化后的 value

type 枚举(DifyPluginToolParameterTypeconfigs.py#L52-L67)包括:stringnumberbooleanselectsecret-inputfilefilesapp-selectormodel-selectoranydynamic-selectcheckboxsystem-filesarrayobject。其中 secret-inputselectcheckboxas_normal_type() 中会归一化为 string

该 DTO 配置了 extra="ignore", from_attributes=True,因此它既能接受 API 侧 ToolParameter 属性对象,也能接受 ToolParameter.model_dump(mode="json") 序列化后的字典,并主动忽略 labelhuman_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 的层。工具层确实声明了这个依赖(见 DifyPluginToolsDepsexecution_context: DifyExecutionContextLayer 必填,shell: DifyShellLayer | None 可选,后者用于沙箱文件上传场景)。

一个容易忽略的前提:示例中的 DifyPluginToolsLayer 不能在客户端侧直接 from_config 构造——tools_layer.py#L167-L172from_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)按顺序执行:

  1. plugin_id 缓存 daemon 客户端:同一插件的多个工具共享一个 DifyPluginDaemonToolClient,客户端由执行上下文层的 create_tool_client() 创建(layer.py#L57-L72),注入 tenant_id、plugin_id、daemon URL/API key 和可选 user_id;
  2. 对每个工具深拷贝一份 effective_parameters,随后调用 _validate_required_hidden_parameters 校验;
  3. 通过 _build_pydantic_ai_tool 把每个工具包装为 Pydantic AI Tool

隐藏参数校验规则(_validate_required_hidden_parameters):任何 form 不是 llmrequired 为真、没有 default、且名字不在 runtime_parameters 中的参数,都会触发 ValueError,错误消息形如 Tool 'xxx' requires non-LLM runtime_parameters for: a, b.。这保证了“隐藏必填输入”必须在配置阶段就补齐,而不是把责任推给模型。

工具注册时始终使用宽松模式(PLUGIN_TOOL_STRICT = Falsetools_layer.py#L49-L53),以容忍插件 schema 差异和较老的 API 准备产物;模型可见 schema 则通过 prepare_tool_definition 回调强制替换为配置中的 parameters_json_schematools_layer.py#L283-L296)。

5.2 参数合并优先级

模型真正发起调用时,_prepare_tool_argumentstools_layer.py#L307-L348)按如下优先级装配 daemon 调用参数,该顺序“有意对齐 Dify 旧工具运行时的契约”:

  1. 以配置中的 runtime_parameters(隐藏/手动输入)为基础;
  2. 模型提供的同名字段覆盖基础值;
  3. 两者都没有时,回退到参数声明的 default
  4. 仍无值且参数为 required,抛出校验错误 tool parameter xxx not found in tool config

只有声明在 effective_parameters 中的参数会执行类型转换;合并后额外出现的键会被原样透传,以便兼容携带额外 daemon 输入的准备产物。

5.3 类型转换规则

_cast_tool_parameter_valuetools_layer.py#L431-L510)把模型输出的 JSON 值与隐藏输入统一转换为 daemon 期望的线上形态:

参数类型 转换行为
string / secret-input / select / checkbox / dynamic-select 非字符串统一 str()None 转为空串。
boolean 字符串 true/yes/y/1Truefalse/no/n/0False;其余按布尔语义转换。
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,请求体包含 providertoolcredentialscredential_typetool_parameters,嵌套在顶层 data 字段下;当共享执行上下文携带 user_id 时,会作为与 data 平级的顶层字段转发,供 daemon 侧审计与凭据逻辑归属到终端用户(tool_client.py#L207-L209);
  • 传输头:X-Api-Key(daemon 服务端 key)、X-Plugin-ID(本工具的 plugin_id)、Content-Type: application/jsontool_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_texttools_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_texttools_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_versionauth_scope)与 LLM 参数(如 queryregion)混合时的合并与转换;
  • file / system-files 参数从 URL、沙箱路径到插件 file 参数的归一化。

DTO 与协议 schema 的独立单测见 test_configs.pytest_protocol_schemas.py

延伸阅读(相对仓库根目录):

登录后查看全文
热门项目推荐
相关项目推荐