首页
/ Dify Agent Ask Human Layer:让模型以结构化方式向人类发起输入请求的完整机制

Dify Agent Ask Human Layer:让模型以结构化方式向人类发起输入请求的完整机制

2026-09-04 21:20:47作者:庞眉杨Will

本篇围绕 Dify Agent 的 ask-human layer(dify.ask_human)展开:它向模型暴露一个 external deferred tool,使 Agent 能在当前 run 无法继续时以结构化请求形式向人类要信息,并将该请求作为 deferred_tool_callrun_succeeded 事件返回给客户端。读完本文,你将掌握如何组装包含该 layer 的 CreateRunRequest、理解每个配置字段的护栏语义、处理延迟工具调用(deferred call)并携人类结果恢复 run 的完整闭环,以及各故障症状的排查方法。

层契约:它做什么、不做什么

ask-human layer 的定位是“模型可见的工具层”,而不是消息投递系统。层契约要点如下:

属性
Type id dify.ask_human
常用 layer 名称 ask_human
Config DTO DifyAskHumanLayerConfig
模型可见工具 默认 ask_human,可用 tool_name 配置
工具类型 pydantic-ai external deferred tool
终止事件 run_succeeded
终止载荷分支 run_succeeded.data.deferred_tool_call

关键在于“不暂停”的语义:Agent run 不会进入 paused 状态。当模型调用 ask-human 工具时,当前 run 以 deferred_tool_call 代替普通 output 成功结束;把延迟调用转成面向人类的流程、收集结果、再发起带 deferred_tool_results 的新 run,全部是客户端的职责。从源码 layer.py 的模块注释也能印证这一设计:layer 只贡献“一个可选 external 工具 + 一段 prompt 提示”,工具在首次 run 中绝不执行 Python,下游系统自行决定投递、接收人、超时与授权。

基础用法:把 layer 加入 run composition

将 ask-human layer 与 prompt、history、LLM 以及可选的结构化输出 layer 放在同一个 composition 中:

from agenton_collections.layers.plain import PromptLayerConfig
from agenton_collections.layers.pydantic_ai import PYDANTIC_AI_HISTORY_LAYER_TYPE_ID
from dify_agent.layers.ask_human import DIFY_ASK_HUMAN_LAYER_TYPE_ID, DifyAskHumanLayerConfig
from dify_agent.layers.dify_plugin import DifyPluginLLMLayerConfig
from dify_agent.layers.execution_context import (
    DIFY_EXECUTION_CONTEXT_LAYER_TYPE_ID,
    DifyExecutionContextLayerConfig,
)
from dify_agent.protocol import DIFY_AGENT_HISTORY_LAYER_ID, DIFY_AGENT_MODEL_LAYER_ID
from dify_agent.protocol.schemas import CreateRunRequest, RunComposition, RunLayerSpec


request = CreateRunRequest(
    composition=RunComposition(
        layers=[
            RunLayerSpec(
                name="prompt",
                type="plain.prompt",
                config=PromptLayerConfig(
                    prefix="You can ask a human only when the missing decision is required to continue.",
                    user="Review the deployment plan and proceed only after getting the required approval.",
                ),
            ),
            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",
                    user_from="account",
                    app_id="replace-with-app-id",
                    agent_mode="single_step",
                    invoke_from="debugger",
                ),
            ),
            RunLayerSpec(
                name=DIFY_AGENT_HISTORY_LAYER_ID,
                type=PYDANTIC_AI_HISTORY_LAYER_TYPE_ID,
            ),
            RunLayerSpec(
                name="ask_human",
                type=DIFY_ASK_HUMAN_LAYER_TYPE_ID,
                config=DifyAskHumanLayerConfig(
                    max_fields=4,
                    max_actions=2,
                    allowed_field_types=["paragraph", "select"],
                    allow_file_fields=False,
                ),
            ),
            RunLayerSpec(
                name=DIFY_AGENT_MODEL_LAYER_ID,
                type="dify.plugin.llm",
                deps={"execution_context": "execution_context"},
                config=DifyPluginLLMLayerConfig(
                    plugin_id="langgenius/openai",
                    model_provider="openai",
                    model="gpt-5.2",
                ),
            ),
        ]
    )
)

