人工介入:挂起、审核与继续执行
2026年9月18日 上午11:57:48
Agent Graph 在转账、发布、删除、审批等关键动作前,经常需要等待人工决定。人工介入不是让线程持续阻塞,而是:保存状态、结束本次执行,收到决定后再恢复。
proposal_agent
-> approval
├─ 未决定:pending
├─ APPROVE:executor_agent
└─ REJECT:reject_finish
1、审核节点
spec.addActivity("approval")
.title("人工审核")
.task((ctx, node) -> {
String decision = ctx.getAs("approval.decision");
if (decision == null) {
AgentSession session = ctx.getAs(Agent.KEY_SESSION);
ctx.put("approval.request", ctx.get("change.proposal"));
session.pending(true, "变更方案等待人工审核");
}
})
.linkAdd("approval_route");
session.pending(true, reason) 已经会:
- 停止当前 Flow 执行;
- 将 Context 标记为 stopped;
- 保存 pending 原因。
不需要再额外调用 ctx.stop()。
不要依赖首次 TeamResponse.getContent() 返回待审核原因。审核原因应从:
session.getPendingReason();
读取。若产品需要明确响应文本,可在挂起前显式设置 TeamTrace.finalAnswer,但它不能替代 pending 状态。
2、审核路由
spec.addExclusive("approval_route")
.linkAdd(executor.name(), link -> link.when(ctx ->
"APPROVE".equals(ctx.getAs("approval.decision"))))
.linkAdd("reject_finish");
拒绝节点明确收敛结果:
spec.addActivity("reject_finish")
.task((ctx, node) -> {
String result = "变更已拒绝:"
+ ctx.getAs("approval.reason");
ctx.put("change.result", result);
TeamTrace trace = TeamTrace.getCurrent(ctx);
if (trace != null) {
trace.setFinalAnswer(result);
}
})
.linkAdd(Agent.ID_END);
3、首次执行
AgentSession session = InMemoryAgentSession.of("change-001");
changeGraph.prompt("升级订单服务并执行数据库变更")
.session(session)
.call();
if (session.isPending()) {
System.out.println(session.getPendingReason());
System.out.println(session.getContext().get("approval.request"));
}
此时应持久化快照并释放当前请求线程,而不是原地等待人工操作。
4、批准后继续
session.getContext().put("approval.decision", "APPROVE");
session.getContext().put("approval.operator", "alice");
session.getContext().put("approval.time", System.currentTimeMillis());
TeamResponse resumed = changeGraph.prompt()
.session(session)
.call();
TeamAgent 恢复调用会在本轮准备阶段清理 pending 标志,因此显式调用 session.pending(false, null) 不是继续执行的必要条件。业务层可以调用它表达“审核任务已解除”,但不能替代写入审核决定。
空参数 prompt() 表示使用 Trace 中的原始 Prompt 续跑,不是发起新的空任务。
5、stop、interrupt 与 pending
| API | 作用范围 | 持久化语义 |
|---|---|---|
ctx.interrupt() | 截断当前分支 | 不代表 Session pending |
ctx.stop() | 停止整个本次 Flow 执行 | 不保存 pending 原因 |
session.pending(true, reason) | 停止整个 Flow,并保存 Session 挂起状态 | 推荐用于可恢复人工等待 |
并行图中,interrupt() 不等于停止其他分支;高风险审批通常应使用 Session pending。
6、审核节点会重新执行
FlowTrace 在进入节点时记录位置。恢复到挂起 Activity 后,该 Activity 的 task 会再次运行。因此审核节点应该只做:
- 检查决定;
- 生成待办;
- 保存待审核数据;
- 切换 pending 状态。
不要这样设计:
扣款 -> pending -> 等审批
应拆成:
准备扣款 -> 等待审批 -> 幂等扣款 -> 记录结果
外部动作还应使用业务幂等键,例如 change-001/execute。
7、Graph HITL 与 ReAct HITL
| 机制 | 适用范围 |
|---|---|
| Graph HITL | 决定整个业务阶段是否继续 |
| ReAct 工具 HITL | 决定某一次工具调用是否允许 |
| Solon Flow Workflow | 正式人员任务、认领、审核、退回等工作流 |
ReAct 工具审核应按调用 UUID 关联:
HITLTask task = HITL.getPendingTaskByCallUuid(session, callUuid);
HITL.submit(session, task,
HITLDecision.approve().comment("管理员已确认"));
agent.prompt().session(session).call();
同名工具可能同时产生多个待审核调用,不要只按工具名称匹配。