资讯动态

AI Agent 脚手架 —— 各节点的增强装配实现

发布时间:2026/9/11 19:46:14 来源:尧图企业网站定制
一、前言前面我们实现了基本的Agent装配主要就是装填API但是其实是有很多可以优化的地方比如我们前面提到的几个问题强制要求最后一个agent-workflows是sequential的但是并没有告诉用户也没有专门设置一个配置项用于解耦出来。同时我们依旧没有实现工作流随心配这个是一定要优化的因为Google ADK本来就提供了高自由度的嵌套我们需要保留这种高自由度。这都是这一节要进行优化的。二、Runner增强既然在前言中提到了那么我们就先对Runner进行优化首先就是增加一个配置项用于配置最后一个workflow。配置预览ai: agent: config: tables: testAgent02: app-name: testAgent02 agent: agent-id: 100002 agent-name: 测试智能体02 agent-desc: 单一智能体 module: ai-api: base-url: https://api.deepseek.com api-key: sk-32xxxxxxxxxxxxcb4 completions-path: v1/chat/completions embeddings-path: v1/embeddings chat-model: model: deepseek-v4-flash tool-mcp-list: - sse: name: baidu-search base-uri: http://appbuilder.baidu.com/v2/ai_search/mcp/ sse-endpoint: sse?api_keyBearerbce-v3/ALTAK-MTeiLi6w7j3rWZCdczXyH/a26ece4e7c7863ff14fb31e074d958293a10024f request-timeout: 5000 agents: - name: onlyAgent description: 小傅哥学习项目计划 instruction: | 通过百度搜索检索小傅哥的AI学习路线并根据检索内容生成学习计划。 runner: agent-name: onlyAgent因此我们要修改值对象新增一个Runner属性。Data public static class Runner { private String agentName; }这里单独把获取Runner的步骤提出来通过拿刚刚的新增配置属性来直接指定Runner因此就解决了之前的约束-强制要求最后一个workflow是sequential的。/** * author 印东升 * description 执行节点 * create 2026-08-31 18:19 */ Slf4j Service public class RunnerNode extends AbstractArmorySupport { Override protected AiAgentRegisterVO doApply(ArmoryCommandEntity requestParameter, DefaultArmoryFactory.DynamicContext dynamicContext) throws Exception { log.info(Ai Agent 装配操作 - RunnerNode); AiAgentConfigTableVO aiAgentConfigTableVO requestParameter.getAiAgentConfigTableVO(); String appName aiAgentConfigTableVO.getAppName(); AiAgentConfigTableVO.Agent agent aiAgentConfigTableVO.getAgent(); String agentId agent.getAgentId(); String agentName agent.getAgentName(); String agentDesc agent.getAgentDesc(); //获取上下文对象 InMemoryRunner runner getRunner(dynamicContext, aiAgentConfigTableVO, appName); AiAgentRegisterVO aiAgentRegisterVO AiAgentRegisterVO.builder() .agentId(agentId) .appName(appName) .agentName(agentName) .agentDesc(agentDesc) .runner(runner) .build(); //注册到容器 registerBean(agentId, AiAgentRegisterVO.class, aiAgentRegisterVO); return aiAgentRegisterVO; } NotNull private static InMemoryRunner getRunner(DefaultArmoryFactory.DynamicContext dynamicContext, AiAgentConfigTableVO aiAgentConfigTableVO, String appName) { AiAgentConfigTableVO.Module.Runner runnerConfig aiAgentConfigTableVO.getModule().getRunner(); String agentName runnerConfig.getAgentName(); if (StringUtils.isBlank(agentName)) { log.error(runner agentName is null); throw new AppException(ResponseCode.ILLEGAL_PARAMETER.getCode(), ResponseCode.ILLEGAL_PARAMETER.getInfo()); } BaseAgent lastAgent dynamicContext.getAgentGroup().get(runnerConfig.getAgentName()); return new InMemoryRunner(lastAgent, appName); } Override public StrategyHandlerArmoryCommandEntity, DefaultArmoryFactory.DynamicContext, AiAgentRegisterVO get(ArmoryCommandEntity requestParameter, DefaultArmoryFactory.DynamicContext dynamicContext) throws Exception { return defaultStrategyHandler; } }三、AgentWorkflow增强这个节点的增强是为了实现前言中提到的第二个优化点——随心配。值得一提的是前面我在做基础装配的时候就已经想到了会这么优化了因为路由部分的重复代码太多了还不如单独让一个节点处理这一块逻辑因此用AgentWorkflowNode来进行流转处理再合适不过了这也是解耦思想的体现。首先在DynamicContext中新增两个属性用于定位当前的工作流我们将弃用前面类似于出栈的方式直接利用计数器通过指定索引找到按顺序的当前待处理工作流。/** * 线程安全的计数器 */ private AtomicInteger currentStepIndex new AtomicInteger(0); /** * 当前工作流 */ AiAgentConfigTableVO.Module.AgentWorkflow currentAgentWorkflow;流转模式从“出栈”变成了“索引”/** * author 印东升 * description 装配智能体工作流 * create 2026-08-31 18:19 */ Slf4j Service public class AgentWorkflowNode extends AbstractArmorySupport { Resource private LoopAgentNode loopAgentNode; Resource private ParallelAgentNode parallelAgentNode; Resource private SequentialAgentNode sequentialAgentNode; Resource private RunnerNode runnerNode; Override protected AiAgentRegisterVO doApply(ArmoryCommandEntity requestParameter, DefaultArmoryFactory.DynamicContext dynamicContext) throws Exception { log.info(Ai Agent 装配操作 - AgentWorkFlowNode); AiAgentConfigTableVO aiAgentConfigTableVO requestParameter.getAiAgentConfigTableVO(); ListAiAgentConfigTableVO.Module.AgentWorkflow agentWorkflows aiAgentConfigTableVO.getModule() .getAgentWorkflows(); if (null agentWorkflows || agentWorkflows.isEmpty() || dynamicContext.getCurrentStepIndex() agentWorkflows.size()) { //设置结果值 dynamicContext.setCurrentAgentWorkflow(null); //路由到下一个节点 return router(requestParameter, dynamicContext); } //通过索引计数来直接指定当前需要装配的workflow - 一个一个来 dynamicContext.setCurrentAgentWorkflow(agentWorkflows.get(dynamicContext.getCurrentStepIndex())); dynamicContext.addCurrentStepIndex(); return router(requestParameter, dynamicContext); } Override public StrategyHandlerArmoryCommandEntity, DefaultArmoryFactory.DynamicContext, AiAgentRegisterVO get(ArmoryCommandEntity requestParameter, DefaultArmoryFactory.DynamicContext dynamicContext) throws Exception { AiAgentConfigTableVO.Module.AgentWorkflow currentAgentWorkflow dynamicContext.getCurrentAgentWorkflow(); if (null currentAgentWorkflow) { //路由到下一个节点 return runnerNode; } String type currentAgentWorkflow.getType(); AgentTypeEnum agentTypeEnum AgentTypeEnum.formType(type); if (null agentTypeEnum) { throw new RuntimeException(agentWorkflow type is error); } String node agentTypeEnum.getNode(); //路由 return switch (node) { case loopAgentNode - loopAgentNode; case parallelAgentNode - parallelAgentNode; case sequentialAgentNode - sequentialAgentNode; default - runnerNode; }; } }循环节点的修改可以看到会清爽很多特别是路由那一部分每次只需要重新回到AgentWorkflow重新路由即可。其他的工作流节点几乎都是一样的处理方式不做赘述了。/** * author 印东升 * description 循环装配节点 * create 2026-08-31 18:19 */ Slf4j Service(loopAgentNode) public class LoopAgentNode extends AbstractArmorySupport { Override protected AiAgentRegisterVO doApply(ArmoryCommandEntity requestParameter, DefaultArmoryFactory.DynamicContext dynamicContext) throws Exception { log.info(Ai Agent 装配操作 - LoopAgentNode); //优化-获取当前待处理的工作流 AiAgentConfigTableVO.Module.AgentWorkflow currentAgentWorkflow dynamicContext.getCurrentAgentWorkflow(); ListBaseAgent subAgents dynamicContext.queryAgentList(currentAgentWorkflow.getSubAgents()); LoopAgent loopAgent LoopAgent.builder() .name(currentAgentWorkflow.getName()) .description(currentAgentWorkflow.getDescription()) .subAgents(subAgents) .maxIterations(currentAgentWorkflow.getMaxIterations()) .build(); dynamicContext.getAgentGroup().put(currentAgentWorkflow.getName(),loopAgent); return router(requestParameter, dynamicContext); } Override public StrategyHandlerArmoryCommandEntity, DefaultArmoryFactory.DynamicContext, AiAgentRegisterVO get(ArmoryCommandEntity requestParameter, DefaultArmoryFactory.DynamicContext dynamicContext) throws Exception { return getBean(agentWorkflowNode); } }最后测试一下Test public void testAgent03() throws InterruptedException { AiAgentRegisterVO aiAgentRegisterVO applicationContext.getBean(100003, AiAgentRegisterVO.class); String appName aiAgentRegisterVO.getAppName(); InMemoryRunner runner aiAgentRegisterVO.getRunner(); Session session runner.sessionService() .createSession(appName, yds) .blockingGet(); Content userMsg Content.fromParts(Part.fromText(你具备哪能力)); FlowableEvent events runner.runAsync(yds, session.id(), userMsg); ListString outputs new ArrayList(); events.blockingForEach(event - outputs.add(event.stringifyContent())); log.info(测试结果:{}, JSON.toJSONString(outputs)); new CountDownLatch(1).await(); }四、MCP增强解耦为啥会想到解耦MCP呢因为前面我们是在ChatModelNode中去处理MCP的各种协议的目前我们只有两种SSE、STDIO其实还差了一种LOCAL我们固然可以再写个if-else去单独处理本地MCP但是会发现这样处理后会导致ChatModelNode极为臃肿这些节点其实是不应该处理太多装配以外的逻辑的比如各种MCP协议的格式规范化处理或者判断协议类型。因此我们单独提出来一个工厂来对这些协议进行处理工厂用于判断协议类型具体的规范化处理交给单独的对象来处理。/** * author 印东升 * description MCP装配工厂 * create 2026-05-27 16:58 */ Service public class DefaultMcpClientFactory { Resource private MapString, IToolMcpCreateService toolMcpCreateServiceGroup; public IToolMcpCreateService getToolMcpCreateService(AiAgentConfigTableVO.Module.ChatModel.ToolMcp toolMcp) throws Exception { if (null ! toolMcp.getSse()) return toolMcpCreateServiceGroup.get(sSEToolMcpCreateService); if (null ! toolMcp.getStdio()) return toolMcpCreateServiceGroup.get(stdioToolMcpCreateService); if (null ! toolMcp.getLocal()) return toolMcpCreateServiceGroup.get(localToolMcpCreateService); throw new AppException(ResponseCode.NOT_FOUND_METHOD.getCode(), ResponseCode.NOT_FOUND_METHOD.getInfo()); } }这三个处理节点并没有什么好讲的其实就是把之前在ChatModelNode中的东西解耦出来。/** * author 印东升 * description * create 2026-09-09 16:07 */ Slf4j Service(localToolMcpCreateService) public class LocalToolMcpCreateService implements IToolMcpCreateService { Resource protected ApplicationContext applicationContext; Override public ToolCallback[] buildToolCallback(AiAgentConfigTableVO.Module.ChatModel.ToolMcp toolMcp) throws Exception { AiAgentConfigTableVO.Module.ChatModel.ToolMcp.LocalParameters local toolMcp.getLocal(); String name local.getName(); ToolCallbackProvider localToolCallbackProvider (ToolCallbackProvider) applicationContext.getBean(local.getName()); log.info(Tool Local MCP initialize:{}, name); return localToolCallbackProvider.getToolCallbacks(); } }/** * author 印东升 * description * create 2026-09-09 16:07 */ Slf4j Service(stdioToolMcpCreateService) public class StdioToolMcpCreateService implements IToolMcpCreateService { Override public ToolCallback[] buildToolCallback(AiAgentConfigTableVO.Module.ChatModel.ToolMcp toolMcp) throws Exception { AiAgentConfigTableVO.Module.ChatModel.ToolMcp.StdioServerParameters stdioConfig toolMcp.getStdio(); 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 SyncMcpToolCallbackProvider.builder() .mcpClients(mcpClient) .build() .getToolCallbacks(); } }/** * author 印东升 * description * create 2026-09-09 16:07 */ Slf4j Service(sSEToolMcpCreateService) public class SSEToolMcpCreateService implements IToolMcpCreateService { Override public ToolCallback[] buildToolCallback(AiAgentConfigTableVO.Module.ChatModel.ToolMcp toolMcp) throws Exception { AiAgentConfigTableVO.Module.ChatModel.ToolMcp.SSEServerParameters sseConfig toolMcp.getSse(); String originalBaseUri sseConfig.getBaseUri(); String baseUri originalBaseUri; String sseEndpoint sseConfig.getSseEndpoint(); //拆sse的路径 if (StringUtils.isBlank(sseEndpoint)) { URL url new URL(originalBaseUri); String protocol url.getProtocol(); String host url.getHost(); int port url.getPort(); String baseUrl port -1 ? protocol :// host : protocol :// host : port; int index originalBaseUri.indexOf(baseUri); if (index ! -1) { sseEndpoint originalBaseUri.substring(index baseUrl.length()); } baseUri baseUrl; } sseEndpoint StringUtils.isBlank(sseEndpoint) ? /sse : sseEndpoint; HttpClientSseClientTransport sseClientTransport HttpClientSseClientTransport.builder(baseUri) .sseEndpoint(sseEndpoint) .build(); McpSyncClient mcpSyncClient McpClient.sync(sseClientTransport) .requestTimeout(Duration.ofMillis(sseConfig.getRequestTimeout())) .build(); McpSchema.InitializeResult initialize mcpSyncClient.initialize(); log.info(Tool SSE MCP Initialized:{}, initialize); return SyncMcpToolCallbackProvider.builder() .mcpClients(mcpSyncClient) .build() .getToolCallbacks(); } }最后ChatModelNode就简明扼要了/** * author 印东升 * description 装配模型和工具 * create 2026-08-31 18:19 */ Slf4j Service public class ChatModelNode extends AbstractArmorySupport { Resource private AgentNode agentNode; Resource private DefaultMcpClientFactory defaultMcpClientFactory; 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(); ListAiAgentConfigTableVO.Module.ChatModel.ToolMcp toolMcpList chatModelConfig.getToolMcpList(); //工厂构建MCP服务 ListToolCallback toolCallbackList new ArrayList(); for (AiAgentConfigTableVO.Module.ChatModel.ToolMcp toolMcp : toolMcpList) { IToolMcpCreateService toolMcpCreateService defaultMcpClientFactory.getToolMcpCreateService(toolMcp); ToolCallback[] toolCallbacks toolMcpCreateService.buildToolCallback(toolMcp); toolCallbackList.addAll(List.of(toolCallbacks)); } //构建chatModel OpenAiChatModel chatModel OpenAiChatModel.builder() .openAiApi(openAiApi) .defaultOptions(OpenAiChatOptions.builder() .model(chatModelConfig.getModel()) .toolCallbacks(toolCallbackList) .build()) .build(); dynamicContext.setOpenAiChatModel(chatModel); return router(requestParameter, dynamicContext); } Override public StrategyHandlerArmoryCommandEntity, DefaultArmoryFactory.DynamicContext, AiAgentRegisterVO get(ArmoryCommandEntity requestParameter, DefaultArmoryFactory.DynamicContext dynamicContext) throws Exception { return agentNode; } }测试阶段我们写一个简单的本地MCP服务用于转换大小写Slf4j Service public class MyTestMcpService { Tool(description 小写字母转换为大写字母) public XxxResponse toUpperCase(XxxRequest request) { XxxResponse xxxResponse new XxxResponse(); xxxResponse.setContent(request.getWord().toUpperCase()); return xxxResponse; } Data JsonInclude(JsonInclude.Include.NON_NULL) public static class XxxRequest { JsonProperty(required true, value word) JsonPropertyDescription(英文单词字符串字母。例如: good,xiaofuge) private String word; } Data JsonInclude(JsonInclude.Include.NON_NULL) public static class XxxResponse { JsonProperty(required true, value content) JsonPropertyDescription(单词转换结果) private String content; } }测试Test public void testAgent04() throws InterruptedException { AiAgentRegisterVO aiAgentRegisterVO applicationContext.getBean(100002, AiAgentRegisterVO.class); String appName aiAgentRegisterVO.getAppName(); InMemoryRunner runner aiAgentRegisterVO.getRunner(); Session session runner.sessionService() .createSession(appName, yds) .blockingGet(); Content userMsg Content.fromParts(Part.fromText(把ydsyyds转换为大写)); FlowableEvent events runner.runAsync(yds, session.id(), userMsg); ListString outputs new ArrayList(); events.blockingForEach(event - outputs.add(event.stringifyContent())); log.info(测试结果:{}, JSON.toJSONString(outputs)); new CountDownLatch(1).await(); }结果如下:五、Plugin增强这个plugin是在Agent的Runner中可以配置的本身也是GoogleADK的一部分主要可以用于获取Agent运行时的信息以及可以打印日志。只需要继承BasePlugin即可使用ADK其实已经高度封装了只需要重写方法就能直接实现/** * author 印东升 * description * create 2026-09-10 11:18 */ Slf4j Service(myTestPlugin) public class MyTestPlugin extends BasePlugin { public MyTestPlugin(String name) { super(name); } public MyTestPlugin() { super(MyTestPlugin); } Override public MaybeContent onUserMessageCallback(InvocationContext invocationContext, Content userMessage) { log.info(用户输入信息:{},userMessage.text()); return super.onUserMessageCallback(invocationContext, userMessage); } Override public MaybeContent beforeAgentCallback(BaseAgent agent, CallbackContext callbackContext) { String name agent.name(); log.info(智能体名称:{},name); return super.beforeAgentCallback(agent, callbackContext); } Override public MaybeLlmResponse beforeModelCallback(CallbackContext callbackContext, LlmRequest llmRequest) { OptionalString model llmRequest.model(); log.info(ai 模型:{},model.orElse()); return super.beforeModelCallback(callbackContext, llmRequest); } }LoggingPlugin本身就是继承BasePlugin的一个封装类里面的方法都是重写过的了直接使用即可。Slf4j Service(myLogPlugin) public class MyLogPlugin extends LoggingPlugin { }配置上插件runner: agent-name: onlyAgent plugin-name-list: - myTestPlugin - myLogPlugin在RunnerNode中对Plugin进行装配用插件名获取Plugin的bean对象然后装配到Runner中。NotNull private InMemoryRunner getRunner(DefaultArmoryFactory.DynamicContext dynamicContext, AiAgentConfigTableVO aiAgentConfigTableVO, String appName) { AiAgentConfigTableVO.Module.Runner runnerConfig aiAgentConfigTableVO.getModule().getRunner(); String agentName runnerConfig.getAgentName(); if (StringUtils.isBlank(agentName)) { log.error(runner agentName is null); throw new AppException(ResponseCode.ILLEGAL_PARAMETER.getCode(), ResponseCode.ILLEGAL_PARAMETER.getInfo()); } BaseAgent lastAgent dynamicContext.getAgentGroup().get(runnerConfig.getAgentName()); //插件装配 ListBasePlugin plugins; ListString pluginNameList runnerConfig.getPluginNameList(); if (null ! pluginNameList !pluginNameList.isEmpty()) { plugins new ArrayList(); for (String pluginName : pluginNameList) { BasePlugin plugin getBean(pluginName); plugins.add(plugin); } } else { plugins ImmutableList.of(); } return new InMemoryRunner(lastAgent, appName, plugins); }测试

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价