AI agent项目复盘(Day-3)
这里我们增强装配了agentWorkflowNode,原来的代码中,我们能够配置的智能体工作流的顺序和种类数都是有限的且需要符合规则的(例如:循环 -> 并行 -> 串行),而且如果这样配置,我们只能让串行工作流作为最后一个节点。六、首先从配置中获取Runner配置,校验入口Agent是否存在,获取到Agent实例,然后加载插件(用来给agent增强能力如:日志,监控,拦截,重试,上下文扩展),最
Day-2最后我们复盘到了流转到AgentWorkflowNode,所以接下来我们从AgentWorkflowNode的装配开始。
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();
List<AiAgentConfigTableVO.Module.AgentWorkflow> agentWorkflows = aiAgentConfigTableVO.getModule().getAgentWorkflows();
// 如果未配置 agentWorkflows 则直接流转到 RunnerNode
if (null == agentWorkflows || agentWorkflows.isEmpty() || dynamicContext.getCurrentStepIndex() >= agentWorkflows.size()) {
// 设置结果值
dynamicContext.setCurrentAgentWorkflow(null);
// 路由下节点
return router(requestParameter, dynamicContext);
}
// 设置当前判断流程对象
dynamicContext.setCurrentAgentWorkflow(agentWorkflows.get(dynamicContext.getCurrentStepIndex()));
// 步骤值增加
dynamicContext.setCurrentStepIndex(dynamicContext.getCurrentStepIndex()+1);
return router(requestParameter,dynamicContext);
}
@Override
public StrategyHandler<ArmoryCommandEntity, DefaultArmoryFactory.DynamicContext, AiAgentRegisterVO> get(ArmoryCommandEntity requestParameter, DefaultArmoryFactory.DynamicContext dynamicContext) throws Exception {
AiAgentConfigTableVO.Module.AgentWorkflow currentAgentWorkflow = dynamicContext.getCurrentAgentWorkflow();
// 没有下一个节点,流转到结束节点
if(currentAgentWorkflow == null){
return runnerNode;
}
String type = currentAgentWorkflow.getType();
AgentTypeEnum agentTypeEnum = AgentTypeEnum.formType(type);
if (agentTypeEnum == null){
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;
};
}
}
五、首先获取AgentWorkflows的配置信息,如果未配置AgentWorkflows或AgentWorkflows为空或遍历配置集合中已经没有了元素,在上下文中设置AgentWorkflow信息为null,则直接流转到RunnerNode。
否则遍历配置集合,设置当前判断流程对象,接着步骤值增加,然后流转到配置的三种不同类型的节点。
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();
List<String> subAgentNames = currentAgentWorkflow.getSubAgents();
List<BaseAgent> subAgents = dynamicContext.queryAgentList(subAgentNames);
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 StrategyHandler<ArmoryCommandEntity, DefaultArmoryFactory.DynamicContext, AiAgentRegisterVO> get(ArmoryCommandEntity requestParameter, DefaultArmoryFactory.DynamicContext dynamicContext) throws Exception {
return getBean("agentWorkflowNode");
}
}
LoopAgentNode循环工作流节点,装配循环工作流智能体,然后流转回agentWorkflowNode。
public class ParallelAgentNode extends AbstractArmorySupport {
@Override
protected AiAgentRegisterVO doApply(ArmoryCommandEntity requestParameter, DefaultArmoryFactory.DynamicContext dynamicContext) throws Exception {
log.info("Ai Agent 装配操作 - parallelAgentNode");
AiAgentConfigTableVO.Module.AgentWorkflow currentAgentWorkflow = dynamicContext.getCurrentAgentWorkflow();
List<String> subAgentNames = currentAgentWorkflow.getSubAgents();
List<BaseAgent> subAgents = dynamicContext.queryAgentList(subAgentNames);
ParallelAgent parallelAgent =
ParallelAgent.builder()
.name(currentAgentWorkflow.getName())
.description(currentAgentWorkflow.getDescription())
.subAgents(subAgents)
.build();
dynamicContext.getAgentGroup().put(currentAgentWorkflow.getName(), parallelAgent);
return router(requestParameter,dynamicContext);
}
@Override
public StrategyHandler<ArmoryCommandEntity, DefaultArmoryFactory.DynamicContext, AiAgentRegisterVO> get(ArmoryCommandEntity requestParameter, DefaultArmoryFactory.DynamicContext dynamicContext) throws Exception {
return getBean("agentWorkflowNode");
}
}
ParallelAgentNode并行工作流节点,装配并行工作流智能体,然后再次流转回agentWorkflowNode。
public class SequentialAgentNode extends AbstractArmorySupport {
@Resource
private RunnerNode runnerNode;
@Override
protected AiAgentRegisterVO doApply(ArmoryCommandEntity requestParameter, DefaultArmoryFactory.DynamicContext dynamicContext) throws Exception {
log.info("Ai Agent 装配操作 - SequentialAgentNode");
AiAgentConfigTableVO.Module.AgentWorkflow currentAgentWorkflow = dynamicContext.getCurrentAgentWorkflow();
List<String> subAgentNames = currentAgentWorkflow.getSubAgents();
List<BaseAgent> subAgents = dynamicContext.queryAgentList(subAgentNames);
SequentialAgent sequentialAgent =
SequentialAgent.builder()
.name(currentAgentWorkflow.getName())
.description(currentAgentWorkflow.getDescription())
.subAgents(subAgents)
.build();
dynamicContext.getAgentGroup().put(currentAgentWorkflow.getName(), sequentialAgent);
return router(requestParameter,dynamicContext);
}
@Override
public StrategyHandler<ArmoryCommandEntity, DefaultArmoryFactory.DynamicContext, AiAgentRegisterVO> get(ArmoryCommandEntity requestParameter, DefaultArmoryFactory.DynamicContext dynamicContext) throws Exception {
return getBean("agentWorkflowNode");
}
}
SequentialAgentNode串行工作流节点,装配串行工作流智能体,然后再次流转回agentWorkflowNode。
这里我们增强装配了agentWorkflowNode,原来的代码中,我们能够配置的智能体工作流的顺序和种类数都是有限的且需要符合规则的(例如:循环 -> 并行 -> 串行),而且如果这样配置,我们只能让串行工作流作为最后一个节点。
现在我们让每个工作流节点装配完后,都再次流转回agentWorkflowNode,然后在流转到其他工作流节点或直接流转到RunnerNode。在这种方式下,我们不仅可以做到可以只配置单个工作流或可以串行完了再走串行,配置的种类更多,支持更复杂的编排。
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()
.appName(appName)
.agentId(agentId)
.agentName(agentName)
.agentDesc(agentDesc)
.runner(runner)
.build();
// 注册到 Spring 容器
registerBean(agentId, AiAgentRegisterVO.class, aiAgentRegisterVO);
return aiAgentRegisterVO;
}
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 baseAgent = dynamicContext.getAgentGroup().get(agentName);
// 扩展插件
List<BasePlugin> plugins;
List<String> pluginNameList = runnerConfig.getPluginNameList();
if(pluginNameList != null && !pluginNameList.isEmpty()){
plugins=new ArrayList<>();
for (String pluginName : pluginNameList) {
BasePlugin basePlugin=getBean(pluginName);
plugins.add(basePlugin);
}
}else {
plugins=ImmutableList.of();
}
InMemoryRunner runner=new InMemoryRunner(baseAgent, appName,plugins);
return runner;
}
@Override
public StrategyHandler<ArmoryCommandEntity, DefaultArmoryFactory.DynamicContext, AiAgentRegisterVO> get(ArmoryCommandEntity requestParameter, DefaultArmoryFactory.DynamicContext dynamicContext) throws Exception {
return defaultStrategyHandler;
}
}
六、首先从配置中获取Runner配置,校验入口Agent是否存在,获取到Agent实例,然后加载插件(用来给agent增强能力如:日志,监控,拦截,重试,上下文扩展),最后根据入口Agent,应用名称,插件列表创建InMemoryRunner内存执行器。
然后我们封装一个完整的可运行的AI Agent实体,把该实体注册到spring容器中,外部可以直接通过agentId获取到agent实体并运行AI。
OK,到这里关于智能体的装配流程就结束了。
我们来加载智能体验证一下装配的是否成功。
@Test
public void test_agent() throws InterruptedException {
AiAgentRegisterVO aiAgentRegisterVO = applicationContext.getBean("100001", AiAgentRegisterVO.class);
String appName = aiAgentRegisterVO.getAppName();
InMemoryRunner runner = aiAgentRegisterVO.getRunner();
Session session = runner.sessionService()
.createSession(appName, "Coderwang")
.blockingGet();
Content userMsg = Content.fromParts(Part.fromText("编写冒泡排序"));
Flowable<Event> events = runner.runAsync("Coderwang", session.id(), userMsg);
List<String> outputs=new ArrayList<>();
events.blockingForEach(event -> outputs.add(event.stringifyContent()));
log.info("测试结果:{}", JSON.toJSONString(outputs));
new CountDownLatch(1).await();
}
从spring容器中拿到已注册的AI Agent实例,获取应用名称,获取核心执行引擎InMemoryRunner,然后根据应用名+用户名创建会话。接着把用户输入的信息封装成统一框架的Content结构。
核心操作:异步运行AI Agent,返返回 Flowable<Event>:响应式流式事件(实时输出),阻塞等待并收集所有流式事件。
25-12-29.17:16:00.676 [main ] INFO AgentNode - Ai Agent 装配操作 - AgentNode
25-12-29.17:16:00.691 [main ] INFO AgentWorkflowNode - Ai Agent 装配操作 - AgentWorkflowNode
25-12-29.17:16:00.691 [main ] INFO SequentialAgentNode - Ai Agent 装配操作 - SequentialAgentNode
25-12-29.17:16:00.691 [main ] INFO AbstractArmorySupport - 成功注册Bean: CodePipelineAgent
25-12-29.17:16:00.691 [main ] INFO RunnerNode - Ai Agent 装配操作 - RunnerNode
25-12-29.17:16:00.693 [main ] INFO AbstractArmorySupport - 成功注册Bean: 100001
25-12-29.17:16:09.528 [main ] INFO AiAgentAutoConfigTest - 测试结果:["```java\npublic class BubbleSort {\n // 冒泡排序实现\n public static void bubbleSort(int[] arr) {\n int n = arr.length;\n boolean swapped;\n for (int i = 0; i < n - 1; i++) {\n swapped = false;\n // 每一趟将最大的“冒泡”到最后\n for (int j = 0; j < n - 1 - i; j++) {\n if (arr[j] > arr[j + 1]) {\n // 交换 arr[j] 和 arr[j+1]\n int temp = arr[j];\n arr[j] = arr[j + 1];\n arr[j + 1] = temp;\n swapped = true;\n }\n }\n // 如果没发生交换,说明已经有序\n if (!swapped) {\n break;\n }\n }\n }\n\n public static void main(String[] args) {\n int[] arr = {5, 2, 9, 1, 5, 6};\n System.out.println(\"排序前:\");\n for (int num : arr) {\n System.out.print(num + \" \");\n }\n System.out.println();\n\n bubbleSort(arr);\n\n System.out.println(\"排序后:\");\n for (int num : arr) {\n System.out.print(num + \" \");\n }\n System.out.println();\n }\n}\n```","No major issues found.","```java\npublic class BubbleSort {\n // 冒泡排序实现\n public static void bubbleSort(int[] arr) {\n int n = arr.length;\n boolean swapped;\n for (int i = 0; i < n - 1; i++) {\n swapped = false;\n // 每一趟将最大的“冒泡”到最后\n for (int j = 0; j < n - 1 - i; j++) {\n if (arr[j] > arr[j + 1]) {\n // 交换 arr[j] 和 arr[j+1]\n int temp = arr[j];\n arr[j] = arr[j + 1];\n arr[j + 1] = temp;\n swapped = true;\n }\n }\n // 如果没发生交换,说明已经有序\n if (!swapped) {\n break;\n }\n }\n }\n\n public static void main(String[] args) {\n int[] arr = {5, 2, 9, 1, 5, 6};\n System.out.println(\"排序前:\");\n for (int num : arr) {\n System.out.print(num + \" \");\n }\n System.out.println();\n\n bubbleSort(arr);\n\n System.out.println(\"排序后:\");\n for (int num : arr) {\n System.out.print(num + \" \");\n }\n System.out.println();\n }\n}\n```"]
测试结果如上图,智能体加载没问题。
在此之后,我们又做了对图片内容的识别,但是却返回了没有上传任何图片的描述。
public void test_handlerMessage_02() throws IOException {
AiAgentRegisterVO aiAgentRegisterVO = applicationContext.getBean("100002", AiAgentRegisterVO.class);
String appName = aiAgentRegisterVO.getAppName();
InMemoryRunner runner = aiAgentRegisterVO.getRunner();
Session session = runner.sessionService()
.createSession(appName, "xiaofuge")
.blockingGet();
Content userMsg = Content.fromParts(
Part.fromText("请描述这张图片的主要内容,并说明图中物品的可能用途。"),
Part.fromBytes(resource.getContentAsByteArray(), MimeTypeUtils.IMAGE_PNG_VALUE));
Flowable<Event> events = runner.runAsync("xiaofuge", session.id(), userMsg);
List<String> outputs = new ArrayList<>();
events.blockingForEach(event -> outputs.add(event.stringifyContent()));
log.info("测试结果:{}", JSON.toJSONString(outputs));
}

