前面我们一直在看 Agent 是怎么调用工具、怎么记录执行过程的。
这次换一个场景:
如果大模型遇到了自己无法确定的事情,需要让用户做选择,Harness 应该怎么设计?
比如用户说:
text
帮我创建一个项目
但没有告诉 Agent:
text
项目叫什么?
用 React 还是 Vue?
要不要初始化 Git?
Agent 显然不能永远靠猜。
它需要停下来问用户。
1. LLM 怎么告诉 Harness:"我需要问用户"?
从大模型的视角来看,它真正能够感知和使用的东西其实很有限:
text
Prompt
+
Messages
+
Tools
如果我们希望模型主动发起一个动作,最自然的方式还是 Tool。
我们平时设计 Agent 基本也是这个思路:
text
LLM = 大脑
Tool = 手脚
想到
↓
Tool Call
↓
做到
所以 DeerFlow 也定义了一个:
python
@tool("ask_clarification", parse_docstring=True, return_direct=True)
def ask_clarification_tool(
question: str,
clarification_type: Literal[
"missing_info",
"ambiguous_requirement",
"approach_choice",
"risk_confirmation",
"suggestion",
],
context: str | None = None,
options: list[str] | None = None,
fields: list[ClarificationFormField] | None = None,
) -> str:
"""Ask the user for clarification when you need more information to proceed."""
...
第一眼看起来很正常:
text
Agent 不清楚
↓
调用 ask_clarification
↓
询问用户
但仔细看会发现一个奇怪的地方:
python
...
这个 Tool 根本没有真正的执行代码。
那定义它有什么意义?
其实这里的 Tool 更像是在定义一份 LLM 和 Harness 之间的协议:
当模型需要用户补充信息时,不要自己随便输出一段文字,而是调用
ask_clarification,并按照固定结构告诉 Harness:我要问什么、有哪些选项、需要哪些字段。
比如:
python
ask_clarification(
question="请选择项目技术栈",
clarification_type="approach_choice",
options=["React", "Vue", "Svelte"],
)
模型负责表达:
text
"我需要用户做选择"
至于这件事情最后怎么展示给用户,并不是 Tool 自己负责。
2. Tool 没有执行,Middleware 把它接管了
真正处理 ask_clarification 的地方在:
text
clarification_middleware.py
核心代码其实就两句:
python
if request.tool_call.get("name") != "ask_clarification":
return handler(request)
return self._handle_clarification(request)
普通 Tool:
text
LLM
↓
ToolCall
↓
Middleware
↓
handler(request)
↓
真正执行 Tool
但是 ask_clarification 不一样:
text
LLM
↓
ask_clarification ToolCall
↓
Middleware
↓
不调用 handler
↓
直接接管
所以这个 Tool 严格来说并不是为了执行某个 Python 函数。
它更像一个:
text
LLM → Harness
的结构化控制信号。
模型只负责说:
我现在需要人类输入。
Harness 再决定:
那我应该展示一个输入框、选择框,还是整个表单?当前 Agent 要不要停止?
这也是为什么 DeerFlow 没有把所有逻辑都塞进 Tool。
Tool 定义的是:
text
模型怎么表达意图
Middleware 处理的是:
text
Runtime 收到这个意图以后怎么办
3. 用户的问题,不一定只是"一句话"
真正处理请求的是:
python
_handle_clarification()
但在进入这个函数之后,DeerFlow 并不是简单地:
python
print(question)
因为用户输入可能有很多形式。
DeerFlow 定义了一套表单字段:
python
class ClarificationFormField(TypedDict, total=False):
name: Required[str]
label: str
type: Literal[
"text",
"textarea",
"number",
"select",
"multi_select",
"checkbox",
"date",
]
required: bool
options: list[str]
placeholder: str
所以 Agent 可以问一个简单问题:
text
项目叫什么?
也可以让用户选:
text
你希望使用哪个框架?
○ React
○ Vue
○ Svelte
甚至可以一次生成一个表单:
text
项目名称
[________________]
技术栈
[ React ▼ ]
初始化 Git
[✓]
截止日期
[ 2026-08-20 ]
例如模型可能产生:
json
{
"question": "请补充项目配置",
"fields": [
{
"name": "project_name",
"label": "项目名称",
"type": "text",
"required": true
},
{
"name": "framework",
"label": "技术栈",
"type": "select",
"options": ["React", "Vue", "Svelte"],
"required": true
},
{
"name": "init_git",
"label": "初始化 Git",
"type": "checkbox"
}
]
}
如果前端支持这种 Schema,就可以直接渲染成真正的 Form。
这显然比让用户自己回复:
text
项目叫 xxx,框架选 React,然后 Git 要初始化
体验好很多。
4. 为什么既有 UI Schema,又有纯文本?
问题来了。
DeerFlow 不一定只运行在自己的 Web 页面。
它可能接入:
text
Web
IM
Slack
Feishu
GitHub
其他客户端
并不是所有客户端都支持动态 Form。
所以 DeerFlow 没有只生成一份 UI Schema,而是同时准备了两份内容。
一份是给人看的纯文本
放在:
python
ToolMessage.content
例如:
text
❓ 请补充项目配置
1. 项目名称 (required)
2. 技术栈 (required) --- options: React / Vue / Svelte
3. 初始化 Git
Please reply with a value for each field.
即使客户端完全不认识 DeerFlow 的 UI 协议,也至少可以把这段文字展示出来。
另一份是给支持 UI 的前端:
python
ToolMessage.artifact["human_input"]
例如:
json
{
"version": 2,
"kind": "human_input_request",
"source": "ask_clarification",
"question": "请补充项目配置",
"input_mode": "form",
"fields": [
{
"name": "project_name",
"label": "项目名称",
"type": "text",
"required": true
},
{
"name": "framework",
"label": "技术栈",
"type": "select",
"required": true,
"options": [
{
"id": "framework-option-1",
"label": "React",
"value": "React"
},
{
"id": "framework-option-2",
"label": "Vue",
"value": "Vue"
}
]
}
]
}
于是同一次 clarification:
text
ask_clarification
│
┌─────────────┴─────────────┐
│ │
ToolMessage.content artifact.human_input
│ │
纯文本 fallback UI Schema
│ │
不支持 UI 的客户端 支持 UI 的前端
它不是维护两套业务逻辑。
而是:
结构化 UI + 纯文本降级。
5. 最后,Agent 为什么会停下来?
格式化完之后,DeerFlow 会生成一个特殊的 ToolMessage:
python
tool_message = ToolMessage(
id=request_id,
content=formatted_message,
tool_call_id=tool_call_id,
name="ask_clarification",
artifact={
"human_input": human_input_payload
},
)
然后返回:
python
return Command(
update={"messages": [tool_message]},
goto=END,
)
这里的:
python
goto=END
非常关键。
如果没有它,流程可能变成:
text
LLM
↓
ask_clarification
↓
生成 ToolMessage
↓
LLM 又继续运行
↓
继续调用工具
但模型明明已经表示:
我缺信息,需要用户回答。
所以正确的流程应该是:
text
LLM
│
│ ToolCall
▼
ClarificationMiddleware
│
┌───────────┴───────────┐
│ │
普通 Tool ask_clarification
│ │
handler() normalize
│
┌─────────────┴─────────────┐
│ │
ToolMessage.content artifact.human_input
│ │
文本 fallback UI Schema
│ │
└─────────────┬─────────────┘
│
Command(update=...)
│
goto=END
│
───── 当前 Run 结束 ─────
│
前端展示表单
│
用户输入
│
下一轮 Agent
结束当前这一次 Agent Run。
用户填写完以后,再通过下一轮消息重新驱动 Agent。
在 Agent Harness 里,ToolCall 不一定只代表"调用一个函数",它也可以成为 LLM 和 Runtime 之间的一种控制协议。
模型负责决定什么时候需要人。
Harness 负责决定人应该看到什么,以及什么时候把控制权交出去。