Solon v4.1.0

人工介入:挂起、审核与继续执行

</> markdown
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();

同名工具可能同时产生多个待审核调用,不要只按工具名称匹配。