第 4 章 Agent 与 ReAct 循环

本章导读:从本章起,全书围绕同一个应用展开:星帆出海的客服 Agent「小帆」——它要回答售前咨询、查询订单、处理退款,逐步长成第 43 章多租户平台上的核心角色。本章先把小帆的最小版本跑起来,再回答把它换成真实业务时马上遇到的三个决定——直接用底层的 ReActAgent,还是用封装好的 HarnessAgent?一次请求用哪种方式发起?实例每次新建还是共享?本章就围绕这些决定展开:讲解 Agent 统一接口、两者的分工、实例生命周期与共享 Builder 实践、call / streamEvents / observe(只记录消息、不触发推理)三种调用方式、RuntimeContext 的用途、人机交互的暂停与恢复、状态持久化与结构化输出。读完本章,你能为业务场景选对调用入口,并独立配置、运行和管理一个智能体。

flowchart LR A["你的需求
把示例换成真实业务
要做三个决定"] --> B["Agent 与 ReAct 循环
统一接口与三种调用方式"] B --> C["选类与调用方式
ReActAgent 还是 HarnessAgent"] B --> D["实例生命周期
共享 Builder · 每请求新建"] B --> E["RuntimeContext
每次调用的身份与瞬态数据"] B --> F["人机交互
暂停与恢复"] C --> G["收益
选对调用入口
独立配置运行和管理智能体"] D --> G E --> G F --> G

本章导览图

你什么时候需要它

把示例换成真实业务时,上面的三个决定正好对应三种典型处境。

要长期运行、能记住用户的助手,从 HarnessAgent 开始。 客服助手、个人助理、运维值班机器人这类应用,需要工作区、会话持久化、记忆压缩、子 Agent 等运行能力——这些 HarnessAgent 已经内置,你只做配置。反过来,如果只是获取一次回复、做一次结构化抽取、执行工作流中的一个步骤,直接用 ReActAgent 自行组合基础能力就够了。

按“用户要不要看过程”选发起方式。 要最终回复(提取数据、工作流步骤),用 call;要在当前请求里逐步展示文本与工具进度(聊天界面、命令行进度),用 streamEvents;要“关闭页面后继续执行、忙时排队、运行中补充要求、中断后接着做”,用 HarnessAgent 的会话操作(AgentSession,见第 19 章)。

实例管理先跟着第 2 章的习惯走。 共享 Builder、每次请求构建新实例、用完关闭;本章“实例生命周期”一节给出完整规则与共享实例的例外。

与已学内容的关系:第 2 章你已经用 HarnessAgent 跑通 call 与 streamEvents,本章把第 3 章地图上“基座层”的这块拆开讲透,也会第一次用到 Toolkit(注册并向模型暴露工具的容器,第 7 章展开)——配置 Agent 时模型与工具先直接使用,两者的细节分别在第 6、7 章。下面从 Agent 统一接口看起。

概述

AgentScope 通过 Agent 接口统一智能体的调用方式,提供底层的 ReActAgent 和在其基础上封装的 HarnessAgent。业务 Agent 开发推荐优先使用 HarnessAgent,也可以直接使用 ReActAgent,自行组合应用所需的能力。

ReAct 是 AgentScope Harness 的执行基础。 ReActAgent 负责“模型推理 → 执行工具 → 将结果交回模型”的循环,直到返回结果或等待外部交互。权限检查(Permission,工具执行前判定允许、拒绝还是询问用户)、人机交互、上下文与状态管理、中间件、实时事件等基础能力都围绕这一循环工作。

HarnessAgent 内部封装 ReActAgent,沿用同一套推理与执行循环,并进一步内置工作区、长期记忆、上下文压缩、技能、子 Agent、沙箱以及会话日志与恢复等能力。因此,使用 Harness 后,整体的推理、执行循环本质上没有变化;更多运行能力由 Harness 统一装配,业务开发者可以直接配置使用。

从快速开始(见第 2 章)可以直接上手 HarnessAgent。本章以 ReActAgent 为例说明两者共有的基础配置与调用方式;Harness 的内置能力和组合方式见第 11 章。

核心接口

Agent 接口常用的方法如下:

方法 描述
call(List<Msg>) / call(List<Msg>, RuntimeContext) 运行推理-行动循环,返回 Mono<Msg>
streamEvents(List<Msg>) / streamEvents(Msg) 同 call,但以流式方式逐一产出 AgentEvent 对象
observe(Msg) / observe(List<Msg>) 将消息添加到上下文,不触发推理(返回 Mono<Void>)

ReActAgent 和 HarnessAgent 在此之上还提供 call(msgs, structuredOutputClass, runtimeContext) 等结构化输出重载,以及通过 RuntimeContext 传递本次调用元数据的便捷入口。

按业务场景选择调用方式

你要实现什么 使用方式
获取回复、提取结构化数据、执行工作流中的一个步骤 agent.call(input, ctx)
在当前请求中逐步展示文本和工具进度 agent.streamEvents(input, ctx)
后台持续执行、忙时排队、运行中补充要求、中断后继续原任务 agent.session(ctx) 的会话操作,仅 HarnessAgent 提供

call 和 streamEvents 都可以用于多轮对话;每次调用可包含多次推理和工具执行。使用默认配置的 HarnessAgent 时,两者也会保存 Session Log 和 checkpoint。仅需要记住上一轮或查询历史时,可以继续直接调用。

先按下文完成直接调用。有后台任务管理需求时,再阅读第 19 章(会话日志与恢复)。通过 HTTP 使用托管 Agent 时,见第 31 章(Session API)。

主循环

ReActAgent 和 HarnessAgent 都通过下面的推理-行动循环执行一次 call。Harness 的内置能力在相应阶段参与工作:

flowchart TD A([输入: 消息 / 事件]) --> B{输入是外部事件\n的结果?} B -- 是 --> C[处理确认或工具结果\n更新挂起调用的状态] B -- 否 --> D[将消息添加到上下文] C --> E D --> E E{检查下一步动作} -- 退出 --> F([返回: 暂停\n等待确认或外部结果]) E -- 推理 --> G[必要时压缩上下文] G --> H[LLM 调用] H -- 无工具调用 --> I([返回最终消息]) H -- 有工具调用 --> Acting subgraph Acting [行动] direction TB J[批量工具调用\n串行 / 并发] --> L[执行工具调用] L --> M{权限\n检查} M -- 允许 --> N[运行工具 → 结果] M -- 询问 / 外部 --> O([暂停并发出事件\n等待用户确认或外部执行]) M -- 拒绝 --> P[将错误结果返回 LLM] end N --> E P --> E

图 4-1:一次 call 的推理-行动循环。图中两处"外部"节点对应人工介入:权限要求用户确认、或工具被标记为外部执行时,Agent 不猜测结果,发出事件后暂停("返回:暂停");外部把结果喂给下一次 call,循环从入口的"外部事件"分支进入,把结果填回挂起的调用、从中断处继续——而不是当作新消息重新推理。接收与恢复的完整代码见本章"人机交互"一节。

先跑通:小帆的第一个版本

不谈概念,先把本书的主角跑起来。十几行代码——一个人格提示、一个模型、一次调用:

import io.agentscope.core.ReActAgent;
import io.agentscope.core.agent.RuntimeContext;
import io.agentscope.core.message.Msg;
import io.agentscope.core.message.UserMessage;
import java.util.List;

ReActAgent.Builder xingfan = ReActAgent.builder()
        .name("xingfan-support")
        .sysPrompt("你是星帆出海的客服小帆,语气友好,只基于事实回答;"
                + "不确定的事说不确定,并引导用户联系人工。")
        .model("dashscope:qwen-plus")
        .toolkit(new Toolkit());   // 工具在第 7 章加入:小帆暂时只能凭提示词回答

RuntimeContext ctx = RuntimeContext.builder()
        .userId("alice").sessionId("session-001").build();

try (ReActAgent agent = xingfan.build()) {
    Msg reply = agent.call(
            List.of(new UserMessage("你们的退货政策是什么?")), ctx).block();
    System.out.println(reply.getTextContent());
}

运行后你会看到一段符合客服口吻的回答。这十几行里已经包含了本章要讲的全部要素:Builder 是共享的配置模板(xingfan),每次请求 build() 出一个实例、用完关闭;call 是"要最终回复"的调用入口;RuntimeContext 告诉框架这次调用属于谁、哪个会话——它决定状态读写到哪个槽位。toolkit 目前是空的:小帆还查不了真实订单,第 7 章给它装上工具。接下来各节把每个部分展开。

实例生命周期

为什么是"共享 Builder、每请求新建实例"? Agent 实例持有运行中的会话上下文与执行状态,跨请求共享一个实例就要回答"两个用户的请求同时进来怎么办";而 Builder 是不可变的配置模板,共享它没有并发问题。用"便宜的实例 + 昂贵的模板"换取免锁的并发模型,这是本章所有示例的公共前提。 选好了 Agent 与调用方式,下一个工程问题是:实例每次新建还是共享、何时关闭。

推荐共享配置 Builder,每次请求 builder.build() 创建新的 Agent,执行结束后关闭。 Builder 在应用启动时配置完成,请求期间只负责构建;用户、会话、请求参数通过新建的 RuntimeContext 传入,不修改共享 Builder。

同步调用用 try-with-resources 包住执行和等待;响应式调用用 Mono.using / Flux.using,让实例在订阅结束、报错或取消时释放。模型客户端、连接池、存储、技能仓库等依赖可以由应用统一管理,应用退出时再关闭;共享工具与中间件应保证线程安全,不能把当前用户或上下文保存在成员字段里。

新实例与新会话是两个概念。Harness 使用稳定的 agentId、userId、sessionId 和相同日志后端恢复已提交状态;普通 ReActAgent 若未配置会话日志或 stateStore,仅保留在旧实例内的上下文不会带到新实例。

也可以共享同一个 Agent 实例。 Agent 采用无状态执行引擎设计,会话状态按身份管理;共享实例由应用在退出时关闭。同一实例内,相同用户与会话的直接调用由执行门(把同一会话的调用排成串行的机制)串行化,不同会话可并行。实例仍有缓存、执行门和后台资源,因此这里的“无状态”不表示没有生命周期。后续示例统一采用每请求创建的推荐方式。

后台 AgentSession、Channel 或托管运行时的生命周期由其管理器持有,不能套用“提交请求返回就关闭”的方式。中断一次直接调用也必须定位到正在执行的实例;临时构建另一个实例不能中断旧实例中的请求。

配置智能体

先通过 ReActAgent.builder() 定义共享配置,再在每次请求中调用 agentBuilder.build()。.model(...) 既接受 ModelRegistry 解析的字符串 id(最常用、自动读取 env),也接受手动 builder 构造的 Model 实例(需要精细控制超时、自定义 endpoint 时用)。ModelRegistry 是模型注册中心,把 <provider>:<model> 形式的字符串解析成模型实例;第一种写法依赖它,第二种完全绕开。

字符串 model id(推荐)

import io.agentscope.core.ReActAgent;
import io.agentscope.core.tool.Toolkit;

ReActAgent.Builder agentBuilder =
        ReActAgent.builder()
                .name("xingfan-support")
                .sysPrompt("你是星帆出海的客服小帆,语气友好,只基于事实回答。")
                // 由 ModelRegistry 解析;自动读取 DASHSCOPE_API_KEY
                // 切换其他厂商时改成 "openai:gpt-5.5" / "anthropic:claude-sonnet-4-5"
                // / "deepseek:deepseek-v4-flash" / "gemini:gemini-2.0-flash" / "ollama:llama3" 即可。
                .model("dashscope:qwen-plus")
                .toolkit(new Toolkit());

显式 Model builder

import io.agentscope.core.ReActAgent;
import io.agentscope.extensions.model.dashscope.formatter.DashScopeChatFormatter;
import io.agentscope.extensions.model.dashscope.DashScopeChatModel;
import io.agentscope.core.tool.Toolkit;

ReActAgent.Builder agentBuilder =
        ReActAgent.builder()
                .name("xingfan-support")
                .sysPrompt("你是星帆出海的客服小帆,语气友好,只基于事实回答。")
                .model(
                        DashScopeChatModel.builder()
                                .apiKey("YOUR_API_KEY")
                                .modelName("qwen-max")
                                .stream(true)
                                .formatter(new DashScopeChatFormatter())
                                .build())
                .toolkit(new Toolkit());

配置 Toolkit / MCP

import io.agentscope.core.ReActAgent;
import io.agentscope.core.tool.Toolkit;
import io.agentscope.core.tool.builtin.TodoTools;
import io.agentscope.core.tool.mcp.McpClientBuilder;
import io.agentscope.core.tool.mcp.McpClientWrapper;

Toolkit toolkit = new Toolkit();
toolkit.registerTool(new TodoTools());          // 通过反射注册带 @Tool 的方法
toolkit.registerTool(new MyCustomTools());      // 自定义工具类(带 @Tool 注解的方法)

McpClientWrapper amap =
        McpClientBuilder.create("amap")
                .streamableHttpTransport(
                        "https://mcp.amap.com/mcp?key=" + System.getenv("AMAP_API_KEY"))
                .buildAsync()
                .block();
toolkit.registerMcpClient(amap).block();

ReActAgent.Builder agentBuilder =
        ReActAgent.builder()
                .name("xingfan-support")
                .sysPrompt("你是星帆出海的客服小帆,语气友好,只基于事实回答。")
                .model("dashscope:qwen-max")
                .toolkit(toolkit);

提示:ModelRegistry 的字符串形式(<provider>:<model>)需要对应的模型扩展模块在 classpath 中。它支持 dashscope / openai / openai-official / deepseek / anthropic / gemini / ollama,会自动从环境变量读取 API key(DASHSCOPE_API_KEY / OPENAI_API_KEY / DEEPSEEK_API_KEY / ANTHROPIC_API_KEY / GEMINI_API_KEY)。需要在长期运行场景下同时获得工作区、会话持久化、记忆压缩、子 agent 等能力,请改用 HarnessAgent(见第 11 章)——它对 ReActAgent 做了一层薄包装,builder 接口大体一致。

上面的三个示例只用到最常用的四个参数:name、sysPrompt、model 与 toolkit。builder 上还有 middlewares、stateStore、defaultSessionId、permissionContext、maxIters 等参数,完整清单连同类型与默认值统一收在本章末的参考速查;正文各节会在第一次用到某个参数时说明它——stateStore 见本章的配置状态持久化一节,permissionContext 参见第 10 章(权限系统与人工介入)。

运行智能体

配置齐了模型与工具,下面把 Agent 真正跑起来。

下文中的 agentBuilder 是应用配置好的共享 Builder;未展示完整作用域的片段中,agent 指当前请求内创建、执行结束后关闭的实例。

call 和 streamEvents 都接受相同的输入消息列表,驱动相同的推理-行动循环,区别在于结果的交付方式。

call 返回 Mono<Msg>,streamEvents 返回 Flux<AgentEvent>,都在订阅后开始执行。命令行可用 block() / blockLast() 等待;WebFlux handler 可以返回这个响应式结果,由框架订阅。调用方控制这次执行的订阅,取消它会取消该次执行。

同一请求选一个入口、订阅一次。streamEvents 用于发起一次带实时事件的执行;查询已提交历史使用日志读取 API。

call

call 在内部消费所有事件,当智能体完成或因外部交互暂停时返回最终 Msg。

import io.agentscope.core.agent.RuntimeContext;
import io.agentscope.core.message.Msg;
import io.agentscope.core.message.UserMessage;
import java.util.List;

UserMessage msg = new UserMessage("你们的退货政策是什么?");
RuntimeContext ctx = RuntimeContext.builder().userId("alice").sessionId("session-001").build();
try (ReActAgent agent = agentBuilder.build()) {
    Msg result = agent.call(List.of(msg), ctx).block();
    System.out.println(result.getTextContent());
}

streamEvents

streamEvents 逐一产出 AgentEvent 对象,让你实时将文本输出、工具调用进度和生命周期事件流式传输给用户。按 event.getType() 分发即可针对每类事件做不同处理:

import io.agentscope.core.event.AgentEventType;
import io.agentscope.core.event.TextBlockDeltaEvent;
import io.agentscope.core.event.ToolCallStartEvent;

try (ReActAgent agent = agentBuilder.build()) {
    agent.streamEvents(new UserMessage("把这条回复再润色得友好一些。"), ctx)
            .doOnNext(event -> {
                if (event.getType() == AgentEventType.TEXT_BLOCK_DELTA) {
                    // 模型返回的流式文本片段 —— 追加到界面或标准输出
                    System.out.print(((TextBlockDeltaEvent) event).getDelta());
                } else if (event.getType() == AgentEventType.TOOL_CALL_START) {
                    // 智能体即将调用工具 —— 展示调用信息
                    System.out.println("\n[tool] " + ((ToolCallStartEvent) event).getToolCallName());
                }
                // 其他事件:思考块、工具结果、回复结束等
            })
            .blockLast();
}

完整事件类型与字段见第 5 章(消息与事件)。

observe

使用 observe 将消息注入智能体上下文而不触发 reply——适用于多智能体场景中,一个智能体需要观察另一个智能体输出的情况。

agent.observe(otherAgentMsg).block();

多用户 / 多会话并发

前面的示例都是单用户单次调用;真实服务要同时面对多个用户、多个会话。共享 Builder 的不同请求各自创建实例和 RuntimeContext。下面的 WebFlux handler 表达式使用前面配置好的 agentBuilder:

import reactor.core.publisher.Mono;

RuntimeContext ctx = RuntimeContext.builder()
        .userId(userId).sessionId(sessionId).build();
return Mono.using(
        agentBuilder::build,
        agent -> agent.call(new UserMessage(userInput), ctx),
        ReActAgent::close);

流式 HTTP 响应可用 Flux.using(agentBuilder::build, agent -> agent.streamEvents(input, ctx), ReActAgent::close)。由 Web 框架订阅,不要在 handler 中提前关闭实例。

不同会话可以并行。每请求创建实例后,单个实例内的执行门不会协调其他实例;同一会话由业务队列串行调度,或交给持有后台生命周期的 AgentSession。原生日志的写入隔离防止并发写入,但不提供跨实例 FIFO 排队;只配置旧 stateStore 也不会获得分布式执行锁。

Spring Boot 完整示例见 agentscope-examples/documentation/.../streaming/StreamingWebExample.java。

会话应用的执行控制

以上都是“请求内完成”的直接调用;产品形态再往前一步,就需要会话级的执行控制。

当聊天产品需要关闭页面后继续执行、保存排队任务或统一处理恢复和待办时,使用 AgentSession 管理这些操作。普通问答、流式展示或自行处理 HITL(human-in-the-loop,人工介入审批)的应用可以继续使用直接调用。

会话方式通过 submit 接收新任务,以 steer / inject 补充信息,以 respond / resume 继续原任务;框架持有后台执行。前端从快照与持久事件读取进度。完整流程见第 19 章(会话日志与恢复)。

同一个会话选择直接调用或会话操作来驱动任务;只读历史查询可以共用。已经 submit 的任务应通过读取事件观察,不能再调用 streamEvents 来订阅它,否则会发起另一次执行。

高级:自行管理单次执行

ReActAgent 和 HarnessAgent 提供 prepareRun(messages, context)(事件流)与 prepareCall(messages, context)(最终回复),返回带有唯一 runId()(本次执行的稳定关联 ID)的 AgentRun<T>。创建句柄不会执行 Agent;订阅一次 stream() 才开始执行。

AgentRun<AgentEvent> run = agent.prepareRun(List.of(new UserMessage("你好")), context);
String runId = run.runId(); // 先在应用的运行管理器中登记句柄。
run.stream().subscribe(this::onEvent, this::onError);

// 另一条请求链路可根据 runId 找到这个句柄。
run.cancel();

运行管理器负责查找、鉴权及终态清理,Agent 不登记 RuntimeContext 对象。取消不会回滚外部副作用,也不能强行终止不响应取消的阻塞工具;立即取消不保证走协作中断的恢复回复和状态保存路径。

中断执行(Interrupt)

下面针对通过 call / streamEvents 启动的执行。跨请求发起中断时,应用应保存并找到目标执行对应的 Agent 实例,执行结束后移除并关闭。需要从外部协作中断时,使用 agent.interrupt;通过 AgentSession 提交的任务使用 session.interrupt(),再按第 19 章(会话日志与恢复)中“中断后继续原任务”一节继续:

import io.agentscope.core.agent.RuntimeContext;

// 构造标识目标 session 的 RuntimeContext
RuntimeContext target = RuntimeContext.builder()
        .userId("alice")
        .sessionId("session-001")
        .build();

// 中断该 session 正在进行的 call
agent.interrupt(target);

// 将消息附加到中断上下文
agent.interrupt(target, new UserMessage("用户已取消操作"));

这个便捷 API 选择指定 (userId, sessionId) 中当前正在执行的调用,不能选择排队中的某次调用;精确操作请使用该次执行的句柄。中断空闲 session 不产生效果。中断信号本身属于执行,不保存在 AgentState 中。

中断后的行为:

也可以直接用 (userId, sessionId) 字符串:

agent.interrupt("alice", "session-001");
agent.interrupt("alice", "session-001", interruptMsg);

RuntimeContext (per-call 上下文)

执行过程中,工具与 middleware 怎么拿到“这次调用”的身份与参数?答案是随调用传入的 RuntimeContext。

RuntimeContext(io.agentscope.core.agent.RuntimeContext)是 per-call 元数据袋。每次调用传入独立实例,工具和 middleware 通过参数接收上下文。Agent 不提供共享的当前上下文 getter,也不向共享 hook(钩子,框架在固定执行位置留给应用的回调点)字段注入上下文。技能仓库操作及 HarnessAgent.promoteSkill(name, reviewerId, ctx) 同样显式接收上下文;不带上下文的仓库操作使用默认命名空间。

RuntimeContext 不是持久化状态——AgentState(聊天上下文、压缩摘要、权限规则、tool state)才是。RuntimeContext 的作用是承载「当前这一次调用」相关的瞬态数据:tenant / userId / request-id、DB 连接、审计 logger、特性开关,等等。

内置字段与属性层

RuntimeContext 有三类「槽位」:

槽位 设置方式 读取方式
会话字段 sessionId(String) / userId(String) getSessionId() / getUserId()
字符串属性(任意 key-value) put(String key, Object value) <T> T get(String key)
类型化属性(按 Class<T> 注入业务 POJO) put(Class<T> type, T value) / put(String key, Class<T> type, T value) <T> T get(Class<T> type) / <T> T get(String key, Class<T> type)

类型化属性是给 tool 用的——@Tool 方法里声明同类型参数即可被框架自动注入,详见第 7 章(工具)“接收 Context”一节。字符串属性通常用于内部协调(例如 middleware 之间传值)。两层互不串扰:类型化层放进去的对象不会出现在 getExtra() 里,反之亦然。

构造并传入

import io.agentscope.core.agent.RuntimeContext;
import io.agentscope.core.message.Msg;
import io.agentscope.core.message.UserMessage;
import java.util.List;

RuntimeContext ctx =
        RuntimeContext.builder()
                .userId("alice")                           // 可选;null 表示匿名
                .sessionId("session-001")                  // 选择状态槽位
                .put("request_id", "req-abc-123")          // 字符串层
                .put(UserContext.class, new UserContext("alice", "en"))  // 类型层(业务 POJO)
                .build();

Msg result = agent.call(List.of(new UserMessage("Hi.")), ctx).block();

ReActAgent 为 call 和 streamEvents 提供 RuntimeContext 重载(另有为兼容保留的已弃用 stream 重载)。事件流使用 streamEvents(msgs, ctx) 显式传入上下文。不传 context 时框架使用 RuntimeContext.empty(),会话字段为 null,属性表为空,此时 agent 回退到 builder 上配置的 defaultSessionId。

谁能读到

与持久化的关系

完整示例:agentscope-examples/documentation/.../context/RuntimeContextExample.java、tool/ToolExecutionContextExample.java。

注意:存在一个旧的 ToolExecutionContext(io.agentscope.core.tool),已标记 @Deprecated,新代码统一使用 RuntimeContext。它在底层会被自动桥接到 RuntimeContext.asToolExecutionContext(),老代码不会立即失效。

人机交互

下文展示直接调用时如何处理工具确认与外部结果;发起请求与交回答案时沿用同一个 ctx。业务自己管理交互界面和后续调用;需要持久待办和自动关联原任务时,可使用 AgentSession 的 respond(见第 19 章)。

当智能体遇到以下两种情况时,会暂停执行并发出特殊事件:需要用户确认的工具调用(权限系统返回 ASK),或标记为外部执行的工具(结果必须来自智能体外部)。两种情况下,都可以通过把结果事件再次喂给 agent 的下一次 call 来恢复执行。

用户确认

当权限系统判断某个工具调用需要用户批准时,智能体会发出 RequireUserConfirmEvent 并暂停。

1. 接收 RequireUserConfirmEvent —— 用 streamEvents 监听暂停。事件携带 getReplyId()(用于恢复)和 getToolCalls() —— 一组 ToolUseBlock,每个暴露 getId() / getName() / getInput()。

import io.agentscope.core.event.RequireUserConfirmEvent;

agent.streamEvents(msg, ctx)
        .doOnNext(event -> {
            if (event instanceof RequireUserConfirmEvent confirm) {
                confirm.getToolCalls()
                        .forEach(
                                tc ->
                                        System.out.println(
                                                "工具: " + tc.getName() + ", 输入: " + tc.getInput()));
            }
        })
        .blockLast();

2. 构建确认结果 —— 为每个待处理工具调用构造一个 ConfirmResult。可以在传回前修改工具输入;如果想让今后相同的调用自动放行,在 rules 参数里显式传入 PermissionRule(建议规则挂在权限引擎的 PermissionDecision 上,而不是工具调用上):

import io.agentscope.core.event.ConfirmResult;
import java.util.ArrayList;
import java.util.List;

List<ConfirmResult> confirmResults = new ArrayList<>();
for (var tc : confirmEvent.getToolCalls()) {
    confirmResults.add(
            new ConfirmResult(
                    /* confirmed = */ true, // false 表示拒绝
                    /* toolCall  = */ tc)); // 传回(可选择修改)
}

3. 恢复智能体 —— 将 confirmResults 通过 metadata 传给下一次 call:

import io.agentscope.core.message.Msg;
import io.agentscope.core.message.UserMessage;
import java.util.Map;

UserMessage resumeMsg =
        UserMessage.builder()
                .metadata(Map.of(
                        Msg.METADATA_CONFIRM_RESULTS, confirmResults))
                .build();

Msg result = agent.call(List.of(resumeMsg), ctx).block();

外部工具执行

当智能体调用 isExternalTool() == true 的工具时,会发出 RequireExternalExecutionEvent 并暂停。工具的逻辑在智能体外部运行——通常由人工操作员或外部系统执行。

1. 接收 RequireExternalExecutionEvent —— 结构与用户确认一致:getReplyId() 加一组等待外部执行的 getToolCalls()。

import io.agentscope.core.event.RequireExternalExecutionEvent;

agent.streamEvents(msg, ctx)
        .doOnNext(event -> {
            if (event instanceof RequireExternalExecutionEvent ext) {
                ext.getToolCalls().forEach(tc ->
                        System.out.println("外部执行: " + tc.getName() + "(" + tc.getInput() + ")"));
            }
        })
        .blockLast();

2. 外部执行并构建结果 —— 在智能体外部完成操作,把每个结果封装为 ToolResultBlock:

import io.agentscope.core.message.TextBlock;
import io.agentscope.core.message.ToolResultBlock;
import io.agentscope.core.message.ToolResultState;
import java.util.ArrayList;
import java.util.List;

List<ToolResultBlock> executionResults = new ArrayList<>();
for (var tc : externalEvent.getToolCalls()) {
    String output = runExternalOperation(tc.getName(), tc.getInput());
    executionResults.add(
            ToolResultBlock.builder()
                    .id(tc.getId())
                    .name(tc.getName())
                    .output(List.of(TextBlock.builder().text(output).build()))
                    .state(ToolResultState.SUCCESS)
                    .build());
}

3. 恢复智能体 —— 将结果作为下一次 call 的输入消息回传。结果校验通过后会被注入智能体上下文,agent 会先发出 ExternalExecutionResultEvent,其 getReplyId() 与之前的 RequireExternalExecutionEvent#getReplyId() 相同,然后从中断处继续推理。

提示:构建交互式 UI 时使用 streamEvents——它可以实时检测暂停事件并立即提示用户。以编程方式处理事件的自动化流程则使用 call。完整可运行示例见 agentscope-examples/documentation/.../hitl/PermissionHITLExample.java。

配置状态持久化(AgentStateStore)

下面是直接构建 ReActAgent 时配置状态存储的方式。HarnessAgent 默认使用 Session Log 保存历史和 checkpoint,无需额外设置 stateStore;其日志后端配置见第 19 章(会话日志与恢复)。两者的关系见第 8 章(上下文)“Session Log 与 AgentStateStore 的关系”一节。

AgentState 是 agent 的全部可恢复状态——对话上下文、压缩摘要、权限规则、工具状态和当前 reply 位置。AgentStateStore 是它的存储抽象(内置与扩展实现见第 24 章)。

只需在 builder 上配 stateStore(...),agent 就会自动持久化与恢复:每次 call 结束把 AgentState 写回,下次用同一 (userId, sessionId) 调用时自动加载。Agent 实例本身对 session 无状态——具体读写哪个槽位由该次调用的 RuntimeContext 决定(缺省回退到 defaultSessionId)。

import io.agentscope.core.agent.RuntimeContext;
import io.agentscope.core.state.JsonFileAgentStateStore;
import java.nio.file.Paths;

ReActAgent agent = ReActAgent.builder()
        .name("xingfan-support")
        .sysPrompt("你是星帆出海的客服小帆,语气友好,只基于事实回答。")
        .model(model)
        .toolkit(new Toolkit())
        .stateStore(new JsonFileAgentStateStore(
                Paths.get(System.getProperty("user.home"), ".agentscope/sessions")))
        .build();

// 选定本次会话的槽位;userId 可选(null 表示匿名)
RuntimeContext rc = RuntimeContext.builder()
        .userId("user_123")
        .sessionId("session_789")
        .build();

// 若 (user_123, session_789) 有历史数据会自动加载;调用结束自动持久化
agent.call(List.of(new UserMessage("继续之前的任务。")), rc).block();

内置与扩展实现:

实现 模块 适用
InMemoryAgentStateStore agentscope-core 单元测试 / 单进程 demo
JsonFileAgentStateStore agentscope-core 单机开发,按 (userId, sessionId) 分目录落 JSON
RedisAgentStateStore agentscope-extensions-redis 多副本生产,跨进程跨机器共享
JdbcAgentStateStore agentscope-extensions-jdbc 需要落关系型库(审计 / 报表)

大多数场景只用一个 sessionId 就够;要按用户分桶就在 RuntimeContext 上同时设置 userId,存储会按 (userId, sessionId) 二元组寻址每个槽位。

通过 agent.getAgentState(userId, sessionId) 或 agent.getAgentState(runtimeContext) 可读取指定会话的状态快照:

AgentState state = agent.getAgentState("alice", "session-001");
state.getContext().size();                  // 当前对话消息数
String json = state.toJson();               // 序列化为 JSON

完整字段、跨节点接续见第 8 章(上下文);压缩 / Plan Mode / 子 agent 的协作细节见第 13 章(上下文管理)。

结构化输出

结构化输出让智能体按照你指定的 JSON Schema 返回结果,而不是自由文本。适用于需要程序化消费 agent 输出的场景——表单填写、数据提取、决策分类等。

基本用法

传一个 Java 类(或 JsonNode schema)给 call 即可:

import io.agentscope.core.message.Msg;
import io.agentscope.core.message.UserMessage;

// 定义输出结构
public record RefundIntent(boolean eligible, String orderSummary, String reason) {}

Msg result = agent.call(
        List.of(new UserMessage("订单 OD-2049 上周到的,鞋盒坏了,能退吗?")),
        RefundIntent.class).block();

// 从结果中取出强类型数据——直接喂给售后工单系统
RefundIntent intent = result.getStructuredData(RefundIntent.class);
System.out.println(intent.eligible());       // true
System.out.println(intent.orderSummary());   // "OD-2049 · 鞋盒破损"

结构化输出与工具可以同时使用——智能体会先调用工具完成任务,最后以指定 schema 输出最终结果。

工作原理

框架根据模型能力自动选择实现路径:

路径 条件 行为
原生路径 模型支持 response_format + tools 并行(OpenAI、DashScope 等) 将 JSON Schema 通过 response_format 直接传给模型 API,模型保证输出合法 JSON,循环自然结束
降级路径 模型不支持原生结构化输出(Anthropic、Ollama 等) 注入 generate_response 合成工具 + 指令提示,模型以 tool call 方式输出结构化结果

无论哪条路径,调用方的代码完全相同——路径选择对用户透明。

┌─── call(msgs, Schema.class) ───┐
│                                │
│   model.supportsNative...?     │
│      ├─ yes → response_format  │  ← 零额外开销,模型原生保证
│      └─ no  → generate_response│  ← 合成工具 + instruction
│                                │
└──── 返回带 schema 数据的 Msg ──┘

从结果中读取数据

call 返回的 Msg 在 metadata 中携带解析后的结构化数据:

// 方式一:强类型提取
RefundIntent data = result.getStructuredData(RefundIntent.class);

// 方式二:作为 Map 读取
@SuppressWarnings("unchecked")
Map<String, Object> map = (Map<String, Object>) result.getMetadata().get("_structured_output");

使用 JsonNode Schema

如果不想定义 Java 类,可以直接传 JSON Schema:

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

ObjectMapper om = new ObjectMapper();
JsonNode schema = om.readTree("""
    {
      "type": "object",
      "properties": {
        "sentiment": { "type": "string", "enum": ["positive", "negative", "neutral"] },
        "confidence": { "type": "number" }
      },
      "required": ["sentiment", "confidence"]
    }
    """);

Msg result = agent.call(List.of(new UserMessage("分析这段评论的情感")), schema).block();

更多能力

以下能力均通过 builder 配置,详情参见各自的文档页面:

模型容错

ReActAgent.builder()
        .model("dashscope:qwen-plus")
        .maxRetries(3)                              // 模型调用失败时自动重试
        .fallbackModel("dashscope:qwen-max")        // 主模型连续失败后切换到备用模型
        .failoverListener((primary, error) ->       // 观察切换:哪个模型失败、因何失败
                metrics.recordFailover(primary.getModelName(), error))
        .build();

failover 监听器在切换现场同步回调并携带原始错误——这是切换唯一的进程内信号,主模型的错误不会到达事件流或中间件。实现须非阻塞、线程安全;实现抛出的异常仅记录日志,不影响切换。

技能系统(Skills)

技能是可热加载的 Markdown 提示词模块,运行时由 LLM 按需激活:

ReActAgent.builder()
        .skillRepository(new MysqlSkillRepository(dataSource))
        .build();

内置工具

Builder 方法 说明
enableMetaTool(true) 注册 list_tools / activate_group 元工具,让 LLM 能发现和切换工具组
enableTaskList() 注册任务列表工具,让 LLM 拆解复杂任务为步骤并逐步完成

本章小结

这一章围绕开头的三个决定,走完了一条配置与运行 Agent 的完整路线:

  1. 选类:要长期运行、记住用户的助手从 HarnessAgent 开始;一次性的回复、抽取或工作流步骤,直接用 ReActAgent 组合基础能力;
  2. 选入口:要最终回复用 call,要在当前请求里逐步展示进度用 streamEvents,只注入上下文不触发推理用 observe;后台持续执行与中断恢复交给 AgentSession(第 19 章);
  3. 管实例:共享 Builder、每请求 builder.build() 创建新实例、用完关闭;共享实例依赖无状态执行引擎,同一会话由执行门串行化;
  4. 跑循环:ReAct 主循环串起推理与行动,权限检查、人机交互、状态持久化都围绕这一循环工作;
  5. 传上下文:RuntimeContext 承载每次调用的身份与瞬态数据,AgentState 才是持久化状态,由 stateStore 按 (userId, sessionId) 槽位读写;
  6. 处理暂停:用户确认与外部执行都会让智能体暂停并发出事件,把结果喂给下一次 call 即可恢复;
  7. 拿结果:需要程序化消费输出时,用结构化输出让智能体按指定 JSON Schema 返回。

下一步建议把模型与工具拆开看:第 6 章讲 .model(...) 背后的模型接入与切换供应商,第 7 章讲 Toolkit 里的工具怎么写、怎么被模型调用。

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

参考速查

本节收拢正文中引用过的查阅性内容,供配置时对照;理解主线不依赖本节。

ReActAgent.Builder 参数

参数 类型 默认值 描述
name String 必填 智能体标识符,用于消息和日志
sysPrompt String 必填 智能体的基础系统提示词
model Model 必填 用于推理的大语言模型(继承自 ChatModelBase)
toolkit Toolkit new Toolkit() 管理工具、MCP 客户端、技能和工具组
middlewares List<? extends MiddlewareBase> List.of() 应用于 agent / reasoning / acting / model call / system prompt 钩子
stateStore AgentStateStore null(不持久化) 配置后 agent 在每次 call 后自动加载/保存 AgentState,按该次调用 RuntimeContext 的 (userId, sessionId) 寻址
defaultSessionId String agent name 当某次调用的 RuntimeContext 没带 sessionId 时的兜底值
permissionContext PermissionContextState 默认 DEFAULT 模式 工具执行的细粒度规则,参见第 10 章(权限系统与人工介入)
maxIters int 10 ReAct 主循环最大迭代次数

stateStore 的配置示例见本章的配置状态持久化(AgentStateStore)一节;permissionContext 对应的判定规则见第 10 章。

延伸阅读