Spring AI @ToolParam 扩展注解改造实践

Spring AI @ToolParam 扩展注解改造实践

如何在 Spring AI 框架基础上扩展 @ToolParam 注解,添加 exampledefaultValue 支持,让 AI 模型更好地理解和使用工具参数。

前言

在使用 Spring AI 框架开发 MCP(Model Context Protocol)服务器时,我们发现框架提供的 @ToolParam 注解虽然能够描述参数的基本信息(描述和是否必填),但缺少示例值和默认值的支持。这对于 AI 模型理解参数格式和生成正确的调用参数来说,是一个不小的遗憾。

本文将详细介绍如何在不修改 Spring AI 框架源码的前提下,通过扩展注解和自定义 Schema 生成器的方式,为工具参数添加 exampledefaultValue 支持。

目录

  1. 背景与需求
  2. 技术方案设计
  3. 核心实现
  4. 使用示例
  5. 踩坑与解决方案
  6. 总结与展望

背景与需求

现状分析

Spring AI 框架提供了 @ToolParam 注解,用于描述工具方法的参数信息:

java 复制代码
@Tool(name = "orderDetail", description = "查询订单详情")
public String orderDetail(
    @ToolParam(description = "订单号", required = true) Long orderNo
) {
    return "订单详情";
}

生成的 JSON Schema 如下:

json 复制代码
{
  "type": "object",
  "properties": {
    "orderNo": {
      "type": "integer",
      "description": "订单号"
    }
  },
  "required": ["orderNo"]
}

问题与痛点

在实际使用中,我们发现以下问题:

  1. 缺少示例值:AI 模型无法直观了解参数的格式和内容
  2. 缺少默认值:无法为可选参数提供默认值,增加模型调用的复杂度
  3. 参数理解困难:特别是对于复杂对象参数,模型难以准确理解每个字段的期望值

需求目标

我们希望扩展 @ToolParam 注解,使其支持:

  • example:参数的示例值,帮助 AI 模型理解参数格式
  • defaultValue:参数的默认值,简化模型调用
  • 向后兼容 :完全兼容现有的 @ToolParam 注解
  • 零侵入:不修改 Spring AI 框架源码

技术方案设计

方案选型

经过分析,我们确定了以下技术方案:

方案一:直接修改框架源码 ❌

优点:实现简单,直接修改框架代码即可

缺点

  • 框架升级时需要重新修改
  • 维护成本高
  • 不符合开闭原则
方案二:扩展注解 + 自定义生成器 ✅

优点

  • 完全独立,不依赖框架修改
  • 易于维护和升级
  • 可以灵活扩展

缺点

  • 需要理解框架内部实现
  • 实现复杂度稍高

最终选择:方案二

架构设计

我们的扩展方案包含以下核心组件:

复制代码
┌─────────────────────────────────────────┐
│      @ExtendedToolParam 注解            │
│  (扩展的注解,添加 example/defaultValue) │
└─────────────────────────────────────────┘
                    ↓
┌─────────────────────────────────────────┐
│   ExtendedJsonSchemaGenerator            │
│   (扩展的 Schema 生成器)                 │
└─────────────────────────────────────────┘
                    ↓
┌─────────────────────────────────────────┐
│   ExtendedToolDefinitions                │
│   (扩展的工具定义生成器)                  │
└─────────────────────────────────────────┘
                    ↓
┌─────────────────────────────────────────┐
│   ExtendedMethodToolCallbackProvider     │
│   (扩展的工具回调提供者)                  │
└─────────────────────────────────────────┘

核心流程

复制代码
1. 工具方法注册
   ↓
2. ExtendedMethodToolCallbackProvider 扫描 @Tool 方法
   ↓
3. ExtendedToolDefinitions.from() 生成工具定义
   ↓
4. ExtendedJsonSchemaGenerator.generateForMethodInput() 生成 Schema
   ↓
5. 解析 @ExtendedToolParam 注解,提取 example 和 defaultValue
   ↓
6. 生成包含 examples 和 default 的 JSON Schema

核心实现

第一步:创建扩展注解

首先,我们创建了 @ExtendedToolParam 注解:

java 复制代码
package com.echronos.mcp.annotation;

