← 返回文章列表

Dify 中级实验(18):插件开发入门——如何把工作流变成 Agent 可调用的工具?

1. 业务场景

先讲一个我们实际遇到的场景。

一家公司的内部 CRM 系统没有现成的插件,销售团队的 AI 助手想查客户信息却查不了——模型只能闲聊,一问「客户 1001 的订单情况」,它就答「我无法访问你们的系统」。公司的 Agent 应用明明能力很强,却因为接不上内部系统,变成了一个高级聊天机器人。他们想过让开发团队写接口,但排期要三周;想过等官方插件,但内部系统大概率永远不会有官方插件。

我们第一次接这类需求时,第一反应也是「等官方插件,或者让开发排期写接口」。后来才想明白——内部系统大概率永远不会有官方插件,等插件等于放弃;好在 Dify 给了第三条路:任何工作流,发布即工具。

这不是个例。任何「Agent 要对接公司内部系统」的场景都是这个模式:查 CRM 客户、查 ERP 库存、查工单系统、查内部 Wiki——官方插件市场覆盖的是通用服务,公司内部系统只能自己造工具。好在 Dify 给了答案:任何工作流发布后都可以变成「自定义工具」,被 Agent 自动发现和调用。

2. 场景痛点

这个场景的痛点,在这家公司的销售 AI 助手项目上体现得最直接:

本质上,工具不在于复杂,而在于输入输出契约清晰——把查询语义做真、把参数枚举收窄,Agent 才能可靠地自主调用

3. 方案:为什么是「工作流发布为自定义工具」

Dify 的扩展机制很直接:任何 Workflow 保存并发布后,都可以变成自定义工具,被 Agent/其他工作流调用。本实验就走通「构建 CRM 查询工作流 → 发布为工具 → 供 Agent 调用」的完整链路。

选它的理由:

这篇文章我们就用它搭一个「CRM 查询工具」工作流:输入 customer_idquery_type,查询客户基本信息/订单/工单,发布为工具后挂载给 Agent 自主调用。

4. 整体架构

graph TD start["开始:customer_id + query_type"] --> crm["模拟CRM API(Code:内置客户表,按 query_type 分发,found/data_json/summary)"] crm --> fmt["格式化回复(LLM:把结构化结果转成自然语言)"] fmt --> end1["结束"]

链路很清晰:入口收两个参数 → 代码节点按 query_type 分发查询 → LLM 把结构化结果格式化成自然语言。4 个节点、3 条边——工具不在于复杂,而在于输入输出契约清晰;发布后 Agent 只要知道「传 customer_id 和 query_type,拿回文本」,就能自主调用。

5. 模块设计

5.1 开始节点

query_type 用下拉枚举,把 Agent 的「自由发挥」收窄成三个合法值——枚举约束是工具参数设计的第一原则:

- label: 客户ID

  required: true

  type: text-input

  variable: customer_id

- label: 查询类型

  options: [basic, orders, support]

  required: true

  type: select

  variable: query_type

5.2 模拟 CRM API(Code,核心)

真实 CRM 接口(GET /customers/{id})在实验里用代码节点模拟,重点是查询语义要真实——查不到就返回「未找到」,绝不塞默认客户数据:

def main(customer_id: str, query_type: str) -> dict:

    import json, time

    time.sleep(0.3)  # 模拟真实 CRM API 延迟

    cid = str(customer_id or "").strip()

    qt = query_type or "basic"

    customers = {

        "1001": {"name": "张三", "level": "vip", "total_orders": 12},

        "1002": {"name": "李四", "level": "normal", "total_orders": 3},

        "1003": {"name": "王五", "level": "new", "total_orders": 0},

    }

    customer = customers.get(cid)

    if not customer:

        return {"found": "false", "data_type": qt,

                "data_json": "{}",

                "summary": "未找到客户 {}".format(cid)}

    if qt == "orders":

        data = [{"order_id": "ORD-1001-1", "product": "产品A", "amount": 299.0, "status": "已完成"}]

        summary = "客户 {} 的订单列表({} 笔)".format(customer["name"], len(data))

    elif qt == "support":

        data = [{"ticket_id": "TK-9001", "title": "产品A 无法登录", "status": "处理中"}]

        summary = "客户 {} 的支持工单({} 张)".format(customer["name"], len(data))

    else:

        data = customer

        summary = "客户 {},等级 {},累计 {} 笔订单".format(

            customer["name"], customer["level"], customer["total_orders"])

    return {"found": "true", "data_type": qt,

            "data_json": json.dumps(data, ensure_ascii=False), "summary": summary}