这个时候猜想,是不是 InMemoryRunner 构建问题,或者 Agent 实例化参数问题。所以,决定把问题缩小,单独验证 Google ADK + Spring AI。
@Slf4j
public class SpringAiTest {
@SneakyThrows
public static void main(String[] args) {
OpenAiApi openAiApi = OpenAiApi.builder()
.baseUrl("https://apis.itedus.cn")
.apiKey("sk-2GQTYTNoQSs7qizlE9F00bD84d254c2994D44d6410B0Ac8f")
.completionsPath("v1/chat/completions")
.embeddingsPath("v1/embeddings")
.build();
ChatModel chatModel = OpenAiChatModel.builder()
.openAiApi(openAiApi)
.defaultOptions(OpenAiChatOptions.builder()
.model("gpt-4.1")
.build())
.build();
LlmAgent agent = LlmAgent.builder()
.name("test")
.description("Chess coach agent")
.model(new SpringAI(chatModel))
.instruction("""
You are a knowledgeable chess coach
who helps chess players train and sharpen their chess skills.
""")
.build();
InMemoryRunner runner = new InMemoryRunner(agent);
Session session = runner
.sessionService()
.createSession("test", "xiaofuge")
.blockingGet();
URL resource = Thread.currentThread().getContextClassLoader().getResource("dog.png");
byte[] bytes;
assert resource != null;
try (InputStream inputStream = resource.openStream()) {
bytes = inputStream.readAllBytes();
}
List<Part> parts = new ArrayList<>();
parts.add(Part.fromText("这是什么图片"));
parts.add(Part.fromBytes(bytes, MimeTypeUtils.IMAGE_PNG_VALUE));
Content content = Content.builder().role("user").parts(parts).build();
Flowable<Event> events = runner.runAsync("xiaofuge", session.id(),
content
);
System.out.print("\nAgent > ");
events.blockingForEach(event -> System.out.println(event.stringifyContent()));
}
}
-
效果;运行结果依然是识别不了,告诉我要上传图片,它才能识别。
-
猜想;这说明单独按照官网案例构建 Agent 并测试依然不行,再细化验证。
public class SpringAiApiTest {
public static void main(String[] args) throws Exception {
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
InputStream resourceAsStream = classLoader.getResourceAsStream("dog.png");
Resource resource = new ClassPathResource("dog.png", classLoader);
assert resourceAsStream != null;
OpenAiApi openAiApi = OpenAiApi.builder()
.baseUrl("https://apis.xxx")
.apiKey("sk-zahsFUzQcpOauNQUD3918eEe95194d...**** *")
.completionsPath("v1/chat/completions")
.embeddingsPath("v1/embeddings")
.build();
ChatModel chatModel = OpenAiChatModel.builder()
.openAiApi(openAiApi)
.defaultOptions(OpenAiChatOptions.builder()
.model("gpt-4o")
.build())
.build();
// 模型测试,没问题可以识别图片
ChatResponse response = chatModel.call(new Prompt(
UserMessage.builder()
.text("请描述这张图片的主要内容,并说明图中物品的可能用途。")
.media(Media.builder()
.mimeType(MimeType.valueOf(MimeTypeUtils.IMAGE_PNG_VALUE))
.data(resource)
.build())
.build(),
OpenAiChatOptions.builder()
.model("gpt-4o")
.build()));
System.out.println("测试结果" + JSON.toJSONString(response));
}
}

