Dify 插件开发实验(09):Agent策略插件——如何控制 Agent 的工具使用策略?

Dify 实验系列 · 插件开发 09/12 | 实验编号:DIFY-106-09
基于 Dify 1.16.1 实测(2026-08)

1. 业务场景

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

客服工单 SaaS 的门户对话 Agent 上线前,业务方提了两条硬要求:第一,限制工具调用次数——单轮最多 3 次工具调用,防止模型反复试错把 token 烧光;第二,强制先检索后回答——涉及知识的问题必须先查知识库再回答,不许凭空编。但默认的 Agent 策略把「要不要调工具、调几次」完全交给模型自主决定——模型试错多少次、先回答还是先检索,业务方一概管不了。

我们第一次接这类需求时,第一反应也是「把约束写进系统提示词不就行了」。真正动手才发现——提示词只是「建议」,不是「约束」:模型在错误路径上反复试错,一次对话烧掉几十次工具调用;知识类问题不先检索就张口就来——业务方要的是「自主但有边界」,这需要一个能落地的策略层,而不是一段话术。

这不是个例。任何要对外提供 Agent 服务的场景都是这个模式:银行客服要限定查询次数防滥用、医疗问答要求必须先检索证据、电商助手要控制工具调用成本——「模型自主」和「业务可控」之间的平衡,是企业定制 Agent 的常见诉求。

2. 场景痛点

这个流程的痛点,在业务方身上体现得最直接:

  • 工具调用次数不可控:模型在错误路径上反复试错,一次对话烧掉几十次工具调用,成本失控。
  • 幻觉直接回答:不先检索就回答,知识类问题张口就来,错误信息发给客户,信任直接崩塌。
  • 业务约束无法落地:业务方说「最多查 3 次」「先查再答」,默认策略根本不认这些规则——约束写在需求文档里,落不到系统里。
  • 行为不可观测:模型为什么调这个工具、为什么不调,过程不可见,出问题无从排查。

本质上,默认策略给的是「模型自主」,企业要的是「自主但有边界」——工具调用次数、检索顺序这类业务约束,需要一个能落地的策略层。

3. 方案:为什么是 Agent 策略插件

选 Agent 策略插件,我们实际对比过:

  • 官方扩展点:agent-strategy 策略插件是 Dify 官方支持的扩展点,自定义 Agent 推理循环(选工具 → 调工具 → 评估结果 → 继续/结束),不改平台代码;
  • 硬约束软约束分层maximum_iterations硬约束(达上限强制结束,不依赖模型自觉),force_retrieve软约束(策略引导先检索,最终由模型执行)——约束可强可弱,按业务需要配置;
  • 可对照验证:同一组问题双策略各跑一遍,行为差异可量化,业务方看得见约束是否生效。

这篇文章我们就用它写一个 limited 策略插件:maximum_iterations(硬约束上限)+ force_retrieve(强制先检索),复用 106-01 的 time_tool 与 106-08 的 retrieve_tool,双策略对照验证约束是否真正落地。

4. 整体架构

【策略切换】

DSL 三字段切换:agent_strategy_provider_name / name / label(limited ↔ function_calling)

【插件结构】

manifest(plugins.agent_strategies)

provider/agent.yaml(strategies 列表)

strategies/limited.yaml(parameters 声明)

strategies/limited.py(_invoke 唯一抽象)

【验证应用(agent)】

有答案

达 maximum_iterations

信息不足

用户问题

Agent 节点(策略:limited)

策略循环:选工具 → 调工具 → 评估结果

评估结果

最终回答

强制结束并说明(硬约束,不再调工具)

继续(受次数上限约束)

链路很清晰:用户问题 → 策略循环(选工具/调工具/评估)→ 有答案或达上限结束。关键设计是硬约束在策略层拦截——达上限时不再把工具调用发给模型,直接强制结束并说明,而不是「建议」模型停止。

5. 模块设计

5.1 策略参数声明(strategies/limited.yaml)

