动态实例化对话模型
一、介绍
整个 ChatModel 对话模型 依赖两类要素:AI API 与 MCP 工具(tool mcp)。本节将分别通过不同的 Node 节点 完成这些要素的实例化,并在此基础上完成 ChatModel 的构建。
二、功能流程
如图,ai api、tool mcp、model,实例化过程;

首先,如图所示,RootNode 负责数据加载,将各构建节点所需的数据依次载入内存(写入到 上下文)。
随后,在上一节完成 API 节点 处理的基础上,开始创建 MCP 服务,继而构建 ChatModel 对话模块。之所以按此顺序,是因为 ChatModel 的创建依赖已就绪的 API 与 MCP 两个要素。
三、编码实现
1. 工程结构

如图所示:
- 绿色部分:新增了
AiClientToolMcpNode与AiClientModelNode。 - 蓝色部分:修改了
RootNode、AiClientApiNode,以及抽象的装配支持类和数据仓库服务。
这些 XxxNode 节点 按照既定顺序依次调用,从一个节点流转到下一个节点,逐步完成各要素的初始化与装配。
2. 以下为本节涉及的主要修改点说明
AiClientModelVO增加toolMcpIds字段- 目的:在构建
Model对话对象时,能够明确关联需要加载的 MCP。 - 实现:
- 在
AiClientModelVO中新增private List<String> toolMcpIds;字段。 - 在
AgentRepository#AiClientModelVOByClientIds方法中,补充对AiClientModel的查询逻辑,并封装toolMcpIds数据。
- 在
- 目的:在构建
- 调整
AgentRepository#AiClientToolMcpVOByClientIds查询结果映射- 需求:查询结果中的
sse、stdio配置,需要分别映射到AiClientToolMcpVO的transportConfigSse和transportConfigStdio字段。 - 实现:
- 在查询方法中,对结果集进行字段映射处理。
- 确保
AiClientToolMcpVO对象能直接携带转换后的配置对象。
- 需求:查询结果中的
- 更新枚举值
AiAgentEnumVO中的AI_CLIENT_TOOL_MCP的 code 修改为tool_mcp,以保证与数据库中的配置值保持一致。
- 修复数据加载策略配置
- 在
AiClientLoadDataStrategy中,AI_CLIENT_SYSTEM_PROMPT与AI_CLIENT_TOOL_MCP的配置位置写反了,需要更正,保证与实际加载逻辑一致。
- 在
ArmoryCommandEntity新增方法- 增加
getLoadDataStrategy()方法。 - 在
RootNode中用此方法替换原有的硬编码逻辑,通过命令动态获取对应的加载策略,使代码结构更清晰。
- 增加
- 新增
AiClientToolMcpNode- 功能:用于构建 MCP 服务节点。
- 实现:根据
sse与stdio两种不同的 MCP 配置,分别实例化对应的实现类,并完成节点的装配。
- 新增
AiClientModelNode- 功能:负责从 Spring 容器中获取
AI API与Tool MCP的实例。 - 实现:
- 将获取到的实例与
Model中的元素关联。 - 完成对应的 Model 对象实例化。
- 将获取到的实例与
- 功能:负责从 Spring 容器中获取
- 数据库表结构调整:
ai_client_configmodel字段只需配置有效关联5003。- 其他无效的配置项请将
status= 0。 - 数据库更新语句将单独提供,执行后即可生效。
- 数据库表结构调整:
ai_client_apiapi_key字段需要填写有效的 Key 值。- 同时需要修改
application-dev.yml配置文件中的ai.openai.api-key,确保与数据库中的配置保持一致。
- 测试用例扩展
- 在
AgentTest测试类中,新增test_aiClientModelNode测试方法。 - 测试内容:验证
OpenAiChatModel是否能被正确构建,并能进行正常的对话交互。
3. 核心改动
3.1 AiClientApiNode
@Slf4j
@Service
public class AiClientApiNode extends AbstractArmorySupport {
@Resource
private AiClientToolMcpNode aiClientToolMcpNode;
@Override
protected String doApply(ArmoryCommandEntity requestParameter, DefaultArmoryStrategyFactory.DynamicContext dynamicContext) throws Exception {
log.info("Ai Agent 构建,API 构建节点 {}", JSON.toJSONString(requestParameter));
List<AiClientApiVO> aiClientApiList = dynamicContext.getValue(AiAgentEnumVO.AI_CLIENT_API.getDataName());
if (aiClientApiList == null || aiClientApiList.isEmpty()) {
log.warn("没有需要被初始化的 ai client api");
return null;
}
for (AiClientApiVO aiClientApiVO : aiClientApiList) {
// 构建 OpenAiApi
OpenAiApi openAiApi = OpenAiApi.builder()
.baseUrl(aiClientApiVO.getBaseUrl())
.apiKey(aiClientApiVO.getApiKey())
.completionsPath(aiClientApiVO.getCompletionsPath())
.embeddingsPath(aiClientApiVO.getEmbeddingsPath())
.build();
// 注册 OpenAiApi Bean 对象
registerBean(AiAgentEnumVO.AI_CLIENT_API.getBeanName(aiClientApiVO.getApiId()), OpenAiApi.class, openAiApi);
}
return router(requestParameter, dynamicContext);
}
@Override
public StrategyHandler<ArmoryCommandEntity, DefaultArmoryStrategyFactory.DynamicContext, String> get(ArmoryCommandEntity armoryCommandEntity, DefaultArmoryStrategyFactory.DynamicContext dynamicContext) throws Exception {
return aiClientToolMcpNode;
}
@Override
protected String beanName(String beanId) {
return AiAgentEnumVO.AI_CLIENT_API.getBeanName(beanId);
}
@Override
protected String dataName() {
return AiAgentEnumVO.AI_CLIENT_API.getDataName();
}
}在
AbstractArmorySupport中新增beanName()、dataName()两个抽象方法,让每个节点类各自实现,便于统一命名与后期维护。在节点实现中通过
dynamicContext.getValue(dataName())获取数据(如aiClientApiList),以dataName()为唯一入口,避免硬编码键名。AiClientApiNode执行完成后,路由到下一个节点AiClientToolMcpNode;在get()方法中新增对应的路由配置,确保流程按 API → MCP 的顺序推进。
3.2 AiClientToolMcpNode
@Slf4j
@Service
public class AiClientToolMcpNode extends AbstractArmorySupport {
@Resource
private AiClientModelNode aiClientModelNode;
@Override
protected String doApply(ArmoryCommandEntity requestParameter, DefaultArmoryStrategyFactory.DynamicContext dynamicContext) throws Exception {
log.info("Ai Agent 构建节点,Tool MCP 工具配置{}", JSON.toJSONString(requestParameter));
List<AiClientToolMcpVO> aiClientToolMcpList = dynamicContext.getValue(dataName());
if (aiClientToolMcpList == null || aiClientToolMcpList.isEmpty()) {
log.warn("没有需要被初始化的 ai client tool mcp");
return router(requestParameter, dynamicContext);
}
for (AiClientToolMcpVO mcpVO : aiClientToolMcpList) {
// 创建 MCP 服务
McpSyncClient mcpSyncClient = createMcpSyncClient(mcpVO);
// 注册 MCP 对象
registerBean(beanName(mcpVO.getMcpId()), McpSyncClient.class, mcpSyncClient);
}
return router(requestParameter, dynamicContext);
}
@Override
public StrategyHandler<ArmoryCommandEntity, DefaultArmoryStrategyFactory.DynamicContext, String> get(ArmoryCommandEntity armoryCommandEntity, DefaultArmoryStrategyFactory.DynamicContext dynamicContext) throws Exception {
return aiClientModelNode;
}
@Override
protected String beanName(String beanId) {
return AiAgentEnumVO.AI_CLIENT_TOOL_MCP.getBeanName(beanId);
}
@Override
protected String dataName() {
return AiAgentEnumVO.AI_CLIENT_TOOL_MCP.getDataName();
}
private McpSyncClient createMcpSyncClient(AiClientToolMcpVO aiClientToolMcpVO) {
String transportType = aiClientToolMcpVO.getTransportType();
switch (transportType) {
case "sse" -> {
AiClientToolMcpVO.TransportConfigSse transportConfigSse = aiClientToolMcpVO.getTransportConfigSse();
// http://127.0.0.1:9999/sse?apikey=DElk89iu8Ehhnbu
String originalBaseUri = transportConfigSse.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 = transportConfigSse.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(aiClientToolMcpVO.getRequestTimeout())).build();
var init_sse = mcpSyncClient.initialize();
log.info("Tool SSE MCP Initialized {}", init_sse);
return mcpSyncClient;
}
case "stdio" -> {
AiClientToolMcpVO.TransportConfigStdio transportConfigStdio = aiClientToolMcpVO.getTransportConfigStdio();
Map<String, AiClientToolMcpVO.TransportConfigStdio.Stdio> stdioMap = transportConfigStdio.getStdio();
AiClientToolMcpVO.TransportConfigStdio.Stdio stdio = stdioMap.get(aiClientToolMcpVO.getMcpName());
// https://github.com/modelcontextprotocol/servers/tree/main/src/filesystem
var stdioParams = ServerParameters.builder(stdio.getCommand())
.args(stdio.getArgs())
.env(stdio.getEnv())
.build();
var mcpClient = McpClient.sync(new StdioClientTransport(stdioParams))
.requestTimeout(Duration.ofSeconds(aiClientToolMcpVO.getRequestTimeout())).build();
var init_stdio = mcpClient.initialize();
log.info("Tool Stdio MCP Initialized {}", init_stdio);
return mcpClient;
}
}
throw new RuntimeException("err! transportType " + transportType + " not exist!");
}
}public McpSyncClient stdioMcpClient() {
// based on
// https://github.com/modelcontextprotocol/servers/tree/main/src/filesystem
// windows -- > npx.cmd -y @modelcontextprotocol/server-filesystem C:\path\to\serve C:\path\to\workspace
// mac/linux --> npx -y @modelcontextprotocol/server-filesystem /path/to/serve /path/to/workspace
var stdioParams = ServerParameters.builder("npx.cmd")
.args("-y", "@modelcontextprotocol/server-filesystem", "C:\\Users\\Dell\\Desktop", "C:\\Users\\Dell\\Desktop")
.build();
var mcpClient = McpClient.sync(new StdioClientTransport(stdioParams))
.requestTimeout(Duration.ofSeconds(10)).build();
var init = mcpClient.initialize();
System.out.println("Stdio MCP Initialized: " + init);
return mcpClient;
}
public McpSyncClient sseMcpClient01() {
HttpClientSseClientTransport sseClientTransport = HttpClientSseClientTransport.builder("http://192.168.1.23:8102").build();
McpSyncClient mcpSyncClient = McpClient.sync(sseClientTransport).requestTimeout(Duration.ofMinutes(180)).build();
var init = mcpSyncClient.initialize();
System.out.println("SSE MCP Initialized: " + init);
return mcpSyncClient;
}
public McpSyncClient sseMcpClient02() {
HttpClientSseClientTransport sseClientTransport = HttpClientSseClientTransport.builder("http://192.168.1.23:8101").build();
McpSyncClient mcpSyncClient = McpClient.sync(sseClientTransport).requestTimeout(Duration.ofMinutes(180)).build();
var init = mcpSyncClient.initialize();
System.out.println("SSE MCP Initialized: " + init);
return mcpSyncClient;
}stdioMcpClient、sseMcpClient01已有各自的编码实现;本节的目标是将原先手动创建的流程下沉到AiClientToolMcpNode中统一实现。抽取核心方法
createMcpSyncClient,根据sse/stdio两种传输方式分别创建对应的 MCP 客户端实例。在
doApply中调用createMcpSyncClient完成实例创建,并将生成的对象 注册到 Spring 容器,实现 MCP 工具的动态装配。
3.3 AiClientModelNode
@Slf4j
@Service
public class AiClientModelNode extends AbstractArmorySupport {
@Override
protected String doApply(ArmoryCommandEntity requestParameter, DefaultArmoryStrategyFactory.DynamicContext dynamicContext) throws Exception {
log.info("Ai Agent 构建节点,Mode 对话模型{}", JSON.toJSONString(requestParameter));
List<AiClientModelVO> aiClientModelList = dynamicContext.getValue(dataName());
if (aiClientModelList == null || aiClientModelList.isEmpty()) {
log.warn("没有需要被初始化的 ai client model");
return router(requestParameter, dynamicContext);
}
for (AiClientModelVO modelVO : aiClientModelList) {
// 获取当前模型关联的 API Bean 对象
OpenAiApi openAiApi = getBean(AiAgentEnumVO.AI_CLIENT_API.getBeanName(modelVO.getApiId()));
if (null == openAiApi) {
throw new RuntimeException("mode 2 api is null");
}
// 获取当前模型关联的 Tool MCP Bean 对象
List<McpSyncClient> mcpSyncClients = new ArrayList<>();
for (String toolMcpId : modelVO.getToolMcpIds()) {
McpSyncClient mcpSyncClient = getBean(AiAgentEnumVO.AI_CLIENT_TOOL_MCP.getBeanName(toolMcpId));
mcpSyncClients.add(mcpSyncClient);
}
// 实例化对话模型(如果有其他模型对接,可以使用 one-api 服务,转换为 openai 模型格式)
OpenAiChatModel chatModel = OpenAiChatModel.builder()
.openAiApi(openAiApi)
.defaultOptions(
OpenAiChatOptions.builder()
.model(modelVO.getModelName())
.toolCallbacks(new SyncMcpToolCallbackProvider(mcpSyncClients).getToolCallbacks())
.build())
.build();
// 注册 Bean 对象
registerBean(beanName(modelVO.getModelId()), OpenAiChatModel.class, chatModel);
}
return router(requestParameter, dynamicContext);
}
@Override
public StrategyHandler<ArmoryCommandEntity, DefaultArmoryStrategyFactory.DynamicContext, String> get(ArmoryCommandEntity armoryCommandEntity, DefaultArmoryStrategyFactory.DynamicContext dynamicContext) throws Exception {
return defaultStrategyHandler;
}
@Override
protected String beanName(String beanId) {
return AiAgentEnumVO.AI_CLIENT_MODEL.getBeanName(beanId);
}
@Override
protected String dataName() {
return AiAgentEnumVO.AI_CLIENT_MODEL.getDataName();
}
}该节点的作用是 构建 ChatModel 对话模型。其依赖的 OpenAiApi 与 McpSyncClient 均通过 Spring 容器获取。
在完成构建后,将生成的 ChatModel 实例 注册回 Spring 容器,即可完成对话模型的动态装配。
四、功能测试
1. 前置说明



