← 返回文章列表

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

1. 业务场景

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

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

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

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

2. 场景痛点

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

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

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

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

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

4. 整体架构

graph TD subgraph app["【验证应用(agent)】"] user["用户问题"] --> agent["Agent 节点(策略:limited)"] --> loop["策略循环:选工具 → 调工具 → 评估结果"] loop --> ans{"评估结果"} ans -- "有答案" --> final["最终回答"] ans -- "达 maximum_iterations" --> stop["强制结束并说明(硬约束,不再调工具)"] ans -- "信息不足" --> cont["继续(受次数上限约束)"] --> loop end subgraph struct["【插件结构】"] m1["manifest(plugins.agent_strategies)"] --> m2["provider/agent.yaml(strategies 列表)"] --> m3["strategies/limited.yaml(parameters 声明)"] --> m4["strategies/limited.py(_invoke 唯一抽象)"] end subgraph sw["【策略切换】"] dsl["DSL 三字段切换:agent_strategy_provider_name / name / label(limited ↔ function_calling)"] end

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

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. 实验文档及源码获取

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

联系我

15088711270

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

微信二维码

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