自定义参数写在 strategy yaml 的 parameters 里,安装后即出现在 Agent 节点配置面板:

parameters:
  - name: maximum_iterations
    type: number
    required: true
    label:
      zh_Hans: 最大迭代次数
    default: 3
    min: 1
    max: 10
  - name: force_retrieve
    type: boolean
    required: false
    label:
      zh_Hans: 强制先检索后回答
    default: false

5.2 硬约束实现(strategies/limited.py)

达上限时不再把工具调用发给模型,直接拦截并说明:

for step in range(1, max_iter + 1):
    result = self.session.model.llm.invoke(
        model_config=model_config, prompt_messages=messages,
        stream=False, tools=prompt_tools)
    msg = result.message
    tool_calls = msg.tool_calls or []
    if not tool_calls:
        yield self.create_text_message(msg.content or "")
        return
    # 达上限:不再调用工具,强制结束并说明(硬约束)
    if step >= max_iter:
        names = ", ".join(tc.function.name for tc in tool_calls)
        yield self.create_text_message(
            f"已达到最大工具调用次数上限({max_iter} 次),已停止调用工具"
            f"(本次意图调用:{names})。请基于已有信息回答。")
        return

5.3 参数类型转换

策略接口收到的 parameters["tools"]/parameters["model"] 是 dict,需要 ToolEntity.model_validate / AgentModelConfig.model_validate 转成实体(官方 FunctionCallingParams(**parameters) 也是靠 pydantic 自动转)。

6. 运行验证

验证项 输入/场景 预期 结果
注册 安装策略插件 工作流 Agent 节点可选 limited 策略 ✅ DSL 三字段可切换
基线 6 题(知识×3/查询×2/闲聊×1)默认策略 全 succeeded ✅ 1.3-4.6s
自定义策略 同 6 题 limited 策略 全 succeeded ✅ 1.6-4.5s
上限硬约束 maximum_iterations=1,构造连续调工具问题 策略直接拦截工具调用并说明 ✅ 明确拦截(非建议模型停止)
强制检索 知识类问题(工单/退款/钉钉) 先调 external_retrieve 再回答 ✅ 回答引用检索内容
双策略对照 6 题 × 双策略 行为差异表 ✅ 延迟接近;知识题 limited 稳定先检索

对照结论:limited 的强制检索指令使知识题稳定先检索(业务约束生效);上限硬约束在构造场景下明确拦截——默认策略无此能力(模型自主决定,不受硬限制)。

7. 实战坑

现象 修复
sdk 版本锁(重要) daemon uv sync 默认装 dify_plugin 0.10.0,AgentStrategy 接口不兼容,报 No module named ‘dify_plugin.invocations.storage’ pyproject 锁 dify_plugin>=0.7.4,<0.8,daemon 装 0.7.4 与本地一致;工具插件不触发 storage,策略插件必须锁
卸载字段 plugin uninstall 传 plugin_installation_id 报 400 用 installation_id 字段卸载
重装实例残留 卸载失败残留旧实例,应用调旧代码(错误特征不变) 正确字段卸载 + 清理 daemon cwd 目录 + 重装
策略参数类型 parameters[“tools”]/[“model”] 是 dict,直接透传报类型错误 ToolEntity.model_validate / AgentModelConfig.model_validate 转换
上限是建议不是约束 默认策略下模型自主决定,工具调用次数不可控 硬约束写在策略层:达 maximum_iterations 直接拦截工具调用并说明(max=1 实测拦截成功)

排障提示:策略执行错误出现在 run error 的 PluginInvokeError 里(无 traceback)——用「故意 raise 带类型信息」的探针 + daemon cwd 代码目录核对运行版本,是本实验实测有效的排障法。

8. 实验文档及源码获取

文章聚焦核心配置与采坑点,完整分步操作与双策略对照实验记录见实验文档原文。

下一篇:Dify 插件开发实验(10):自定义节点扩展——不改平台代码,插件如何补节点能力?

💬 你在这个实验的场景里踩过什么坑?欢迎评论区分享你的实战经验。

Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