基于 AgentScope 框架的后端 Tool 与前端 Tool 分工交互原理
基于 AgentScope 框架的后端 Tool 与前端 Tool 分工交互原理
大多数 AI Agent 教程止步于"agent 怎么调工具",但企业业务系统真正的架构难题是:agent 的工具,一部分在后端执行(查库、调接口、读写文件),一部分必须由前端完成(弹审批框、收集表单、页面跳转)——两者怎么分工、怎么协作、怎么在 HTTP 的单向约束下完成挂起与恢复?本文以 agentscope 框架的 externalTool 机制为切入点,从源码级别把后端 Tool 与前端 Tool 的分工交互原理彻底拆透,配以五套完整时序图覆盖自动执行、HITL 审批、混合调用、拒绝取消等全部场景。对于企业级 Agent 落地,这是最该先读懂的那块拼图。
本文档用 Mermaid 时序图描述 AG-UI 协议下 agentscope-java agent 与前端交互的全部主要场景,覆盖后端 tool、前端浏览器 tool(自动 / HITL)、混合调用、拒绝取消等。
场景索引
| # | 场景 | 是否阻塞用户 | 是否中断 run | 典型 tool |
|---|---|---|---|---|
| 1 | 后端 Tool 自动执行 | 否 | 否 | queryDB、readFile、调内部 API |
| 2 | 前端浏览器 Tool 自动执行 | 否 | 是(立即恢复) | openPage、showToast、navigate、highlight |
| 3 | 前端 HITL Tool 中断式交互 | 是 | 是(等用户后恢复) | confirmAction、requestApproval、collectInput |
| 4 | 一次会话混合调用(后端 + 前端 HITL) | 部分 | 是 | 先查单、再请求退款审批 |
| 5 | HITL 被用户拒绝 / 取消 | 是 | 是(恢复为拒绝) | confirmAction → 用户点"拒绝" |
核心判定标准:externalTool 标志位
⭐ 这是 agentscope-java 区分"后端 tool"与"前端 tool"的唯一根本判据,贯穿下文全部场景。读每个图时盯住这个标志即可。
agentscope 的工具模型中,每个工具都有一个 boolean externalTool 标志位。工具执行器在调用工具自身的执行逻辑之前先检查它,由此决定是本地执行还是挂起。
① 字段定义 —— 工具基类中的标志位声明:
public abstract class ToolBase implements AgentTool {
// ...
private final boolean externalTool;
// ...
}
② 短路判定 —— 工具执行器在调用执行逻辑之前先检查该标志,命中即挂起、不调用执行逻辑:
// External tool short-circuit: surface the call to the caller without running schema
// validation, preset injection, or scheduling. SchemaOnlyTool and any
// @Tool(externalTool=true) method end up here.
if (tool instanceof ToolBase tb && tb.isExternalTool()) {
return Mono.just(ToolResultBlock.suspended(toolCall, new ToolSuspendException()));
}
以上源码块即定位锚点:复制其中任意一行(如
External tool short-circuit或isExternalTool())到源码中搜索,即可跳到对应位置。
externalTool |
含义 | 执行器行为 | 结果来源 | 对应场景 |
|---|---|---|---|---|
false(默认) |
后端 tool:有执行逻辑,服务端可执行 | 调用执行逻辑,本地执行 → 正常结果 | 服务端产出 | 场景 1 |
true |
前端 / 外部 tool:无实现,结果须由外部提供 | 短路,不调用执行逻辑 → 挂起结果 → 挂起 | 外部(前端)resume 回传 |
场景 2 / 3 / 4 / 5 |
三个关键事实(每条附源码块,便于在源码中定位):
- 经
runAgent请求tools字段注入的前端工具,会被包装成"仅含 schema 的工具"(schema-only),其构造时硬编码externalTool=true——这就是"前端 tool 注入后必然挂起"的根因:
// schema-only 工具构造时硬编码 externalTool=true
super(
ToolBase.builder()
.name(Objects.requireNonNull(name, "name cannot be null"))
.description(
Objects.requireNonNull(description, "description cannot be null"))
.inputSchema(
parameters != null
? Collections.unmodifiableMap(new HashMap<>(parameters))
: Collections.emptyMap())
.externalTool(true) // ← 关键:标记为外部工具
.readOnly(false)
.concurrencySafe(true));
- 通过注解把某个后端方法标记为
externalTool=true,同样走短路,机制完全一致:
// 注解式工具读取 externalTool 配置
.externalTool(annotation.externalTool())
// 命中时同样产出挂起结果,不执行方法本体
if (isExternalTool()) {
return Mono.just(
ToolResultBlock.suspended(param.getToolUseBlock(), new ToolSuspendException()));
}
- 对
externalTool=true的工具,其执行逻辑里抛出的挂起异常只是兜底;正常路径下执行器提前短路,根本不会调用它。
因此下文每个场景都会显式标注工具的
externalTool取值:false→ 服务端执行;true→ 挂起、结果来自外部(前端)。
参与方
| 简称 | 角色 |
|---|---|
| User | 终端用户 |
| FE | 前端(浏览器,AG-UI 兼容客户端) |
| Adapter | AG-UI 适配器(含请求处理器、恢复协调器) |
| Agent | agentscope Agent,持有工具集 |
| TEx | 工具执行器(agent 内部组件,实际调度工具执行) |
| LLM | 大模型 |
| BTool | 后端工具(externalTool=false,有执行逻辑) |
| FTool | 前端工具(externalTool=true,仅含 schema,无实现) |
协议约束:AG-UI 的
runAgent是「POST 请求 → 单向 SSE 响应」。前端无法在一次 run 的流式响应中回传数据,因此任何前端 tool 的结果都必须通过"结束本次 run → 发起下一次 run(带 resume)"回传。这就是前端 tool 必然伴随 interrupt 的根本原因,与"是否需要用户参与"无关。关于端点路径:下文时序图中的
POST /runAgent是 AG-UI「运行 agent」操作的示意标记,非字面 URL。实际路径由部署项目决定(agentscope-java 默认为POST /agui/run,AG-UI 协议标准为POST /run)。
场景 1:后端 Tool 自动执行
LLM 选中带实现的工具,服务端直接执行、产出结果、agent 继续推进。一次 run 完成,不中断、不回前端。
场景 2:前端浏览器 Tool 自动执行(无需用户等待)
LLM 选中前端 tool(如页面跳转、弹 toast、高亮元素)。这类动作前端可以立即程序化执行,无需用户决策。机制上仍走"挂起→interrupt→resume",但前端收到 interrupt 后立即执行、立即回传,用户基本无感知。
场景 3:前端 HITL Tool 中断式交互(需用户决策)
LLM 选中需要人类判断的前端 tool(审批、确认、收集输入)。前端收到 interrupt 后渲染 UI 并阻塞等待用户操作,用户决策后再回传 resume。这是最典型的 human-in-the-loop。
场景 4:一次会话混合调用(后端 Tool → 前端 HITL Tool)
真实业务中最常见的形态:agent 先用后端 tool 查数据(自动执行),再根据结果调用前端 HITL tool 请求用户审批。展示两种 tool 在同一会话中的衔接。
场景 5:HITL 被用户拒绝 / 取消
用户在审批弹框中点"拒绝"。resume 可以用 status:"resolved" + payload.approved:false(推荐,表达业务决策),或 status:"cancelled"(表达中断本身被取消)。agent 拿到拒绝结果后应优雅终止,不强行执行。
底层机制:挂起为什么导致"前端执行"
本节回答三个底层问题:① "无实现"是什么意思?② 挂起和"前端工具"有关系吗?③ 为什么走到挂起就由前端执行?
① "无实现"是什么意思
前端工具被包装成"仅含 schema 的工具"(schema-only):只携带 name + description + parameters(JSON Schema),刚好够拼成发给 LLM 的工具定义;但它没有任何业务逻辑——不查库、不调 API、不读写文件。它是"空壳":LLM 能看见、能选择,但服务端无法执行。
② 挂起和"前端"无关——它是通用的"需要外部提供结果"信号
到达挂起状态有两条独立路径,殊途同归:
| 路径 | 触发点 | 谁会用 |
|---|---|---|
A. externalTool=true 标志 |
工具执行器短路(不调用执行逻辑) | schema-only 工具、被注解标记为外部工具的后端方法 |
| B. 执行逻辑运行中主动抛挂起信号 | 工具执行器的异常捕获分支 | 后端工具执行到一半发现需人工输入 |
两路都产出同一个"挂起的工具结果"(内部标记为挂起)。所以挂起 ⊃ “前端工具”——纯后端工具也能挂起(路径 B)。"前端 tool"只是"用 schema-only 工具(路径 A)+ 由前端 resume 提供结果"的一种用法。
⚠️ 对 schema-only 工具,执行逻辑里抛出的挂起异常只是兜底;正常路径下执行器因
externalTool=true提前短路,根本不会调用执行逻辑。
③ 为什么结果最终来自前端(协议约定,非框架驱动)
框架并不会"把工具送到前端"。它只是逐级降级,最后把球停在 HTTP 边界上,由 AG-UI 协议约定消费方(前端)来接:
核心结论:框架只说"这个工具我本地跑不了,结果需要外部提供",然后停下来。"前端执行"是 AG-UI 协议消费方(浏览器)的职责,不是框架驱动的。 同一套机制挂在 CLI 或另一个后端服务里,接球的就是它们——框架代码一行不用改。
三类 Tool 核心对照
| 维度 | 后端 Tool | 前端自动 Tool | 前端 HITL Tool |
|---|---|---|---|
externalTool |
false(默认) |
true |
true |
| 注册位置 | agent 工具集(有实现) | 请求 tools 字段 → schema-only 工具 |
请求 tools 字段 → schema-only 工具 |
| 执行触发 | 工具执行器调用执行逻辑,本地执行 | externalTool 短路(不调用执行逻辑) |
externalTool 短路(不调用执行逻辑) |
| 是否中断 run | 否 | 是 | 是 |
| 前端收到 interrupt 后 | — | 立即执行,瞬时 resume | 渲染 UI,等用户操作 |
| 结果来源 | 服务端执行产出 | 前端程序化执行 → resume.payload |
用户决策 → resume.payload |
| 用户感知 | 无 | 几乎无(看到页面变化) | 明确暂停,需主动操作 |
| run 次数 | 1 次 | 2 次(挂起 + 恢复) | 2 次(挂起 + 恢复) |
合并模式对注入的影响
参考
- AG-UI 集成文档(中文):https://java.agentscope.io/v2/zh/integration/protocol/agui.html
- AG-UI 协议规范(Tools):https://docs.ag-ui.com/concepts/tools
(END)
更多推荐
所有评论(0)