Dify Agent Ask Human Layer:让模型以结构化方式向人类发起输入请求的完整机制
本篇围绕 Dify Agent 的 ask-human layer(dify.ask_human)展开:它向模型暴露一个 external deferred tool,使 Agent 能在当前 run 无法继续时以结构化请求形式向人类要信息,并将该请求作为 deferred_tool_call 随 run_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_composition(layer.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 |
实现上有两处值得注意:
tool_name必须是合法标识符。字段校验器用正则^[A-Za-z_][A-Za-z0-9_]*$全匹配校验(configs.py),非法命名会在配置校验时直接报错。- 文件字段的“双开关”策略。
_validate_file_field_policy模型校验器要求:只有当allow_file_fields=True且allowed_field_types中包含file/file-list时,文件字段才合法;allow_file_fields=False却在允许列表里写了文件类型,配置本身就会被拒绝(configs.py)。文件字段变体目前属于“为前向兼容预留的词汇表”,默认不开放。
layer 会把这些上限自动转写成 prompt 提示。build_prompt_hint(layer.py)生成的文本包含允许的字段类型、文件上传是否启用、字段/动作数上限、question/markdown 与 label 长度限制,并告知模型“若省略 actions,系统会自动补一个 primary 样式的 Submit 动作”。客户端因此无需在系统提示里重复罗列限制,但可追加业务侧指导(例如何时才适合问人)。
模型能请求什么:AskHumanToolArgs 契约
启用后,layer 暴露一个 external deferred tool,其参数形状为 AskHumanToolArgs(schema.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:自由文本输入,支持placeholder、default;select:单选输入,选项value必须非空且唯一,default必须命中某个选项值(schema.py);file:单文件输入,仅在允许文件字段时可用;file-list:多文件输入,仅在允许文件字段时可用,可配max_files(schema.py)。
所有字段和动作都执行 extra="forbid",即不允许携带未知属性;字段 name 与动作 id 必须满足标识符规则。
工具参数在模型调用后还会被再校验一次。 校验入口 _validate_tool_args(layer.py)捕获 ValidationError/ValueError 并转成 ModelRetry——也就是说,模型一次越界的调用不会直接终结 run,而是触发模型带着错误信息重试,直到产出合法请求后才可能发出终止成功事件。归一化函数 _validate_and_normalize_tool_args 具体执行的护栏包括:字段数不超过 max_fields;动作为空时补默认 Submit;动作数不超过 max_actions;question/markdown/各 label 的长度上限;字段类型必须在 allowed_field_types 内;未开 allow_file_fields 时拒绝文件字段(layer.py)。
另一个值得理解的细节:工具函数本体 _never_executed_tool 只会抛出 RuntimeError(layer.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_payload(layer.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(字段名到值)、message 与 rendered_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 中包含 file 或 file-list(configs.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_call 与 output 互斥地表达 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。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00