要点:

5.3 发布为工具

  1. 保存并发布这个 Workflow 应用
  2. 进入「工具 → 自定义工具 → 通过工作流创建工具」,选择刚发布的应用
  3. 工具自动生成参数面板(对应 start 变量),配置后即可在 Agent 的「工具」列表里挂载

⚠️ 发布为工具后,工具内部注册 ID(provider_id)是发布时生成的 UUID,不是应用 ID;子工作流重新导入并重新发布后,注册 ID 会变化,调用方必须同步更新。

6. 运行验证

场景 输入 预期 实测结果
查询基本信息 customer_id=1001, query_type=basic found=true,返回张三的等级与订单数 模拟 API 命中,LLM 正常格式化 ✓
查询订单 customer_id=1001, query_type=orders 返回订单列表 JSON + 摘要 订单分支触发 ✓
客户不存在 customer_id=9999, query_type=basic found=false,提示「未找到客户 9999」 未找到分支正确返回,不编造数据 ✓
Agent 调用 在 Agent 应用挂载工具后说「查一下客户 1001」 Agent 自动调用工具并组织回答 Agent 识别意图并触发工具调用 ✓

7. 实战坑

现象 修复
模拟 API 查不到也返回默认客户 问不存在的客户 ID,得到一份「默认客户」的假信息 兜底分支返回「未找到」语义(found=false + 空信息),让下游 LLM 引导用户核对
found 用 boolean 输出 下游 IF-ELSE 判断不到 展平为 string "true"/"false"
customer_id 当参数名但代码签名写别的 运行报 main() got an unexpected keyword argument code 节点 variables 的 variable 名必须等于 def main(...) 签名参数名(Dify 按名传参)
工作流发布为工具后 provider_id 写死旧值 重新导入发布后调用方报「workflow provider not found」 每次重新发布后从工具列表动态查询最新 provider_id 并同步
真实 API 场景直接写 http-request 没接降级 外网 CRM 超时整条链路失败 生产接入时用 http-request + 超时重试 + 失败分支(见实验 17 容错架构)

采坑点来自本实验 DSL 生成与运行验证的真实记录(代码模拟 API 约定、found 语义、按名传参、provider_id 变化)。

8. 实验文档及源码获取

文章聚焦核心配置与采坑点;实验文档还包含正式插件开发(plugin.json/main.py/providers/tools 结构、凭据 secret-input、OpenAPI/Swagger 导入建工具、多工具组合插件包)的完整示例。

本系列 · Dify 实验 · 中级
  1. Dify 中级实验(01):参数提取器实战——如何从自然语言中提取结构化数据?
  2. Dify 中级实验(02):问题分类器——智能路由引擎如何四路分发?
  3. Dify 中级实验(03):模板转换实战——如何用零 Token 完成文本加工?
  4. Dify 中级实验(04):迭代进阶——如何批量处理数据并守住性能边界?
  5. Dify 中级实验(05):并行执行——如何让多路任务同时跑?
  6. Dify 中级实验(06):变量聚合——如何确定性合并多路分支结果?
  7. Dify 中级实验(07):子工作流——如何把公共逻辑做成可复用积木?
  8. Dify 中级实验(08):代码节点进阶——如何用标准库处理文件与数据?
  9. Dify 中级实验(09):HTTP 节点进阶——如何搞定认证、分页与错误重试?
  10. Dify 中级实验(10):知识库深度调优——如何科学评估检索质量?
  11. Dify 中级实验(11):高级 RAG 流水线——如何搭建多路检索与精排?
  12. Dify 中级实验(12):Agent 深度配置——如何让智能体自主调用工具?
  13. Dify 中级实验(13):多 Agent 协作——如何编排多个智能体分工干活?
  14. Dify 中级实验(14):对话变量与状态管理——如何让工作流记住多轮对话的状态?
  15. Dify 中级实验(15):条件分支高阶策略——多条件路由如何避免分支爆炸?
  16. Dify 中级实验(16):错误处理与降级——工作流如何有尊严地失败?
  17. Dify 中级实验(17):调试监控与性能优化——响应慢和 Token 超支如何定位?
  18. Dify 中级实验(18):插件开发入门——如何把工作流变成 Agent 可调用的工具?
  19. Dify 中级实验(19):综合实战——如何把 19 个实验串成一条生产级流水线?
  20. Dify 中级实验(20):综合实战——自动化报告生成流水线如何从数据到周报一步到位?

联系我

15088711270

手机端点击号码可直接拨打 · 桌面端可复制

微信二维码

扫码加微信 · 备注「门户」更快通过