在 Solon AI 中,智能体(Agent)提供了两种主要的交互模式:同步等待的 call() 和异步流式的 stream()。这两种接口分别对应了不同的业务场景。
1、交互模式对比
| 模式 | 方法 | 返回类型 | 适用场景 |
| 同步 | call() | AgentResponse | 后台任务、批量处理、不需要实时展示过程 |
| 流式 | stream() | Flux<AgentEvent> | Web 对话界面、实时展示思考过程、进度反馈 |
2、同步调用(call)
ReActAgent agent = ReActAgent.of(chatModel)
.defaultToolAdd(new WeatherTools())
.build();
// 同步获取最终结果(阻塞直到推理完成)
AgentResponse resp = agent.prompt("北京今天天气怎么样?")
.call();
System.out.println(resp.getContent());
AgentResponse 主要方法
| 方法 | 描述 |
getContent() | 获取最终答案文本 |
getMessage() | 获取原始 Message 对象(含 role/content) |
getTrace() | 获取推理轨迹(思考步骤、工具调用记录) |
getSession() | 获取当前绑定的会话对象 |
getContext() | 获取计算图上下文(任务状态快照,TeamAgent 可用) |
getMetrics() | 获取执行指标(Token 消耗、耗时等) |
toBean(Class<T>) | 将答案内容反序列化为结构化对象(JSON 输出场景) |
不同智能体的响应子类:
| 响应类型 | 对应智能体 | 说明 |
SimpleResponse | SimpleAgent | 简单直接调用结果 |
ReActResponse | ReActAgent | 含完整推理轨迹 |
TeamResponse | TeamAgent | 含各子节点执行结果 |
3、流式响应(stream)
stream() 基于 Project Reactor 的 Flux<AgentEvent> 返回事件流,前端可据此逐帧渲染思考过程、工具调用和最终答案,实现类 ChatGPT 的打字机效果。
Flux<AgentEvent> events = agent.prompt("北京今天天气怎么样?")
.stream();
events.doOnNext(event -> {
if (event instanceof ReasonDeltaEvent e) {
System.out.print("[思考] " + e.getContent()); // 实时渲染"灰色思考气泡"
} else if (event instanceof ToolCallStartEvent e) {
System.out.println("[调用工具] " + e.getToolName() + " 参数: " + e.getToolArgs());
} else if (event instanceof ToolCallEndEvent e) {
System.out.println("[工具结果] " + e.getToolName() + " → " + e.getContent());
} else if (event instanceof RunEndEvent e) {
System.out.println("[最终答案] " + e.getContent()); // 渲染为正式回答气泡
}
})
.doOnError(err -> System.err.println("流式错误: " + err.getMessage()))
.blockLast();
关于异常处理:call() 会在异常时直接抛给调用方;而 stream() 的异常不会自动传播到外层 —— 必须在 doOnError 或 onErrorResume 中主动捕获,否则异常将静默丢失。
全事件对照表
| 归属智能体 | 事件类型 | 触发时机 | 最低版本 |
| 所有 | AgentEvent | 事件接口基类 | — |
| SimpleAgent | SimpleStartEvent | 运行开始 | v4.0.4+ |
| | ChatDeltaEvent | 生成增量 | — |
| | SimpleEndEvent | 运行结束 | — |
| ReActAgent | RunStartEvent | 推理循环开始 | v4.0.4+ |
| | ReasonStartEvent | 每轮思考开始 | v4.0.4+ |
| | ReasonDeltaEvent | 思考过程增量(含 isThinking 深度思考标志) | v4.0.4+ |
| | PlanEvent | 计划制定/更新/修订(仅 PlanReAct 模式) | — |
| | ActionStartEvent | 工具批次开始 | — |
| | ToolCallStartEvent | 单工具调用开始 | v4.0.4+ |
| | ToolCallEndEvent | 单工具调用结束,含执行结果 | v4.0.4+ |
| | ActionEndEvent | 工具批次结束 | — |
| | ReasonEndEvent | 每轮思考结束,含完整输出 | v4.0.4+ |
| | ContextSizeEvent | 上下文大小检查(压缩时标记) | v4.0.0+ |
| | HITLPendingEvent | 人工审批挂起 | v4.0.4+ |
| | HITLDecidedEvent | 人工审批决策返回 | v4.0.4+ |
| | RunEndEvent | 推理循环结束,含最终答案 | — |
| TeamAgent | TeamStartEvent | 团队任务开始 | v4.0.4+ |
| | NodeStartEvent | 子节点开始 | — |
| | SupervisorDeltaEvent | Supervisor 决策增量 | v4.0.4+ |
| | NodeEndEvent | 子节点结束 | — |
| | TeamEndEvent | 团队任务结束 | v4.0.4+ |
TeamAgent 内部的子智能体(ReActAgent / SimpleAgent)也会各自发出其完整事件链。因此,订阅 TeamAgent 的流时可能收到上表中的所有事件类型,可通过 event.getAgentName() 区分来源。
4、AgentEvent 公共接口
所有事件均实现 AgentEvent 接口,提供以下基础方法:
| 方法 | 描述 |
getRunId() | 本次推理运行的唯一 ID |
getAgentName() | 产生该事件的智能体名称 |
getSession() | 所属会话对象 |
getMessage() | 关联的消息对象(部分事件可能为 null) |
getMeta() | 事件元数据(Map 结构,用于扩展信息) |
hasMeta() | 是否包含元数据 |
getContent() | 事件内容文本(各事件含义不同,见下文) |
hasContent() | 是否有内容文本 |
5、SimpleAgent 事件详解
SimpleAgent 流式时事件顺序固定:SimpleStartEvent → ChatDeltaEvent(×N) → SimpleEndEvent
SimpleStartEvent — 运行开始
events.doOnNext(event -> {
if (event instanceof SimpleStartEvent e) {
System.out.println("SimpleAgent [" + e.getAgentName() + "] 开始运行, runId=" + e.getRunId());
}
});
| 字段/方法 | 说明 |
getRunId() | 本次运行 ID |
getAgentName() | 智能体名称 |
ChatDeltaEvent — 内容增量块
流式推送期间持续触发,每次携带一小段文本(token 粒度)。
StringBuilder sb = new StringBuilder();
events.doOnNext(event -> {
if (event instanceof ChatDeltaEvent e) {
sb.append(e.getContent()); // 累积增量片段
System.out.print(e.getContent()); // 实时打印,模拟打字机效果
}
});
| 字段/方法 | 说明 |
getContent() | 当前增量文本片段(非累积,需自行拼接) |
SimpleEndEvent — 运行结束
events.doOnNext(event -> {
if (event instanceof SimpleEndEvent e) {
System.out.println("\n[完整答案] " + e.getContent());
System.out.println("[Token 消耗] " + e.getMetrics());
}
});
| 字段/方法 | 说明 |
getContent() | 完整的聚合答案(所有增量拼接后的结果) |
getMessage() | 完整 Message 对象 |
getMetrics() | 执行指标(耗时、token 数等) |
6、ReActAgent 事件详解
ReActAgent 每轮推理的典型事件顺序:
RunStartEvent
→ ReasonStartEvent
→ ReasonDeltaEvent (×N) ← 思考增量
→ [PlanEvent] ← 仅 PlanReAct 模式
→ ReasonEndEvent
→ ActionStartEvent
→ ToolCallStartEvent (×M)
→ ToolCallEndEvent (×M)
→ ActionEndEvent
→ [ContextSizeEvent] ← 上下文过大时触发
→ ReasonStartEvent ← 下一轮继续...
...
RunEndEvent ← 最终答案
若启用了 HITL,HITLPendingEvent 会在某轮 ActionEndEvent 之后插入,等待人工决策后再继续。
RunStartEvent — 推理运行开始(v4.0.4+)
events.doOnNext(event -> {
if (event instanceof RunStartEvent e) {
System.out.println("推理开始 runId=" + e.getRunId());
}
});
| 字段/方法 | 说明 |
getRunId() | 本次运行唯一 ID(整个推理过程共享) |
getAgentName() | 智能体名称 |
ReasonStartEvent — 单轮思考开始(v4.0.4+)
每次 LLM 开始生成(包含多轮 ReAct 循环中的每一轮)时触发。
events.doOnNext(event -> {
if (event instanceof ReasonStartEvent e) {
System.out.println("第 " + e.getTurn() + " 轮思考开始");
}
});
| 字段/方法 | 说明 |
getTurn() | 当前是第几轮推理(从 1 开始) |
ReasonDeltaEvent — 思考增量块(v4.0.4+)
LLM 流式生成过程中持续触发。需通过 isThinking() 区分普通输出和深度思考(CoT)内容。
StringBuilder thought = new StringBuilder();
StringBuilder answer = new StringBuilder();
events.doOnNext(event -> {
if (event instanceof ReasonDeltaEvent e) {
if (e.isThinking()) {
// 深度思考内容(如 DeepSeek-R1、Claude 3.7 的 <thinking> 部分)
thought.append(e.getContent());
System.out.print("💭 " + e.getContent()); // 渲染为灰色小字
} else {
// 普通生成内容(Final Answer 前的推理文字 / Thought 部分)
answer.append(e.getContent());
System.out.print(e.getContent());
}
}
});
| 字段/方法 | 说明 |
getContent() | 当前增量文本片段 |
isThinking() | true 表示该片段属于模型深度思考(reasoning token);false 表示普通输出 |
PlanEvent — 规划事件(PlanReAct 模式专属)
仅在开启 planningMode(true) 的 PlanReActAgent 中出现,贯穿整个任务生命周期。
events.doOnNext(event -> {
if (event instanceof PlanEvent e) {
switch (e.getStage()) {
case CREATE -> System.out.println("[计划已制定]\n" + e.getContent());
case PROGRESS -> System.out.println("[计划更新]\n" + e.getContent());
case REVISE -> System.out.println("[计划已修订]\n" + e.getContent());
}
}
});
| 字段/方法 | 说明 |
getStage() | 规划阶段:CREATE(初次生成)/ PROGRESS(执行进度更新)/ REVISE(计划被修订) |
getContent() | 计划内容文本(Markdown 格式的步骤列表) |
ActionStartEvent — 工具调用批次开始
一轮思考完成后,若 LLM 决定调用工具,则先触发 ActionStartEvent,再依次触发各工具的事件。
events.doOnNext(event -> {
if (event instanceof ActionStartEvent e) {
System.out.println("--- 开始执行工具 (第 " + e.getTurn() + " 轮) ---");
}
});
ToolCallStartEvent — 单个工具调用开始(v4.0.4+)
events.doOnNext(event -> {
if (event instanceof ToolCallStartEvent e) {
System.out.println("调用工具: " + e.getToolName());
System.out.println(" 参数: " + e.getToolArgs()); // JSON 字符串
}
});
| 字段/方法 | 说明 |
getToolName() | 工具名称(即注册时的 name) |
getToolArgs() | 工具入参(JSON 字符串格式) |
ToolCallEndEvent — 单个工具调用结束(v4.0.4+)
events.doOnNext(event -> {
if (event instanceof ToolCallEndEvent e) {
System.out.println("工具 [" + e.getToolName() + "] 执行完毕");
System.out.println(" 结果: " + e.getContent());
if (e.isError()) {
System.err.println(" ⚠ 执行异常: " + e.getErrorMessage());
}
}
});
| 字段/方法 | 说明 |
getToolName() | 工具名称 |
getToolArgs() | 工具入参(同 ToolCallStartEvent) |
getContent() | 工具执行返回的观察结果(Observation) |
isError() | 工具是否执行失败 |
getErrorMessage() | 错误信息(isError() 为 true 时有值) |
ActionEndEvent — 工具调用批次结束
events.doOnNext(event -> {
if (event instanceof ActionEndEvent e) {
System.out.println("--- 工具执行完毕 (第 " + e.getTurn() + " 轮) ---");
}
});
ReasonEndEvent — 单轮思考结束(v4.0.4+)
本轮 LLM 推理完成后触发,包含完整的本轮输出内容(各 Delta 的聚合结果)。
events.doOnNext(event -> {
if (event instanceof ReasonEndEvent e) {
System.out.println("[第 " + e.getTurn() + " 轮推理完成]");
System.out.println(" 完整输出: " + e.getContent());
}
});
| 字段/方法 | 说明 |
getTurn() | 所在推理轮次 |
getContent() | 本轮 LLM 输出的完整文本(ReasonDelta 的聚合) |
getMessage() | 本轮完整 Message 对象 |
ContextSizeEvent — 上下文大小状态(v4.0.0+)
在每轮工具执行后触发,反映当前上下文窗口的使用情况。当触发压缩时,isCompressed() 为 true。
events.doOnNext(event -> {
if (event instanceof ContextSizeEvent e) {
System.out.printf("上下文: %d / %d tokens%n", e.getCurrentSize(), e.getMaxSize());
if (e.isCompressed()) {
System.out.println(" ↳ 已自动压缩历史上下文");
}
}
});
| 字段/方法 | 说明 |
getCurrentSize() | 当前上下文占用的 token 数 |
getMaxSize() | 配置的最大上下文 token 限制 |
isCompressed() | 本轮是否触发了上下文压缩 |
HITLPendingEvent — 人工介入挂起(v4.0.4+)
当智能体将要调用某个需要人工审批的工具时,推理暂停并发出此事件,等待人工介入决策。
events.doOnNext(event -> {
if (event instanceof HITLPendingEvent e) {
System.out.println("⏸ 推理已挂起,等待人工审批:");
e.getPendingList().forEach(item ->
System.out.println(" - 工具: " + item.getToolName() + " 参数: " + item.getToolArgs())
);
// 通知前端渲染审批 UI
}
});
| 字段/方法 | 说明 |
getPendingList() | 待审批的工具调用列表(List<ToolCallItem>) |
getSessionPending() | 会话挂起对象,可通过它提交审批决策 |
HITLDecidedEvent — 人工介入决策结果(v4.0.4+)
events.doOnNext(event -> {
if (event instanceof HITLDecidedEvent e) {
if (e.isApproved()) {
System.out.println("✅ 审批通过,继续执行");
} else {
System.out.println("❌ 审批拒绝,已跳过: " + e.getReason());
}
}
});
| 字段/方法 | 说明 |
isApproved() | 是否批准执行 |
getReason() | 拒绝时的理由文本(可选) |
RunEndEvent — 推理运行结束
events.doOnNext(event -> {
if (event instanceof RunEndEvent e) {
System.out.println("[最终答案] " + e.getContent());
System.out.println("[轮次] " + e.getTurns());
System.out.println("[Token] " + e.getMetrics());
}
});
| 字段/方法 | 说明 |
getContent() | 最终答案文本 |
getMessage() | 最终 Message 对象 |
getTurns() | 本次推理共经历的轮次数 |
getTrace() | 完整推理轨迹(ReActTrace) |
getMetrics() | 执行指标(耗时、token 消耗) |
7、TeamAgent 事件详解
TeamAgent 内部由多个子智能体协作完成任务,每个子节点的执行均会产生独立事件。
典型事件顺序:
TeamStartEvent
→ NodeStartEvent (子节点1)
→ [RunStartEvent ... RunEndEvent] ← 子节点内部的 ReActAgent 事件
→ NodeEndEvent (子节点1)
→ SupervisorDeltaEvent (×N) ← Supervisor 调度决策增量
→ NodeStartEvent (子节点2)
...
TeamEndEvent
TeamStartEvent — 团队任务开始(v4.0.4+)
events.doOnNext(event -> {
if (event instanceof TeamStartEvent e) {
System.out.println("团队任务开始: " + e.getAgentName() + " runId=" + e.getRunId());
}
});
| 字段/方法 | 说明 |
getRunId() | 本次运行 ID |
getAgentName() | TeamAgent 名称 |
NodeStartEvent — 子节点执行开始
某个子智能体(成员节点)被 Supervisor 指派开始执行时触发。
events.doOnNext(event -> {
if (event instanceof NodeStartEvent e) {
System.out.println("▶ 子智能体 [" + e.getNodeName() + "] 开始执行");
System.out.println(" 任务: " + e.getContent());
}
});
| 字段/方法 | 说明 |
getNodeName() | 正在执行的子智能体名称 |
getContent() | Supervisor 给该子节点分配的任务描述 |
SupervisorDeltaEvent — 调度者决策增量(v4.0.4+)
Supervisor(ReAct 或 LLM 协调器)在作出调度决策时流式输出其决策过程。
StringBuilder supervisorThought = new StringBuilder();
events.doOnNext(event -> {
if (event instanceof SupervisorDeltaEvent e) {
supervisorThought.append(e.getContent());
System.out.print("[Supervisor] " + e.getContent());
}
});
| 字段/方法 | 说明 |
getContent() | 调度决策的增量文本片段 |
NodeEndEvent — 子节点执行结束
events.doOnNext(event -> {
if (event instanceof NodeEndEvent e) {
System.out.println("■ 子智能体 [" + e.getNodeName() + "] 执行完毕");
System.out.println(" 输出: " + e.getContent());
}
});
| 字段/方法 | 说明 |
getNodeName() | 已执行完毕的子智能体名称 |
getContent() | 该子节点的输出结果 |
TeamEndEvent — 团队任务结束(v4.0.4+)
events.doOnNext(event -> {
if (event instanceof TeamEndEvent e) {
System.out.println("[团队最终结果] " + e.getContent());
System.out.println("[指标] " + e.getMetrics());
}
});
| 字段/方法 | 说明 |
getContent() | 整个团队协作的最终答案 |
getMessage() | 最终 Message 对象 |
getMetrics() | 执行指标(耗时、token 等) |
9、完整流式处理示例(综合版)
agent.prompt("帮我查北京今天的天气,并翻译成英文")
.stream()
.doOnNext(event -> {
if (event instanceof RunStartEvent e) {
System.out.println("=== 推理开始 ===");
} else if (event instanceof ReasonDeltaEvent e) {
if (e.isThinking()) {
System.out.print("\033[90m" + e.getContent() + "\033[0m"); // 深度思考渲染为灰色
} else {
System.out.print(e.getContent());
}
} else if (event instanceof ToolCallStartEvent e) {
System.out.printf("%n[→ 调用 %s] %s%n", e.getToolName(), e.getToolArgs());
} else if (event instanceof ToolCallEndEvent e) {
if (e.isError()) {
System.err.printf("[✗ %s 失败] %s%n", e.getToolName(), e.getErrorMessage());
} else {
System.out.printf("[← %s 返回] %s%n", e.getToolName(), e.getContent());
}
} else if (event instanceof ContextSizeEvent e) {
if (e.isCompressed()) {
System.out.printf("%n[上下文已压缩: %d → 压缩后]%n", e.getCurrentSize());
}
} else if (event instanceof HITLPendingEvent e) {
System.out.println("\n⏸ 等待人工审批...");
e.getPendingList().forEach(item ->
System.out.println(" 需审批: " + item.getToolName()));
} else if (event instanceof RunEndEvent e) {
System.out.println("\n=== 最终答案 ===");
System.out.println(e.getContent());
System.out.println("共 " + e.getTurns() + " 轮," + e.getMetrics());
}
})
.doOnError(err -> System.err.println("错误: " + err.getMessage()))
.blockLast();
10、开发建议
- UI 渲染分层:
ReasonDeltaEvent(isThinking=true)→ 灰色思考气泡;ReasonDeltaEvent(isThinking=false)→ 推理过程展示;RunEndEvent / SimpleEndEvent / TeamEndEvent → 正式答案气泡。
- 错误捕获:务必注册
doOnError / onErrorResume,否则 Reactor 流中的异常默认只打印 warn 日志,不会抛到调用方。
- 资源回收:使用
blockLast() 等阻塞操作时,确保在 Web 场景下使用 SSE/WebFlux 订阅,避免阻塞 IO 线程。
- 防死循环:为 ReActAgent 设置合理的
maxTurns,防止工具调用陷入死循环。
- TeamAgent 事件过滤:TeamAgent 的流会包含子节点内部的所有 ReAct 事件,建议通过
event.getAgentName() 区分来源。