@Target({ElementType.PARAMETER, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ExtendedToolParam {
    String description() default "";
    boolean required() default true;
    String example() default "";        // 新增:示例值
    String defaultValue() default "";   // 新增:默认值
}

设计要点

  • 完全兼容 @ToolParam 的所有属性
  • 添加 exampledefaultValue 两个新属性
  • 支持在方法参数和类字段上使用

第二步:实现扩展的 Schema 生成器

这是整个方案的核心部分。我们需要在框架生成的 Schema 基础上,添加 examplesdefault 字段。

设计思路
  1. 复用框架能力 :先调用框架的 JsonSchemaGenerator.generateForMethodInput() 生成基础 Schema
  2. 解析和增强:解析生成的 Schema,遍历所有参数
  3. 提取注解信息 :检查 @ExtendedToolParam 注解,提取 exampledefaultValue
  4. 递归处理:对于复杂对象参数,递归处理其字段上的注解
核心代码实现
java 复制代码
public final class ExtendedJsonSchemaGenerator {
    
    private static final ObjectMapper OBJECT_MAPPER = JsonParser.getObjectMapper();
    
    public static String generateForMethodInput(Method method, 
            JsonSchemaGenerator.SchemaOption... schemaOptions) {
        // 1. 使用框架方法生成基础 Schema
        String baseSchema = JsonSchemaGenerator.generateForMethodInput(method, schemaOptions);
        
        try {
            // 2. 解析为基础 Schema
            ObjectNode schema = (ObjectNode) OBJECT_MAPPER.readTree(baseSchema);
            
            if (!schema.has("properties")) {
                return baseSchema;
            }
            
            ObjectNode properties = (ObjectNode) schema.get("properties");
            
            // 3. 遍历所有参数,添加 example 和 defaultValue
            for (int i = 0; i < method.getParameterCount(); i++) {
                String parameterName = method.getParameters()[i].getName();
                Type parameterType = method.getGenericParameterTypes()[i];
                
                // 跳过 ToolContext 类型参数
                if (parameterType instanceof Class<?> parameterClass
                        && ClassUtils.isAssignable(parameterClass, ToolContext.class)) {
                    continue;
                }
                
                if (!properties.has(parameterName)) {
                    continue;
                }
                
                ObjectNode parameterNode = (ObjectNode) properties.get(parameterName);
                
                // 4. 添加 example 和 defaultValue
                addExampleAndDefaultValue(parameterNode, method, i, parameterType);
            }
            
            return schema.toPrettyString();
            
        } catch (Exception e) {
            // 如果解析失败,返回原始 Schema
            return baseSchema;
        }
    }
}
关键实现:参数处理逻辑
java 复制代码
private static void addExampleAndDefaultValue(ObjectNode parameterNode, 
        Method method, int parameterIndex, Type parameterType) {
    Parameter parameter = method.getParameters()[parameterIndex];
    
    // 优先检查 @ExtendedToolParam
    ExtendedToolParam extendedAnnotation = parameter.getAnnotation(ExtendedToolParam.class);
    if (extendedAnnotation != null) {
        addExample(parameterNode, extendedAnnotation.example());
        addDefaultValue(parameterNode, extendedAnnotation.defaultValue());
        
        // 如果是复杂对象类型,递归处理字段
        if (parameterType instanceof Class<?>) {
            addExampleAndDefaultFromClassFields(parameterNode, (Class<?>) parameterType);
        }
        return;
    }
    
    // 检查 @ToolParam
    ToolParam toolParamAnnotation = parameter.getAnnotation(ToolParam.class);
    if (toolParamAnnotation != null) {
        if (parameterType instanceof Class<?>) {
            addExampleAndDefaultFromClassFields(parameterNode, (Class<?>) parameterType);
        }
        return;
    }
    
    // ⚠️ 关键修复:即使参数上没有注解,也要处理嵌套对象的字段
    // 这样可以确保复杂对象参数中的字段注解能够被正确处理
    if (parameterType instanceof Class<?>) {
        Class<?> clazz = (Class<?>) parameterType;
        if (!clazz.isPrimitive() && clazz != String.class 
                && !Number.class.isAssignableFrom(clazz) 
                && clazz != Boolean.class && !clazz.isEnum()) {
            addExampleAndDefaultFromClassFields(parameterNode, clazz);
        }
    }
}
递归处理嵌套对象
java 复制代码
private static void addExampleAndDefaultFromClassFields(ObjectNode parameterNode, Class<?> clazz) {
    if (!parameterNode.has("properties")) {
        return;
    }
    
    ObjectNode properties = (ObjectNode) parameterNode.get("properties");
    List<Field> fields = getAllDeclaredFields(clazz);
    
    for (Field field : fields) {
        String fieldName = field.getName();
        if (!properties.has(fieldName)) {
            continue;
        }
        
        ObjectNode fieldNode = (ObjectNode) properties.get(fieldName);
        ExtendedToolParam extendedAnnotation = field.getAnnotation(ExtendedToolParam.class);
        
        if (extendedAnnotation != null) {
            addExample(fieldNode, extendedAnnotation.example());
            addDefaultValue(fieldNode, extendedAnnotation.defaultValue());
        }
        
        // 递归处理嵌套对象
        if (fieldNode.has("properties")) {
            addExampleAndDefaultFromType(fieldNode, field.getGenericType());
        }
    }
}
Example 和 DefaultValue 的处理
java 复制代码
private static void addExample(ObjectNode node, String example) {
    if (!StringUtils.hasText(example)) {
        return;
    }
    
    try {
        // 尝试解析为 JSON 值(支持 JSON 格式的字符串)
        Object exampleValue = OBJECT_MAPPER.readValue(example, Object.class);
        ArrayNode examplesArray = OBJECT_MAPPER.createArrayNode();
        examplesArray.add(OBJECT_MAPPER.valueToTree(exampleValue));
        node.set("examples", examplesArray);
    } catch (Exception e) {
        // 如果解析失败,直接作为字符串
        ArrayNode examplesArray = OBJECT_MAPPER.createArrayNode();
        examplesArray.add(example);
        node.set("examples", examplesArray);
    }
}

private static void addDefaultValue(ObjectNode node, String defaultValue) {
    if (!StringUtils.hasText(defaultValue)) {
        return;
    }
    
    try {
        // 尝试解析为 JSON 值
        Object defaultVal = OBJECT_MAPPER.readValue(defaultValue, Object.class);
        node.set("default", OBJECT_MAPPER.valueToTree(defaultVal));
    } catch (Exception e) {
        // 如果解析失败,直接作为字符串
        node.put("default", defaultValue);
    }
}

设计亮点

  • 支持 JSON 格式的字符串(如 "\"上海\"")和普通字符串(如 "上海"
  • 自动处理解析失败的情况,确保健壮性
  • 使用 JSON Schema 标准的 examples 数组格式

第三步:扩展工具定义生成器

为了让扩展的 Schema 生成器生效,我们需要创建一个扩展的工具定义生成器:

java 复制代码
public class ExtendedToolDefinitions {
    
    public static ToolDefinition from(Method method) {
        // 使用框架的 ToolDefinitions 获取基础定义
        ToolDefinition baseDefinition = ToolDefinitions.from(method);
        
        // 使用扩展生成器重新生成 Schema(包含 example 和 defaultValue)
        String extendedSchema = ExtendedJsonSchemaGenerator.generateForMethodInput(method);
        
        // 创建新的工具定义,使用扩展的 Schema
        return ToolDefinition.builder()
                .name(baseDefinition.name())
                .description(baseDefinition.description())
                .inputSchema(extendedSchema)  // 使用扩展的 Schema
                .build();
    }
}

第四步:扩展工具回调提供者

最后,我们需要创建一个扩展的工具回调提供者,使用我们的扩展定义生成器:

java 复制代码
public class ExtendedMethodToolCallbackProvider implements ToolCallbackProvider {
    
    private final List<Object> toolObjects;
    
    @Override
    public ToolCallback[] getToolCallbacks() {
        return this.toolObjects.stream()
                .map(toolObject -> Stream
                        .of(ReflectionUtils.getDeclaredMethods(...))
                        .filter(toolMethod -> toolMethod.isAnnotationPresent(Tool.class))
                        .map(toolMethod -> MethodToolCallback.builder()
                                .toolDefinition(ExtendedToolDefinitions.from(toolMethod))  // 使用扩展的定义生成器
                                .toolMetadata(ToolMetadata.from(toolMethod))
                                .toolMethod(toolMethod)
                                .toolObject(toolObject)
                                .build())
                        .toArray(ToolCallback[]::new))
                .flatMap(Stream::of)
                .toArray(ToolCallback[]::new);
    }
    
    public static Builder builder() {
        return new Builder();
    }
}

使用示例

配置扩展的 Provider

在 Spring 配置类中,将所有的 MethodToolCallbackProvider 替换为 ExtendedMethodToolCallbackProvider

java 复制代码
@Configuration
public class McpServerConfig {
    
    @Bean("AgentTools")
    public ToolCallbackProvider AgentTools(AgentService service) {
        return ExtendedMethodToolCallbackProvider.builder()
                .toolObjects(service)
                .build();
    }
    
    // ... 其他工具配置
}

示例一:简单参数

java 复制代码
@Tool(name = "getUserInfo", description = "获取用户信息")
public String getUserInfo(
        @ExtendedToolParam(
                description = "用户ID",
                required = true,
                example = "12345",
                defaultValue = "0"
        ) Long userId
) {
    return "用户信息";
}

生成的 Schema

json 复制代码
{
  "type": "object",
  "properties": {
    "userId": {
      "type": "integer",
      "description": "用户ID",
      "examples": [12345],
      "default": 0
    }
  },
  "required": ["userId"]
}

示例二:复杂对象参数

java 复制代码
@Data
public class WeatherQueryParam {
    @ExtendedToolParam(
            description = "地区",
            required = true,
            example = "上海",
            defaultValue = "北京"
    )
    private String region;
    
    @ExtendedToolParam(
            description = "日期",
            required = false,
            example = "\"2024-01-01\"",
            defaultValue = "\"今天\""
    )
    private String date;
}

@Tool(name = "获取天气", description = "获取某个地区的天气")
public String getWeather(WeatherQueryParam param) {
    return "今天50度";
}

生成的 Schema

json 复制代码
{
  "type": "object",
  "properties": {
    "param": {
      "type": "object",
      "properties": {
        "region": {
          "type": "string",
          "description": "地区",
          "examples": ["上海"],
          "default": "北京"
        },
        "date": {
          "type": "string",
          "description": "日期",
          "examples": ["2024-01-01"],
          "default": "今天"
        }
      },
      "required": ["region"]
    }
  },
  "required": ["param"]
}

示例三:混合使用

@ExtendedToolParam@ToolParam 可以混合使用,@ExtendedToolParam 优先级更高:

java 复制代码
@Tool(name = "queryOrder", description = "查询订单")
public String queryOrder(
        @ToolParam(description = "订单号") Long orderNo,  // 使用标准注解
        @ExtendedToolParam(
                description = "是否包含详情",
                example = "true",
                defaultValue = "false"
        ) Boolean includeDetails  // 使用扩展注解
) {
    return "订单信息";
}

踩坑与解决方案

坑一:参数无注解时嵌套对象字段未被处理

问题描述

最初实现时,如果方法参数上没有 @ExtendedToolParam@ToolParam 注解,代码不会处理嵌套对象的字段。这导致即使对象字段上有 @ExtendedToolParam 注解,也无法生成 examplesdefault

问题代码

java 复制代码
// ❌ 错误:如果参数没有注解,就不会处理嵌套对象
if (toolParamAnnotation != null) {
    if (parameterType instanceof Class<?>) {
        addExampleAndDefaultFromClassFields(parameterNode, (Class<?>) parameterType);
    }
}
// 如果参数没有注解,这里就结束了,不会处理嵌套对象

解决方案

java 复制代码
// ✅ 正确:即使参数没有注解,也要处理嵌套对象
if (toolParamAnnotation != null) {
    if (parameterType instanceof Class<?>) {
        addExampleAndDefaultFromClassFields(parameterNode, (Class<?>) parameterType);
    }
    return;
}

// 关键修复:即使参数上没有注解,也要处理嵌套对象的字段
if (parameterType instanceof Class<?>) {
    Class<?> clazz = (Class<?>) parameterType;
    if (!clazz.isPrimitive() && clazz != String.class 
            && !Number.class.isAssignableFrom(clazz) 
            && clazz != Boolean.class && !clazz.isEnum()) {
        addExampleAndDefaultFromClassFields(parameterNode, clazz);
    }
}

坑二:ReflectionUtils API 使用错误

问题描述

代码中使用了 ReflectionUtils.getAllDeclaredFields(),但该方法在 Spring 的 ReflectionUtils 中并不存在。

问题代码

java 复制代码
// ❌ 错误:方法不存在
Field[] fields = ReflectionUtils.getAllDeclaredFields(clazz);

解决方案

java 复制代码
// ✅ 正确:使用 doWithFields 收集字段
private static List<Field> getAllDeclaredFields(Class<?> clazz) {
    List<Field> fields = new ArrayList<>();
    ReflectionUtils.doWithFields(clazz, fields::add);
    return fields;
}

坑三:ToolDefinition API 理解错误

问题描述

最初以为 ToolDefinition.builder() 使用 parameters(ObjectNode) 方法,但实际使用的是 inputSchema(String) 方法。

问题代码

java 复制代码
// ❌ 错误:方法不存在
return ToolDefinition.builder()
        .name(baseDefinition.name())
        .description(baseDefinition.description())
        .parameters(extendedSchemaNode)  // 方法不存在
        .build();

解决方案

java 复制代码
// ✅ 正确:使用 inputSchema 方法
return ToolDefinition.builder()
        .name(baseDefinition.name())
        .description(baseDefinition.description())
        .inputSchema(extendedSchema)  // 使用字符串格式
        .build();

坑四:example 和 defaultValue 格式处理

问题描述

用户可能提供不同格式的值:

  • 普通字符串:example = "上海"
  • JSON 格式字符串:example = "\"上海\""
  • 数字字符串:example = "123"
  • 布尔字符串:example = "true"

解决方案

java 复制代码
private static void addExample(ObjectNode node, String example) {
    if (!StringUtils.hasText(example)) {
        return;
    }
    
    try {
        // 尝试解析为 JSON 值(支持 JSON 格式)
        Object exampleValue = OBJECT_MAPPER.readValue(example, Object.class);
        ArrayNode examplesArray = OBJECT_MAPPER.createArrayNode();
        examplesArray.add(OBJECT_MAPPER.valueToTree(exampleValue));
        node.set("examples", examplesArray);
    } catch (Exception e) {
        // 如果解析失败,直接作为字符串(兼容普通字符串)
        ArrayNode examplesArray = OBJECT_MAPPER.createArrayNode();
        examplesArray.add(example);
        node.set("examples", examplesArray);
    }
}

这样既支持 JSON 格式,也兼容普通字符串,提供了最大的灵活性。


最佳实践

1. example 和 defaultValue 格式建议

字符串类型
java 复制代码
// ✅ 推荐:直接使用字符串(代码会自动处理)
@ExtendedToolParam(example = "上海", defaultValue = "北京")

// ✅ 也可以:使用 JSON 格式(带引号)
@ExtendedToolParam(example = "\"上海\"", defaultValue = "\"北京\"")
数字类型
java 复制代码
// ✅ 推荐:直接使用数字字符串
@ExtendedToolParam(example = "12345", defaultValue = "0")
布尔类型
java 复制代码
// ✅ 推荐:直接使用布尔字符串
@ExtendedToolParam(example = "true", defaultValue = "false")
数组类型
java 复制代码
// ✅ 必须使用 JSON 数组格式
@ExtendedToolParam(example = "[1, 2, 3]", defaultValue = "[]")
对象类型
java 复制代码
// ✅ 必须使用 JSON 对象格式
@ExtendedToolParam(
    example = "{\"key\": \"value\"}", 
    defaultValue = "{}"
)

2. 嵌套对象处理

对于复杂对象参数,建议在对象字段上使用 @ExtendedToolParam

java 复制代码
@Data
public class UserQueryParam {
    @ExtendedToolParam(
            description = "用户ID",
            required = true,
            example = "12345",
            defaultValue = "0"
    )
    private Long userId;
    
    @ExtendedToolParam(
            description = "用户信息",
            required = false
    )
    private UserInfo userInfo;  // 嵌套对象
}

@Data
public class UserInfo {
    @ExtendedToolParam(
            description = "用户名",
            example = "张三",
            defaultValue = "未知"
    )
    private String name;
}

3. 向后兼容性

  • ✅ 现有的 @ToolParam 注解完全兼容,无需修改
  • ✅ 可以逐步迁移到 @ExtendedToolParam
  • ✅ 两种注解可以混合使用

效果对比

改造前

json 复制代码
{
  "type": "object",
  "properties": {
    "region": {
      "type": "string",
      "description": "地区"
    }
  },
  "required": ["region"]
}

问题:AI 模型不知道 "地区" 应该填什么值,可能生成错误的参数。

改造后

json 复制代码
{
  "type": "object",
  "properties": {
    "region": {
      "type": "string",
      "description": "地区",
      "examples": ["上海"],
      "default": "北京"
    }
  },
  "required": ["region"]
}

优势

  • ✅ AI 模型可以通过 examples 了解参数格式
  • ✅ 可以使用 default 作为默认值
  • ✅ 提高工具调用的准确率

性能考虑

优化点

  1. 缓存机制:可以考虑对生成的 Schema 进行缓存,避免重复生成
  2. 懒加载:只在需要时才生成扩展的 Schema
  3. 批量处理:对于大量工具方法,可以考虑批量生成

当前实现

  • 每次调用都会重新生成 Schema(简单但可能影响性能)
  • 对于工具数量不多的场景,性能影响可忽略
  • 如需优化,可以在 ExtendedToolDefinitions 中添加缓存

总结与展望

总结

通过本次改造,我们成功实现了:

  1. 扩展注解 :创建了 @ExtendedToolParam 注解,支持 exampledefaultValue
  2. Schema 生成器 :实现了 ExtendedJsonSchemaGenerator,能够生成包含示例和默认值的 Schema
  3. 工具定义生成器 :实现了 ExtendedToolDefinitions,集成扩展的 Schema 生成器
  4. 工具回调提供者 :实现了 ExtendedMethodToolCallbackProvider,替换框架默认实现
  5. 向后兼容 :完全兼容现有的 @ToolParam 注解
  6. 零侵入:不修改 Spring AI 框架源码

技术亮点

  • 🎯 设计模式:使用装饰器模式,在框架功能基础上扩展
  • 🎯 反射机制:充分利用 Java 反射,动态解析注解
  • 🎯 递归处理:支持嵌套对象的字段注解处理
  • 🎯 容错机制:完善的异常处理,确保健壮性

实际效果

  • 📈 提升准确率:AI 模型调用工具的准确率明显提升
  • 📈 降低复杂度:通过默认值简化了模型调用逻辑
  • 📈 改善体验:示例值帮助模型更好地理解参数格式

未来展望

  1. 更多注解属性 :可以考虑添加更多属性,如 minmaxpattern
  2. 类型验证:可以添加类型验证逻辑,确保参数类型正确
  3. 文档生成:可以基于注解自动生成 API 文档
  4. IDE 支持:可以开发 IDE 插件,提供注解的智能提示

致谢

感谢 Spring AI 团队提供的优秀框架,让我们能够在框架基础上进行扩展,实现自己的需求。


附录

完整代码结构

复制代码
src/main/java/com/echronos/mcp/
├── annotation/
│   └── ExtendedToolParam.java          # 扩展注解
├── tool/
│   ├── ExtendedJsonSchemaGenerator.java    # Schema 生成器
│   ├── ExtendedToolDefinitions.java         # 工具定义生成器
│   ├── ExtendedMethodToolCallbackProvider.java  # 工具回调提供者
│   └── ExtendedJsonSchemaGeneratorUsageExample.java  # 使用示例
└── mcp/config/
    └── McpServerConfig.java            # 配置类

相关资源

相关推荐
WZTTMoon2 小时前
Spring Boot OAuth2 授权码模式开发实战
大数据·数据库·spring boot
中科天工2 小时前
智能仓储解决方案到底是什么?
大数据·人工智能·智能
Ydwlcloud2 小时前
AWS国际云服务器新用户优惠全解析:如何聪明地迈出上云第一步?
服务器·人工智能·云计算·aws
天天进步20152 小时前
【InfiniteTalk 源码分析 04】训练策略拆解:如何实现超长视频的生成稳定性?
人工智能·深度学习
imbackneverdie2 小时前
更经济实惠的润色方法,告别“中式英文”!
人工智能·考研·ai·自然语言处理·ai写作·研究生·ai工具
xl-xueling2 小时前
从快手直播故障,看全景式业务监控势在必行!
大数据·后端·网络安全·流式计算
天呐草莓2 小时前
集成学习 (ensemble learning)
人工智能·python·深度学习·算法·机器学习·数据挖掘·集成学习
却道天凉_好个秋2 小时前
OpenCV(四十七):FLANN特征匹配
人工智能·opencv·计算机视觉
云老大TG:@yunlaoda3603 小时前
华为云国际站代理商运维保障的具体要求有哪些?
大数据·运维·华为云