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 提供了两种方式来实现人类反馈:

  1. InterruptionMetadata 模式 :可以在任意节点随时中断,通过实现 InterruptableAction 接口来控制中断时机
  2. interruptBefore 模式:需要提前在编译配置中定义中断点,在指定节点执行前中断

HumanInTheLoopHook

  1. 实现InterruptableAction接口,因此在ReactAgent中选择的是InterruptionMetadata 模式,关注interrupt方法分析是如何决策出需要中断的,中断决策的"三道防线"

    • 第一道防线(内容检查):AI 是否要调工具?(否 -》 不中断)
    • 第二道防线(配置匹配):要调用的工具是否在 approvalOn 映射表中?(否 -》 不中断)
    • 第三道防线(反馈检查):如果需要审批,人类是否已经给出了有效的审批结果?(是 -》 不再中断,开始执行逻辑)
  2. 继承ModelHook类,在AFTER_MODEL位置进行拦截(即模型调用后),关注afterModel方法是如进行人工接介入的

    • 如果结果是 APPROVED:保留原有的 toolCall。

    • 如果结果是 EDITED:用人类修改后的参数创建一个 editedToolCall。

    • 如果结果是 REJECTED:向消息列表中注入一个 rejectedMessage(工具响应),告诉 AI "人类拒绝了这次操作,理由是..."。

  3. 实现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();
	}

}
相关推荐
wuqingshun3141591 小时前
说一下什么是fail-fast
java·开发语言·jvm
linux_cfan1 小时前
拒绝“黑屏”与“哑剧”:Web视频播放器UX体验与自动播放选型指南 (2026版)
前端·javascript·音视频·html5·ux
wuqingshun3141592 小时前
知道java NIO吗?和java IO有什么区别?
java·开发语言·jvm
小庄梦蝶2 小时前
宝塔使用nodejs管理器下载nodejs版本失败解决方式之一
linux·运维·前端
be or not to be2 小时前
假期js学习汇总
前端·javascript·学习
SuperEugene2 小时前
日期与时间处理:不用库和用 dayjs 的两种思路
前端·javascript
Zik----2 小时前
Leetcode22 —— 括号生成
java·开发语言
芒克芒克2 小时前
深入浅出Java线程池(三)
java·开发语言
A懿轩A2 小时前
【Java 基础编程】Java 常用类速查:包装类、String/StringBuilder、Math、日期类一篇搞定
java·开发语言·python·java常用类