很多 Java 开发者第一次看 AgentScope Java,最容易产生的错觉是,这不就是把大模型 API 包了一层吗?
你看快速开始里的代码,好像确实很像。
ReActAgent agent = ReActAgent.builder()
.name("Assistant")
.sysPrompt("You are a helpful AI assistant.")
.model(DashScopeChatModel.builder()
.apiKey(System.getenv("DASHSCOPE_API_KEY"))
.modelName("qwen-max")
.build())
.build();
Msg response = agent.call(Msg.builder()
.textContent("Hello!")
.build()).block();
System.out.println(response.getTextContent());这段代码在 README_zh.md 里就能看到。
如果只看调用姿势,很容易把 agent.call(...) 理解成一次 chat(...)。
用户输入进去。
模型输出回来。
完事。
但如果真这么理解,后面看工具调用、Memory、Hook、HITL、多 Agent、Session 持久化,就会一路拧巴。
因为在 AgentScope Java 里,模型调用只是 Agent 运行过程中的一个环节。
不是全部。

这篇先不讲复杂概念,我们就从 Java 后端开发者最熟悉的东西说起,普通方法调用。
如果你写一个传统后端接口,流程大概是这样的。
Controller
↓
Service
↓
DAO / Remote Client
↓
Response这个链路的特点是,下一步去哪,是代码写死的。
用户请求进来,Controller 调哪个 Service,Service 调哪个 Repository,要不要查缓存,要不要写数据库,通常都是开发者提前决定好的。
最多是中间有几个 if else。
但 Agent 不一样。
Agent 的关键差异不在于它也会调模型。
而在于,下一步做什么,有一部分是在运行时由模型决定的。
这个点对 Java 后端同学非常重要。
因为这会直接影响你怎么设计权限、审计、异常处理、并发隔离和系统边界。
先看普通模型调用。
在 AgentScope Java 的源码里,模型抽象是 io.agentscope.core.model.Model。
它的核心接口非常克制。
public interface Model {
Flux<ChatResponse> stream(
List<Msg> messages,
List<ToolSchema> tools,
GenerateOptions options);
String getModelName();
}也就是说,一次模型调用本身接收三类东西。
一组 Msg。
一组可选的 ToolSchema。
一份 GenerateOptions。
然后返回一个 Flux<ChatResponse>。
如果你不用 Agent,只直接调模型,代码大概会长这样。
DashScopeChatModel model = DashScopeChatModel.builder()
.apiKey(System.getenv("DASHSCOPE_API_KEY"))
.modelName("qwen-max")
.stream(false)
.build();
Msg userMsg = Msg.builder()
.role(MsgRole.USER)
.textContent("你好,介绍一下 AgentScope Java")
.build();
List<ChatResponse> responses = model.stream(
List.of(userMsg),
List.of(),
GenerateOptions.builder().build())
.collectList()
.block();这里为了演示用了 .block()。
在真正的 Web 服务、Agent 内部逻辑或者响应式链路里,不应该随手 .block(),AgentScope Java 本身就是基于 Reactor 的 Mono / Flux 来组织调用的。
这段直接模型调用,链路很短。
User Msg
↓
Model.stream(...)
↓
Formatter
↓
Provider HTTP API
↓
ChatResponse这个链路也不是说没价值。
比如 DashScopeChatModel 会把 AgentScope 的 Msg 转成 DashScope 请求格式,构造 DashScopeRequest,通过 HTTP client 调 DashScope API,再把响应解析成 ChatResponse。
OpenAIChatModel 也是类似思路,只是它走 OpenAI-compatible 请求结构,由对应的 Formatter 处理消息、工具和生成参数。
ChatModelBase.stream(...) 还会包一层 tracing,然后交给具体模型实现的 doStream(...)。
这些都很重要。
但它们解决的是模型适配问题。
不是 Agent 运行问题。