- 注意,导入的库表数据,
ai_client_tool_mcpfilesystem npx Windows 用户需要修改。Mac、linux 可以直接使用。 - 注意,需要修改
ai_client_model、ai_client_api默认的模型和apikey渠道。
2. 单元测试
@Test
public void test_aiClientModelNode() throws Exception {
var armoryStrategyHandler =
defaultArmoryStrategyFactory.armoryStrategyHandler();
String apply = armoryStrategyHandler.apply(
ArmoryCommandEntity.builder()
.commandType(AiAgentEnumVO.AI_CLIENT.getCode())
.commandIdList(Arrays.asList("3001"))
.build(),
new DefaultArmoryStrategyFactory.DynamicContext());
OpenAiChatModel openAiChatModel = (OpenAiChatModel) applicationContext.getBean(AiAgentEnumVO.AI_CLIENT_MODEL.getBeanName("2001"));
log.info("模型构建:{}", openAiChatModel);
// 1. 有哪些工具可以使用
// 2. 在 C:\Users\Dell\Desktop 创建 txt.md 文件
Prompt prompt = Prompt.builder()
.messages(new UserMessage(
"""
在 C:\\Users\\Dell\\Desktop 创建 hello-txt.md 文件, 并写入内容"hello world"。
"""))
.build();
ChatResponse chatResponse = openAiChatModel.call(prompt);
log.info("测试结果(call):{}", JSON.toJSONString(chatResponse));
}25-09-11.13:03:56.016 [pool-2-thread-1 ] INFO AiClientLoadDataStrategy - 查询配置数据(ai_client_api) [3001]
25-09-11.13:03:56.016 [pool-2-thread-2 ] INFO AiClientLoadDataStrategy - 查询配置数据(ai_client_model) [3001]
25-09-11.13:03:56.017 [pool-2-thread-3 ] INFO AiClientLoadDataStrategy - 查询配置数据(ai_client_tool_mcp) [3001]
25-09-11.13:03:56.017 [pool-2-thread-4 ] INFO AiClientLoadDataStrategy - 查询配置数据(ai_client_system_prompt) [3001]
25-09-11.13:03:56.017 [pool-2-thread-5 ] INFO AiClientLoadDataStrategy - 查询配置数据(ai_client_advisor) [3001]
25-09-11.13:03:56.017 [pool-2-thread-6 ] INFO AiClientLoadDataStrategy - 查询配置数据(ai_client) [3001]
25-09-11.13:03:56.102 [pool-2-thread-2 ] INFO HikariDataSource - MainHikariPool - Starting...
25-09-11.13:03:56.712 [pool-2-thread-2 ] INFO HikariPool - MainHikariPool - Added connection com.mysql.cj.jdbc.ConnectionImpl@69b7217f
25-09-11.13:03:56.713 [pool-2-thread-2 ] INFO HikariDataSource - MainHikariPool - Start completed.
25-09-11.13:03:57.136 [main ] INFO RootNode - Ai Agent 构建,数据加载节点 {"commandIdList":["3001"],"commandType":"client","loadDataStrategy":"aiClientLoadDataStrategy"}
25-09-11.13:03:57.136 [main ] INFO AiClientApiNode - Ai Agent 构建,API 构建节点 {"commandIdList":["3001"],"commandType":"client","loadDataStrategy":"aiClientLoadDataStrategy"}
25-09-11.13:03:57.141 [main ] INFO AbstractArmorySupport - 注册Bean: ai_client_api_1001 -> org.springframework.ai.openai.api.OpenAiApi@2b02eebf
25-09-11.13:03:57.141 [main ] INFO AiClientToolMcpNode - Ai Agent 构建节点,Tool MCP 工具配置{"commandIdList":["3001"],"commandType":"client","loadDataStrategy":"aiClientLoadDataStrategy"}
25-09-11.13:04:08.179 [pool-6-thread-1 ] INFO StdioClientTransport - STDERR Message received: Secure MCP Filesystem Server running on stdio
25-09-11.13:04:08.362 [pool-3-thread-1 ] INFO McpAsyncClient - Server response with Protocol: 2024-11-05, Capabilities: ServerCapabilities[completions=null, experimental=null, logging=null, prompts=null, resources=null, tools=ToolCapabilities[listChanged=null]], Info: Implementation[name=secure-filesystem-server, version=0.2.0] and Instructions null
25-09-11.13:04:08.369 [main ] INFO AiClientToolMcpNode - Tool Stdio MCP Initialized InitializeResult[protocolVersion=2024-11-05, capabilities=ServerCapabilities[completions=null, experimental=null, logging=null, prompts=null, resources=null, tools=ToolCapabilities[listChanged=null]], serverInfo=Implementation[name=secure-filesystem-server, version=0.2.0], instructions=null]
25-09-11.13:04:08.371 [main ] INFO AbstractArmorySupport - 注册Bean: ai_client_tool_mcp_5003 -> io.modelcontextprotocol.client.McpSyncClient@2121ed76
25-09-11.13:04:08.371 [main ] INFO AiClientModelNode - Ai Agent 构建节点,Mode 对话模型{"commandIdList":["3001"],"commandType":"client","loadDataStrategy":"aiClientLoadDataStrategy"}
25-09-11.13:04:08.468 [pool-6-thread-1 ] INFO StdioClientTransport - STDERR Message received: Client does not support MCP Roots, using allowed directories set from server args: [ 'C:\\Users\\Dell\\Desktop', 'C:\\Users\\Dell\\Desktop' ]
25-09-11.13:04:08.541 [main ] INFO AbstractArmorySupport - 注册Bean: ai_client_model_2001 -> OpenAiChatModel [defaultOptions=OpenAiChatOptions: {"streamUsage":false,"model":"gpt-5"}]
25-09-11.13:04:08.557 [main ] INFO AgentTest - 模型构建:OpenAiChatModel [defaultOptions=OpenAiChatOptions: {"streamUsage":false,"model":"gpt-5"}]
25-09-11.13:04:24.623 [main ] INFO AgentTest - 测试结果(call):{"metadata":{"empty":false,"id":"chatcmpl-CETlmp7gXRDQu0O0oL25wQuxJ3q18","model":"gpt-5-2025-08-07","rateLimit":{},"usage":{"promptTokens":4236,"completionTokens":558,"totalTokens":4794}},"result":{"metadata":{"contentFilters":[],"empty":true,"finishReason":"STOP"},"output":{"media":[],"messageType":"ASSISTANT","metadata":{"role":"ASSISTANT","messageType":"ASSISTANT","finishReason":"STOP","refusal":"","index":0,"annotations":[],"id":"chatcmpl-CETlmp7gXRDQu0O0oL25wQuxJ3q18"},"text":"已在 C:\\Users\\Dell\\Desktop 创建文件 hello-txt.md,并写入内容:\nhello world\n\n如果还需要我检查文件内容或修改内容,请告诉我。","toolCalls":[]}},"results":[{"$ref":"$.metadata.rateLimit.usage.result"}]}- 3001 客户端 AI ID,2001 ChatModel 对话模型ID,一个是根据 3001 客户端 ID 加载,另外一个 2001 是根据 ChatMode 对话模型 ID 获取对象
- 当获取了 OpenAiChatModel 对象后,就可以发起对话了,这里我们可以询问
有哪些工具可以使用,也可以询问在 C:\Users\Dell\Desktop 创建 hello-txt.md 文件, 并写入内容"hello world。