组合中各 layer 的角色:plain.prompt 负责业务指令;execution_context 提供租户/用户/应用上下文;PYDANTIC_AI_HISTORY_LAYER_TYPE_ID 提供消息历史;dify.plugin.llm 是模型 layer,通过 deps 消费 execution_context layer。

history layer 是恢复语义的硬前提。 只要预期在人类回答后继续对话,就必须包含 history layer。悬而未决的工具调用被存放在 pydantic-ai 消息历史里,因此恢复 run 需要两样东西:上一次返回的 session_snapshot,以及保持 history layer 仍存在的同一逻辑 composition。runner 侧对此有明确校验——见 runner.py

if deferred_tool_results is not None and history_layer is None:
    raise AgentRunValidationError(
        "Deferred tool results require a 'history' layer with prior message history."
    )

此外 composition 层面只允许存在一个 ask-human layer,validate_ask_human_layer_compositionlayer.py)会拒绝同名 type 的多个 layer。

配置字段与双层护栏

DifyAskHumanLayerConfig 只控制“面向模型的工具身份与护栏”,刻意不包含任何投递配置。字段定义见 configs.py

字段 类型 默认值 含义
enabled bool True 为 false 时,该 layer 既不暴露工具也不注入 prompt 引导
tool_name str "ask_human" 模型可见的工具名,必须是合法标识符
tool_description str | None 默认描述文本 可选的模型可见工具描述
max_fields int 8 模型可请求的字段数上限,0 表示仅允许动作型请求
max_actions int 4 模型可请求的人类动作数上限
allowed_field_types list["paragraph" | "select" | "file" | "file-list"] ["paragraph", "select"] 运行时校验接受的字段类型
allow_file_fields bool False 未开启时文件字段类型直接拒绝;开启后还须列入 allowed_field_types
max_markdown_chars int 8000 可选 markdown 正文的最大长度
max_question_chars int 1000 必填 question 的最大长度
max_field_label_chars int 120 每个字段 label 的最大长度
max_action_label_chars int 80 每个动作 label 的最大长度

配置上限之上还有服务端硬上限,两者取较小值生效;若配置超过硬上限,请求校验阶段即失败、run 无法执行。硬上限常量定义在 configs.py

字段 硬上限
max_fields 16
max_actions 8
max_markdown_chars 20000
max_question_chars 4000
max_field_label_chars 200
max_action_label_chars 120

实现上有两处值得注意:

  1. tool_name 必须是合法标识符。字段校验器用正则 ^[A-Za-z_][A-Za-z0-9_]*$ 全匹配校验(configs.py),非法命名会在配置校验时直接报错。
  2. 文件字段的“双开关”策略_validate_file_field_policy 模型校验器要求:只有当 allow_file_fields=Trueallowed_field_types 中包含 file/file-list 时,文件字段才合法;allow_file_fields=False 却在允许列表里写了文件类型,配置本身就会被拒绝(configs.py)。文件字段变体目前属于“为前向兼容预留的词汇表”,默认不开放。

layer 会把这些上限自动转写成 prompt 提示。build_prompt_hintlayer.py)生成的文本包含允许的字段类型、文件上传是否启用、字段/动作数上限、question/markdown 与 label 长度限制,并告知模型“若省略 actions,系统会自动补一个 primary 样式的 Submit 动作”。客户端因此无需在系统提示里重复罗列限制,但可追加业务侧指导(例如何时才适合问人)。

模型能请求什么:AskHumanToolArgs 契约

启用后,layer 暴露一个 external deferred tool,其参数形状为 AskHumanToolArgsschema.py):

字段 类型 含义
title str | None 面向人类请求的可选短标题
question str 必填的问题/指令,不能为空白
markdown str | None 可选的较长 Markdown 正文,应按不可信的用户可见内容对待
fields list[AskHumanField] 供人类填写的可选结构化字段,name 全局唯一
actions list[AskHumanAction] 可选的动作按钮;若省略,Dify Agent 归一化为单个 primary Submit 动作
urgency "normal" | "high" 给下游系统的提示,不是投递策略