-
效果;直接用 Spring AI 原有的功能进行验证,验证通过,可以识别图片。
-
猜想;大概率是 Google ADK 和 Spring AI 对接的问题,需要debug调试验证,找到对接点的参数传递。
最后我们根据打断点调试,找到了问题:
当 Debug 到 MessageConverter 消息转换的时候发现,它压根就没处理图片类的东西,写了个 TODO。
实现一个自己的 MyMessageConverter 继承 MessageConverter,之后把 mediaList 设置到 llmPrompt 用户请求参数中。
public class MyMessageConverter extends MessageConverter {
public MyMessageConverter(ObjectMapper objectMapper) {
super(objectMapper);
}
@Override
public Prompt toLlmPrompt(LlmRequest llmRequest) {
List<Media> mediaList = new ArrayList<>();
for (Content content : llmRequest.contents()) {
for (Part part : content.parts().orElse(List.of())) {
if (part.inlineData().isPresent()) {
// Handle inline media data (images, audio, video, etc.)
com.google.genai.types.Blob blob = part.inlineData().get();
if (blob.mimeType().isPresent() && blob.data().isPresent()) {
try {
MimeType mimeType = MimeType.valueOf(blob.mimeType().get());
// Create Media object from inline data using ByteArrayResource
org.springframework.core.io.ByteArrayResource resource =
new org.springframework.core.io.ByteArrayResource(blob.data().get());
mediaList.add(new Media(mimeType, resource));
} catch (Exception e) {
// Log warning but continue processing other parts
// In production, consider proper logging framework
System.err.println(
"Warning: Failed to parse media mime type: " + blob.mimeType().get());
}
}
} else if (part.fileData().isPresent()) {
// Handle file-based media (URI references)
com.google.genai.types.FileData fileData = part.fileData().get();
if (fileData.mimeType().isPresent() && fileData.fileUri().isPresent()) {
try {
MimeType mimeType = MimeType.valueOf(fileData.mimeType().get());
// Create Media object from file URI
URI uri = URI.create(fileData.fileUri().get());
mediaList.add(new Media(mimeType, uri));
} catch (Exception e) {
System.err.println(
"Warning: Failed to parse media mime type: " + fileData.mimeType().get());
}
}
}
}
}
Prompt llmPrompt = super.toLlmPrompt(llmRequest);
llmPrompt.getUserMessage().getMedia().addAll(mediaList);
return llmPrompt;
}
}
这样在遍历所有Part时,会把解析出来的图片,塞进UserMessage。
public class MySpringAI extends BaseLlm {
private final ChatModel chatModel;
private final StreamingChatModel streamingChatModel;
private final ObjectMapper objectMapper;
private final MessageConverter messageConverter;
private final SpringAIObservabilityHandler observabilityHandler;
public MySpringAI(ChatModel chatModel) {
super(extractModelName(chatModel));
this.chatModel = Objects.requireNonNull(chatModel, "chatModel cannot be null");
this.streamingChatModel =
(chatModel instanceof StreamingChatModel) ? (StreamingChatModel) chatModel : null;
this.objectMapper = new ObjectMapper();
this.messageConverter = new MyMessageConverter(objectMapper);
this.observabilityHandler =
new SpringAIObservabilityHandler(createDefaultObservabilityConfig());
}
// ... 省略部分,其他也是类似修改
}
然后重写连接SpringAI:
-
这部分重点在于
this.messageConverter = new MyMessageConverter(objectMapper);设置的对象,为我们新实现的对象。
-
通过这样的操作,我们就可以把其他类型参数设置到 file/bytes 设置到请求对象中了。
经过这个修改后,我们的Agent就可以实现多模态的功能了,可以正常识别图片和文本。
更多推荐




所有评论(0)