回到 ReActAgent。
同样是用户输入一句话,表面上你调用的是这个。
Msg response = agent.call(userMsg).block();但这个 call(Msg) 不是 ReActAgent 自己直接声明出来的孤立方法。
它来自 CallableAgent 的默认方法。
CallableAgent.call(Msg) 会把单条消息包成 List<Msg>,再进入核心的 call(List<Msg>)。
真正开始统一处理的是 AgentBase.call(List<Msg>)。
这个基类做的第一件事,就已经不像普通 SDK 调用了。
它会进入执行占用控制,调用 beforeAgentExecution(...),触发 PreCallEvent,再调用子类的 doCall(...),最后触发 PostCallEvent,中间还接入 tracing 和错误处理。
也就是说,还没轮到 ReAct 逻辑,AgentScope Java 已经把一次调用放进了一个 Agent 生命周期里。
到 ReActAgent.doCall(...),事情才真正变得有意思。
它会先检查当前 Memory 里有没有还没完成的工具调用。
如果没有 pending tool,就把用户输入写入 Memory,然后从第 0 轮开始执行。
源码里的关键动作大概是这样。
protected Mono<Msg> doCall(List<Msg> msgs) {
Set<String> pendingIds = getPendingToolUseIds();
if (pendingIds.isEmpty()) {
addToMemory(msgs);
return executeIteration(0);
}
// 有 pending tool 时进入恢复或校验逻辑
}注意这里。
用户消息不是简单拼进 prompt。
它先进入 Memory。
然后 Agent 从 Memory 里取上下文,进入 ReAct 循环。
executeIteration(0) 实际上会走到 reasoning(...)。
在 reasoning(...) 里,ReActAgent 会触发 PreReasoningEvent,拿到有效的 GenerateOptions,把系统消息拼到模型输入前面,然后调用模型。
关键调用长这样。
return model.stream(modelInput, toolkit.getToolSchemas(), options)
.concatMap(chunk -> checkInterruptedAsync().thenReturn(chunk));这行很重要。
普通模型调用通常只关心 messages。
但 ReActAgent 调模型时,会把 toolkit.getToolSchemas() 一起交给模型。
这就是 Agent 和普通 Chat API 的一个分水岭。

模型拿到的不只是用户问题。
它还拿到了当前 Agent 可以使用哪些工具、这些工具叫什么、参数结构是什么。
模型的输出,也不一定只是文本。
它可能输出 TextBlock。
也可能输出 ThinkingBlock。
还可能输出 ToolUseBlock。
而 Msg 的设计,正是为了表达这些结构。
Msg 不是一个字符串。
它有 id、name、role、content、metadata、timestamp。
其中 content 是 ContentBlock 列表。
中文快速开始文档里也明确列了几类内容块,TextBlock、ImageBlock、AudioBlock、VideoBlock、ThinkingBlock、ToolUseBlock、ToolResultBlock。
所以 AgentScope Java 不是把字符串丢给模型,再拿字符串回来。
它在维护一个结构化消息流。
这对工具调用尤其关键。
当 reasoning(...) 收到模型流式返回的 chunk 后,会通过 ReasoningContext 累积成最终的 Msg,然后触发 PostReasoningEvent,再把这条 assistant 消息写入 Memory。
接下来 ReActAgent 会判断这条消息是不是已经完成。
判断逻辑其实很直接。
private boolean isFinished(Msg msg) {
if (msg == null) {
return true;
}
List<ToolUseBlock> toolCalls = msg.getContentBlocks(ToolUseBlock.class);
return toolCalls.isEmpty();
}没有 ToolUseBlock,这轮就结束。
有 ToolUseBlock,说明模型不是要直接回答,而是要行动。
然后才进入 acting(...)。
这就是 ReAct 里的 Act。
acting(...) 会找出最近 assistant 消息里的 pending tool calls,触发 PreActingEvent,然后调用 executeToolCalls(...)。
真正执行工具的地方,是 Toolkit。
return toolkit.callTools(toolCalls, toolExecutionConfig, this, buildMergedToolContext())这也说明一个很容易误解的点。
模型不会直接执行 Java 方法。
模型只是输出一个结构化的工具调用意图,也就是 ToolUseBlock。
Java 代码再通过 Toolkit 找到对应工具,做参数转换、方法调用、结果转换。
工具执行完成后,ReActAgent 会用 ToolResultMessageBuilder 构造工具结果消息,经过 PostActingEvent,再写回 Memory。
然后呢?
不是直接把工具结果吐给用户。
而是进入下一轮 executeIteration(iter + 1)。
也就是说,模型会再次看到工具结果,再基于观察结果继续推理,最后生成真正给用户看的回答。
完整链路大概是这样。
User Msg
↓
CallableAgent.call(Msg)
↓
AgentBase.call(List<Msg>)
↓
PreCallEvent / tracing / execution guard
↓
ReActAgent.doCall(...)
↓
Memory.addMessage(userMsg)
↓
reasoning(...)
↓
model.stream(memory + systemMsg, toolkit.getToolSchemas(), options)
↓
Assistant Msg
↓
是否包含 ToolUseBlock
↓
如果没有,直接返回
↓
如果有,acting(...)
↓
Toolkit.callTools(...)
↓
ToolResultBlock 写回 Memory
↓
下一轮 reasoning(...)
↓
Final Response这条链路一跑起来,就已经不是一次普通模型调用了。
它更像一个小型运行时。
有输入消息。
有短期记忆。
有系统提示。
有模型适配层。
有工具 schema。
有工具执行器。
有 Hook 生命周期。
有中断检查。
有最大迭代次数。
有工具失败时的错误结果回填。
这也是为什么我觉得,Java 开发者理解 AgentScope Java,第一步不是背 API,而是先把心智模型从 SDK 调用切到 Runtime。
普通模型 SDK 解决的是,我怎么把消息发给模型。
Agent Runtime 解决的是,模型决定下一步之后,我怎么让这件事在 Java 程序里可执行、可观察、可中断、可恢复。
这俩不是一个层级的问题。
我们可以拿传统后端做个类比。
对比项 | 普通 Java 后端流程 | AgentScope Java ReActAgent |
|---|---|---|
下一步由谁决定 | 代码提前写死 | 模型在 reasoning 阶段根据上下文和工具 schema 决定 |
核心输入 | DTO / Request |
|
核心输出 | Response DTO | 最终 |
外部能力调用 | Service 直接调用 Client / DAO | 模型先提出工具调用, |
状态管理 | 通常由数据库、缓存、Session 管 | Agent 实例持有 Memory、Toolkit 状态和配置 |
生命周期扩展 | Filter、Interceptor、AOP | PreCall、PreReasoning、PreActing、PostActing 等 Hook |
风险点 | 参数校验、事务、权限 | 还要额外考虑工具选择、上下文污染、HITL、最大迭代、并发隔离 |
看完这个表,你大概能理解为什么不能把 Agent 直接做成一个普通无状态 Service。
项目中文概念文档里也写得很直接,AgentScope 中的 Agent 是有状态对象,每个 Agent 实例持有自己的 Memory、Toolkit 和配置。
AgentBase 的源码注释也强调,Agent 实例不是为并发执行设计的,同一个实例不应该被多个线程同时 call() 或 stream()。
这不是文档洁癖。
这是 Agent 这种运行方式自然带来的结果。
因为 Memory 在变。
Toolkit 的 active groups 可能在变。
Hook 可能改变执行过程。
工具结果会写回上下文。
如果你把一个带 Memory 的 ReActAgent 当成 Spring 单例,多个用户共享同一个 Agent 实例,那就很容易出现上下文串话。
用户 A 的订单问题,跑进用户 B 的推理上下文里。
这事儿听着就后背发凉。