支持的字段变体(判别字段为 type):

  • paragraph:自由文本输入,支持 placeholderdefault
  • select:单选输入,选项 value 必须非空且唯一,default 必须命中某个选项值(schema.py);
  • file:单文件输入,仅在允许文件字段时可用;
  • file-list:多文件输入,仅在允许文件字段时可用,可配 max_filesschema.py)。

所有字段和动作都执行 extra="forbid",即不允许携带未知属性;字段 name 与动作 id 必须满足标识符规则。

工具参数在模型调用后还会被再校验一次。 校验入口 _validate_tool_argslayer.py)捕获 ValidationError/ValueError 并转成 ModelRetry——也就是说,模型一次越界的调用不会直接终结 run,而是触发模型带着错误信息重试,直到产出合法请求后才可能发出终止成功事件。归一化函数 _validate_and_normalize_tool_args 具体执行的护栏包括:字段数不超过 max_fields;动作为空时补默认 Submit;动作数不超过 max_actionsquestion/markdown/各 label 的长度上限;字段类型必须在 allowed_field_types 内;未开 allow_file_fields 时拒绝文件字段(layer.py)。

另一个值得理解的细节:工具函数本体 _never_executed_tool 只会抛出 RuntimeErrorlayer.py),因为该工具通过 _prepare_tool_definition 被改写成 kind="external" 的 deferred 工具(layer.py),参数 schema 直接取自 AskHumanToolArgs.model_json_schema()。它的设计意图就是“永远不该在首次 run 中执行”,真正的返回值来自人类,经后续 run 注入。

处理延迟的人类请求

像处理普通事件一样流式或轮询 run 事件。成功的最终回答带 event.data.output;成功的人类请求带 event.data.deferred_tool_call;两者恰好只有一个分支被置值(runner 中以 result_kind 区分,见 runner.py):

deferred_call = None
snapshot = None

async for event in client.stream_events(run_id):
    if event.type != "run_succeeded":
        continue
    snapshot = event.data.session_snapshot
    if event.data.deferred_tool_call is not None:
        deferred_call = event.data.deferred_tool_call
    else:
        final_output = event.data.output
    break

if deferred_call is not None:
    # 渲染你自己的面向人类的表单、入队通知、暂停外层工作流,
    # 或把请求存起来稍后处理。Dify Agent 不负责这部分。
    print(deferred_call.tool_call_id, deferred_call.args)

典型的延迟载荷长这样:

{
  "tool_call_id": "call_01H...",
  "tool_name": "ask_human",
  "args": {
    "title": "Deployment approval",
    "question": "Can we deploy version 2026.06.10 to production now?",
    "fields": [
      {
        "type": "paragraph",
        "name": "comment",
        "label": "Approval comment",
        "required": false
      }
    ],
    "actions": [
      {"id": "approve", "label": "Approve", "style": "primary"},
      {"id": "reject", "label": "Reject", "style": "destructive"}
    ],
    "urgency": "normal"
  },
  "metadata": {
    "layer_type": "dify.ask_human",
    "tool_name": "ask_human",
    "schema_version": 1
  }
}

metadata 的构造对应 build_deferred_tool_call_payloadlayer.py),其中 schema_version 当前为 1;该方法还强制三条约束:不支持 approval 请求、每次 run 恰好一个 deferred call(当前版本为 MVP 限制)、工具名必须与配置的 tool_name 一致——这三条正是后续排障表中故障症状的直接来源。

安全提醒:args 是模型生成的内容。 渲染给最终用户之前必须做校验与净化(尤其 markdown 字段,按不可信的用户可见内容对待)。

用人类结果恢复 run

客户端收集到人类答案后,用三要素创建新 run:

  • 上一次的 session_snapshot
  • 仍然包含 history layer 与 ask-human layer 的匹配 composition(同名同序);
  • deferred_tool_results.calls[tool_call_id] 中放入人类结果:
