feat: 完善模型能力识别与验证

- 自动识别模型类型、视觉、推理和工具能力并保留手动覆盖

- 使用 AgentScope 工具与视觉探测并统一管理端配置反馈
This commit is contained in:
2026-07-27 19:40:23 +08:00
parent 0dc5c3ca55
commit 567fd12706
21 changed files with 1059 additions and 378 deletions

View File

@@ -80,8 +80,8 @@ public class Model extends ModelBase {
deepseekConfig.setThinkingProtocol("deepseek");
deepseekConfig.setNeedReasoningContentForToolMessage(Boolean.TRUE);
deepseekConfig.setSupportImageBase64Only(getSupportImageB64Only());
if (getSupportToolMessage() != null) {
deepseekConfig.setSupportToolMessage(getSupportToolMessage());
if (getSupportTool() != null) {
deepseekConfig.setSupportToolMessage(getSupportTool());
}
return new DeepseekChatModel(deepseekConfig);
default:
@@ -92,8 +92,8 @@ public class Model extends ModelBase {
openAIChatConfig.setModel(checkAndGetModelName());
openAIChatConfig.setRequestPath(checkAndGetRequestPath());
openAIChatConfig.setSupportImageBase64Only(getSupportImageB64Only());
if (getSupportToolMessage() != null) {
openAIChatConfig.setSupportToolMessage(getSupportToolMessage());
if (getSupportTool() != null) {
openAIChatConfig.setSupportToolMessage(getSupportTool());
}
return new OpenAIChatModel(openAIChatConfig);
}

View File

@@ -77,10 +77,12 @@ public class UnifiedModelInvokeServiceImpl implements UnifiedModelInvokeService
throw ModelInvokeException.badRequest("当前模型仅支持 base64 图片输入", "messages", "image_base64_only");
}
}
if (request.getTools() != null && !request.getTools().isEmpty() && !Boolean.TRUE.equals(model.getSupportTool())) {
if (request.getTools() != null
&& !request.getTools().isEmpty()
&& Boolean.FALSE.equals(model.getSupportTool())) {
throw ModelInvokeException.badRequest("当前模型不支持 tools 参数", "tools", "tool_not_supported");
}
if (hasToolMessage(messages) && !Boolean.TRUE.equals(model.getSupportToolMessage())) {
if (hasToolMessage(messages) && Boolean.FALSE.equals(model.getSupportTool())) {
throw ModelInvokeException.badRequest("当前模型不支持 tool 消息透传", "messages", "tool_message_not_supported");
}
}

View File

@@ -2,6 +2,7 @@ package tech.easyflow.ai.service;
import com.mybatisflex.core.service.IService;
import tech.easyflow.ai.entity.Model;
import tech.easyflow.ai.service.capability.ModelCapabilityResolution;
import java.math.BigInteger;
import java.util.List;
@@ -19,6 +20,15 @@ public interface ModelService extends IService<Model> {
Map<String, Object> verifyModelConfig(Model llm);
/**
* 根据供应商和模型 ID 自动解析模型能力。
*
* @param providerId 供应商 ID
* @param modelName 模型 ID
* @return 模型能力识别结果
*/
ModelCapabilityResolution resolveModelCapabilities(BigInteger providerId, String modelName);
Map<String, Map<String, List<Model>>> getList(Model entity);
void removeByEntity(Model entity);

View File

@@ -8,6 +8,7 @@ import com.easyagents.core.model.embedding.EmbeddingModel;
import com.easyagents.core.model.rerank.RerankModel;
import com.easyagents.core.store.VectorData;
import com.mybatisflex.core.query.QueryWrapper;
import com.mybatisflex.core.update.UpdateChain;
import com.mybatisflex.core.util.StringUtil;
import com.mybatisflex.spring.service.impl.ServiceImpl;
import org.slf4j.Logger;
@@ -21,7 +22,11 @@ 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.capability.ModelCapabilityResolution;
import tech.easyflow.ai.service.capability.ModelCapabilityResolver;
import tech.easyflow.ai.service.capability.ModelCapabilitySource;
import tech.easyflow.ai.service.verification.ChatModelConnectivityVerifier;
import tech.easyflow.ai.service.verification.ChatModelVerificationResult;
import tech.easyflow.common.tree.Tree;
import tech.easyflow.common.util.SqlOperatorsUtil;
import tech.easyflow.common.util.SqlUtil;
@@ -47,6 +52,10 @@ public class ModelServiceImpl extends ServiceImpl<ModelMapper, Model> implements
@Autowired
ModelProviderService modelProviderService;
/** 统一模型能力解析器。 */
@Autowired
private ModelCapabilityResolver modelCapabilityResolver;
@Resource
private Cache<String, Object> cache;
@@ -92,6 +101,18 @@ public class ModelServiceImpl extends ServiceImpl<ModelMapper, Model> implements
}
/**
* 根据供应商和模型 ID 自动解析模型能力。
*
* @param providerId 供应商 ID
* @param modelName 模型 ID
* @return 模型能力识别结果
*/
@Override
public ModelCapabilityResolution resolveModelCapabilities(BigInteger providerId, String modelName) {
return modelCapabilityResolver.resolve(resolveProviderType(providerId, null), modelName);
}
@Override
public Map<String, Map<String, List<Model>>> getList(Model entity) {
Map<String, Map<String, List<Model>>> result = new HashMap<>();
@@ -166,7 +187,15 @@ public class ModelServiceImpl extends ServiceImpl<ModelMapper, Model> implements
if (chatModelConnectivityVerifier == null) {
throw new BusinessException("Agent 模型连通性验证组件未加载");
}
return chatModelConnectivityVerifier.verify(model).toMap();
ChatModelVerificationResult result = chatModelConnectivityVerifier.verify(model);
if (result.getSupportTool() != null && model.getId() != null) {
UpdateChain<Model> updateChain = updateChain();
updateChain.set(Model::getSupportTool, result.getSupportTool());
updateChain.set(Model::getSupportToolMessage, result.getSupportTool());
updateChain.eq(Model::getId, model.getId());
updateChain.update();
}
return result.toMap();
}
@Override
@@ -199,6 +228,7 @@ public class ModelServiceImpl extends ServiceImpl<ModelMapper, Model> implements
if (entity == null) {
throw new BusinessException("模型配置不能为空");
}
applyAutoCapabilities(entity);
if (entity.getPublishEnabled() == null) {
entity.setPublishEnabled(Boolean.FALSE);
}
@@ -239,6 +269,101 @@ public class ModelServiceImpl extends ServiceImpl<ModelMapper, Model> implements
entity.setInvokeCode(invokeCode);
}
/**
* 将自动识别结果写入待保存模型,并清理已下线的展示能力字段。
*
* @param entity 待保存模型
*/
private void applyAutoCapabilities(Model entity) {
String providerType = resolveProviderType(entity.getProviderId(), entity.getModelProvider());
ModelCapabilityResolution resolution = modelCapabilityResolver.resolve(
providerType, entity.getModelName());
boolean modelIdentityChanged = hasModelIdentityChanged(entity);
if (resolution.getSource() != ModelCapabilitySource.DEFAULT) {
entity.setModelType(resolution.getModelType());
applyResolvedChatCapabilities(entity, resolution);
} else if (StrUtil.isBlank(entity.getModelType())) {
entity.setModelType(Model.MODEL_TYPES[0]);
}
if (!Model.MODEL_TYPES[0].equals(entity.getModelType())) {
entity.setSupportImage(Boolean.FALSE);
entity.setSupportThinking(Boolean.FALSE);
entity.setSupportTool(Boolean.FALSE);
} else if (modelIdentityChanged) {
// 切换模型后,仅将仍无法识别且未被用户设置的能力归零。
entity.setSupportImage(Boolean.TRUE.equals(entity.getSupportImage()));
entity.setSupportThinking(Boolean.TRUE.equals(entity.getSupportThinking()));
entity.setSupportTool(Boolean.TRUE.equals(entity.getSupportTool()));
}
// 视频、音频尚未接入模型调用链,保存时保持关闭。
entity.setSupportVideo(Boolean.FALSE);
entity.setSupportAudio(Boolean.FALSE);
// tool 消息能力跟随工具调用能力,不再由前端单独配置。
entity.setSupportToolMessage(entity.getSupportTool());
}
/**
* 判断更新请求是否切换了实际模型或供应商。
*
* @param entity 待更新模型
* @return 模型标识发生变化返回 true
*/
private boolean hasModelIdentityChanged(Model entity) {
if (entity.getId() == null) {
return false;
}
Model stored = modelMapper.selectOneById(entity.getId());
if (stored == null) {
return false;
}
boolean modelChanged = StrUtil.isNotBlank(entity.getModelName())
&& !StrUtil.equalsIgnoreCase(
StrUtil.trim(entity.getModelName()),
StrUtil.trim(stored.getModelName()));
boolean providerChanged = entity.getProviderId() != null
&& !Objects.equals(entity.getProviderId(), stored.getProviderId());
return modelChanged || providerChanged;
}
/**
* 使用识别结果补齐尚未明确配置的对话能力,保留用户手动设置。
*
* @param entity 待保存模型
* @param resolution 模型能力识别结果
*/
private void applyResolvedChatCapabilities(Model entity, ModelCapabilityResolution resolution) {
if (entity.getSupportImage() == null && resolution.getSupportImage() != null) {
entity.setSupportImage(resolution.getSupportImage());
}
if (entity.getSupportThinking() == null && resolution.getSupportThinking() != null) {
entity.setSupportThinking(resolution.getSupportThinking());
}
if (entity.getSupportTool() == null && resolution.getSupportTool() != null) {
entity.setSupportTool(resolution.getSupportTool());
}
}
/**
* 获取模型配置对应的供应商类型。
*
* @param providerId 供应商 ID
* @param provider 已加载的供应商对象
* @return 供应商类型,供应商不存在时返回 null
*/
private String resolveProviderType(BigInteger providerId, ModelProvider provider) {
if (provider != null && StrUtil.isNotBlank(provider.getProviderType())) {
return provider.getProviderType();
}
if (providerId == null) {
return null;
}
ModelProvider storedProvider = modelProviderService.getById(providerId);
return storedProvider == null ? null : storedProvider.getProviderType();
}
@Override
public List<Model> listInvokeModels() {
QueryWrapper queryWrapper = QueryWrapper.create().eq(Model::getModelType, Model.MODEL_TYPES[0]);

View File

@@ -4,7 +4,7 @@ import java.util.LinkedHashMap;
import java.util.Map;
/**
* Chat Model 与 VLM 的双阶段连通性验证结果。
* Chat Model 与 VLM 的连接和工具能力验证结果。
*/
public final class ChatModelVerificationResult {
@@ -16,6 +16,8 @@ public final class ChatModelVerificationResult {
private final ModelVerificationStatus streaming;
/** 实际生效的 HTTP 版本策略。 */
private final String effectiveHttpVersion;
/** 当前端点是否通过工具调用探测。 */
private final Boolean supportTool;
/** 用户可见的简洁结果说明。 */
private final String message;
@@ -26,48 +28,39 @@ public final class ChatModelVerificationResult {
* @param nonStreaming 非流式验证状态
* @param streaming 流式验证状态
* @param effectiveHttpVersion 实际生效的 HTTP 版本策略
* @param supportTool 当前端点是否通过工具调用探测
* @param message 用户可见结果说明
*/
private ChatModelVerificationResult(ModelVerificationStatus status,
ModelVerificationStatus nonStreaming,
ModelVerificationStatus streaming,
String effectiveHttpVersion,
Boolean supportTool,
String message) {
this.status = status;
this.nonStreaming = nonStreaming;
this.streaming = streaming;
this.effectiveHttpVersion = effectiveHttpVersion;
this.supportTool = supportTool;
this.message = message;
}
/**
* 创建双阶段全部通过的结果。
* 创建一次连接与工具探测通过的结果。
*
* @param effectiveHttpVersion 实际生效的 HTTP 版本策略
* @return 全部通过结果
* @param supportTool 当前端点是否通过工具调用探测
* @return 验证通过结果
*/
public static ChatModelVerificationResult passed(String effectiveHttpVersion) {
public static ChatModelVerificationResult passed(String effectiveHttpVersion,
boolean supportTool) {
return new ChatModelVerificationResult(
ModelVerificationStatus.PASSED,
ModelVerificationStatus.PASSED,
ModelVerificationStatus.PASSED,
ModelVerificationStatus.SKIPPED,
effectiveHttpVersion,
"验证成功");
}
/**
* 创建基础连接通过但流式阶段失败的结果。
*
* @param effectiveHttpVersion 实际生效的 HTTP 版本策略
* @return 部分通过结果
*/
public static ChatModelVerificationResult streamingUnavailable(String effectiveHttpVersion) {
return new ChatModelVerificationResult(
ModelVerificationStatus.PARTIAL,
ModelVerificationStatus.PASSED,
ModelVerificationStatus.FAILED,
effectiveHttpVersion,
"连接成功,流式响应不可用,可关闭智能体的模型流式响应。");
supportTool,
"验证通过");
}
/**
@@ -106,6 +99,15 @@ public final class ChatModelVerificationResult {
return effectiveHttpVersion;
}
/**
* 获取工具调用探测结果。
*
* @return 是否通过工具调用探测
*/
public Boolean getSupportTool() {
return supportTool;
}
/**
* 获取用户可见结果说明。
*
@@ -126,6 +128,9 @@ public final class ChatModelVerificationResult {
result.put("nonStreaming", nonStreaming.name());
result.put("streaming", streaming.name());
result.put("effectiveHttpVersion", effectiveHttpVersion);
if (supportTool != null) {
result.put("supportTool", supportTool);
}
result.put("message", message);
return result;
}

View File

@@ -0,0 +1,80 @@
package tech.easyflow.ai.service.impl;
import org.junit.Assert;
import org.junit.Test;
import tech.easyflow.ai.entity.Model;
import tech.easyflow.ai.service.capability.ModelCapabilityResolution;
import tech.easyflow.ai.service.capability.ModelCapabilitySource;
import java.lang.reflect.Method;
/**
* 模型能力自动识别与手动覆盖合并规则测试。
*/
public class ModelServiceImplCapabilityOverrideTest {
/**
* 验证用户明确关闭的能力不会被模型库重新打开。
*
* @throws ReflectiveOperationException 无法调用待测试方法时抛出
*/
@Test
public void shouldPreserveExplicitCapabilityOverrides() throws ReflectiveOperationException {
Model model = new Model();
model.setSupportImage(Boolean.FALSE);
model.setSupportThinking(Boolean.FALSE);
model.setSupportTool(Boolean.FALSE);
applyResolvedCapabilities(model, detectedCapabilities());
Assert.assertEquals(Boolean.FALSE, model.getSupportImage());
Assert.assertEquals(Boolean.FALSE, model.getSupportThinking());
Assert.assertEquals(Boolean.FALSE, model.getSupportTool());
}
/**
* 验证空能力值会由模型库自动补齐。
*
* @throws ReflectiveOperationException 无法调用待测试方法时抛出
*/
@Test
public void shouldFillCapabilitiesWhenNotConfigured() throws ReflectiveOperationException {
Model model = new Model();
applyResolvedCapabilities(model, detectedCapabilities());
Assert.assertEquals(Boolean.TRUE, model.getSupportImage());
Assert.assertEquals(Boolean.TRUE, model.getSupportThinking());
Assert.assertEquals(Boolean.TRUE, model.getSupportTool());
}
/**
* 创建模型库已确认的对话能力。
*
* @return 全部开启的模型能力
*/
private ModelCapabilityResolution detectedCapabilities() {
return new ModelCapabilityResolution(
Model.MODEL_TYPES[0],
Boolean.TRUE,
Boolean.TRUE,
Boolean.TRUE,
ModelCapabilitySource.CATALOG);
}
/**
* 调用服务内部的能力合并逻辑。
*
* @param model 待合并模型
* @param resolution 自动识别结果
* @throws ReflectiveOperationException 无法调用待测试方法时抛出
*/
private void applyResolvedCapabilities(Model model, ModelCapabilityResolution resolution)
throws ReflectiveOperationException {
ModelServiceImpl service = new ModelServiceImpl();
Method method = ModelServiceImpl.class.getDeclaredMethod(
"applyResolvedChatCapabilities", Model.class, ModelCapabilityResolution.class);
method.setAccessible(true);
method.invoke(service, model, resolution);
}
}