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 index b6ca972b..3578f141 100644 --- 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 @@ -3,6 +3,7 @@ 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 com.easyagents.agent.runtime.model.AgentSystemContentFormat; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import tech.easyflow.ai.entity.Model; @@ -60,6 +61,7 @@ public final class AgentModelSpecMapper { spec.setSupportImage(Boolean.TRUE.equals(model.getSupportImage())); spec.setSupportImageBase64Only(Boolean.TRUE.equals(model.getSupportImageB64Only())); spec.setHttpVersionPolicy(parseHttpVersionPolicy(model)); + spec.setSystemContentFormat(parseSystemContentFormat(model)); spec.getMetadata().put("modelId", model.getId()); if (providerType != null && !providerType.isBlank()) { spec.getMetadata().put("sourceProviderType", providerType); @@ -90,6 +92,29 @@ public final class AgentModelSpecMapper { } } + /** + * 解析模型配置中的 Agent system content 格式。 + * + * @param model 模型配置 + * @return system content 格式,缺失或非法时返回 STRING + */ + private static AgentSystemContentFormat parseSystemContentFormat(Model model) { + Object rawFormat = model.getOptions() == null + ? null + : model.getOptions().get("agentSystemContentFormat"); + if (rawFormat == null || String.valueOf(rawFormat).isBlank()) { + return AgentSystemContentFormat.STRING; + } + String normalizedFormat = String.valueOf(rawFormat).trim().toUpperCase(Locale.ROOT); + try { + return AgentSystemContentFormat.valueOf(normalizedFormat); + } catch (IllegalArgumentException exception) { + LOG.warn("Invalid Agent system content format '{}' for model {}, fallback to STRING", + rawFormat, model.getId()); + return AgentSystemContentFormat.STRING; + } + } + /** * 解析 AgentScope 支持的模型供应商类型。 * 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 index 4fc51518..52a0187f 100644 --- 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 @@ -43,6 +43,8 @@ public class AgentScopeChatModelConnectivityVerifier implements ChatModelConnect /** 为未识别关闭思考扩展参数的推理模型保留足够的小额输出预算。 */ private static final int MAX_TOKENS = 256; private static final Duration DEFAULT_PHASE_TIMEOUT = Duration.ofSeconds(60); + private static final String VERIFICATION_SYSTEM_PROMPT = + "You are verifying model connectivity. Follow the user's instruction exactly."; /** AgentScope 模型工厂。 */ private final AgentModelFactory modelFactory; @@ -128,7 +130,11 @@ public class AgentScopeChatModelConnectivityVerifier implements ChatModelConnect generationOptions.setMaxTokens(MAX_TOKENS); io.agentscope.core.model.Model agentScopeModel = modelFactory.create(modelSpec, generationOptions); - Msg message = messageAdapter.toMsg(verificationMessage); + List messages = List.of( + messageAdapter.toMsg(AgentMessage.text( + AgentMessageRole.SYSTEM, + VERIFICATION_SYSTEM_PROMPT)), + messageAdapter.toMsg(verificationMessage)); GenerateOptions requestOptions = GenerateOptions.builder() .stream(stream) .maxTokens(MAX_TOKENS) @@ -138,7 +144,7 @@ public class AgentScopeChatModelConnectivityVerifier implements ChatModelConnect .build()) .build(); List responses = agentScopeModel - .stream(List.of(message), List.of(), requestOptions) + .stream(messages, List.of(), requestOptions) .timeout(phaseTimeout) .collectList() .block(phaseTimeout.plusSeconds(1)); 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 00a5e196..34d3a925 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 @@ -4,6 +4,7 @@ import com.easyagents.agent.runtime.memory.AgentMemoryPolicy; import com.easyagents.agent.runtime.model.AgentGenerationOptions; import com.easyagents.agent.runtime.model.AgentHttpVersionPolicy; import com.easyagents.agent.runtime.model.AgentModelSpec; +import com.easyagents.agent.runtime.model.AgentSystemContentFormat; import org.junit.Assert; import org.junit.Test; import tech.easyflow.agent.entity.Agent; @@ -66,6 +67,36 @@ public class AgentRuntimeCompilerModelConfigTest { Assert.assertEquals(AgentHttpVersionPolicy.AUTO, spec.getHttpVersionPolicy()); } + /** + * 验证模型 options 中的 system content 格式会编译到中立模型声明。 + * + * @throws Exception 反射调用失败时抛出 + */ + @Test + public void modelSystemContentFormatShouldCompileFromOptions() throws Exception { + Model model = model(Map.of("agentSystemContentFormat", "TEXT_PARTS")); + AgentRuntimeCompiler compiler = compiler(model); + + AgentModelSpec spec = invokeModelSpec(compiler); + + Assert.assertEquals(AgentSystemContentFormat.TEXT_PARTS, spec.getSystemContentFormat()); + } + + /** + * 验证未知 system content 格式安全回退到字符串。 + * + * @throws Exception 反射调用失败时抛出 + */ + @Test + public void unknownSystemContentFormatShouldFallbackToString() throws Exception { + Model model = model(Map.of("agentSystemContentFormat", "PARTS")); + AgentRuntimeCompiler compiler = compiler(model); + + AgentModelSpec spec = invokeModelSpec(compiler); + + Assert.assertEquals(AgentSystemContentFormat.STRING, spec.getSystemContentFormat()); + } + /** * 验证 EasyFlow 忽略新旧消息数阈值,仅保留 Token 压缩配置。 * 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 index 961f3f7b..26b90cc2 100644 --- 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 @@ -7,6 +7,7 @@ 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.MsgRole; import io.agentscope.core.message.TextBlock; import io.agentscope.core.model.ChatResponse; import io.agentscope.core.model.GenerateOptions; @@ -48,6 +49,7 @@ public class AgentScopeChatModelConnectivityVerifierTest { 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(MsgRole.SYSTEM, MsgRole.USER), factory.getMessageRoles().get(0)); Assert.assertEquals(List.of(false, false), factory.getEnableThinkingValues()); Assert.assertEquals(List.of(false, false), factory.getChatTemplateThinkingValues()); } @@ -190,6 +192,8 @@ public class AgentScopeChatModelConnectivityVerifierTest { private final List requestStreams = new ArrayList<>(); /** 模型请求收到的消息。 */ private final List messages = new ArrayList<>(); + /** 各阶段请求消息的角色顺序。 */ + private final List> messageRoles = new ArrayList<>(); /** OpenAI-compatible 请求中的思考开关。 */ private final List enableThinkingValues = new ArrayList<>(); /** GPUStack 模板参数中的思考开关。 */ @@ -241,7 +245,8 @@ public class AgentScopeChatModelConnectivityVerifierTest { List tools, GenerateOptions options) { requestStreams.add(Boolean.TRUE.equals(options.getStream())); - messages.add(inputMessages.get(0)); + messages.add(inputMessages.get(inputMessages.size() - 1)); + messageRoles.add(inputMessages.stream().map(Msg::getRole).toList()); return responses; } @@ -284,6 +289,15 @@ public class AgentScopeChatModelConnectivityVerifierTest { return messages; } + /** + * 获取各阶段请求消息的角色顺序。 + * + * @return 消息角色顺序 + */ + private List> getMessageRoles() { + return messageRoles; + } + /** * 获取 OpenAI-compatible 请求中的思考开关。 * diff --git a/easyflow-ui-admin/app/src/views/ai/model/AddModelModal.vue b/easyflow-ui-admin/app/src/views/ai/model/AddModelModal.vue index e4592414..43eb581a 100644 --- a/easyflow-ui-admin/app/src/views/ai/model/AddModelModal.vue +++ b/easyflow-ui-admin/app/src/views/ai/model/AddModelModal.vue @@ -27,9 +27,11 @@ import { } from '#/views/ai/model/modelUtils/model-ability-utils'; type AgentHttpVersionPolicy = 'AUTO' | 'HTTP_1_1' | 'HTTP_2_PREFERRED'; +type AgentSystemContentFormat = 'STRING' | 'TEXT_PARTS'; interface ModelOptions { agentHttpVersionPolicy: AgentHttpVersionPolicy; + agentSystemContentFormat: AgentSystemContentFormat; [key: string]: unknown; } @@ -102,6 +104,7 @@ const formData = reactive({ supportToolMessage: true, options: { agentHttpVersionPolicy: 'AUTO', + agentSystemContentFormat: 'STRING', }, }); @@ -111,6 +114,11 @@ const agentHttpVersionOptions = [ { label: 'HTTP/2 优先', value: 'HTTP_2_PREFERRED' }, ] as const; +const agentSystemContentFormatOptions = [ + { label: '字符串(默认)', value: 'STRING' }, + { label: '内容块数组', value: 'TEXT_PARTS' }, +] as const; + const normalizeAgentHttpVersionPolicy = ( value?: unknown, ): AgentHttpVersionPolicy => { @@ -120,6 +128,12 @@ const normalizeAgentHttpVersionPolicy = ( return 'AUTO'; }; +const normalizeAgentSystemContentFormat = ( + value?: unknown, +): AgentSystemContentFormat => { + return value === 'TEXT_PARTS' ? 'TEXT_PARTS' : 'STRING'; +}; + const normalizeModelOptions = (options?: unknown): ModelOptions => { const source = options && typeof options === 'object' && !Array.isArray(options) @@ -130,6 +144,9 @@ const normalizeModelOptions = (options?: unknown): ModelOptions => { agentHttpVersionPolicy: normalizeAgentHttpVersionPolicy( source.agentHttpVersionPolicy, ), + agentSystemContentFormat: normalizeAgentSystemContentFormat( + source.agentSystemContentFormat, + ), }; }; @@ -448,6 +465,19 @@ const save = async () => { /> + + + + +