From 791649c7d5dc9d53d8410418dbc16b743540a741 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=AD=90=E9=BB=98?= <925456043@qq.com> Date: Fri, 17 Jul 2026 19:57:06 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=BC=BA=E6=99=BA=E8=83=BD?= =?UTF-8?q?=E4=BD=93=E6=A8=A1=E5=9E=8B=E8=B0=83=E7=94=A8=E5=85=BC=E5=AE=B9?= =?UTF-8?q?=E8=83=BD=E5=8A=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 增加模型流式开关和 HTTP 传输策略配置 - 使用 AgentScope 执行基础连接、流式与 VLM 双阶段验证 - 固定多模态校验图片并统一验证状态展示 --- .../agent/runtime/AgentModelSpecMapper.java | 122 +++++++ .../agent/runtime/AgentRuntimeCompiler.java | 25 +- ...entScopeChatModelConnectivityVerifier.java | 275 ++++++++++++++++ .../AgentRuntimeCompilerModelConfigTest.java | 130 ++++++++ ...copeChatModelConnectivityVerifierTest.java | 305 ++++++++++++++++++ .../ai/service/impl/ModelServiceImpl.java | 38 +-- .../service/support/VlmVerificationImage.java | 90 ++++++ .../ChatModelConnectivityVerifier.java | 17 + .../ChatModelVerificationResult.java | 132 ++++++++ .../verification/ModelVerificationStatus.java | 15 + .../resources/images/vlm-verification.png | Bin 0 -> 78448 bytes .../support/VlmVerificationImageTest.java | 32 ++ easyflow-ui-admin/app/src/api/ai/llm.ts | 15 + .../ai/agents/components/AgentBaseForm.vue | 26 +- .../composables/useAgentDesignerState.test.ts | 33 ++ .../composables/useAgentDesignerState.ts | 9 + .../views/ai/model/ActiveModelWorkspace.vue | 44 +-- .../app/src/views/ai/model/AddModelModal.vue | 64 +++- .../src/views/ai/model/ModelVerifyConfig.vue | 45 ++- .../views/ai/model/ModelViewItemOperation.vue | 44 +-- .../__tests__/model-verification.test.ts | 63 ++++ .../ai/model/modelUtils/model-verification.ts | 75 +++++ 22 files changed, 1492 insertions(+), 107 deletions(-) create mode 100644 easyflow-modules/easyflow-module-agent/src/main/java/tech/easyflow/agent/runtime/AgentModelSpecMapper.java create mode 100644 easyflow-modules/easyflow-module-agent/src/main/java/tech/easyflow/agent/runtime/AgentScopeChatModelConnectivityVerifier.java create mode 100644 easyflow-modules/easyflow-module-agent/src/test/java/tech/easyflow/agent/runtime/AgentRuntimeCompilerModelConfigTest.java create mode 100644 easyflow-modules/easyflow-module-agent/src/test/java/tech/easyflow/agent/runtime/AgentScopeChatModelConnectivityVerifierTest.java create mode 100644 easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/support/VlmVerificationImage.java create mode 100644 easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/verification/ChatModelConnectivityVerifier.java create mode 100644 easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/verification/ChatModelVerificationResult.java create mode 100644 easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/verification/ModelVerificationStatus.java create mode 100644 easyflow-modules/easyflow-module-ai/src/main/resources/images/vlm-verification.png create mode 100644 easyflow-modules/easyflow-module-ai/src/test/java/tech/easyflow/ai/service/support/VlmVerificationImageTest.java create mode 100644 easyflow-ui-admin/app/src/views/ai/agents/composables/useAgentDesignerState.test.ts create mode 100644 easyflow-ui-admin/app/src/views/ai/model/modelUtils/__tests__/model-verification.test.ts create mode 100644 easyflow-ui-admin/app/src/views/ai/model/modelUtils/model-verification.ts diff --git a/easyflow-modules/easyflow-module-agent/src/main/java/tech/easyflow/agent/runtime/AgentModelSpecMapper.java b/easyflow-modules/easyflow-module-agent/src/main/java/tech/easyflow/agent/runtime/AgentModelSpecMapper.java new file mode 100644 index 00000000..b6ca972b --- /dev/null +++ b/easyflow-modules/easyflow-module-agent/src/main/java/tech/easyflow/agent/runtime/AgentModelSpecMapper.java @@ -0,0 +1,122 @@ +package tech.easyflow.agent.runtime; + +import com.easyagents.agent.runtime.model.AgentHttpVersionPolicy; +import com.easyagents.agent.runtime.model.AgentModelProviderType; +import com.easyagents.agent.runtime.model.AgentModelSpec; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import tech.easyflow.ai.entity.Model; + +import java.util.Locale; +import java.util.Map; + +/** + * 将 EasyFlow 模型配置映射为智能体运行时模型声明。 + */ +public final class AgentModelSpecMapper { + + private static final Logger LOG = LoggerFactory.getLogger(AgentModelSpecMapper.class); + + /** + * 禁止实例化工具类。 + */ + private AgentModelSpecMapper() { + } + + /** + * 按模型持久化配置创建运行时模型声明。 + * + * @param model 已补齐供应商默认配置的模型 + * @return 运行时模型声明 + * @throws IllegalArgumentException 模型为空时抛出 + */ + public static AgentModelSpec fromModel(Model model) { + return fromModel(model, Map.of()); + } + + /** + * 按模型配置和 Agent 快照覆盖项创建运行时模型声明。 + * + * @param model 已补齐供应商默认配置的模型 + * @param overrides Agent 发布快照中的模型覆盖项 + * @return 运行时模型声明 + * @throws IllegalArgumentException 模型为空时抛出 + */ + public static AgentModelSpec fromModel(Model model, Map overrides) { + if (model == null) { + throw new IllegalArgumentException("模型配置不能为空"); + } + Map safeOverrides = overrides == null ? Map.of() : overrides; + AgentModelSpec spec = new AgentModelSpec(); + String providerType = stringValue( + safeOverrides, + "providerType", + model.getModelProvider() == null ? null : model.getModelProvider().getProviderType()); + spec.setProviderType(parseProviderType(providerType)); + spec.setModelName(stringValue(safeOverrides, "modelName", model.getModelName())); + spec.setBaseUrl(stringValue(safeOverrides, "baseUrl", model.getEndpoint())); + spec.setEndpointPath(stringValue(safeOverrides, "endpointPath", model.getRequestPath())); + spec.setApiKey(stringValue(safeOverrides, "apiKey", model.getApiKey())); + spec.setSupportImage(Boolean.TRUE.equals(model.getSupportImage())); + spec.setSupportImageBase64Only(Boolean.TRUE.equals(model.getSupportImageB64Only())); + spec.setHttpVersionPolicy(parseHttpVersionPolicy(model)); + spec.getMetadata().put("modelId", model.getId()); + if (providerType != null && !providerType.isBlank()) { + spec.getMetadata().put("sourceProviderType", providerType); + } + return spec; + } + + /** + * 解析模型配置中的 Agent HTTP 版本策略。 + * + * @param model 模型配置 + * @return HTTP 版本策略,缺失或非法时返回 AUTO + */ + private static AgentHttpVersionPolicy parseHttpVersionPolicy(Model model) { + Object rawPolicy = model.getOptions() == null + ? null + : model.getOptions().get("agentHttpVersionPolicy"); + if (rawPolicy == null || String.valueOf(rawPolicy).isBlank()) { + return AgentHttpVersionPolicy.AUTO; + } + String normalizedPolicy = String.valueOf(rawPolicy).trim().toUpperCase(Locale.ROOT); + try { + return AgentHttpVersionPolicy.valueOf(normalizedPolicy); + } catch (IllegalArgumentException exception) { + LOG.warn("Invalid Agent HTTP version policy '{}' for model {}, fallback to AUTO", + rawPolicy, model.getId()); + return AgentHttpVersionPolicy.AUTO; + } + } + + /** + * 解析 AgentScope 支持的模型供应商类型。 + * + * @param providerType 供应商类型 + * @return 运行时供应商类型,未知值按 OpenAI-compatible 处理 + */ + private static AgentModelProviderType parseProviderType(String providerType) { + if (providerType == null || providerType.isBlank()) { + return AgentModelProviderType.OPENAI_COMPATIBLE; + } + try { + return AgentModelProviderType.valueOf(providerType.trim().toUpperCase(Locale.ROOT)); + } catch (IllegalArgumentException ignored) { + return AgentModelProviderType.OPENAI_COMPATIBLE; + } + } + + /** + * 读取字符串覆盖项。 + * + * @param values 配置映射 + * @param key 字段名 + * @param defaultValue 默认值 + * @return 覆盖值或默认值 + */ + private static String stringValue(Map values, String key, String defaultValue) { + Object value = values.get(key); + return value == null ? defaultValue : String.valueOf(value); + } +} 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 227b4f46..9a55758f 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 @@ -13,7 +13,6 @@ import com.easyagents.agent.runtime.memory.AgentMemoryType; import com.easyagents.agent.runtime.mcp.McpSpec; import com.easyagents.agent.runtime.mcp.McpTransportType; import com.easyagents.agent.runtime.model.AgentGenerationOptions; -import com.easyagents.agent.runtime.model.AgentModelProviderType; import com.easyagents.agent.runtime.model.AgentModelSpec; import com.easyagents.agent.runtime.tool.AgentToolCategory; import com.easyagents.agent.runtime.tool.AgentToolResult; @@ -104,18 +103,7 @@ public class AgentRuntimeCompiler { if (model == null) { throw new BusinessException("Agent 模型不存在"); } - Map config = agent.getModelConfigJson(); - AgentModelSpec spec = new AgentModelSpec(); - String providerType = stringValue(config, "providerType", model.getModelProvider() == null ? null : model.getModelProvider().getProviderType()); - spec.setProviderType(parseProviderType(providerType)); - spec.setModelName(stringValue(config, "modelName", model.getModelName())); - spec.setBaseUrl(stringValue(config, "baseUrl", model.getEndpoint())); - spec.setEndpointPath(stringValue(config, "endpointPath", model.getRequestPath())); - spec.setApiKey(stringValue(config, "apiKey", model.getApiKey())); - spec.setSupportImage(Boolean.TRUE.equals(model.getSupportImage())); - spec.setSupportImageBase64Only(Boolean.TRUE.equals(model.getSupportImageB64Only())); - spec.getMetadata().put("modelId", model.getId()); - return spec; + return AgentModelSpecMapper.fromModel(model, agent.getModelConfigJson()); } private AgentGenerationOptions buildGenerationOptions(Map config) { @@ -756,17 +744,6 @@ public class AgentRuntimeCompiler { return description == null ? "" : description; } - private AgentModelProviderType parseProviderType(String providerType) { - if (providerType == null || providerType.isBlank()) { - return AgentModelProviderType.OPENAI_COMPATIBLE; - } - try { - return AgentModelProviderType.valueOf(providerType.trim().toUpperCase()); - } catch (IllegalArgumentException ignored) { - return AgentModelProviderType.OPENAI_COMPATIBLE; - } - } - private AgentMemoryType memoryTypeValue(Map map, String key) { String value = stringValue(map, key, AgentMemoryType.AUTO_CONTEXT.name()); try { diff --git a/easyflow-modules/easyflow-module-agent/src/main/java/tech/easyflow/agent/runtime/AgentScopeChatModelConnectivityVerifier.java b/easyflow-modules/easyflow-module-agent/src/main/java/tech/easyflow/agent/runtime/AgentScopeChatModelConnectivityVerifier.java new file mode 100644 index 00000000..4fc51518 --- /dev/null +++ b/easyflow-modules/easyflow-module-agent/src/main/java/tech/easyflow/agent/runtime/AgentScopeChatModelConnectivityVerifier.java @@ -0,0 +1,275 @@ +package tech.easyflow.agent.runtime; + +import com.easyagents.agent.runtime.agentscope.AgentHttpTransportProvider; +import com.easyagents.agent.runtime.agentscope.AgentScopeMessageAdapter; +import com.easyagents.agent.runtime.agentscope.AgentScopeModelFactory; +import com.easyagents.agent.runtime.message.AgentContentBlock; +import com.easyagents.agent.runtime.message.AgentMediaBlock; +import com.easyagents.agent.runtime.message.AgentMessage; +import com.easyagents.agent.runtime.message.AgentMessageRole; +import com.easyagents.agent.runtime.message.AgentTextBlock; +import com.easyagents.agent.runtime.model.AgentGenerationOptions; +import com.easyagents.agent.runtime.model.AgentModelFactory; +import com.easyagents.agent.runtime.model.AgentModelProviderType; +import com.easyagents.agent.runtime.model.AgentModelSpec; +import io.agentscope.core.message.ContentBlock; +import io.agentscope.core.message.Msg; +import io.agentscope.core.message.TextBlock; +import io.agentscope.core.model.ChatResponse; +import io.agentscope.core.model.ExecutionConfig; +import io.agentscope.core.model.GenerateOptions; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; +import tech.easyflow.ai.entity.Model; +import tech.easyflow.ai.service.support.VlmVerificationImage; +import tech.easyflow.ai.service.verification.ChatModelConnectivityVerifier; +import tech.easyflow.ai.service.verification.ChatModelVerificationResult; +import tech.easyflow.common.web.exceptions.BusinessException; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.Base64; +import java.util.List; +import java.util.Map; + +/** + * 使用 AgentScope 真实运行链路验证 Chat Model 与 VLM 连通性。 + */ +@Component +public class AgentScopeChatModelConnectivityVerifier implements ChatModelConnectivityVerifier { + + private static final Logger LOG = LoggerFactory.getLogger(AgentScopeChatModelConnectivityVerifier.class); + /** 为未识别关闭思考扩展参数的推理模型保留足够的小额输出预算。 */ + private static final int MAX_TOKENS = 256; + private static final Duration DEFAULT_PHASE_TIMEOUT = Duration.ofSeconds(60); + + /** AgentScope 模型工厂。 */ + private final AgentModelFactory modelFactory; + /** EasyAgents 与 AgentScope 的消息适配器。 */ + private final AgentScopeMessageAdapter messageAdapter; + /** 单阶段最大等待时间。 */ + private final Duration phaseTimeout; + + /** + * 使用生产运行时组件创建验证器。 + */ + public AgentScopeChatModelConnectivityVerifier() { + this(new AgentScopeModelFactory(), new AgentScopeMessageAdapter(), DEFAULT_PHASE_TIMEOUT); + } + + /** + * 使用指定依赖创建验证器,供测试和受控运行环境使用。 + * + * @param modelFactory AgentScope 模型工厂 + * @param messageAdapter 消息适配器 + * @param phaseTimeout 单阶段最大等待时间 + */ + AgentScopeChatModelConnectivityVerifier( + AgentModelFactory modelFactory, + AgentScopeMessageAdapter messageAdapter, + Duration phaseTimeout) { + this.modelFactory = modelFactory; + this.messageAdapter = messageAdapter; + this.phaseTimeout = phaseTimeout; + } + + /** + * 依次验证非流式基础连接与流式响应能力。 + * + * @param model 已补齐供应商默认配置的模型 + * @return 双阶段验证结果 + * @throws BusinessException 非流式基础连接失败时抛出 + */ + @Override + public ChatModelVerificationResult verify(Model model) { + AgentModelSpec modelSpec = AgentModelSpecMapper.fromModel(model); + String effectiveHttpVersion = AgentHttpTransportProvider.resolveEffectivePolicy( + modelSpec.getHttpVersionPolicy(), modelSpec.getBaseUrl()).name(); + AgentMessage verificationMessage = buildVerificationMessage(modelSpec.isSupportImage()); + + try { + verifyPhase(modelSpec, verificationMessage, false); + } catch (BusinessException exception) { + LOG.error("AgentScope model base connectivity verification failed, modelId={}, httpPolicy={}", + model.getId(), effectiveHttpVersion, exception); + throw exception; + } catch (Exception exception) { + LOG.error("AgentScope model base connectivity verification failed, modelId={}, httpPolicy={}", + model.getId(), effectiveHttpVersion, exception); + throw new BusinessException(400, 1, "模型基础连接验证失败,请查看后端日志", exception); + } + + try { + verifyPhase(modelSpec, verificationMessage, true); + return ChatModelVerificationResult.passed(effectiveHttpVersion); + } catch (Exception exception) { + LOG.warn("AgentScope model streaming verification failed, modelId={}, httpPolicy={}", + model.getId(), effectiveHttpVersion, exception); + return ChatModelVerificationResult.streamingUnavailable(effectiveHttpVersion); + } + } + + /** + * 执行一次指定流式模式的模型请求并校验响应。 + * + * @param modelSpec 运行时模型声明 + * @param verificationMessage 验证消息 + * @param stream 是否启用流式响应 + * @throws BusinessException 响应为空或 VLM 图片识别错误时抛出 + */ + private void verifyPhase(AgentModelSpec modelSpec, + AgentMessage verificationMessage, + boolean stream) { + AgentGenerationOptions generationOptions = new AgentGenerationOptions(); + generationOptions.setStream(stream); + generationOptions.setThinkingEnabled(false); + disableOpenAiCompatibleThinking(modelSpec, generationOptions); + generationOptions.setMaxTokens(MAX_TOKENS); + io.agentscope.core.model.Model agentScopeModel = modelFactory.create(modelSpec, generationOptions); + + Msg message = messageAdapter.toMsg(verificationMessage); + GenerateOptions requestOptions = GenerateOptions.builder() + .stream(stream) + .maxTokens(MAX_TOKENS) + .executionConfig(ExecutionConfig.builder() + .timeout(phaseTimeout) + .maxAttempts(1) + .build()) + .build(); + List responses = agentScopeModel + .stream(List.of(message), List.of(), requestOptions) + .timeout(phaseTimeout) + .collectList() + .block(phaseTimeout.plusSeconds(1)); + String responseText = aggregateText(responses); + validateResponse(modelSpec.isSupportImage(), responseText); + } + + /** + * 为支持该扩展字段的 OpenAI-compatible 服务显式关闭思考。 + * + *

AgentScope 的通用 OpenAI Formatter 不会读取中立的 + * {@code thinkingEnabled} 字段,需通过扩展请求体传递;原生 OpenAI、 + * DeepSeek 等服务不接收该非标准字段,因此仅对已知兼容入口设置。

+ * + * @param modelSpec 运行时模型声明 + * @param generationOptions 验证生成参数 + */ + private void disableOpenAiCompatibleThinking( + AgentModelSpec modelSpec, + AgentGenerationOptions generationOptions) { + AgentModelProviderType providerType = modelSpec.getProviderType(); + if (providerType == AgentModelProviderType.OPENAI_COMPATIBLE + || providerType == AgentModelProviderType.CUSTOM + || providerType == AgentModelProviderType.SILICONFLOW) { + generationOptions.getAdditionalBodyParams().put("enable_thinking", false); + } + Object sourceProviderType = modelSpec.getMetadata().get("sourceProviderType"); + if (sourceProviderType != null + && "gpustack".equalsIgnoreCase(String.valueOf(sourceProviderType))) { + // GPUStack 托管的 vLLM 模型通过 chat_template_kwargs 控制 Qwen 思考模式。 + generationOptions.getAdditionalBodyParams().put( + "chat_template_kwargs", + Map.of("enable_thinking", false)); + } + } + + /** + * 创建文字模型或 VLM 的最小验证消息。 + * + * @param supportImage 是否验证图片理解能力 + * @return 验证消息 + */ + private AgentMessage buildVerificationMessage(boolean supportImage) { + if (!supportImage) { + return AgentMessage.text( + AgentMessageRole.USER, + "请直接回复“你好”,不要补充其他内容。"); + } + List blocks = new ArrayList<>(); + blocks.add(new AgentTextBlock("请直接输出图片中的内容,不要补充其他内容。")); + AgentMediaBlock image = new AgentMediaBlock("image"); + image.setMimeType("image/png"); + image.setData(Base64.getEncoder().encodeToString(VlmVerificationImage.pngBytes())); + blocks.add(image); + AgentMessage message = new AgentMessage(); + message.setRole(AgentMessageRole.USER); + message.setContentBlocks(blocks); + return message; + } + + /** + * 聚合流式响应中的全部文本增量。 + * + * @param responses AgentScope 响应片段 + * @return 聚合后的文本 + */ + private String aggregateText(List responses) { + StringBuilder result = new StringBuilder(); + if (responses == null) { + return result.toString(); + } + for (ChatResponse response : responses) { + if (response == null || response.getContent() == null) { + continue; + } + for (ContentBlock block : response.getContent()) { + if (block instanceof TextBlock textBlock && textBlock.getText() != null) { + result.append(textBlock.getText()); + } + } + } + return result.toString(); + } + + /** + * 校验模型返回内容。 + * + * @param supportImage 是否执行 VLM 图片识别校验 + * @param responseText 聚合后的响应文本 + * @throws BusinessException 响应为空或图片识别结果不匹配时抛出 + */ + private void validateResponse(boolean supportImage, String responseText) { + if (responseText == null || responseText.isBlank()) { + throw new BusinessException("模型未返回有效内容"); + } + if (supportImage + && !normalizeVerificationText(responseText) + .contains(VlmVerificationImage.VERIFICATION_CODE)) { + LOG.warn("VLM verification response did not contain expected code, responseSummary={}", + responseSummary(responseText)); + throw new BusinessException("多模态校验未通过,模型未正确识别验证图片"); + } + } + + /** + * 归一化 VLM 对固定验证码的常见排版输出。 + * + *

模型可能将连续数字输出为 {@code 5 8 3 9} 或 {@code 5,8,3,9}, + * 这不影响图片识别结论,因此移除非字母数字字符后再校验。

+ * + * @param responseText 模型验证回复 + * @return 仅保留字母与数字的响应文本 + */ + private String normalizeVerificationText(String responseText) { + return responseText == null + ? "" + : responseText.replaceAll("[^\\p{L}\\p{N}]", ""); + } + + /** + * 生成固定验证回复的安全日志摘要。 + * + * @param responseText 模型验证回复 + * @return 移除控制字符且最长 160 字符的摘要 + */ + private String responseSummary(String responseText) { + String normalized = responseText == null + ? "" + : responseText.replaceAll("[\\p{Cntrl}]", " ").trim(); + return normalized.length() <= 160 + ? normalized + : normalized.substring(0, 160) + "..."; + } +} 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 new file mode 100644 index 00000000..d780901f --- /dev/null +++ b/easyflow-modules/easyflow-module-agent/src/test/java/tech/easyflow/agent/runtime/AgentRuntimeCompilerModelConfigTest.java @@ -0,0 +1,130 @@ +package tech.easyflow.agent.runtime; + +import com.easyagents.agent.runtime.model.AgentGenerationOptions; +import com.easyagents.agent.runtime.model.AgentHttpVersionPolicy; +import com.easyagents.agent.runtime.model.AgentModelSpec; +import org.junit.Assert; +import org.junit.Test; +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.Method; +import java.math.BigInteger; +import java.util.Map; + +/** + * Agent 模型生成和 HTTP 传输配置编译测试。 + */ +public class AgentRuntimeCompilerModelConfigTest { + + /** + * 验证缺少 stream 时默认开启,显式关闭时保持关闭。 + * + * @throws Exception 反射调用失败时抛出 + */ + @Test + public void generationStreamShouldDefaultToTrueAndAllowFalse() throws Exception { + AgentRuntimeCompiler compiler = new AgentRuntimeCompiler(); + + AgentGenerationOptions defaultOptions = invokeGenerationOptions(compiler, Map.of()); + AgentGenerationOptions disabledOptions = invokeGenerationOptions(compiler, Map.of("stream", false)); + + Assert.assertTrue(defaultOptions.getStream()); + Assert.assertFalse(disabledOptions.getStream()); + } + + /** + * 验证模型 options 中的 HTTP 策略会编译到中立模型声明。 + * + * @throws Exception 反射调用失败时抛出 + */ + @Test + public void modelHttpPolicyShouldCompileFromOptions() throws Exception { + Model model = model(Map.of("agentHttpVersionPolicy", "HTTP_1_1")); + AgentRuntimeCompiler compiler = compiler(model); + + AgentModelSpec spec = invokeModelSpec(compiler); + + Assert.assertEquals(AgentHttpVersionPolicy.HTTP_1_1, spec.getHttpVersionPolicy()); + } + + /** + * 验证未知 HTTP 策略安全回退到 AUTO。 + * + * @throws Exception 反射调用失败时抛出 + */ + @Test + public void unknownModelHttpPolicyShouldFallbackToAuto() throws Exception { + Model model = model(Map.of("agentHttpVersionPolicy", "HTTP_3")); + AgentRuntimeCompiler compiler = compiler(model); + + AgentModelSpec spec = invokeModelSpec(compiler); + + Assert.assertEquals(AgentHttpVersionPolicy.AUTO, spec.getHttpVersionPolicy()); + } + + /** + * 创建已注入模型服务的编译器。 + * + * @param model 模型 + * @return 编译器 + * @throws Exception 注入失败时抛出 + */ + private AgentRuntimeCompiler compiler(Model model) throws Exception { + AgentRuntimeCompiler compiler = new AgentRuntimeCompiler(); + ModelService modelService = (ModelService) java.lang.reflect.Proxy.newProxyInstance( + ModelService.class.getClassLoader(), + new Class[]{ModelService.class}, + (proxy, method, args) -> "getModelInstance".equals(method.getName()) ? model : null); + Field field = AgentRuntimeCompiler.class.getDeclaredField("modelService"); + field.setAccessible(true); + field.set(compiler, modelService); + return compiler; + } + + /** + * 创建测试模型。 + * + * @param options 模型扩展配置 + * @return 测试模型 + */ + private Model model(Map options) { + Model model = new Model(); + model.setId(BigInteger.TEN); + model.setModelName("test-model"); + model.setOptions(options); + return model; + } + + /** + * 调用私有生成参数编译方法。 + * + * @param compiler 编译器 + * @param config 生成配置 + * @return 生成参数 + * @throws Exception 反射调用失败时抛出 + */ + private AgentGenerationOptions invokeGenerationOptions(AgentRuntimeCompiler compiler, + Map config) throws Exception { + Method method = AgentRuntimeCompiler.class.getDeclaredMethod("buildGenerationOptions", Map.class); + method.setAccessible(true); + return (AgentGenerationOptions) method.invoke(compiler, config); + } + + /** + * 调用私有模型声明编译方法。 + * + * @param compiler 编译器 + * @return 模型声明 + * @throws Exception 反射调用失败时抛出 + */ + private AgentModelSpec invokeModelSpec(AgentRuntimeCompiler compiler) throws Exception { + Agent agent = new Agent(); + agent.setModelId(BigInteger.TEN); + Method method = AgentRuntimeCompiler.class.getDeclaredMethod("buildModelSpec", Agent.class); + method.setAccessible(true); + return (AgentModelSpec) method.invoke(compiler, agent); + } +} diff --git a/easyflow-modules/easyflow-module-agent/src/test/java/tech/easyflow/agent/runtime/AgentScopeChatModelConnectivityVerifierTest.java b/easyflow-modules/easyflow-module-agent/src/test/java/tech/easyflow/agent/runtime/AgentScopeChatModelConnectivityVerifierTest.java new file mode 100644 index 00000000..961f3f7b --- /dev/null +++ b/easyflow-modules/easyflow-module-agent/src/test/java/tech/easyflow/agent/runtime/AgentScopeChatModelConnectivityVerifierTest.java @@ -0,0 +1,305 @@ +package tech.easyflow.agent.runtime; + +import com.easyagents.agent.runtime.agentscope.AgentScopeMessageAdapter; +import com.easyagents.agent.runtime.model.AgentGenerationOptions; +import com.easyagents.agent.runtime.model.AgentModelFactory; +import com.easyagents.agent.runtime.model.AgentModelSpec; +import io.agentscope.core.message.Base64Source; +import io.agentscope.core.message.ImageBlock; +import io.agentscope.core.message.Msg; +import io.agentscope.core.message.TextBlock; +import io.agentscope.core.model.ChatResponse; +import io.agentscope.core.model.GenerateOptions; +import io.agentscope.core.model.ToolSchema; +import org.junit.Assert; +import org.junit.Test; +import reactor.core.publisher.Flux; +import tech.easyflow.ai.entity.Model; +import tech.easyflow.ai.entity.ModelProvider; +import tech.easyflow.ai.service.support.VlmVerificationImage; +import tech.easyflow.ai.service.verification.ChatModelVerificationResult; +import tech.easyflow.ai.service.verification.ModelVerificationStatus; +import tech.easyflow.common.web.exceptions.BusinessException; + +import java.math.BigInteger; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Base64; +import java.util.List; + +/** + * AgentScope 双阶段模型连通性验证测试。 + */ +public class AgentScopeChatModelConnectivityVerifierTest { + + /** + * 验证非流式与流式阶段按顺序执行并返回通过状态。 + */ + @Test + public void shouldPassWhenBothPhasesReturnText() { + RecordingModelFactory factory = new RecordingModelFactory( + Flux.just(response("你好")), + Flux.just(response("你"), response("好"))); + AgentScopeChatModelConnectivityVerifier verifier = verifier(factory); + + ChatModelVerificationResult result = verifier.verify(model(false)); + + Assert.assertEquals(ModelVerificationStatus.PASSED, result.getStatus()); + Assert.assertEquals("HTTP_1_1", result.getEffectiveHttpVersion()); + Assert.assertEquals(List.of(false, true), factory.getFactoryStreams()); + Assert.assertEquals(List.of(false, true), factory.getRequestStreams()); + Assert.assertEquals(List.of(false, false), factory.getEnableThinkingValues()); + Assert.assertEquals(List.of(false, false), factory.getChatTemplateThinkingValues()); + } + + /** + * 验证基础连接通过而流式阶段失败时返回部分通过结果。 + */ + @Test + public void shouldReturnPartialWhenStreamingPhaseFails() { + RecordingModelFactory factory = new RecordingModelFactory( + Flux.just(response("你好")), + Flux.error(new IllegalStateException("stream failed"))); + + ChatModelVerificationResult result = verifier(factory).verify(model(false)); + + Assert.assertEquals(ModelVerificationStatus.PARTIAL, result.getStatus()); + Assert.assertEquals(ModelVerificationStatus.PASSED, result.getNonStreaming()); + Assert.assertEquals(ModelVerificationStatus.FAILED, result.getStreaming()); + Assert.assertTrue(result.getMessage().contains("流式响应不可用")); + } + + /** + * 验证基础连接失败时立即终止且返回业务失败。 + */ + @Test + public void shouldStopWhenNonStreamingPhaseFails() { + RecordingModelFactory factory = new RecordingModelFactory( + Flux.error(new IllegalStateException("connection failed"))); + + try { + verifier(factory).verify(model(false)); + Assert.fail("Expected base connectivity verification failure"); + } catch (BusinessException exception) { + Assert.assertTrue(exception.getMessage().contains("基础连接验证失败")); + Assert.assertFalse(exception.getMessage().contains("connection failed")); + } + Assert.assertEquals(List.of(false), factory.getFactoryStreams()); + } + + /** + * 验证流式阶段超时被归类为部分可用且不暴露底层异常。 + */ + @Test + public void shouldReturnPartialWhenStreamingPhaseTimesOut() { + RecordingModelFactory factory = new RecordingModelFactory( + Flux.just(response("你好")), + Flux.never()); + AgentScopeChatModelConnectivityVerifier verifier = new AgentScopeChatModelConnectivityVerifier( + factory, + new AgentScopeMessageAdapter(), + Duration.ofMillis(20)); + + ChatModelVerificationResult result = verifier.verify(model(false)); + + Assert.assertEquals(ModelVerificationStatus.PARTIAL, result.getStatus()); + Assert.assertEquals("连接成功,流式响应不可用,可关闭智能体的模型流式响应。", + result.getMessage()); + } + + /** + * 验证 VLM 使用 Base64 图片,并能聚合流式文本增量。 + */ + @Test + public void shouldVerifyVlmImageAndAggregateStreamingChunks() { + RecordingModelFactory factory = new RecordingModelFactory( + Flux.just(response("图片中的内容是“" + VlmVerificationImage.VERIFICATION_CODE + "”。")), + Flux.just(response("识别结果:58"), response("39"))); + + ChatModelVerificationResult result = verifier(factory).verify(model(true)); + + Assert.assertEquals(ModelVerificationStatus.PASSED, result.getStatus()); + Msg message = factory.getMessages().get(0); + ImageBlock image = message.getContent().stream() + .filter(ImageBlock.class::isInstance) + .map(ImageBlock.class::cast) + .findFirst() + .orElseThrow(); + Assert.assertTrue(image.getSource() instanceof Base64Source); + byte[] imageBytes = Base64.getDecoder().decode(((Base64Source) image.getSource()).getData()); + Assert.assertEquals((byte) 0x89, imageBytes[0]); + Assert.assertEquals((byte) 0x50, imageBytes[1]); + } + + /** + * 创建待验证模型。 + * + * @param supportImage 是否支持图片 + * @return 测试模型 + */ + private Model model(boolean supportImage) { + Model model = new Model(); + model.setId(BigInteger.TEN); + model.setModelName("test-model"); + model.setEndpoint("http://model.example.com"); + model.setRequestPath("/v1/chat/completions"); + model.setApiKey("test-key"); + model.setSupportImage(supportImage); + ModelProvider provider = new ModelProvider(); + provider.setProviderType("gpustack"); + model.setModelProvider(provider); + return model; + } + + /** + * 创建使用测试工厂的验证器。 + * + * @param factory 记录型模型工厂 + * @return 验证器 + */ + private AgentScopeChatModelConnectivityVerifier verifier(RecordingModelFactory factory) { + return new AgentScopeChatModelConnectivityVerifier( + factory, + new AgentScopeMessageAdapter(), + Duration.ofSeconds(2)); + } + + /** + * 创建单个文本响应片段。 + * + * @param text 文本内容 + * @return AgentScope 响应 + */ + private ChatResponse response(String text) { + return ChatResponse.builder() + .content(List.of(TextBlock.builder().text(text).build())) + .build(); + } + + /** + * 按阶段返回预设响应并记录调用参数的模型工厂。 + */ + private static final class RecordingModelFactory + implements AgentModelFactory { + + /** 每个阶段的预设响应。 */ + private final List> phaseResponses; + /** 模型工厂收到的流式参数。 */ + private final List factoryStreams = new ArrayList<>(); + /** 模型请求收到的流式参数。 */ + private final List requestStreams = new ArrayList<>(); + /** 模型请求收到的消息。 */ + private final List messages = new ArrayList<>(); + /** OpenAI-compatible 请求中的思考开关。 */ + private final List enableThinkingValues = new ArrayList<>(); + /** GPUStack 模板参数中的思考开关。 */ + private final List chatTemplateThinkingValues = new ArrayList<>(); + + /** + * 创建记录型模型工厂。 + * + * @param phaseResponses 每个阶段的预设响应 + */ + @SafeVarargs + private RecordingModelFactory(Flux... phaseResponses) { + this.phaseResponses = List.of(phaseResponses); + } + + /** + * 创建当前验证阶段的测试模型。 + * + * @param modelSpec 模型声明 + * @param generationOptions 生成参数 + * @return 测试模型 + */ + @Override + public io.agentscope.core.model.Model create( + AgentModelSpec modelSpec, + AgentGenerationOptions generationOptions) { + int phaseIndex = factoryStreams.size(); + factoryStreams.add(Boolean.TRUE.equals(generationOptions.getStream())); + enableThinkingValues.add( + generationOptions.getAdditionalBodyParams().get("enable_thinking")); + Object templateOptions = generationOptions.getAdditionalBodyParams() + .get("chat_template_kwargs"); + chatTemplateThinkingValues.add(templateOptions instanceof java.util.Map map + ? map.get("enable_thinking") + : null); + Flux responses = phaseResponses.get(phaseIndex); + return new io.agentscope.core.model.Model() { + /** + * 返回预设响应并记录真实请求参数。 + * + * @param inputMessages 模型消息 + * @param tools 工具声明 + * @param options 生成参数 + * @return 预设响应 + */ + @Override + public Flux stream( + List inputMessages, + List tools, + GenerateOptions options) { + requestStreams.add(Boolean.TRUE.equals(options.getStream())); + messages.add(inputMessages.get(0)); + return responses; + } + + /** + * 返回测试模型名称。 + * + * @return 测试模型名称 + */ + @Override + public String getModelName() { + return modelSpec.getModelName(); + } + }; + } + + /** + * 获取工厂流式参数记录。 + * + * @return 流式参数列表 + */ + private List getFactoryStreams() { + return factoryStreams; + } + + /** + * 获取请求流式参数记录。 + * + * @return 流式参数列表 + */ + private List getRequestStreams() { + return requestStreams; + } + + /** + * 获取请求消息记录。 + * + * @return 消息列表 + */ + private List getMessages() { + return messages; + } + + /** + * 获取 OpenAI-compatible 请求中的思考开关。 + * + * @return 各阶段思考开关 + */ + private List getEnableThinkingValues() { + return enableThinkingValues; + } + + /** + * 获取 GPUStack 模板参数中的思考开关。 + * + * @return 各阶段模板思考开关 + */ + private List getChatTemplateThinkingValues() { + return chatTemplateThinkingValues; + } + } +} diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/impl/ModelServiceImpl.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/impl/ModelServiceImpl.java index 60a4a8ca..0bf5ff9d 100644 --- a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/impl/ModelServiceImpl.java +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/impl/ModelServiceImpl.java @@ -4,8 +4,6 @@ package tech.easyflow.ai.service.impl; import cn.hutool.core.util.StrUtil; import com.alicp.jetcache.Cache; import com.easyagents.core.document.Document; -import com.easyagents.core.model.chat.ChatModel; -import com.easyagents.core.model.chat.ChatOptions; import com.easyagents.core.model.embedding.EmbeddingModel; import com.easyagents.core.model.rerank.RerankModel; import com.easyagents.core.store.VectorData; @@ -23,6 +21,7 @@ import tech.easyflow.ai.entity.ModelProvider; import tech.easyflow.ai.mapper.ModelMapper; import tech.easyflow.ai.service.ModelProviderService; import tech.easyflow.ai.service.ModelService; +import tech.easyflow.ai.service.verification.ChatModelConnectivityVerifier; import tech.easyflow.common.tree.Tree; import tech.easyflow.common.util.SqlOperatorsUtil; import tech.easyflow.common.util.SqlUtil; @@ -51,6 +50,10 @@ public class ModelServiceImpl extends ServiceImpl implements @Resource private Cache cache; + /** 与智能体运行时同链路的 Chat Model 连通性验证器。 */ + @Autowired(required = false) + private ChatModelConnectivityVerifier chatModelConnectivityVerifier; + @Override public boolean addAiLlm(Model entity) { @@ -69,8 +72,7 @@ public class ModelServiceImpl extends ServiceImpl implements Map resMap = new HashMap<>(); // 走聊天验证逻辑 if (Model.MODEL_TYPES[0].equals(modelType)) { - verifyChatLlm(model); - return null; + return verifyChatLlm(model); } // 走向量化验证逻辑 if (Model.MODEL_TYPES[1].equals(modelType)) { @@ -154,25 +156,17 @@ public class ModelServiceImpl extends ServiceImpl implements } } - private void verifyChatLlm(Model llm) { - - ChatModel chatModel = llm.toChatModel(); - if (chatModel == null) { - throw new BusinessException("chatModel为空"); + /** + * 使用智能体同链路验证 Chat Model 或 VLM。 + * + * @param model 已补齐供应商默认配置的模型 + * @return 结构化双阶段验证结果 + */ + private Map verifyChatLlm(Model model) { + if (chatModelConnectivityVerifier == null) { + throw new BusinessException("Agent 模型连通性验证组件未加载"); } - try { - ChatOptions options=new ChatOptions(); - options.setThinkingEnabled(false); - String response = chatModel.chat("我在对模型配置进行校验,你收到这条消息无需做任何思考,直接回复一个“你好”即可!",options); - if (response == null) { - throw new BusinessException("校验未通过,请前往后端日志查看详情!"); - } - log.info("校验结果:{}", response); - } catch (Exception e) { - log.error("校验失败:{}", e.getMessage()); - throw new BusinessException(e.getMessage()); - } - + return chatModelConnectivityVerifier.verify(model).toMap(); } @Override diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/support/VlmVerificationImage.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/support/VlmVerificationImage.java new file mode 100644 index 00000000..5d2f0ebc --- /dev/null +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/support/VlmVerificationImage.java @@ -0,0 +1,90 @@ +package tech.easyflow.ai.service.support; + +import tech.easyflow.common.web.exceptions.BusinessException; + +import java.io.IOException; +import java.io.InputStream; + +/** + * 提供模型连接验证专用的固定 PNG 图片。 + */ +public final class VlmVerificationImage { + + /** 模型需要识别并返回的固定数字。 */ + public static final String VERIFICATION_CODE = "5839"; + + /** 固定验证图片的 classpath 路径。 */ + private static final String RESOURCE_PATH = "/images/vlm-verification.png"; + /** PNG 文件签名字节。 */ + private static final byte[] PNG_SIGNATURE = { + (byte) 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A + }; + /** 启动后复用的固定 PNG 字节。 */ + private static final byte[] PNG_BYTES = loadPng(); + + /** + * 禁止实例化工具类。 + */ + private VlmVerificationImage() { + } + + /** + * 返回固定连接验证 PNG 的字节副本。 + * + * @return PNG 字节副本 + */ + public static byte[] pngBytes() { + return PNG_BYTES.clone(); + } + + /** + * 从 classpath 加载固定验证图片。 + * + * @return PNG 文件字节 + * @throws BusinessException 资源缺失、读取失败或文件格式非法时抛出 + */ + private static byte[] loadPng() { + try (InputStream input = VlmVerificationImage.class.getResourceAsStream(RESOURCE_PATH)) { + if (input == null) { + throw new BusinessException("VLM 校验图片资源不存在:" + RESOURCE_PATH); + } + byte[] bytes = input.readAllBytes(); + if (!hasPngSignature(bytes)) { + throw new BusinessException("VLM 校验图片资源不是有效的 PNG 文件"); + } + return bytes; + } catch (IOException error) { + throw new BusinessException("VLM 校验图片读取失败:" + safeMessage(error)); + } + } + + /** + * 检查文件是否包含标准 PNG 签名。 + * + * @param bytes 待检查文件字节 + * @return 包含完整 PNG 签名时返回 true + */ + private static boolean hasPngSignature(byte[] bytes) { + if (bytes == null || bytes.length < PNG_SIGNATURE.length) { + return false; + } + for (int index = 0; index < PNG_SIGNATURE.length; index++) { + if (bytes[index] != PNG_SIGNATURE[index]) { + return false; + } + } + return true; + } + + /** + * 获取非空异常描述。 + * + * @param error 原始异常 + * @return 可展示的异常描述 + */ + private static String safeMessage(Exception error) { + return error.getMessage() == null || error.getMessage().isBlank() + ? "未知错误" + : error.getMessage(); + } +} diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/verification/ChatModelConnectivityVerifier.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/verification/ChatModelConnectivityVerifier.java new file mode 100644 index 00000000..aed7b850 --- /dev/null +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/verification/ChatModelConnectivityVerifier.java @@ -0,0 +1,17 @@ +package tech.easyflow.ai.service.verification; + +import tech.easyflow.ai.entity.Model; + +/** + * Chat Model 与 VLM 的运行时同链路连通性验证器。 + */ +public interface ChatModelConnectivityVerifier { + + /** + * 验证模型的非流式基础连接与流式响应能力。 + * + * @param model 已补齐供应商默认配置的模型 + * @return 双阶段验证结果 + */ + ChatModelVerificationResult verify(Model model); +} diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/verification/ChatModelVerificationResult.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/verification/ChatModelVerificationResult.java new file mode 100644 index 00000000..16e4b324 --- /dev/null +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/verification/ChatModelVerificationResult.java @@ -0,0 +1,132 @@ +package tech.easyflow.ai.service.verification; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Chat Model 与 VLM 的双阶段连通性验证结果。 + */ +public final class ChatModelVerificationResult { + + /** 整体验证状态。 */ + private final ModelVerificationStatus status; + /** 非流式基础连接验证状态。 */ + private final ModelVerificationStatus nonStreaming; + /** 流式响应验证状态。 */ + private final ModelVerificationStatus streaming; + /** 实际生效的 HTTP 版本策略。 */ + private final String effectiveHttpVersion; + /** 用户可见的简洁结果说明。 */ + private final String message; + + /** + * 创建验证结果。 + * + * @param status 整体验证状态 + * @param nonStreaming 非流式验证状态 + * @param streaming 流式验证状态 + * @param effectiveHttpVersion 实际生效的 HTTP 版本策略 + * @param message 用户可见结果说明 + */ + private ChatModelVerificationResult(ModelVerificationStatus status, + ModelVerificationStatus nonStreaming, + ModelVerificationStatus streaming, + String effectiveHttpVersion, + String message) { + this.status = status; + this.nonStreaming = nonStreaming; + this.streaming = streaming; + this.effectiveHttpVersion = effectiveHttpVersion; + this.message = message; + } + + /** + * 创建双阶段全部通过的结果。 + * + * @param effectiveHttpVersion 实际生效的 HTTP 版本策略 + * @return 全部通过结果 + */ + public static ChatModelVerificationResult passed(String effectiveHttpVersion) { + return new ChatModelVerificationResult( + ModelVerificationStatus.PASSED, + ModelVerificationStatus.PASSED, + ModelVerificationStatus.PASSED, + effectiveHttpVersion, + "验证成功"); + } + + /** + * 创建基础连接通过但流式阶段失败的结果。 + * + * @param effectiveHttpVersion 实际生效的 HTTP 版本策略 + * @return 部分通过结果 + */ + public static ChatModelVerificationResult streamingUnavailable(String effectiveHttpVersion) { + return new ChatModelVerificationResult( + ModelVerificationStatus.PARTIAL, + ModelVerificationStatus.PASSED, + ModelVerificationStatus.FAILED, + effectiveHttpVersion, + "连接成功,流式响应不可用,可关闭智能体的模型流式响应。"); + } + + /** + * 获取整体验证状态。 + * + * @return 整体验证状态 + */ + public ModelVerificationStatus getStatus() { + return status; + } + + /** + * 获取非流式基础连接验证状态。 + * + * @return 非流式验证状态 + */ + public ModelVerificationStatus getNonStreaming() { + return nonStreaming; + } + + /** + * 获取流式响应验证状态。 + * + * @return 流式验证状态 + */ + public ModelVerificationStatus getStreaming() { + return streaming; + } + + /** + * 获取实际生效的 HTTP 版本策略。 + * + * @return HTTP 版本策略名称 + */ + public String getEffectiveHttpVersion() { + return effectiveHttpVersion; + } + + /** + * 获取用户可见结果说明。 + * + * @return 结果说明 + */ + public String getMessage() { + return message; + } + + /** + * 转换为现有模型验证接口使用的响应结构。 + * + * @return 有序响应字段 + */ + public Map toMap() { + Map result = new LinkedHashMap<>(); + result.put("status", status.name()); + result.put("nonStreaming", nonStreaming.name()); + result.put("streaming", streaming.name()); + result.put("effectiveHttpVersion", effectiveHttpVersion); + result.put("message", message); + return result; + } +} diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/verification/ModelVerificationStatus.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/verification/ModelVerificationStatus.java new file mode 100644 index 00000000..801b96fd --- /dev/null +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/verification/ModelVerificationStatus.java @@ -0,0 +1,15 @@ +package tech.easyflow.ai.service.verification; + +/** + * 模型连通性验证状态。 + */ +public enum ModelVerificationStatus { + /** 所有要求的验证阶段均通过。 */ + PASSED, + /** 基础连接通过,但增强能力验证未通过。 */ + PARTIAL, + /** 验证失败。 */ + FAILED, + /** 前置阶段失败,当前阶段未执行。 */ + SKIPPED +} diff --git a/easyflow-modules/easyflow-module-ai/src/main/resources/images/vlm-verification.png b/easyflow-modules/easyflow-module-ai/src/main/resources/images/vlm-verification.png new file mode 100644 index 0000000000000000000000000000000000000000..b3aeb69bebca565079c26a374c6334de778f4537 GIT binary patch literal 78448 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

C0ZK7fJP>V;6x^(Au6(%I1)bMg!~@ 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

    #fF`v)J>SU4 zF{%cV@Mi&GVpX6bM(rC4$D_hwPiqd{0jKCp05wB6*_gs8Of-eyLVz+n#@P^6+6045 zWJ{{aLF&R0JgwZdzBoZKcZ)D}P1J-NMr3z3qa%?~r&=4?16qdOoWlxIuiHh6^+c8Kz_5h?ZDHoOuG1v&w zN?cv=0MsiR$Xh6qmLo|d4Tilz_)F8U104loJJMDl-8vxN5!-}4`9iM@**P3M0x(D$ zjim}}WG280F|EPFrp)YH)FB_Z09$9{vM+BYWM$ilfo0fC2v4YZ!{*V^>t{CYz&@$W$^FqADGp zkp)_O;>KdI{P|WQ6bNZya90E8EJfpGpBbleF-axhwV)vAD6ZvJB*bEtgfW4cd)OeA zsRS;1@JP%DCnAl)SDln37(|7Un6C4;3^|Q~aN0CT7iA?_j}lnFDW6)S*eVIeVet?s z$#RG`GZwvFVPd^I^gu;zfMSI{aM31LK&V|W@4(re#a4vU5Gli^oh|{8j4B6XtjmR#YRMtMCuqlPE?knuA?4RFmZ|rJ^c+qiAy8N)Y0DccSx~K z3=YUtjEN=Gcp^+0>fg-j0bob#9ck6doUR8_OJvo^$|8{zk7ARgF?rtxwls-nw;l(C z&-L?sW^9i=3A<}FI5GTat1WrsEia-P3I#-4$5Gl$`Wtk-an8FgLV4B2?QGya6nM$NSG3&oahLy>(w}GcK zlg}iULCgzp8pkgIm=ds7;!XmGggrOU2F_6@yx8Qp5!ec86ADXBh{S=wV=_XaD&|q&OoYO%7%e2qOp}KY6h>nk##YlO;$!#j(xWFcvxF4nn$&I@&<%r06)K2}T`+YIu~Z@RtxkAb4xc z<=3Pmii!AbhyhWC+^Z(e5@sdQGZwJ>cNp;vZxj$Y_hOfGRxAGr8%8R?sG>Ch39VDL zzV_wPoj7WBg&M1<*js`J7b;^1<){LV`<4n;X$~zFwxF?NZ{q5-WRb~#NQ?;PCBCwFOT#C35C}Ip~8ims%z}9>1xyJ^vQQ9~bh{*QJUMoP zDaIx$grxD>qK%*@g4&NHg@*yUq{$Fv{?Re?A=tPyw3#*5CJr+-K7q}obTMvN8)Bn| zraJ}fa|@cPc+(=O#2$%{p*#SiZSPR8tIknWRjam|-W}XrC40?{D;pp?4 zkQz|4ZGlNdv#oSFMq*d97(ljFgt5qky(Y3uG?iTzA$Z97S2SgwcqQ%UhSE4TF9C;} zP#XhhJSCMXipj3wFTeJIhm=oF=;K(;7&9=k&^}wVk=EAXI$p1YB4`j7S$5)X-BxEo z><|mnd(&7WYdKb2rl{~@GbrqX4Sci|q?sT{zj-acZ5`VtAxto&!uZMY)|i=~6v~o> zlT0NqE3z;yn3U3%@G(?1vZ=$lmO=t?Ol%xc#ou<3Q+`u{GU?zEjigk>sL`R?L}|LAi9G8Mry68AN8UlF`qY$?~{s zV`uk8605PC$@LtWAD=))k1H5amo?7R#dkRhn4uf4u#5wf1>685exR5a8XQ9i+{p$5 zbE-bHOk&C63#Vn$ER#fqfi`}EbV9f)GWkpl1`gRJ4pZ73mXstRwi758?Of{OB)YIw z46uZ`>XBgrGF{Tc1tn=I(N{7RAZa*g0}$H^PvpbilEHP^ju`|>vsp3MBse|#;q9S# z=$LFuc@3#Z6;u!iExv$iop$J&LV_%nn@w-tdsZ=P{`Ozf$s!sFXnW`4bl*1A;8Hjv*}Wp$4YI2 z^2lLl&hTOa;sw%nBe>)aFhLXmWJPw{?eh~Z#_+jRV*-npx*1kn)cu$g3rsGfBa7d7 zNR;tVfm4Tp%<&RnvARc)Wv?+OG_>Xw(EAd|p(Sl%$`dXOEO3!!M=`*49~FbSLf&eO z7g}=}(%|FDOyhLGx=|9Qt_B5{Q}@_Znx%oMzzx)h;GeLvu{K0f=7iY1!9&d`!bCIw zKqZGRy~P4HlRlf0b^3DDu%*IfYF)HRDxOnDF~le#X-#_pH0R~FgEV$tt{z<&99-|KLGO<+$BdfVYn4lr6IO*flvL@l05vD z(21fDh+DCZ(9p5VP^}K<;)87PZXu}Pi?(*QZa#)VT*PUQv4@02%LYaRNvD)$2zzq} zYnT*ogn0ekMTv|&>@#F|b@q~xp8g9--DtH29D=XtAi?AHDSb<mE2;c+GvhZ1sZbvn069t^yDcoVV#%VP6rF znudlD8cn;@toeZkLxKl{h4W!*>ShEWke0VNu}&biMAs$hdqObmjR`rL)R_u_3BdlbY185$TfNaIC{H35W+O!F*p$biBLQj*Yg*->syaGJZP7yos=usZ z_+(g>tsF*9oQKAU><3Swl9{yhTQ2pBDtLCbL~X4Dpl{FY)i7;kvY^q~M3S;fuOpWr zgbIvu$HG3FaSz|FLAet$`oeqK>8U{uS%96a@*pcGY((NiWfNmY0R&T9mpcd?*l|^{ z;T%{Suvd_g0UTv*8uXAFCelXL*4x@}HH0!0I0Up%yNkfdEBn?-EE+YZMp`SCClHBf9ZvxvV2x$jMCs{_ zA$aS^Q-cKMe$NO3ltUwiVvMknOhLkvIj$VTsjZ{4ZAwQM5_`7M;Im{vTns~nNiu+A zOQ#%=TnJEh7dEdP5z_#;pH8Qmh`Uf72$3slf7H{|-9NJtz1a!Oo`h<=p{%lNP_rLg z3~XzkWz|w<*khraRE$p`0hFL|1LPo?oI8X1CjJ@X!3{>l{v5RSHQ9+kbQ299tSHVU zYHetYtCTs8m3cuIR6F|3(&ADkWyRht8%l!i3*Y7nDgB7z1`fq0Mw&D(0~SJ^I-~_Ab5T2DfwQ-P zx#K-4;J9LJ_IQLb1nR1j=n}sMj$p`G)YGTwr@~E1Uj{Y56J95&Y-L}G3Wssz2)r>7 zm5gh4S%E~DS_fBpi|qz70%9Ir;A9~*A^AVPaV`n!%mljfXB!3_%*hC)UD9!L))Ljt z^JWLZ)fQIU$wpnp#At}aqjEdNgVTtwzI9gEo5mqvtbpk!Hg%<~>P2LGcOe)WxCpA_ zWE+(^1CU(&`u87w=OSaJ!Wqxr>62h;A&5FaZ5{g-mxIlLgb|g7gJzTX^Uy-%`Y(`8 zO4+P1_l>D131saW1eqnu<3pXhp%l4ZG6gwkn*s1{J2kwjXLF8BwWF+9o!(coayq zjX9HxOsW8j1!&fhD9jQy)cP|CeF(K@gB-usGiT}y-+)??Nis+d!eGIWrzVwHHoXyh~^Hy#@>=wu+OGPB&~&OcxX zjMoMOTbr3~t$%mXhZ-n$u3qdi5{~s^9es!)m6;Tzk_nh?2uYk38M3#|t`AFB~sBA5;1(C+BgT^Qb0Ua)4 z2ch`Pi8}7WMsP`3>JkY81}Ijlmk(wZ(pdLEe%Iip0blg4-c4w z0;GZdsGI=;$z4JU(KO2Pn*nj}UlQkU4VRo|f!u%BeG zu_qv5^00G)bSoEDAQ&go5Vf~zpz~5GyIveYdm0PvaWr_Z8aNC> zEkYbu)QK=;_CkzROh@RT;tjS$t$pvu%E2T9V&*XQV%5%_9*HR|m7*u;e#kK(I)L^T zn7A{xE(**W2D4G?%%hIgse+@>IAmfYT=acHiZ>2QO0f{M@G-!uiU`V730|BYQbEKO zehNBT3}!1WjE<_jcy=Sfjcbq?I?SDlwswiHg+hepbg&~w9pDbOvpp%fa46uE*f9dfxR?;j`fFP?Ery;PJ++41W78)MQLX;b~yjZ84C8 zl1PuBwIvfZQLE#0B#b6Q$Hd0j zN_VJI<97x`?X;zE13|+yy7UkknXuG{5FO~0!Re}R5n@^&vSKxmvh=l!RlvhGkrq_- z8R|P`7oX!5dLm5oFx4dKAS^MQF{!ZgUyCQ%BhlQLOCs&w{HGs3)=FOP#tu zEWwsVbvbCTw-ydSY~u$7fzpf-HIPco25g*89AI0nVimd~F|oIMBrRYaY())IjL@|BYI&KkO&<|X*l8TXzD@M5`bd@>ZR;3_ zJXAG=wW#ADbx>a{<3wE29$T?A%$OmvCdurwQi-aZ4l#7XIIr*wBXqAMs~ok<0l$Os zbi}T5D7N@^ZR4<4ucw;xMun6#y;_l&vO0Z$Q%{-|+N3__#{i|4G7(}EJEKUpm%ViW zqcy&RCBYJQHB;3vyf(2a#{^PYjO;DJP{QO0qBS4G4PNQ^j87c4z<<0HNi!uJnUl+8 zp*mn9H3udpVz5K#_-9Vlkc}{wG+javoe?b_OtG)JOwsoBm=4rcVmYkL;4O;ca@N60 zWp3OdadVJG9lut!z;Y~s*PqEzL(`MPwa24P1CNRzZG0J>lt&is8qi?iOAvm0h1qE| zUE}B&b($jw%xG#;$l#e~A7V>^gijbl7&)A(Lnkz>0-7`tUV>sKXrl&^DX6QNQ!!*M z_;RQi`Kou9(le}ii-AN?8)mgZ2$MkaG<%TIhorHWO(&@_Rselel)H+@gjt5T=U`yh zvK@mVGoaP6MGN_8QthY=v7tE&?a5tzdaVu~u|c=gmUNmLq$vvAYMxXoWqNbk#=#7YOWj4D;FD)8yyz#W>1M&V$J5vJ$V*6l2FYIZJboYMeTi&| zAK+F!#&B##h=UA6dA^*bx`W8xIN4G|%OM3HJjC-ys1V$N82BM=pXi&V`JcJLX+D7dWj9Dei1&0^Xord+7a{Ba>+fIx_bl-OrDAcJ zvelBpg(X7L>*=N#z|R3=Ip>)ywXo5&fE15BqoP&4z_JYq3|qWw_gd0&Upr)-a&t4~ zP|VCOCy@ot9RIAy$&XiKo`S1}%2ysw8{l=Jq(rv52{V_DW`$~aQzmO7xEo^?9{|Z? zB5e^aEpl(0kqaHq-Kr>rb%yK|p@uQElmY2&qs)AG*w_T@#n<--W^|;?P{znpP zb1+S;YxM#<)0Kc}Dv4+2#lHb#o^fQ^u}uNFca7v>nLWCtHbeSsx>;fp4IvxHCARSQ zhwq!NaL5#pBF;LCxfy4)Cs31ctnS@ZErE(jeri)djP>&P3{U6D5??FeG`DM^dWUYV zQ+Q6kWymY7Y?OY3&ST~2m1*~`rK%EcmZYNSP_XAU2+2ZH4#1>C~CU?~2b;y%XOiJK8cW?RtsMXu}3hJJovnnn1s`|uytX(GATxNq1g znCub^ART~IdQQQ$7mj+_G4T{5lI@Y`vLeh5$aXPL>Vo&jd5L&nAZA2^;hY-xg3QFq z!;K1?3#_kE&FV+-IEjUbg@sE8LLL|6i@Y>O{zr>l64FfEgR~{dD}B9CJ!71Q>f-jG zUZ+HD4UFK~q?ENDnSnz*RMa!@UC&dJlnH$>`pZKQ3?Exk3trfh&Q)$Mv&WJ$wmkSG zeP|nAGr$cQAg9v3y{woAoE0%Sl@+ylI8Dde`Owe5gLZ2~z;s0+^MIBI7tEgZ z_0I7t)*C42Oop(5!tpAHed%-^*xirm6dOY2y;2g6w^iRla-0QxuZ3PjRkOba5%Vl4 zQ$Wb8PPQ@sVWdV5DIakq83yeh35Fvv<4|c(%51`O3C33#tAaFwoxLdz&LRxC#n~RH zu24!iuvbQ3Q!Nxiu&I2{@(OEZZ`3=Gc8bEFRpdr&y|dzsx)*(0KqWK zaNni1(RilnvNn7idKXb%3FB+1oN_EP@jI56eNt1!khL2Vs9rP_pqmNg zevGB$Eu!|+*JGl7sIJ%(@rwH#Q@3Q}VN+%)*NwT3@TX)#2IGaA#IA6+)HI&?-` z72qJ$I;go9n=Z}nPh(y3r1dGzB010*O9%%V9=0RhqlZV4fBpLVpVZN`)>NShE4hBX zffTRi@_!u*Xdj-+uKFO}QQvjH9Rs_N0n9L8muDfC9{~GoU_xF-Fd3EpQ{?k54J?5d zewHlaKc4kI-3KsNb7nx#DeVcaL!z04f}9^?UH9DyS6upkn@+7^Gd)Veyu3tuv6`6u z7;w|?XVqog%#3jD$E3BzB_H8)9yz&YpbuWV!Oh{4BwNYe4Q>W-ayA=LeOq8agpl48 zDqWMw_d=B4qUi4q?J)QOW#RAIx-WJ1ukFB6-k#F-L++9UKe7AvmUfeND_|y?LwoGY z`HmzOsH}TQCWbb)6B`y8>Ci9)tdTScun(?DN$CP?8LPy>_|-5W@^N(M3Pb`T7+3XK zlX^TJQmv?nyiWU}80wRvq;Wtc*HR#LFs4@x5+FDsQk z7%k+!>?vVYRi0GjJ>;^65P#BLcmTg`Avy4o1a|g|5{QxXl3D+@G1hAKc6G^dZ{fki($$OK2XZSLTNG z5cjQWm;u#4bf*d!+Vd8YQ;Q3`PBRBEa}OaIpRde7lX;~QyOwL+-5>at3Z%n3n(nA= zj4+rpRE1g1S>721OoaRc;}4kxr+|S_L&7f>P}qrW_b}*K*c4(oq!$s8_L+95!G?a2 z>oy%^kN3$tVV{2o9Z-+tWRA#ur%)0-<1;T6LSBsUPB$~06K!_BLcEs1|j3=$YAa6i!5 zux&zDuSqs1B6^jJA2J(58G6G(iWNsL_As)ZsXu!L>v+{J{PRTwArcAurjP;69IO;hXiGv$=#r?(7hkdC zgOX-hV1j0jwOvVJW+2mVgW$aVy6D&bXked(Qb-z;K>ns0KtXLa5h-&Pi_9{vaZe /* eslint-disable vue/no-mutating-props */ -import type {AgentInfo, AgentOption} from '../types'; +import type { AgentInfo, AgentOption } from '../types'; -import {InfoFilled} from '@element-plus/icons-vue'; +import { InfoFilled } from '@element-plus/icons-vue'; import { ElForm, ElFormItem, @@ -11,6 +11,7 @@ import { ElInputNumber, ElOption, ElSelect, + ElSwitch, ElTooltip, } from 'element-plus'; @@ -75,6 +76,27 @@ const emit = defineEmits<{ change: [] }>(); /> + + + + { + it('defaults new and legacy agents to streaming', () => { + expect(createEmptyAgent().generationConfigJson?.stream).toBe(true); + + const designer = useAgentDesignerState(); + designer.reset({ name: '旧智能体' }); + + expect(designer.state.agent.generationConfigJson?.stream).toBe(true); + expect(designer.buildPayloadAgent().generationConfigJson?.stream).toBe( + true, + ); + }); + + it('preserves an explicitly disabled stream value in the payload', () => { + const designer = useAgentDesignerState(); + designer.reset({ + name: '非流式智能体', + generationConfigJson: { stream: false }, + }); + + expect(designer.state.agent.generationConfigJson?.stream).toBe(false); + expect(designer.buildPayloadAgent().generationConfigJson?.stream).toBe( + false, + ); + }); +}); diff --git a/easyflow-ui-admin/app/src/views/ai/agents/composables/useAgentDesignerState.ts b/easyflow-ui-admin/app/src/views/ai/agents/composables/useAgentDesignerState.ts index 3ac18c55..5384568f 100644 --- a/easyflow-ui-admin/app/src/views/ai/agents/composables/useAgentDesignerState.ts +++ b/easyflow-ui-admin/app/src/views/ai/agents/composables/useAgentDesignerState.ts @@ -84,6 +84,7 @@ export function createEmptyAgent(): AgentInfo { categoryId: '', modelId: '', promptConfigJson: { systemPrompt: '' }, + generationConfigJson: { stream: true }, memoryConfigJson: { compressionParameter: { enabled: true, @@ -111,6 +112,10 @@ function normalizeAgent(agent?: AgentInfo): AgentInfo { systemPrompt: '', ...source.promptConfigJson, }, + generationConfigJson: { + ...source.generationConfigJson, + stream: source.generationConfigJson?.stream !== false, + }, memoryConfigJson: { ...memoryConfig, compressionParameter: { @@ -356,6 +361,10 @@ export function useAgentDesignerState() { interactionConfigJson: buildInteractionConfigPayload( state.agent.interactionConfigJson, ), + generationConfigJson: { + ...state.agent.generationConfigJson, + stream: state.agent.generationConfigJson?.stream !== false, + }, memoryConfigJson: { ...restMemoryConfigJson, compressionParameter: { diff --git a/easyflow-ui-admin/app/src/views/ai/model/ActiveModelWorkspace.vue b/easyflow-ui-admin/app/src/views/ai/model/ActiveModelWorkspace.vue index a679d67b..80e37e00 100644 --- a/easyflow-ui-admin/app/src/views/ai/model/ActiveModelWorkspace.vue +++ b/easyflow-ui-admin/app/src/views/ai/model/ActiveModelWorkspace.vue @@ -1,6 +1,7 @@