SAA ReactAgent工作原理
文章目录
- [SAA ReactAgent工作原理](#SAA ReactAgent工作原理)
核心执行流程
Spring AI Alibaba 中的ReactAgent 基于 Graph 运行时构建。Graph 由节点(Nodes)和条件边(ConditionalEdges)组成,定义了 Agent 如何处理信息。Agent 在这个 Graph 中移动,执行如下节点:
- AgentLlmNode (模型节点):调用 LLM 进行推理和决策
- AgentToolNode (工具节点):执行工具扩展ToolInterceptors的调用
- HookNode (钩子节点) :Hooks 允许在 Agent 执行的关键点插入自定义逻辑,分为:AgentHook和ModelHook
- AgentHook:Agent 整体执行前后,只执行一次
- ModelHook:Agent 每次模型调用前后, Loop 循环执行
- ModelToToolsEdge (模型到工具的边) :判断是否需要进入工具节点,基于最后的Message是AssistantMessage还是ToolResponseMessage来判断
- AssistantMessage:如果AI消息中有工具调用,则进入AgentToolNode工具节点
- ToolResponseMessage:如果AI消息中的工具调用都有响应则进入模型节点AgentLlmNode ,否则进入AgentToolNode工具节点
- ToolsToModelEdge(工具到模型的边):观察是否进入模型节点,如果所有的工具执行都是响应return_direct则退出,否则进入模型节点

核心组件
- entryNode:全流程的总入口
- 如果有 AgentHook(BEFORE_AGENT)钩子(全局前置任务),入口就是第一个钩子。否则,看是否有 ModelHook(BEFORE_MODEL)钩子。再否则,入口就是大模型节点(AgentLlmNode)。
- loopEntryNode:ReAct 循环的起点
- 指向第一个 ModelHook(BEFORE_MODEL)钩子。如果没有钩子,直接指向大模型节点(AgentLlmNode)。
- loopExitNode:决策分发点,这个节点负责承接输出并准备进行路由判断:如果输出包含工具调用 去行动(Tool Node)。
- 指向第一个 ModelHook (AFTER_MODEL)钩子。如果没有钩子,直接指向大模型节点(AgentLlmNode)。
- exitNode:全流程的总出口
- 指向最后一个AgentHook AFTER_AGENT 钩子。如果没有钩子,直接指向 StateGraph.END。
- ModelInterceptor:在模型调用之前修改请求 - 多次调用处理程序(重试逻辑) - 在模型调用响应后 - 处理异常并提供回退方案
- ToolInterceptor:在工具调用之前修改请求 - 多次调用处理程序(重试逻辑) - 在工具调用响应 后- 添加缓存、日志记录、监控等功能。
- AgentLlmNode:大模型节点,负责接收上下文产生决策,充当的是决策大脑的角色
- AgentToolNode:负责执行具体的 Java 方法(Tools)。只有在 hasTools 为 true 时才会添加。

AgentLlmNode (模型节点)
- com.alibaba.cloud.ai.graph.agent.node.AgentLlmNode#apply
java
@Override
public Map<String, Object> apply(OverAllState state, RunnableConfig config) throws Exception {
// 校验管理迭代次数
final AtomicInteger iterations;
if (!config.context().containsKey(MODEL_ITERATION_KEY)) {
iterations = new AtomicInteger(0);
config.context().put(MODEL_ITERATION_KEY, iterations);
} else {
iterations = (AtomicInteger) config.context().get(MODEL_ITERATION_KEY);
iterations.incrementAndGet();
}
// 校验管理上下文消息
List<Message> messages = new ArrayList<>();
if (state.value("messages").isEmpty()) {
// 第一次处理没有消息,先添加用户输入的消息
if (state.value("input").isPresent()) {
messages.add(new UserMessage(state.value("input").get().toString()));
}
} else {
messages = (List<Message>) state.value("messages").get();
}
//通过输出格式schema增强消息
augmentUserMessage(messages, outputSchema);
//通过参数渲染模板消息
renderTemplatedUserMessage(messages, state.data());
// 创建模型请求
ModelRequest.Builder requestBuilder = ModelRequest.builder()
.messages(messages)
.options(chatOptions.copy())
.context(config.metadata().orElse(new HashMap<>()));
// 提取工具名称和描述
if (toolCallbacks != null && !toolCallbacks.isEmpty()) {
List<String> toolNames = new ArrayList<>();
Map<String, String> toolDescriptions = new HashMap<>();
for (ToolCallback callback : toolCallbacks) {
String name = callback.getToolDefinition().name();
String description = callback.getToolDefinition().description();
toolNames.add(name);
if (description != null && !description.isEmpty()) {
toolDescriptions.put(name, description);
}
}
requestBuilder.tools(toolNames);
requestBuilder.toolDescriptions(toolDescriptions);
}
if (StringUtils.hasLength(this.systemPrompt)) {
requestBuilder.systemMessage(new SystemMessage(this.systemPrompt));
}
ModelRequest modelRequest = requestBuilder.build();
// 流式模式支持
boolean stream = config.metadata("_stream_", new TypeRef<Boolean>(){}).orElse(true);
if (stream) {
ModelCallHandler baseHandler = request -> {
try {
// 注意点:这里禁用了spring-ai的自动工具调用chatOptions.setInternalToolExecutionEnabled(false);
Flux<ChatResponse> chatResponseFlux = buildChatClientRequestSpec(request).stream().chatResponse();
return ModelResponse.of(chatResponseFlux);
} catch (Exception e) {
logger.error("Exception during streaming model call: ", e);
return ModelResponse.of(new AssistantMessage("Exception: " + e.getMessage()));
}
};
// 构建模型调用链
ModelCallHandler chainedHandler = InterceptorChain.chainModelInterceptors(
modelInterceptors, baseHandler);
// 执行模型的调用链
ModelResponse modelResponse = chainedHandler.call(modelRequest);
return Map.of(StringUtils.hasLength(this.outputKey) ? this.outputKey : "messages", modelResponse.getMessage());
} else {
// Create base handler that actually calls the model
ModelCallHandler baseHandler = request -> {
try {
ChatResponse response = buildChatClientRequestSpec(request).call().chatResponse();
AssistantMessage responseMessage = new AssistantMessage("Empty response from model for unknown reason");
if (response != null && response.getResult() != null) {
responseMessage = response.getResult().getOutput();
}
return ModelResponse.of(responseMessage, response);
} catch (Exception e) {
logger.error("Exception during invoking model call: ", e);
return ModelResponse.of(new AssistantMessage("Exception: " + e.getMessage()));
}
};
// 构建模型调用链
ModelCallHandler chainedHandler = InterceptorChain.chainModelInterceptors(
modelInterceptors, baseHandler);
// 执行模型的调用链
ModelResponse modelResponse = chainedHandler.call(modelRequest);
Usage tokenUsage = modelResponse.getChatResponse() != null ? modelResponse.getChatResponse().getMetadata()
.getUsage() : new EmptyUsage();
Map<String, Object> updatedState = new HashMap<>();
updatedState.put("_TOKEN_USAGE_", tokenUsage);
updatedState.put("messages", modelResponse.getMessage());
if (StringUtils.hasLength(this.outputKey)) {
updatedState.put(this.outputKey, modelResponse.getMessage());
}
return updatedState;
}
}
AgentToolNode (工具节点)
- com.alibaba.cloud.ai.graph.agent.node.AgentToolNode#apply
java
@Override
public Map<String, Object> apply(OverAllState state, RunnableConfig config) throws Exception {
//获取最后一条消息
List<Message> messages = (List<Message>) state.value("messages").orElseThrow();
Message lastMessage = messages.get(messages.size() - 1);
Map<String, Object> updatedState = new HashMap<>();
Map<String, Object> extraStateFromToolCall = new HashMap<>();
if (lastMessage instanceof AssistantMessage assistantMessage) {
// 最后一条消息是AI消息
List<ToolResponseMessage.ToolResponse> toolResponses = new ArrayList<>();
for (AssistantMessage.ToolCall toolCall : assistantMessage.getToolCalls()) {
// 执行工具调用链 并执行工具
ToolCallResponse response = executeToolCallWithInterceptors(toolCall, state, config, extraStateFromToolCall);
toolResponses.add(response.toToolResponse());
}
ToolResponseMessage toolResponseMessage = ToolResponseMessage.builder().responses(toolResponses)
.build();
updatedState.put("messages", toolResponseMessage);
} else if (lastMessage instanceof ToolResponseMessage toolResponseMessage) {
// 最后一条消息是工具响应消息,倒数第二条消息必定是AI消息,否则抛出异常
if (messages.size() < 2) {
throw new IllegalStateException("Cannot find AssistantMessage before ToolResponseMessage");
}
Message secondLastMessage = messages.get(messages.size() - 2);
if (!(secondLastMessage instanceof AssistantMessage assistantMessage)) {
throw new IllegalStateException("Message before ToolResponseMessage is not an AssistantMessage");
}
List<ToolResponseMessage.ToolResponse> existingResponses = toolResponseMessage.getResponses();
List<ToolResponseMessage.ToolResponse> allResponses = new ArrayList<>(existingResponses);
Set<String> executedToolIds = existingResponses.stream()
.map(ToolResponseMessage.ToolResponse::id)
.collect(Collectors.toSet());
for (AssistantMessage.ToolCall toolCall : assistantMessage.getToolCalls()) {
// 排除掉已经执行过的
if (executedToolIds.contains(toolCall.id())) {
continue;
}
// 执行工具调用链 并执行工具
ToolCallResponse response = executeToolCallWithInterceptors(toolCall, state, config, extraStateFromToolCall);
allResponses.add(response.toToolResponse());
}
List<Object> newMessages = new ArrayList<>();
ToolResponseMessage newToolResponseMessage =
ToolResponseMessage.builder().responses(allResponses).build();
newMessages.add(newToolResponseMessage);
newMessages.add(new RemoveByHash<>(toolResponseMessage));
updatedState.put("messages", newMessages);
} else {
throw new IllegalStateException("Last message is neither an AssistantMessage nor an ToolResponseMessage");
}
// Merge extra state from tool calls
updatedState.putAll(extraStateFromToolCall);
return updatedState;
}
ModelToToolsEdge
- com.alibaba.cloud.ai.graph.agent.ReactAgent#makeModelToTools
- 基于最后一条判断:
- 如果是AI消息,有调用工具则去工具节点,否则进入结束节点
- 如果是工具响应消息:所有工具都执行完成则进入ReAct循环的起点,否则继续工具节点
java
private EdgeAction makeModelToTools(String modelDestination, String endDestination) {
return state -> {
List<Message> messages = (List<Message>) state.value("messages").orElse(List.of());
if (messages.isEmpty()) {
return endDestination;
}
Message lastMessage = messages.get(messages.size() - 1);
// 1. 校验最后一条消息的类型
if (lastMessage instanceof AssistantMessage assistantMessage) {
// 2. 如果是AI消息,有调用工具则去工具节点,否则进入结束节点
if (assistantMessage.hasToolCalls()) {
return AGENT_TOOL_NAME;
} else {
return endDestination;
}
} else if (lastMessage instanceof ToolResponseMessage) {
// 3. 如果是工具响应消息:所有工具都执行完成则进入ReAct循环的起点,否则继续工具节点
if (messages.size() < 2) {
// Should not happen in a valid ReAct loop, but as a safeguard.
throw new RuntimeException("Less than 2 messages in state when last message is ToolResponseMessage");
}
Message secondLastMessage = messages.get(messages.size() - 2);
if (secondLastMessage instanceof AssistantMessage) {
AssistantMessage assistantMessage = (AssistantMessage) secondLastMessage;
ToolResponseMessage toolResponseMessage = (ToolResponseMessage) lastMessage;
if (assistantMessage.hasToolCalls()) {
Set<String> requestedToolIds = assistantMessage.getToolCalls().stream()
.map(AssistantMessage.ToolCall::id)
.collect(java.util.stream.Collectors.toSet());
Set<String> executedToolIds = toolResponseMessage.getResponses().stream()
.map(ToolResponseMessage.ToolResponse::id)
.collect(java.util.stream.Collectors.toSet());
if (executedToolIds.containsAll(requestedToolIds)) {
return modelDestination; // All requested tools were executed or responded
} else {
return AGENT_TOOL_NAME; // Some tools are still pending
}
}
}
}
return endDestination;
};
}
ToolsToModelEdge
- com.alibaba.cloud.ai.graph.agent.ReactAgent#makeToolsToModelEdge
java
private EdgeAction makeToolsToModelEdge(String modelDestination, String endDestination) {
return state -> {
// 1. 提取最后一条工具响应消息
ToolResponseMessage toolResponseMessage = fetchLastToolResponseMessage(state);
// 2. 提出条件: 所有的工具调用返回return_direct=True,进入结束节点
if (toolResponseMessage != null && !toolResponseMessage.getResponses().isEmpty()) {
boolean allReturnDirect = toolResponseMessage.getResponses().stream().allMatch(toolResponse -> {
String toolName = toolResponse.name();
return false; // FIXME
});
if (allReturnDirect) {
return endDestination;
}
}
// 3. 默认值:ReAct循环的起点,继续循环,工具执行成功完成,返回模型,以便模型可以处理工具结果并决定下一步操作。
return modelDestination;
};
}
private ToolResponseMessage fetchLastToolResponseMessage(OverAllState state) {
List<Message> messages = (List<Message>) state.value("messages").orElse(List.of());
ToolResponseMessage toolResponseMessage = null;
for (int i = messages.size() - 1; i >= 0; i--) {
if (messages.get(i) instanceof ToolResponseMessage) {
toolResponseMessage = (ToolResponseMessage) messages.get(i);
break;
}
}
return toolResponseMessage;
}
中断机制与人工介入 (Human-in-the-loop)
目前Spring AI Alibaba Graph 提供了两种方式来实现人类反馈:
- InterruptionMetadata 模式 :可以在任意节点随时中断,通过实现
InterruptableAction接口来控制中断时机 - interruptBefore 模式:需要提前在编译配置中定义中断点,在指定节点执行前中断
HumanInTheLoopHook
-
实现InterruptableAction接口,因此在ReactAgent中选择的是InterruptionMetadata 模式,关注interrupt方法分析是如何决策出需要中断的,中断决策的"三道防线"
- 第一道防线(内容检查):AI 是否要调工具?(否 -》 不中断)
- 第二道防线(配置匹配):要调用的工具是否在 approvalOn 映射表中?(否 -》 不中断)
- 第三道防线(反馈检查):如果需要审批,人类是否已经给出了有效的审批结果?(是 -》 不再中断,开始执行逻辑)
-
继承ModelHook类,在AFTER_MODEL位置进行拦截(即模型调用后),关注afterModel方法是如进行人工接介入的
-
如果结果是 APPROVED:保留原有的 toolCall。
-
如果结果是 EDITED:用人类修改后的参数创建一个 editedToolCall。
-
如果结果是 REJECTED:向消息列表中注入一个 rejectedMessage(工具响应),告诉 AI "人类拒绝了这次操作,理由是..."。
-
-
实现AsyncNodeActionWithConfig接口:支持携带配置的节点执行,直接调用afterModel方法
java
@HookPositions(HookPosition.AFTER_MODEL)
public class HumanInTheLoopHook extends ModelHook implements AsyncNodeActionWithConfig, InterruptableAction {
@Override
public CompletableFuture<Map<String, Object>> apply(OverAllState state, RunnableConfig config) {
//直接调用afterModel方法
return afterModel(state, config);
}
@Override
public CompletableFuture<Map<String, Object>> afterModel(OverAllState state, RunnableConfig config) {
//它从 RunnableConfig 中获取人类提交的 JSON 数据(包含每个工具的审批结果),移除Remove是确保反馈数据只被处理一次,防止在循环中出现重复逻辑。
Optional<InterruptionMetadata> feedback = config.getMetadataAndRemove(RunnableConfig.HUMAN_FEEDBACK_METADATA_KEY, new TypeRef<InterruptionMetadata>() { });
InterruptionMetadata interruptionMetadata = feedback.orElse(null);
if (interruptionMetadata == null) {
log.debug("No human feedback found in the runnable config metadata, no tool to execute or none needs feedback.");
return CompletableFuture.completedFuture(Map.of());
}
//它通过 getLastAssistantMessage 找到中断发生前,AI 产出的那个包含 tool_calls 的消息。
AssistantMessage assistantMessage = getLastAssistantMessage(state);
if (assistantMessage != null) {
if (!assistantMessage.hasToolCalls()) {
return CompletableFuture.completedFuture(Map.of());
}
List<AssistantMessage.ToolCall> newToolCalls = new ArrayList<>();
List<ToolResponseMessage.ToolResponse> responses = new ArrayList<>();
ToolResponseMessage rejectedMessage = ToolResponseMessage.builder().responses(responses).build();
//代码遍历 AI 提出的每一个工具调用(toolCall),并根据人类的反馈(ToolFeedback)决定如何修改这个指令:
for (AssistantMessage.ToolCall toolCall : assistantMessage.getToolCalls()) {
// 匹配人类对该特定 ID 工具调用的反馈
Optional<ToolFeedback> toolFeedbackOpt = interruptionMetadata.toolFeedbacks().stream()
.filter(tf -> tf.getId().equals(toolCall.id()))
.findFirst();
// 1. 同意:保留原始指令
if (toolFeedbackOpt.isPresent()) {
ToolFeedback toolFeedback = toolFeedbackOpt.get();
FeedbackResult result = toolFeedback.getResult();
if (result == FeedbackResult.APPROVED) {
newToolCalls.add(toolCall);
}
// 2. 修改:创建一个 ID 相同但参数不同(人类改过的)新指令
else if (result == FeedbackResult.EDITED) {
AssistantMessage.ToolCall editedToolCall = new AssistantMessage.ToolCall(toolCall.id(), toolCall.type(), toolCall.name(), toolFeedback.getArguments());
newToolCalls.add(editedToolCall);
}
// 3. 拒绝:标记该指令,并构造一个"虚假"的工具回执告知 AI 被拒
else if (result == FeedbackResult.REJECTED) {
// 依然保留 ID 对应关系
newToolCalls.add(toolCall);
ToolResponseMessage.ToolResponse response = new ToolResponseMessage.ToolResponse(toolCall.id(), toolCall.name(), String.format("Tool call request for %s has been rejected by human. The reason for why this tool is rejected and the suggestion for next possible tool choose is listed as below:\n %s.", toolFeedback.getName(), toolFeedback.getDescription()));
responses.add(response);
}
}
else {
// 如果某个需要审批的工具没有收到任何反馈,则视为已获批准,可以继续使用。
newToolCalls.add(toolCall);
}
}
Map<String, Object> updates = new HashMap<>();
List<Object> newMessages = new ArrayList<>();
if (!newToolCalls.isEmpty()) {
// A. 构造一个新的 AssistantMessage 替换掉旧的
newMessages.add(AssistantMessage.builder()
.content(assistantMessage.getText())
.properties(assistantMessage.getMetadata())
.toolCalls(newToolCalls)
.media(assistantMessage.getMedia())
.build());
// B. 使用 RemoveByHash 标记删除旧的、未审批的消息
newMessages.add(new RemoveByHash<>(assistantMessage));
}
// C. 如果有被拒绝的,把"拒绝信息"作为工具回执塞进去
if (!rejectedMessage.getResponses().isEmpty()) {
newMessages.add(rejectedMessage);
}
updates.put("messages", newMessages);
return CompletableFuture.completedFuture(updates);
}
else {
log.warn("Last message is not an AssistantMessage, cannot process human feedback.");
}
return CompletableFuture.completedFuture(Map.of());
}
@Override
public Optional<InterruptionMetadata> interrupt(String nodeId, OverAllState state, RunnableConfig config) {
AssistantMessage lastMessage = getLastAssistantMessage(state);
// 内容检查:如果大模型最后没说话,或者说话内容里没有要求调用工具,则不需要中断
if (lastMessage == null || !lastMessage.hasToolCalls()) {
return Optional.empty();
}
//当用户给出反馈(例如点击"同意")并恢复执行流时,引擎再次进入 interrupt 方法,但此时 RunnableConfig 中已经带了 HUMAN_FEEDBACK_METADATA_KEY
Optional<Object> feedback = config.metadata(RunnableConfig.HUMAN_FEEDBACK_METADATA_KEY);
if (feedback.isPresent()) {
if (!(feedback.get() instanceof InterruptionMetadata)) {
throw new IllegalArgumentException("Human feedback metadata must be of type InterruptionMetadata.");
}
// 反馈检查:验证反馈是否完整(用户是否已经对每个敏感工具都选了 APPROVED/REJECTED/EDITED)
if (!validateFeedback((InterruptionMetadata) feedback.get(), lastMessage.getToolCalls())) {
return buildInterruptionMetadata(state, lastMessage);
}
// 验证通过,反馈已准备好,不再中断,流程将进入 apply() 执行真正的反馈处理逻辑
return Optional.empty();
}
// 2. 配置匹配:要调用的工具是否在 approvalOn 映射表中
return buildInterruptionMetadata(state, lastMessage);
}
private Optional<InterruptionMetadata> buildInterruptionMetadata(OverAllState state, AssistantMessage lastMessage) {
boolean needsInterruption = false;
InterruptionMetadata.Builder builder = InterruptionMetadata.builder(Hook.getFullHookName(this), state);
for (AssistantMessage.ToolCall toolCall : lastMessage.getToolCalls()) {
// 关键决策:当前的工具名是否在配置类中通过 .approvalOn("xxx") 注册过?
if (approvalOn.containsKey(toolCall.name())) {
ToolConfig toolConfig = approvalOn.get(toolCall.name());
String description = toolConfig.getDescription();
String content = "The AI is requesting to use the tool: " + toolCall.name() + ".\n"
+ (description != null ? ("Description: " + description + "\n") : "")
+ "With the following arguments: " + toolCall.arguments() + "\n"
+ "Do you approve?";
// TODO, create a designated tool metadata field in InterruptionMetadata?
builder.addToolFeedback(ToolFeedback.builder().id(toolCall.id())
.name(toolCall.name()).description(content).arguments(toolCall.arguments()).build())
.build();
needsInterruption = true;
} else {
// 自动审批不需要人工介入的工具
builder.addToolsAutomaticallyApproved(toolCall);
}
}
return needsInterruption ? Optional.of(builder.build()) : Optional.empty();
}
}