diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/entity/Model.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/entity/Model.java index 8ac2a9f1..634cb1ce 100644 --- a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/entity/Model.java +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/entity/Model.java @@ -2,6 +2,7 @@ package tech.easyflow.ai.entity; import cn.hutool.core.util.StrUtil; +import com.easyagents.core.model.chat.ChatMessageContentFormat; import com.easyagents.core.model.chat.ChatModel; import com.easyagents.core.model.embedding.EmbeddingModel; import com.easyagents.core.model.rerank.RerankModel; @@ -27,6 +28,9 @@ import tech.easyflow.ai.entity.base.ModelBase; import tech.easyflow.common.util.StringUtil; import tech.easyflow.common.web.exceptions.BusinessException; +import java.util.Locale; +import java.util.Map; + /** * 实体类。 * @@ -118,6 +122,7 @@ public class Model extends ModelBase { ollamaChatConfig.setProvider(getModelProvider().getProviderName()); ollamaChatConfig.setSupportImage(getSupportImage()); ollamaChatConfig.setSupportImageBase64Only(getSupportImageB64Only()); + ollamaChatConfig.setMessageContentFormat(resolveMessageContentFormat()); return new OllamaChatModel(ollamaChatConfig); case "deepseek": DeepseekConfig deepseekConfig = new DeepseekConfig(); @@ -131,6 +136,7 @@ public class Model extends ModelBase { deepseekConfig.setNeedReasoningContentForToolMessage(Boolean.TRUE); deepseekConfig.setSupportImage(getSupportImage()); deepseekConfig.setSupportImageBase64Only(getSupportImageB64Only()); + deepseekConfig.setMessageContentFormat(resolveMessageContentFormat()); if (getSupportTool() != null) { deepseekConfig.setSupportToolMessage(getSupportTool()); } @@ -144,6 +150,7 @@ public class Model extends ModelBase { openAIChatConfig.setRequestPath(checkAndGetRequestPath()); openAIChatConfig.setSupportImage(getSupportImage()); openAIChatConfig.setSupportImageBase64Only(getSupportImageB64Only()); + openAIChatConfig.setMessageContentFormat(resolveMessageContentFormat()); if (getSupportTool() != null) { openAIChatConfig.setSupportToolMessage(getSupportTool()); } @@ -151,6 +158,37 @@ public class Model extends ModelBase { } } + /** + * 解析模型高级配置中的 OpenAI-compatible 消息 content 格式。 + * 新配置优先,旧 system 配置用于兼容历史数据。 + * + * @return 消息 content 格式;缺失或非法时返回标准格式 + */ + private ChatMessageContentFormat resolveMessageContentFormat() { + Map modelOptions = getOptions(); + if (modelOptions == null || modelOptions.isEmpty()) { + return ChatMessageContentFormat.STANDARD; + } + + Object rawFormat = modelOptions.get("agentMessageContentFormat"); + if (rawFormat == null || String.valueOf(rawFormat).isBlank()) { + rawFormat = modelOptions.get("agentSystemContentFormat"); + } + if (rawFormat == null || String.valueOf(rawFormat).isBlank()) { + return ChatMessageContentFormat.STANDARD; + } + + String normalizedFormat = String.valueOf(rawFormat).trim().toUpperCase(Locale.ROOT); + if ("STRING".equals(normalizedFormat)) { + return ChatMessageContentFormat.STANDARD; + } + try { + return ChatMessageContentFormat.valueOf(normalizedFormat); + } catch (IllegalArgumentException ignored) { + return ChatMessageContentFormat.STANDARD; + } + } + public RerankModel toRerankModel() { switch (modelProvider.getProviderType().toLowerCase()) { case "gitee": diff --git a/easyflow-modules/easyflow-module-ai/src/test/java/tech/easyflow/ai/entity/ModelMessageContentFormatTest.java b/easyflow-modules/easyflow-module-ai/src/test/java/tech/easyflow/ai/entity/ModelMessageContentFormatTest.java new file mode 100644 index 00000000..44b5460e --- /dev/null +++ b/easyflow-modules/easyflow-module-ai/src/test/java/tech/easyflow/ai/entity/ModelMessageContentFormatTest.java @@ -0,0 +1,131 @@ +package tech.easyflow.ai.entity; + +import com.easyagents.core.message.SystemMessage; +import com.easyagents.core.message.UserMessage; +import com.easyagents.core.model.chat.BaseChatModel; +import com.easyagents.core.model.chat.ChatConfig; +import com.easyagents.core.model.chat.ChatMessageContentFormat; +import com.easyagents.core.model.client.OpenAIChatMessageSerializer; +import org.junit.Assert; +import org.junit.Test; + +import java.util.List; +import java.util.Map; + +/** + * 模型高级配置向工作流聊天模型透传的消息格式测试。 + */ +public class ModelMessageContentFormatTest { + + /** + * 验证 OpenAI-compatible、DeepSeek 与 Ollama 模型都会接收内容块数组配置。 + */ + @Test + public void shouldApplyTextPartsFormatToOpenAiCompatibleModels() { + Map options = Map.of("agentMessageContentFormat", "TEXT_PARTS"); + + Assert.assertEquals(ChatMessageContentFormat.TEXT_PARTS, + chatConfig(model("custom", options)).getMessageContentFormat()); + Assert.assertEquals(ChatMessageContentFormat.TEXT_PARTS, + chatConfig(model("deepseek", options)).getMessageContentFormat()); + Assert.assertEquals(ChatMessageContentFormat.TEXT_PARTS, + chatConfig(model("ollama", options)).getMessageContentFormat()); + } + + /** + * 验证旧 system 内容块配置会迁移到消息级内容块格式。 + */ + @Test + public void shouldMigrateLegacySystemTextPartsFormat() { + Model model = model("custom", Map.of("agentSystemContentFormat", "TEXT_PARTS")); + + Assert.assertEquals(ChatMessageContentFormat.TEXT_PARTS, + chatConfig(model).getMessageContentFormat()); + } + + /** + * 验证新旧配置并存时使用新的消息级配置。 + */ + @Test + public void shouldPreferCurrentMessageFormatSetting() { + Model model = model("custom", Map.of( + "agentMessageContentFormat", "STANDARD", + "agentSystemContentFormat", "TEXT_PARTS")); + + Assert.assertEquals(ChatMessageContentFormat.STANDARD, + chatConfig(model).getMessageContentFormat()); + } + + /** + * 验证非法配置会安全回退到标准格式。 + */ + @Test + public void shouldFallbackToStandardForUnknownFormat() { + Model model = model("custom", Map.of("agentMessageContentFormat", "PARTS")); + + Assert.assertEquals(ChatMessageContentFormat.STANDARD, + chatConfig(model).getMessageContentFormat()); + } + + /** + * 验证工作流选择内容块模式模型后,请求消息按数组格式序列化。 + */ + @Test + public void shouldSerializeWorkflowModelMessagesAsTextParts() { + ChatConfig config = chatConfig(model( + "custom", Map.of("agentMessageContentFormat", "TEXT_PARTS"))); + + List> messages = new OpenAIChatMessageSerializer().serializeMessages( + List.of(SystemMessage.of("系统提示"), new UserMessage("用户问题")), + config); + + assertTextPart(messages.get(0), "系统提示"); + assertTextPart(messages.get(1), "用户问题"); + } + + /** + * 创建指定供应商和高级配置的聊天模型记录。 + * + * @param providerType 供应商类型 + * @param options 模型高级配置 + * @return 模型记录 + */ + private Model model(String providerType, Map options) { + ModelProvider provider = new ModelProvider(); + provider.setProviderType(providerType); + provider.setProviderName(providerType); + + Model model = new Model(); + model.setModelProvider(provider); + model.setEndpoint("https://model.example.com"); + model.setApiKey("sk-test"); + model.setModelName("test-model"); + model.setRequestPath("/v1/chat/completions"); + model.setOptions(options); + return model; + } + + /** + * 获取模型生成的聊天配置。 + * + * @param model 模型记录 + * @return 聊天配置 + */ + private ChatConfig chatConfig(Model model) { + return ((BaseChatModel) model.toChatModel()).getConfig(); + } + + /** + * 断言消息 content 只包含指定文本内容块。 + * + * @param message 已序列化消息 + * @param expectedText 预期文本 + */ + private void assertTextPart(Map message, String expectedText) { + List content = (List) message.get("content"); + Assert.assertEquals(1, content.size()); + Map textPart = (Map) content.get(0); + Assert.assertEquals("text", textPart.get("type")); + Assert.assertEquals(expectedText, textPart.get("text")); + } +}