from dify_agent.layers.ask_human import AskHumanToolResult
from dify_agent.protocol import DeferredToolResultsPayload


human_result = AskHumanToolResult(
    status="submitted",
    action={"id": "approve", "label": "Approve"},
    values={"comment": "Approved for the planned window."},
    message="The human approved the deployment.",
)

resume_request = CreateRunRequest(
    composition=composition_with_same_layer_names_and_order,
    session_snapshot=snapshot,
    deferred_tool_results=DeferredToolResultsPayload(
        calls={deferred_call.tool_call_id: human_result.model_dump(mode="json")},
    ),
)

恢复结果的形状由 AskHumanToolResult 定义(schema.py),status 取值为 submitted / timeout / cancelled / unavailable 之一,除 status 外还有可选的 action(含合法标识符 id 与非空 label)、values(字段名到值)、messagerendered_content

Dify Agent 会把提供的结果作为原 external 工具调用的返回值交回 pydantic-ai,模型随后继续运行。恢复的 run 可能产出最终 output,也可能再次产出 deferred_tool_call——即 Agent 还需要又一轮人类交互。runner 在恢复 run 中会跳过新的 user 输入注入,直接以 deferred_tool_results 驱动模型(runner.py)。

超时与“人类不可用”也应作为工具结果回传,而不是当作 Agent run 失败处理:

{
  "status": "timeout",
  "action": {"id": "__timeout", "label": "Timeout"},
  "values": {},
  "message": "The human did not respond before the workflow timeout."
}

客户端职责边界

ask-human layer 刻意把产品决策留给调用方。客户端必须自行决定:

  • 如何持久化延迟调用并与面向人类的任务做关联;
  • 如何渲染并净化请求中的字段/动作;
  • 如何选择接收人、渠道与超时策略;
  • 授权谁可以作答;
  • 如何把人类提交转换为 AskHumanToolResult
  • 如何携返回的 session_snapshot 与匹配 composition 恢复 run。

反过来有一条安全红线:不要把收件人邮箱、workspace 成员 id、公开 URL、鉴权 token、超时策略放进工具参数。面向模型的请求是不可信内容,不应让它反过来控制投递或授权——这正是 DifyAskHumanLayerConfig 文档字符串强调“Delivery, recipient selection, timeout policy, and other operational behavior are intentionally out of scope”的动机(configs.py)。

排障速查表

症状 检查项
run 报 Deferred tool results require a 'history' layer 补上 history layer,并携带上一次 snapshot 恢复(对应 runner.py 的校验)
run 报 pending tool call can be resumed 在首次产生 deferred 调用的 run 中保持 history layer 处于激活状态(runner.py
run 报 exactly one deferred call 当前版本每次 run 仅支持一个 ask-human 调用,应提示模型一次只问一个问题(layer.py
run 报 tool name must be ... 使用配置的 tool_name,不要只在下游表单代码里改名(layer.py
文件字段被拒绝 设置 allow_file_fields=True,并在 allowed_field_types 中包含 filefile-listconfigs.py
run_succeeded.data.output 缺失 检查 run_succeeded.data.deferred_tool_call——这是“人类请求成功”,不是 run 失败

小结

ask-human layer 把“Agent 何时该停下来问人”这件事收敛成一个模型可见的 external deferred tool:配置层用 DifyAskHumanLayerConfig 声明护栏,服务端硬上限兜底;运行时层把工具改写为 kind="external"、以 prompt 提示 + 双重校验(ModelRetry + 归一化)保证请求合法;终止事件用 deferred_tool_calloutput 互斥地表达 run 结果。客户端则承担投递、授权、渲染与恢复的全部产品职责,用 session_snapshot + 相同 composition + deferred_tool_results 完成闭环。相关实现集中在 dify-agent/src/dify_agent/layers/ask_human/ 包(configs.py / schema.py / layer.py),runner 侧的分支与校验在 dify-agent/src/dify_agent/runtime/runner.py,配套文档为 dify-agent/docs/dify-agent/user-manual/ask-human-layer/index.md

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