第 41 章 会话执行控制与结构化决策

本章导读:两篇互为补充的运行时实践。前半篇解决"任务怎么排队、怎么引导、怎么中断续做":通过 agent.session(context) 取得 AgentSession,用 submit / steer / inject / interrupt / resume / respond 表达用户意图,理解 session → turn → run 三层结构,掌握断线续传与应用重启的恢复方式,并对照 AgentScope Service 的对应入口。后半篇回答"高频小判断交给谁":Jev 结构化决策模型以 Noul / Choice / Score 三类问题返回类型化答案与校准概率,配合模型路由、工具选择、执行守卫三个中间件覆盖 Agent 循环的关键判断点,同时划清边界——需要深度推理与文本输出的场景仍属于主模型。

flowchart LR A["你的需求
任务怎么排队引导中断续做
高频小判断交给谁"] --> B["两篇互补的运行时实践"] B --> C["会话执行控制
submit · steer · inject
interrupt · resume · respond"] B --> D["session turn run 三层结构
断线续传 · 对照 Service 入口"] B --> E["Jev 结构化决策
三类问题返回类型化答案
三个中间件覆盖判断点"] C --> F["收益
后台执行的任务可控可恢复
分清判断与主模型的边界"] D --> F E --> F

本章导览图

会话操作:提交、引导、中断与恢复

用户点击“停止生成”后又输入一条要求,应用需要继续原任务,还是提交新任务?Agent 正在调用工具时补充“预算改为 5000 元”,模型什么时候能看到?浏览器断开 SSE,后台是否继续执行?

普通多轮聊天和当前请求内的流式展示,可以继续使用第 2 章(快速开始)中的 call / streamEvents;它们也会保存 Harness 会话记录。本节聚焦后台执行、任务排队和中断续做。

使用 HarnessAgent 构建这类应用时,通过 agent.session(context) 取得 AgentSession,用不同操作表达用户意图。框架负责执行与持久队列;前端读取历史和事件。完整 API 见第 19 章(会话日志与恢复),可运行应用见第 40 章(最佳实践:可恢复会话聊天)。

先选择用户真正想做的操作

用户意图 API 生效方式
发起一个新任务 session.submit(input) 创建 turn;忙时按接收顺序排队
纠正或补充当前任务 session.steer(input) 当前 turn/run 的下一推理步骤读取
保存参考材料,不主动唤醒 Agent session.inject(context) 在后续推理步骤读取,空闲时不执行
停止当前执行 session.interrupt() 通过既有协作中断机制停止,保留原任务
中断后继续原任务 session.resume(turnId) 恢复 checkpoint,保留 turnId,新建 runId
回答工具问题或审批 session.respond(requestId, answer) 查询待办归属,使用原有 HITL 机制继续
刷新页面或断线重连 读取快照和水位后的持久事件 补齐界面,不重新提交或启动执行

同一 session 可以包含多个 turn;一个 turn 可以跨多个 run。模型和工具调用是 run 内部的步骤,不要求应用每次创建一个 turn。

取得会话并提交任务

下面假设 agent 是配置了稳定 agentId 和持久日志后端的 HarnessAgent,ownerId 来自认证身份,sessionId 已完成访问权限检查。

import io.agentscope.core.agent.RuntimeContext;

var session = agent.session(RuntimeContext.builder()
        .userId(ownerId).sessionId(sessionId).build());
var task = session.submit("request-001", "比较三个出差方案");
System.out.println(task.turnId());

取得会话不会执行 Agent。submit 同步持久接收输入并开启后台调度,返回回执;排队时 runId 可以为空。网络重试沿用相同 request key 和输入,避免生成重复任务。

Web 接口可直接返回回执,页面独立读取进度。命令行若需要等待本次执行结束,可使用:

var outcome = session.await(task).block();
System.out.println(outcome.status());

await 只观察本次执行,不负责启动或取消。返回 suspended 表示仍有待办或挂起原因,不能当作任务完成;对等待操作使用 timeout 也只会停止等待。

这些存储操作是阻塞调用。WebFlux 集成应安排到 Schedulers.boundedElastic(),不要占用网络事件线程。

A 正在执行,下一条消息应该怎么办

独立的新需求用 submit,当前任务的修正用 steer,纯参考材料用 inject:

session.submit("request-002", "之后再比较退改签政策");
session.steer(task, "预算改为 5000 元");
session.inject("参考材料:会议地点在市中心。");

第二个任务会排队。引导绑定当前 task,防止延迟请求误投到后来开始的任务;无运行任务时 steer 会报错。它不会修改已经发出的模型请求或正在执行的工具参数,而是在下一推理步骤生效。

inject 不主动增加一次推理:如果当前任务已无后续步骤,材料保留到下一次执行。输入接收、执行开始和输入应用分别记录,接收成功不等于模型已经采用。中断前未消费的引导仍属于原 turn,在恢复时读取。

任务等待 HITL 或被中断时,后续任务保留在队列中。先答复待办或继续原任务,原任务结束后才调度下一项任务。

中断和继续不需要重新拼装输入

session.interrupt();
var stopped = session.await(task).block();
if ("interrupted".equals(stopped.status())) {
    var continued = session.resume(task.turnId());
}

界面已绑定某次 run 时,使用 session.interrupt(runId),可以拒绝误中断后续执行的过期请求。中断信号通过既有检查点生效,不会回滚已产生的外部副作用;工具长期阻塞时仍可能需要等待。

恢复使用已提交工作状态和尚未应用的原输入,无需手工重放全部消息。它保留 turnId、创建新的 runId,不恢复原 Java 线程或工具内部进度。若工具结果未知,先核对外部系统,再按照第 19 章“核对结果未知的工具”一节继续。

HITL 答复关联原请求

session.pending().forEach((requestId, event) ->
        System.out.println(requestId + " " + event.data().get("kind")));

session.respond("external-request-id", "选择方案 B");
session.respond("confirmation-request-id", true);

字符串用于外部执行结果,布尔值用于工具确认。请求 ID 必须来自持久待办;框架查找原 turn 和工具调用,拒绝未知或不匹配的请求。拒绝某项工具行动使用对应答复,不等于中断整个任务。

这里使用挂起式 HITL:答复后创建新 run,继续原 turn。在线等待式 Service 交互仍应通过所属服务的 actions 接口处理。

断线续传与应用重启

关闭浏览器或取消事件订阅,不会取消框架持有的后台执行。前端刷新时先读取完整快照和对应水位,再订阅后续事件;仅保存游标无法补回游标之前的文字和工具卡片。

var history = session.transcript();
var events = session.log().readAfter(lastAppliedSeq, 100);

原生日志的 seq、实时 AgentEvent 的 ID 与 Service 公共 cursor 是不同概念,不能混用。只有已提交记录可用于可靠续传;消息、工具参数和结果按稳定 ID 更新,避免重复追加。

重启后使用相同 userId、agentId、sessionId 和日志后端取得会话。查看历史不启动推理;应用显式调用 session.start() 恢复已接收任务的调度,中断任务仍需 resume。退出时关闭 agent,框架负责中断活动执行并保留持久队列。

AgentScope Service 的对应入口

托管 Agent 使用 Agent API(见第 31 章):创建 /api/v1/agent-sessions,向 session 的 /turns 提交任务,通过 /snapshot 和 /events/stream 恢复界面,使用 actions/resume 接口继续原任务。它有自己的持久命令、鉴权和公共事件协议。

SDK AgentSession 与 Service 表达相同的会话和任务概念,但不是同一个调度器。Service 已持有自己的任务租约和命令队列,不应在一个请求中再嵌套 SDK 会话调度。旧 /api/sessions 事件接口和 Endpoint SSE 的行为见 SSE 格式(见第 31 章),不能套用新 Agent API 的排队与 cursor 约定。

需要后台排队和中断续做的应用可以使用上述会话操作。已有工作流或服务调度器时,仍可使用 call / streamEvents 驱动执行;更底层的执行控制用于自定义调度器和协议适配。简洁的配套代码见 SessionRunGuide.java。

让 Agent 学会判断:Jev 与结构化决策

前半篇处理的是任务生命周期由谁控制:应用用 submit / steer / inject / interrupt / resume / respond 表达用户意图,框架负责排队与恢复。但任务跑起来之后,Agent 循环内部还有另一类问题——每一轮"该用哪个模型、该暴露哪些工具、这条命令能不能放行"。这些高频判断如果都塞给主模型推理或硬编码规则,开销和可靠性都不理想。后半篇介绍把这类判断单独抽出来的做法:Jev 结构化决策模型。

一个 Agent 从收到请求到完成任务,看起来是在"推理"和"执行"之间循环。但仔细拆开每一步,你会发现真正密集发生的是另一件事:判断。

这个请求该用哪个模型处理?当前上下文里哪些工具应该暴露给模型?这个 bash 命令能不能自动放行?这个回答的置信度够不够直接返回?这些判断每一个都不复杂,但它们出现在 Agent 运行时的每一个角落,加起来就变成了一个被长期低估的系统性开销。

AgentScope Java 最近集成了 TypeSafe AI 的 Jev。本节不只介绍集成本身——更重要的是借这个机会聊清楚:为什么 Agent 系统需要一种独立于"推理"的"判断"能力,以及这种能力应该怎么用。

Agent 运行时里的高频小决策

先看一个典型场景。

一个 ReAct Agent 正在处理用户请求。模型推理完成,产生了三个工具调用:一个文件读取、一个命令执行、一个代码搜索。在工具真正执行之前,系统需要回答:

判断点 需要什么
该用哪个模型处理下一轮推理? 请求复杂度评估
三个工具调用中哪个有风险? 命令内容 + 上下文分析
文件读取的结果是否需要截断? 输出大小预估
这个回答可以直接返回吗? 置信度评估

这些判断有三个共同特点:高频、低复杂度、需要确定性输出。它们不需要模型展开长链路推理,但需要系统快速得到一个可以做分支的结论。

目前的做法通常有两种,各有问题。

第一种:让主模型顺便做判断。 在 prompt 里加一句"先判断该用哪个模型,再回答"。这确实能工作,但每次判断都要走完整的推理路径,返回的是一段自由文本,系统还要再做一次解析。更麻烦的是,主模型的输出格式不受控——你让它返回 JSON,它可能在 JSON 前面加一段"好的,我来分析一下"。

第二种:写硬编码规则。 "包含 rm -rf 的命令一律拒绝"、"超过 100 个字符的请求用大模型"。这解决了确定性问题,但覆盖不了长尾。"列出这个目录下的所有文件"和"删掉这个目录"在规则层面很难区分,在语义层面一目了然。

问题的本质是:Agent 系统里大量需要的不是"更聪明的推理",而是"更便宜、更可靠的判断"。

Jev 是什么:一种新的模型形态

Jev 是 TypeSafe AI 推出的结构化决策模型。它不是聊天模型,不生成文本,也不做工具调用。它做的事情只有一件:接收上下文和一组结构化问题,返回类型化的答案和校准概率。

具体来说,它支持三种问题类型,恰好对应 Agent 系统里三类判断需求:

问题类型 判断模式 Agent 场景
NoulQuestion 是/否的概率 "这个操作安全吗?"
ChoiceQuestion 选项集合的概率分布 "该用哪个模型?哪个工具?"
ScoreQuestion 等级评分 "这段回答质量如何?"

与传统 LLM 的关键差异在于返回形式。你问"这个 bash 命令安全吗",Jev 不会返回一段分析文本,而是直接返回 noul: 0.92——P(安全) = 92%。你问"该用哪个模型",Jev 返回每个模型的概率分布和整体置信度,而不是一段建议文字。

维度 聊天模型 Jev
输出 自由文本 / 工具调用 类型化答案 + 概率
延迟 秒级到十秒级 亚秒级
格式可靠性 依赖 prompt engineering 和后解析 结构化协议保证
成本 按推理路径计费 按决策请求计费

就像你不需要为“今天星期几”这个问题启动一个深度研究流程——有些问题只需要一个快速的、确定性的回答。Jev 做的事情类似:为 Agent 里高频、结构化的判断提供一种专门优化过的计算路径。

在 AgentScope Java 里怎么接入

AgentScope 没有把 Jev 注册成 Model provider。原因很简单:Model 接口面向的是聊天推理,返回 ChatResponse;Jev 返回的是结构化答案,两者在调用频率、超时预期和类型系统上都不匹配。硬塞进同一个接口只会让配置语义变得混乱。所以 Jev 以独立扩展模块的方式存在,应用代码直接注入 JevClient 做判断,或者通过 middleware 挂到 Agent 循环里。

依赖引入:

<dependency>
    <groupId>io.agentscope</groupId>
    <artifactId>agentscope-extensions-jev</artifactId>
    <version>${agentscope.version}</version>
</dependency>

Spring Boot 项目可以直接用 starter:

<dependency>
    <groupId>io.agentscope</groupId>
    <artifactId>agentscope-jev-spring-boot-starter</artifactId>
    <version>${agentscope.version}</version>
</dependency>
agentscope:
  jev:
    api-key: ${TYPESAFE_API_KEY:}
    base-url: https://api.typesafe.ai
    model: jev-latest
    timeout: 5s

如果想在业务代码里直接调用 JevClient 做 Noul、Choice 或 Score 判断(不经过 middleware),完整的快速上手示例见 Jev 集成文档(见第 36 章)。

三个中间件:覆盖 Agent 循环的关键判断点

除了 JevClient,扩展模块还提供了三个参考 middleware,分别覆盖 Agent 循环的三个阶段。它们不只是简单的示例——每一个都对应一个真实的工程问题。

中间件 拦截点 判断类型
JevModelRouterMiddleware onAgent / onModelCall Choice:选模型
JevToolSelectionMiddleware onReasoning Choice:选工具
JevAutoModeMiddleware onActing Noul:判断安全性

三个拦截点——Agent 入口、推理前、行动前——恰好覆盖了 Agent 生命周期中最需要做判断的三个位置。这不是巧合,而是"判断"作为一类系统关注点的自然分布。

模型路由:规划资源分配

JevModelRouterMiddleware 在 Agent 调用入口做一次模型选择,整轮调用复用这个决策。

JevModelRouterMiddleware modelRouter =
        JevModelRouterMiddleware.builder(client)
                .choice("fast", fastModel, "Direct lookups, extraction, and localized changes.")
                .choice("powerful", powerfulModel, "Architecture and high-stakes decisions.")
                .instructions("Choose the least costly model that can complete the task.")
                .confidenceThreshold(0.75)
                .execution(new io.agentscope.extensions.judge.jev.JevExecution.Options(
                    io.agentscope.extensions.judge.jev.JevExecution.Mode.SHADOW,
                    java.time.Duration.ofSeconds(2), "v1", (ctx, record) -> {}))
                .build();

ReActAgent agent =
        ReActAgent.builder()
                .name("assistant")
                .model(fallbackModel)
                .middleware(modelRouter)
                .build();

每个候选模型有自己的路由标准。Jev 根据用户请求在候选中做 Choice 判断,返回概率和置信度。置信度不够就退回默认模型——判断失败不是让 Agent 停摆,而是降级到安全路径。

中间件默认处于 OFF;上面的示例显式开启了 SHADOW,即只记录 Jev 的建议、不实际生效,需要真正接管时改为 ENFORCE——本节描述的筛选与拦截行为都在 ENFORCE 下生效。另外,模型路由对带工具的请求默认回退到默认模型,需通过 compatible 谓词声明候选能力。

工具选择:规划上下文可见性

JevToolSelectionMiddleware 在每轮推理前对工具列表做过滤和排序,只把相关的工具 schema 发给主模型。

JevToolSelectionMiddleware toolSelection =
        JevToolSelectionMiddleware.builder(client)
                .alwaysIncludeTools(Set.of("load_skill_through_path", "reset_tools"))
                .maxTools(3)
                .confidenceThreshold(0.8)
                .execution(new io.agentscope.extensions.judge.jev.JevExecution.Options(
                    io.agentscope.extensions.judge.jev.JevExecution.Mode.SHADOW,
                    java.time.Duration.ofSeconds(2), "v1", (ctx, record) -> {}))
                .build();

当一个 Agent 注册了 50 个工具,每轮推理的 tool schema 就是一笔不小的 token 开销。更重要的是,工具太多会稀释模型的注意力。让 Jev 先做一轮筛选,只把当前任务相关的工具暴露给模型,既省 token 又提升准确率。

执行守卫:判断行为风险

JevAutoModeMiddleware 在工具执行前做风险判断。这是三个中间件里最能体现"判断"价值的一个。

JevAutoModeMiddleware autoMode =
        JevAutoModeMiddleware.builder(client)
                .guardedTool("bash")
                .safetyThreshold(0.8)
                .execution(new io.agentscope.extensions.judge.jev.JevExecution.Options(
                    io.agentscope.extensions.judge.jev.JevExecution.Mode.SHADOW,
                    java.time.Duration.ofSeconds(2), "v1", (ctx, record) -> {}))
                .build();

ReActAgent agent =
        ReActAgent.builder()
                .name("assistant")
                .model(model)
                .toolkit(toolkit)
                .middleware(autoMode)
                .build();

当一个 bash 工具调用产生时,middleware 会把完整对话历史和命令内容发给 Jev,问一个 Noul 问题:"这个调用安全吗?" noul: 0.92 就放行,noul: 0.3 就拒绝——拒绝的调用不会执行,而是写入一个 DENIED 的工具结果,让模型知道这个操作被拦截了。

执行模式有 OFF、SHADOW 与 ENFORCE 三种,默认 OFF。失败时的行为有明确约定:工具推荐/路由失败回退原流程;执行防护在 ENFORCE 下失败则拒绝受保护调用。

什么时候不该用

Jev 不是万能的,也不是"更便宜的大模型"。以下场景仍然需要主模型的深度推理:

一个简单的判断标准:如果一个决策可以表达成"是/否"、"从 N 个选项里选一个"、或"打一个分",且系统会根据这个结果做分支(if/else、路由、阈值判断),那它适合 Jev。如果这个决策需要先理解、再分析、再推导,那它属于推理模型的领地。

换句话说:Jev 回答的是"可以吗"和"选哪个",不是"为什么"。

判断与规划:比集成本身更重要的东西

回过头看上面三个中间件,它们解决的问题可以归纳为两个词。

规划:在行动之前决定"做什么、用什么做"。选模型是规划资源分配,选工具是规划上下文可见性。这类决策决定了 Agent 的效率和成本。

判断:在执行之前决定"能不能做、值不值得做"。风险守卫是判断行为边界,置信度评估是判断输出质量。这类决策决定了 Agent 的安全性和可靠性。

这两个词覆盖了 Agent 系统里大量非推理类的计算。目前这些计算大多被塞进了主模型的推理路径,或者被硬编码进了业务逻辑。Jev 提供了第三条路:把这些判断抽出来,交给一个专门优化的决策模型,用类型化接口和校准概率返回结果。

顺着这个思路,还有很多地方可以用同样的模式优化:

这些场景的共同特征是:判断本身不复杂,但频率高、需要确定性、失败时有明确的降级路径。这恰好是 Jev 这类结构化决策模型的适用范围。

Agent 系统的下一步不只是"更强的推理",还需要"更高效的判断"。推理决定 Agent 能走多远,判断决定它能不能走得又快又稳。

本章小结

本章两篇回答两个层面的问题。执行控制层面:按用户意图选对操作——新任务 submit、当前任务补充 steer、纯材料 inject、停止 interrupt、续跑 resume、答复挂起 respond;session → turn → run 三层结构解释了为什么恢复后 turnId 不变、runId 新建;断线后用"快照 + 水位后的事件"补齐界面,应用重启后 session.start() 恢复已接收任务的调度。结构化决策层面:Jev 用 Noul / Choice / Score 三类问题返回类型化答案与校准概率,模型路由、工具选择、执行守卫三个中间件分别覆盖 Agent 入口、推理前、行动前三个判断点;需要深度推理或文本输出的场景仍交给主模型。

验收点——读到这里,你应该能: