fix: 让 LLM 节点应用模型消息格式配置

- 从模型高级配置解析内容块数组模式并兼容旧配置项

- 向 OpenAI 兼容、DeepSeek 与 Ollama 聊天配置透传消息格式

- 补充配置优先级、回退和请求序列化测试
This commit is contained in:
2026-08-11 20:56:09 +08:00
parent ac9e200a15
commit fe41d62b8a
2 changed files with 169 additions and 0 deletions

View File

@@ -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<String, Object> 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":

View File

@@ -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<String, Object> 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<Map<String, Object>> 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<String, Object> 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<String, Object> 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"));
}
}