From ff5f90121b2e71bff5c5f5e3f77b3a354068e7a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=AD=90=E9=BB=98?= <925456043@qq.com> Date: Thu, 30 Jul 2026 15:05:42 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BD=BF=E7=94=A8=E5=8F=91=E5=B8=83?= =?UTF-8?q?=E5=BF=AB=E7=85=A7=E6=A0=A1=E9=AA=8C=E5=9B=BE=E7=89=87=E8=BE=93?= =?UTF-8?q?=E5=85=A5=E8=83=BD=E5=8A=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 正式聊天和模型编译统一读取发布快照中的图片能力 - 草稿态保留实时模型能力并为旧快照提供安全关闭策略 --- .../runtime/AgentModelCapabilityResolver.java | 80 +++++++++ .../agent/runtime/AgentRunService.java | 9 +- .../agent/runtime/AgentRuntimeCompiler.java | 7 +- .../AgentRunServiceImageCapabilityTest.java | 167 ++++++++++++++++++ .../AgentRuntimeCompilerModelConfigTest.java | 84 ++++++++- 5 files changed, 340 insertions(+), 7 deletions(-) create mode 100644 easyflow-modules/easyflow-module-agent/src/main/java/tech/easyflow/agent/runtime/AgentModelCapabilityResolver.java create mode 100644 easyflow-modules/easyflow-module-agent/src/test/java/tech/easyflow/agent/runtime/AgentRunServiceImageCapabilityTest.java diff --git a/easyflow-modules/easyflow-module-agent/src/main/java/tech/easyflow/agent/runtime/AgentModelCapabilityResolver.java b/easyflow-modules/easyflow-module-agent/src/main/java/tech/easyflow/agent/runtime/AgentModelCapabilityResolver.java new file mode 100644 index 00000000..b744305f --- /dev/null +++ b/easyflow-modules/easyflow-module-agent/src/main/java/tech/easyflow/agent/runtime/AgentModelCapabilityResolver.java @@ -0,0 +1,80 @@ +package tech.easyflow.agent.runtime; + +import tech.easyflow.agent.entity.Agent; +import tech.easyflow.ai.entity.Model; + +import java.util.Map; +import java.util.function.Supplier; + +/** + * 解析 Agent 运行时应使用的模型输入能力。 + * + *

正式运行严格使用发布快照;草稿试运行使用当前模型配置。

+ */ +public final class AgentModelCapabilityResolver { + + private static final String MODEL_SUMMARY = "modelSummary"; + private static final String SUPPORT_IMAGE = "supportImage"; + private static final String SUPPORT_IMAGE_BASE64_ONLY = "supportImageB64Only"; + + /** + * 禁止实例化能力解析工具。 + */ + private AgentModelCapabilityResolver() { + } + + /** + * 解析 Agent 当前运行模式对应的模型能力。 + * + *

只要 Agent 携带发布快照,就不会读取实时模型能力。旧快照缺少能力字段时按不支持处理。

+ * + * @param agent Agent 运行视图 + * @param liveModelSupplier 草稿态实时模型提供器 + * @return 规范化模型能力 + */ + public static Resolution resolve(Agent agent, Supplier liveModelSupplier) { + Map publishedSnapshot = agent == null + ? null : agent.getPublishedSnapshotJson(); + if (publishedSnapshot != null && !publishedSnapshot.isEmpty()) { + return fromPublishedSnapshot(publishedSnapshot); + } + Model liveModel = liveModelSupplier == null ? null : liveModelSupplier.get(); + return new Resolution( + liveModel != null && Boolean.TRUE.equals(liveModel.getSupportImage()), + liveModel != null && Boolean.TRUE.equals(liveModel.getSupportImageB64Only())); + } + + /** + * 从发布快照读取模型能力。 + * + * @param publishedSnapshot Agent 发布快照 + * @return 快照中的模型能力 + */ + private static Resolution fromPublishedSnapshot(Map publishedSnapshot) { + Object summaryValue = publishedSnapshot.get(MODEL_SUMMARY); + if (!(summaryValue instanceof Map modelSummary)) { + return Resolution.disabled(); + } + return new Resolution( + Boolean.TRUE.equals(modelSummary.get(SUPPORT_IMAGE)), + Boolean.TRUE.equals(modelSummary.get(SUPPORT_IMAGE_BASE64_ONLY))); + } + + /** + * 模型输入能力解析结果。 + * + * @param supportImage 是否支持图片输入 + * @param supportImageBase64Only 是否仅支持 Base64 图片输入 + */ + public record Resolution(boolean supportImage, boolean supportImageBase64Only) { + + /** + * 创建关闭全部图片能力的结果。 + * + * @return 关闭图片能力的结果 + */ + private static Resolution disabled() { + return new Resolution(false, false); + } + } +} diff --git a/easyflow-modules/easyflow-module-agent/src/main/java/tech/easyflow/agent/runtime/AgentRunService.java b/easyflow-modules/easyflow-module-agent/src/main/java/tech/easyflow/agent/runtime/AgentRunService.java index 5fe84ee9..06eea038 100644 --- a/easyflow-modules/easyflow-module-agent/src/main/java/tech/easyflow/agent/runtime/AgentRunService.java +++ b/easyflow-modules/easyflow-module-agent/src/main/java/tech/easyflow/agent/runtime/AgentRunService.java @@ -1482,10 +1482,11 @@ public class AgentRunService { if (imageUploadIds == null || imageUploadIds.isEmpty()) { return; } - tech.easyflow.ai.entity.Model model = agent == null || agent.getModelId() == null - ? null - : modelService.getModelInstance(agent.getModelId()); - if (model == null || !Boolean.TRUE.equals(model.getSupportImage())) { + AgentModelCapabilityResolver.Resolution capabilities = + AgentModelCapabilityResolver.resolve(agent, () -> + agent == null || agent.getModelId() == null + ? null : modelService.getModelInstance(agent.getModelId())); + if (!capabilities.supportImage()) { throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "当前 Agent 模型未启用多模态图片能力"); } } diff --git a/easyflow-modules/easyflow-module-agent/src/main/java/tech/easyflow/agent/runtime/AgentRuntimeCompiler.java b/easyflow-modules/easyflow-module-agent/src/main/java/tech/easyflow/agent/runtime/AgentRuntimeCompiler.java index d7c94aba..c36188b6 100644 --- a/easyflow-modules/easyflow-module-agent/src/main/java/tech/easyflow/agent/runtime/AgentRuntimeCompiler.java +++ b/easyflow-modules/easyflow-module-agent/src/main/java/tech/easyflow/agent/runtime/AgentRuntimeCompiler.java @@ -108,7 +108,12 @@ public class AgentRuntimeCompiler { if (model == null) { throw new BusinessException("Agent 模型不存在"); } - return AgentModelSpecMapper.fromModel(model, agent.getModelConfigJson()); + AgentModelSpec spec = AgentModelSpecMapper.fromModel(model, agent.getModelConfigJson()); + AgentModelCapabilityResolver.Resolution capabilities = + AgentModelCapabilityResolver.resolve(agent, () -> model); + spec.setSupportImage(capabilities.supportImage()); + spec.setSupportImageBase64Only(capabilities.supportImageBase64Only()); + return spec; } private AgentGenerationOptions buildGenerationOptions(Map config) { diff --git a/easyflow-modules/easyflow-module-agent/src/test/java/tech/easyflow/agent/runtime/AgentRunServiceImageCapabilityTest.java b/easyflow-modules/easyflow-module-agent/src/test/java/tech/easyflow/agent/runtime/AgentRunServiceImageCapabilityTest.java new file mode 100644 index 00000000..ff407a76 --- /dev/null +++ b/easyflow-modules/easyflow-module-agent/src/test/java/tech/easyflow/agent/runtime/AgentRunServiceImageCapabilityTest.java @@ -0,0 +1,167 @@ +package tech.easyflow.agent.runtime; + +import org.junit.Assert; +import org.junit.Test; +import org.mockito.Mockito; +import org.springframework.web.server.ResponseStatusException; +import tech.easyflow.agent.entity.Agent; +import tech.easyflow.ai.entity.Model; +import tech.easyflow.ai.service.ModelService; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.math.BigInteger; +import java.util.List; +import java.util.Map; + +/** + * Agent 正式与草稿聊天图片能力边界测试。 + */ +public class AgentRunServiceImageCapabilityTest { + + /** + * 验证正式聊天允许快照已启用的图片能力,且不读取实时模型能力。 + * + * @throws Exception 反射调用失败时抛出 + */ + @Test + public void formalChatShouldUseEnabledPublishedCapability() throws Exception { + ModelService modelService = Mockito.mock(ModelService.class); + AgentRunService service = service(modelService); + Agent agent = publishedAgent(true); + + invokeAssertImageCapability(service, agent); + + Mockito.verifyNoInteractions(modelService); + } + + /** + * 验证正式聊天拒绝快照未启用的图片能力,且不读取实时模型能力。 + * + * @throws Exception 反射调用失败时抛出 + */ + @Test + public void formalChatShouldRejectDisabledPublishedCapability() throws Exception { + ModelService modelService = Mockito.mock(ModelService.class); + AgentRunService service = service(modelService); + Agent agent = publishedAgent(false); + + ResponseStatusException error = expectImageCapabilityError(service, agent); + + Assert.assertEquals(400, error.getStatusCode().value()); + Mockito.verifyNoInteractions(modelService); + } + + /** + * 验证旧快照缺少图片能力字段时安全拒绝图片。 + * + * @throws Exception 反射调用失败时抛出 + */ + @Test + public void formalChatShouldFailClosedForLegacySnapshot() throws Exception { + ModelService modelService = Mockito.mock(ModelService.class); + AgentRunService service = service(modelService); + Agent agent = new Agent(); + agent.setModelId(BigInteger.TEN); + agent.setPublishedSnapshotJson(Map.of( + "modelSummary", Map.of("modelName", "legacy-model"))); + + ResponseStatusException error = expectImageCapabilityError(service, agent); + + Assert.assertEquals(400, error.getStatusCode().value()); + Mockito.verifyNoInteractions(modelService); + } + + /** + * 验证草稿聊天继续读取实时模型图片能力。 + * + * @throws Exception 反射调用失败时抛出 + */ + @Test + public void draftChatShouldUseLiveModelCapability() throws Exception { + Model model = new Model(); + model.setSupportImage(true); + ModelService modelService = Mockito.mock(ModelService.class); + Mockito.when(modelService.getModelInstance(BigInteger.TEN)).thenReturn(model); + AgentRunService service = service(modelService); + Agent agent = new Agent(); + agent.setModelId(BigInteger.TEN); + + invokeAssertImageCapability(service, agent); + + Mockito.verify(modelService).getModelInstance(BigInteger.TEN); + } + + /** + * 创建已注入模型服务的运行服务。 + * + * @param modelService 模型服务 + * @return Agent 运行服务 + * @throws Exception 字段注入失败时抛出 + */ + private AgentRunService service(ModelService modelService) throws Exception { + AgentRunService service = new AgentRunService(); + Field field = AgentRunService.class.getDeclaredField("modelService"); + field.setAccessible(true); + field.set(service, modelService); + return service; + } + + /** + * 创建携带图片能力的正式运行 Agent。 + * + * @param supportImage 是否支持图片 + * @return 正式运行 Agent + */ + private Agent publishedAgent(boolean supportImage) { + Agent agent = new Agent(); + agent.setModelId(BigInteger.TEN); + agent.setPublishedSnapshotJson(Map.of( + "modelSummary", Map.of( + "supportImage", supportImage, + "supportImageB64Only", false))); + return agent; + } + + /** + * 调用图片能力校验。 + * + * @param service Agent 运行服务 + * @param agent Agent 运行视图 + * @throws Exception 反射调用失败或能力校验失败时抛出 + */ + private void invokeAssertImageCapability(AgentRunService service, + Agent agent) throws Exception { + Method method = AgentRunService.class.getDeclaredMethod( + "assertImageCapability", Agent.class, List.class); + method.setAccessible(true); + try { + method.invoke(service, agent, List.of("image-upload-id")); + } catch (InvocationTargetException error) { + if (error.getCause() instanceof Exception cause) { + throw cause; + } + throw error; + } + } + + /** + * 调用图片能力校验并返回预期的业务错误。 + * + * @param service Agent 运行服务 + * @param agent Agent 运行视图 + * @return 图片能力错误 + * @throws Exception 反射调用失败时抛出 + */ + private ResponseStatusException expectImageCapabilityError(AgentRunService service, + Agent agent) throws Exception { + try { + invokeAssertImageCapability(service, agent); + Assert.fail("expected ResponseStatusException"); + return null; + } catch (ResponseStatusException error) { + return error; + } + } +} diff --git a/easyflow-modules/easyflow-module-agent/src/test/java/tech/easyflow/agent/runtime/AgentRuntimeCompilerModelConfigTest.java b/easyflow-modules/easyflow-module-agent/src/test/java/tech/easyflow/agent/runtime/AgentRuntimeCompilerModelConfigTest.java index 7d916cd5..aae1b12a 100644 --- a/easyflow-modules/easyflow-module-agent/src/test/java/tech/easyflow/agent/runtime/AgentRuntimeCompilerModelConfigTest.java +++ b/easyflow-modules/easyflow-module-agent/src/test/java/tech/easyflow/agent/runtime/AgentRuntimeCompilerModelConfigTest.java @@ -144,6 +144,62 @@ public class AgentRuntimeCompilerModelConfigTest { Assert.assertEquals(AgentMessageContentFormat.STANDARD, spec.getMessageContentFormat()); } + /** + * 验证正式运行的图片能力严格使用发布快照。 + * + * @throws Exception 反射调用失败时抛出 + */ + @Test + public void publishedImageCapabilityShouldOverrideLiveModel() throws Exception { + Model model = model(Map.of()); + model.setSupportImage(false); + model.setSupportImageB64Only(false); + Agent agent = agent(Map.of( + "modelSummary", Map.of( + "supportImage", true, + "supportImageB64Only", true))); + + AgentModelSpec spec = invokeModelSpec(compiler(model), agent); + + Assert.assertTrue(spec.isSupportImage()); + Assert.assertTrue(spec.isSupportImageBase64Only()); + } + + /** + * 验证旧发布快照缺少能力字段时关闭图片能力。 + * + * @throws Exception 反射调用失败时抛出 + */ + @Test + public void missingPublishedImageCapabilityShouldFailClosed() throws Exception { + Model model = model(Map.of()); + model.setSupportImage(true); + model.setSupportImageB64Only(true); + Agent agent = agent(Map.of("modelSummary", Map.of("modelName", "legacy-model"))); + + AgentModelSpec spec = invokeModelSpec(compiler(model), agent); + + Assert.assertFalse(spec.isSupportImage()); + Assert.assertFalse(spec.isSupportImageBase64Only()); + } + + /** + * 验证草稿运行继续使用实时模型图片能力。 + * + * @throws Exception 反射调用失败时抛出 + */ + @Test + public void draftImageCapabilityShouldUseLiveModel() throws Exception { + Model model = model(Map.of()); + model.setSupportImage(true); + model.setSupportImageB64Only(false); + + AgentModelSpec spec = invokeModelSpec(compiler(model)); + + Assert.assertTrue(spec.isSupportImage()); + Assert.assertFalse(spec.isSupportImageBase64Only()); + } + /** * 验证 EasyFlow 忽略新旧消息数阈值,仅保留 Token 压缩配置。 * @@ -230,13 +286,37 @@ public class AgentRuntimeCompilerModelConfigTest { * @throws Exception 反射调用失败时抛出 */ private AgentModelSpec invokeModelSpec(AgentRuntimeCompiler compiler) throws Exception { - Agent agent = new Agent(); - agent.setModelId(BigInteger.TEN); + return invokeModelSpec(compiler, agent(Map.of())); + } + + /** + * 调用私有模型声明编译方法。 + * + * @param compiler 编译器 + * @param agent Agent 运行视图 + * @return 模型声明 + * @throws Exception 反射调用失败时抛出 + */ + private AgentModelSpec invokeModelSpec(AgentRuntimeCompiler compiler, + Agent agent) throws Exception { Method method = AgentRuntimeCompiler.class.getDeclaredMethod("buildModelSpec", Agent.class); method.setAccessible(true); return (AgentModelSpec) method.invoke(compiler, agent); } + /** + * 创建测试 Agent。 + * + * @param publishedSnapshot 发布快照;空映射表示草稿态 + * @return 测试 Agent + */ + private Agent agent(Map publishedSnapshot) { + Agent agent = new Agent(); + agent.setModelId(BigInteger.TEN); + agent.setPublishedSnapshotJson(publishedSnapshot); + return agent; + } + /** * 调用私有记忆策略编译方法。 *