fix: 让 LLM 节点应用模型消息格式配置
- 从模型高级配置解析内容块数组模式并兼容旧配置项 - 向 OpenAI 兼容、DeepSeek 与 Ollama 聊天配置透传消息格式 - 补充配置优先级、回退和请求序列化测试
This commit is contained in:
@@ -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":
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user