agent - 会话挂起(Session Pending)
2026年9月7日 上午9:59:09
在 Agent 的执行过程中,有时需要中断当前的计算流。例如:触发 HITL(Human-In-The-Loop,人机交互)等待人工审批,或是在特定条件下进行外部强行中断。
注意:
- 当前
ReActAgent和TeamAgent在准备新的请求时会清除挂起标记,因此使用同一个 Session 发起恢复请求时通常可以继续执行;自定义 Session 或执行流程不应假定一定存在此行为。 - 挂起控制计算图后续推进,但不等同于 Reactor cancel,也不保证正在执行的阻塞模型调用立即停止。
1、会话的挂起接口
通过 AgentSession 对象,可以对当前会话的运行状态进行精细化控制。一旦进入挂起状态,Agent 内部的计算图(Execution Graph)将停止推进。
| 方法 | 描述 |
|---|---|
AgentSession::pending(pending, reason) | 设置挂起状态(true 为挂起,并需提供挂起原因;false 为恢复执行,此时 reason 传 null 会清除挂起原因) |
AgentSession::isPending() | 判断当前会话是否处于挂起状态 |
AgentSession::getPendingReason() | 获取挂起原因(通常用于前端展示或后续逻辑判断) |
内部机制说明
pending() 方法的默认实现(在 AgentSession 接口中)的工作流程如下:
default void pending(boolean pending, String reason) {
FlowContextInternal contextInternal = ((FlowContextInternal) getContext());
if (pending == true) {
// 1. 调用 contextInternal.stop() 停止执行流
// 2. 设置 stopped(true) 标记,后续计算图检测到此标记后停止推进
contextInternal.stop();
contextInternal.stopped(true);
} else {
// 清除 stopped 标记,允许继续执行
contextInternal.stopped(false);
}
if (reason == null) {
// 清除挂起原因
getContext().remove(Agent.KEY_PENDING_REASON);
} else {
// 记录挂起原因(可用于前端展示)
getContext().put(Agent.KEY_PENDING_REASON, reason);
}
}
2、挂起应用场景
会话挂起主要分为 内部逻辑拦截 和 外部异步中断 两种模式。
通过拦截器挂起(适用于:同步、异步、流式响应)
这是最常用的场景,通常用于在工具执行(Action)前进行合规检查或人工授权。
public void useInterceptor() throws Throwable {
ReActAgent agent = ReActAgent.of(null)
.defaultInterceptorAdd(new ReActInterceptor() {
@Override
public void onToolCallStart(ReActTrace trace, ToolExchanger toolExchanger) {
// 模拟 HITL 场景:根据业务逻辑决定是否挂起
if ("delete_user".equals(toolExchanger.getToolName())) {
trace.getSession().pending(true, "敏感操作,等待人工审批");
}
}
})
.build();
AgentSession session = InMemoryAgentSession.of();
// 发起同步调用
ReActResponse resp = agent.prompt("删除 ID 为 1001 的用户").session(session).call();
// 检查挂起状态
if (session.isPending()) {
System.out.println("执行已挂起,原因:" + session.getPendingReason());
}
}
注意:旧拦截器方法
onAction已在 4.0.4 标记为@Deprecated,推荐使用onToolCallStart。
外部主动中断(适用于:异步调用、流式响应)
在异步或流式任务执行过程中,外部根据用户指令或其他监控事件强行挂起会话。
- 示例 A:异步调用中断
public void asyncInterrupt() throws Throwable {
ReActAgent agent = ReActAgent.of(null).build();
AgentSession session = InMemoryAgentSession.of();
// 发起异步调用
agent.prompt("生成一份万字长文报告").session(session).callAsync()
.whenComplete((resp, err) -> {
// 处理最终结果
});
// 模拟外部干预:立即强行挂起
session.pending(true, "用户点击了停止生成");
}
- 示例 B:流式调用中断
public void streamInterrupt() {
ReActAgent agent = ReActAgent.of(null).build();
AgentSession session = InMemoryAgentSession.of();
// 保存 Disposable,主动停止流时调用 dispose()
Disposable disposable = agent.prompt("查一下杭州今天的天气").session(session).stream()
.subscribe(
event -> {
// 处理流事件内容
},
error -> handleError(error));
// session.pending(...) 记录并控制会话挂起;它不等同于取消当前订阅
session.pending(true, "资源配额不足,自动挂起");
// 如果业务要求立即停止向订阅方传输事件:
disposable.dispose();
}
3、恢复执行
挂起的会话可以通过两种方式恢复:
方式一:重新发起调用时自动恢复
当重新对同一个 Session 调用 agent.prompt("新的指令").session(session).call() 时,Trace 的 prepare() 方法会自动调用 session.pending(false, null),清除挂起状态使计算图继续推进。
方式二:手动恢复
// 恢复执行
session.pending(false, null);
// 调用 agent.prompt().session(session).call() 继续推进
4、HITL 场景的完整配合
在标准 HITL 路径中,事件分属两次请求:
- 首次请求:
ReasonEndEvent -> HITLPendingEvent -> RunEndEvent(isAbnormal=true) -> onComplete。此时 Session 仍为 pending,且不会产生 Action 或 ToolCall 的开始/结束事件。 - 外部提交决策后,使用同一个 Session 和空 Prompt 发起恢复请求;恢复流中产生
HITLDecidedEvent,再按批准、拒绝或跳过结果继续。 - 普通代码直接调用
session.pending(true, reason)时,不能假定系统一定会生成HITLPendingEvent。
主动取消属于 Reactor cancel,不会补发 RunEndEvent;未处理异常通过 onError 结束,也不会补发顶层结束事件。
详细请参考 HITL 相关专题文章。