一、本章介绍
本节将继续完成节点流转逻辑,从 AiApiNode 进入 ChatModelNode 模型对话节点,并实现 ChatModelNode 的实例化与装配过程。
二、流程设计
如图,智能体装配中 ChatModelNode 部分;

首先,该节点是在
AiApiNode执行完成后流转过来的。节点之间的衔接是在doApply方法处理结束后,通过执行router路由逻辑来完成的。路由过程中会调用当前实现类的get方法,以确定下一个需要执行的节点。通过这种方式,将业务处理与流程流转解耦,有助于提升整体代码的可维护性。接着,在进入
ChatModelNode后,借助 Spring AI 完成模型的构建。在此之前,需要从上下文中获取在AiApiNode中创建好的OpenAiApi实例,并将其作为参数注入到ChatModel的初始化过程中。另外,
ChatModelNode还涉及 MCP 相关的配置构建,这里是将 MCP 能力整合进模型中一并使用。这部分同样是基于 Spring AI 实现的。当然,除了 Spring AI,也可以选择使用 LangChain4j 或 Google ADK 来完成 MCP 相关的集成。
三、功能实现
1. 工程结构
新增
ChatModelNode节点,用于完成对话模型的构建,并将 MCP 工具集成到模型能力中。同时,需要将
ChatModelNode注入到AiApiNode中,使流程在AiApiNode执行完成后,能够顺利流转到ChatModelNode,从而完成节点之间的关联与衔接。
2. 核心模块
2.1 节点流转
@Slf4j
@Service
public class AiApiNode extends AbstractArmorySupport {
@Resource
private ChatModelNode chatModelNode;
@Override
protected AiAgentRegisterVO doApply(ArmoryCommandEntity requestParameter, DefaultArmoryFactory.DynamicContext dynamicContext) throws Exception {
log.info("Ai Agent 装配操作 - AiApiNode");
// ... 省略
return router(requestParameter, dynamicContext);
}
@Override
public StrategyHandler<ArmoryCommandEntity, DefaultArmoryFactory.DynamicContext, AiAgentRegisterVO> get(ArmoryCommandEntity requestParameter, DefaultArmoryFactory.DynamicContext dynamicContext) throws Exception {
return chatModelNode;
}
}- 在
AiApiNode中引入ChatModelNode,当doApply业务逻辑执行完成后,会通过router(requestParameter, dynamicContext)进入路由流程。随后,框架会调用当前节点的get方法,用于确定下一个需要执行的节点,此时返回的就是ChatModelNode,从而实现节点之间的顺序衔接与流转。
2.2 对话模型
@Slf4j
@Service
public class ChatModelNode extends AbstractArmorySupport {
@Override
protected AiAgentRegisterVO doApply(ArmoryCommandEntity requestParameter, DefaultArmoryFactory.DynamicContext dynamicContext) throws Exception {
log.info("Ai Agent 装配操作 - ChatModelNode");
OpenAiApi openAiApi = dynamicContext.getOpenAiApi();
AiAgentConfigTableVO aiAgentConfigTableVO = requestParameter.getAiAgentConfigTableVO();
AiAgentConfigTableVO.Module.ChatModel chatModelConfig = aiAgentConfigTableVO.getModule().getChatModel();
// 构建tool mcp
List<McpSyncClient> mcpSyncClients = new ArrayList<>();
List<AiAgentConfigTableVO.Module.ChatModel.ToolMcp> toolMcpList = chatModelConfig.getToolMcpList();
for (AiAgentConfigTableVO.Module.ChatModel.ToolMcp toolMcp : toolMcpList) {
mcpSyncClients.add(createMcpSyncClient(toolMcp));
}
ChatModel chatModel = OpenAiChatModel.builder()
.openAiApi(openAiApi)
.defaultOptions(OpenAiChatOptions.builder()
.model(chatModelConfig.getModel())
.toolCallbacks(SyncMcpToolCallbackProvider.builder().mcpClients(mcpSyncClients).build().getToolCallbacks())
.build())
.build();
dynamicContext.setChatModel(chatModel);
return router(requestParameter, dynamicContext);
}
@Override
public StrategyHandler<ArmoryCommandEntity, DefaultArmoryFactory.DynamicContext, AiAgentRegisterVO> get(ArmoryCommandEntity requestParameter, DefaultArmoryFactory.DynamicContext dynamicContext) throws Exception {
return defaultStrategyHandler;
}
private McpSyncClient createMcpSyncClient(AiAgentConfigTableVO.Module.ChatModel.ToolMcp toolMcp) {
AiAgentConfigTableVO.Module.ChatModel.ToolMcp.SSEServerParameters sseConfig = toolMcp.getSse();
AiAgentConfigTableVO.Module.ChatModel.ToolMcp.StdioServerParameters stdioConfig = toolMcp.getStdio();
if (null != sseConfig) {
// https://127.0.0.1:9999/sse?apikey=DElk89iu8Ehhnbu
String originalBaseUri = sseConfig.getBaseUri();
String baseUri;
String sseEndpoint;
int queryParamStartIndex = originalBaseUri.indexOf("sse");
if (queryParamStartIndex != -1) {
baseUri = originalBaseUri.substring(0, queryParamStartIndex - 1);
sseEndpoint = originalBaseUri.substring(queryParamStartIndex - 1);
} else {
baseUri = originalBaseUri;
sseEndpoint = sseConfig.getSseEndpoint();
}
sseEndpoint = StringUtils.isBlank(sseEndpoint) ? "/sse" : sseEndpoint;
HttpClientSseClientTransport sseClientTransport = HttpClientSseClientTransport
.builder(baseUri) // 使用截取后的 baseUri
.sseEndpoint(sseEndpoint) // 使用截取或默认的 sseEndpoint
.build();
McpSyncClient mcpSyncClient = McpClient.sync(sseClientTransport).requestTimeout(Duration.ofMinutes(sseConfig.getRequestTimeout())).build();
var init_sse = mcpSyncClient.initialize();
log.info("Tool SSE MCP Initialized {}", init_sse);
return mcpSyncClient;
}
if (null != stdioConfig) {
AiAgentConfigTableVO.Module.ChatModel.ToolMcp.StdioServerParameters.ServerParameters serverParameters = stdioConfig.getServerParameters();
// https://github.com/modelcontextprotocol/servers/tree/main/src/filesystem
var stdioParams = ServerParameters.builder(serverParameters.getCommand())
.args(serverParameters.getArgs())
.env(serverParameters.getEnv())
.build();
var mcpClient = McpClient.sync(new StdioClientTransport(stdioParams, new JacksonMcpJsonMapper(new ObjectMapper())))
.requestTimeout(Duration.ofSeconds(stdioConfig.getRequestTimeout())).build();
var init_stdio = mcpClient.initialize();
log.info("Tool Stdio MCP Initialized {}", init_stdio);
return mcpClient;
}
throw new RuntimeException("toolMcp sse and stdio is null!");
}
}进入
doApply方法后,开始进行ChatModel的构建。一个完整的ChatModel初始化通常依赖多项参数,包括openAiApi以及toolCallbacks(即 MCP 相关能力)。其中,openAiApi可以直接从上下文中获取,而 MCP 则需要额外进行组装。MCP 主要分为
SSE和stdio两种类型,因此需要封装一个createMcpSyncClient方法,用于根据不同类型创建对应的 MCP 客户端。对于基于 SSE 的 MCP 服务,通常会涉及sseEndpoint端点配置。如果用户提供的是完整 URL,这里需要对地址进行适当拆分处理,例如提取最后的sse作为端点(当然,如果服务端有其他规则,比如从特定路径开始作为端点,也需要额外做兼容处理)。另一类是stdio方式的 MCP 服务,其构建逻辑相对独立。最后,在完成
ChatModel实例创建后,需要将其写入到上下文中,以便后续节点继续复用该模型实例。