字节跳动 DeerFlow:Agent Harness 怎么让大模型主动向用户提问?

0 阅读5分钟

前面我们一直在看 Agent 是怎么调用工具、怎么记录执行过程的。

这次换一个场景:

如果大模型遇到了自己无法确定的事情,需要让用户做选择,Harness 应该怎么设计?

比如用户说:

帮我创建一个项目

但没有告诉 Agent:

项目叫什么?
用 React 还是 Vue?
要不要初始化 Git?

Agent 显然不能永远靠猜。

它需要停下来问用户。


1. LLM 怎么告诉 Harness:“我需要问用户”?

从大模型的视角来看,它真正能够感知和使用的东西其实很有限:

Prompt
+
Messages
+
Tools

如果我们希望模型主动发起一个动作,最自然的方式还是 Tool。

我们平时设计 Agent 基本也是这个思路:

LLM = 大脑
Tool = 手脚

想到
 ↓
Tool Call
 ↓
做到

所以 DeerFlow 也定义了一个:

@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."""
    ...

第一眼看起来很正常:

Agent 不清楚
 ↓
调用 ask_clarification
 ↓
询问用户

但仔细看会发现一个奇怪的地方:

...

这个 Tool 根本没有真正的执行代码

那定义它有什么意义?

其实这里的 Tool 更像是在定义一份 LLM 和 Harness 之间的协议

当模型需要用户补充信息时,不要自己随便输出一段文字,而是调用 ask_clarification,并按照固定结构告诉 Harness:我要问什么、有哪些选项、需要哪些字段。

比如:

ask_clarification(
    question="请选择项目技术栈",
    clarification_type="approach_choice",
    options=["React", "Vue", "Svelte"],
)

模型负责表达:

“我需要用户做选择”

至于这件事情最后怎么展示给用户,并不是 Tool 自己负责。


2. Tool 没有执行,Middleware 把它接管了

真正处理 ask_clarification 的地方在:

clarification_middleware.py

核心代码其实就两句:

if request.tool_call.get("name") != "ask_clarification":
    return handler(request)

return self._handle_clarification(request)

普通 Tool:

LLM
 ↓
ToolCall
 ↓
Middleware
 ↓
handler(request)
 ↓
真正执行 Tool

但是 ask_clarification 不一样:

LLM
 ↓
ask_clarification ToolCall
 ↓
Middleware
 ↓
不调用 handler
 ↓
直接接管

所以这个 Tool 严格来说并不是为了执行某个 Python 函数。

它更像一个:

LLM → Harness

的结构化控制信号。

模型只负责说:

我现在需要人类输入。

Harness 再决定:

那我应该展示一个输入框、选择框,还是整个表单?当前 Agent 要不要停止?

这也是为什么 DeerFlow 没有把所有逻辑都塞进 Tool。

Tool 定义的是:

模型怎么表达意图

Middleware 处理的是:

Runtime 收到这个意图以后怎么办

3. 用户的问题,不一定只是“一句话”

真正处理请求的是:

_handle_clarification()

但在进入这个函数之后,DeerFlow 并不是简单地:

print(question)

因为用户输入可能有很多形式。

DeerFlow 定义了一套表单字段:

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 可以问一个简单问题:

项目叫什么?

也可以让用户选:

你希望使用哪个框架?

○ React
○ Vue
○ Svelte

甚至可以一次生成一个表单:

项目名称
[________________]

技术栈
[ React ▼ ]

初始化 Git
[✓]

截止日期
[ 2026-08-20 ]

例如模型可能产生:

{
  "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。

这显然比让用户自己回复:

项目叫 xxx,框架选 React,然后 Git 要初始化

体验好很多。


4. 为什么既有 UI Schema,又有纯文本?

问题来了。

DeerFlow 不一定只运行在自己的 Web 页面。

它可能接入:

Web
IM
Slack
Feishu
GitHub
其他客户端

并不是所有客户端都支持动态 Form。

所以 DeerFlow 没有只生成一份 UI Schema,而是同时准备了两份内容。

一份是给人看的纯文本

放在:

ToolMessage.content

例如:

❓ 请补充项目配置

  1. 项目名称 (required)
  2. 技术栈 (required) — options: React / Vue / Svelte
  3. 初始化 Git

Please reply with a value for each field.

即使客户端完全不认识 DeerFlow 的 UI 协议,也至少可以把这段文字展示出来。


另一份是给支持 UI 的前端:

ToolMessage.artifact["human_input"]

例如:

{
  "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:

                      ask_clarification
                              │
                ┌─────────────┴─────────────┐
                │                           │
        ToolMessage.content        artifact.human_input
                │                           │
          纯文本 fallback                UI Schema
                │                           │
       不支持 UI 的客户端             支持 UI 的前端

它不是维护两套业务逻辑。

而是:

结构化 UI + 纯文本降级。


5. 最后,Agent 为什么会停下来?

格式化完之后,DeerFlow 会生成一个特殊的 ToolMessage

tool_message = ToolMessage(
    id=request_id,
    content=formatted_message,
    tool_call_id=tool_call_id,
    name="ask_clarification",
    artifact={
        "human_input": human_input_payload
    },
)

然后返回:

return Command(
    update={"messages": [tool_message]},
    goto=END,
)

这里的:

goto=END

非常关键。

如果没有它,流程可能变成:

LLM
 ↓
ask_clarification
 ↓
生成 ToolMessage
 ↓
LLM 又继续运行
 ↓
继续调用工具

但模型明明已经表示:

我缺信息,需要用户回答。

所以正确的流程应该是:

                     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 负责决定人应该看到什么,以及什么时候把控制权交出去。