所以从企业项目角度看,第一篇最想留下的结论其实很简单。
AgentScope Java 不是让你少写一层 DashScope 或 OpenAI SDK 封装。
它的价值在于,把一次不可完全预先写死的模型驱动流程,放进 Java 工程可以管理的运行框架里。
这个框架至少帮你处理了几件事。
能力 | 项目里的实际对应 |
|---|---|
消息结构化 |
|
模型适配 |
|
ReAct 循环 |
|
工具暴露与执行 |
|
执行生命周期 |
|
状态管理 |
|
如果只调模型,你面对的是一个请求响应问题。
如果运行 Agent,你面对的是一个动态流程治理问题。
这就是差异。
当然,不是每个场景都需要 Agent。
如果你的业务只是把用户输入润色一下,或者做一个固定格式的摘要,一次普通模型调用完全够用。
甚至更简单、更稳定、更便宜。
但只要你的需求开始出现这些特征,Agent 框架的价值就会变明显。
用户的问题需要拆步骤。
模型需要判断是否查数据库。
需要调用多个工具。
工具结果要回填给模型继续推理。
关键写操作前要人工确认。
中间过程要审计。
不同用户的上下文要隔离。
执行到一半可能被中断,后面还要恢复。
这时你就不是在写一个 Chat API wrapper。
你是在写一个 Agent Runtime。
AgentScope Java 的 ReActAgent,本质上就是在 Java 里工程化实现这套运行时。
第一篇我们先把这个分界线画清楚。
普通模型调用是这样。
输入 -> 模型 -> 输出Agent 调用是这样。
输入 -> Memory -> 模型 -> 工具判断 -> 工具执行 -> 结果回填 -> 再次推理 -> 输出这两行看着只差几个节点。
但对后端工程来说,差的是整个系统设计方式。
下一篇就可以顺着这条线继续往下拆。
我们不再泛泛聊 Agent,而是直接从 agent.call() 开始,一步一步看一个 ReActAgent 到底做了什么。
到那一步,你会发现,AgentScope Java 里很多看起来复杂的设计,其实都在服务同一个目标。
让模型驱动的动态流程,尽可能变成 Java 工程里可理解、可控制、可落地的东西。
这才是 AgentScope Java 和普通模型 SDK 最大的区别。