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

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

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

前言

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

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

目录

  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 的所有属性
  • 添加 example 和 defaultValue 两个新属性
  • 支持在方法参数和类字段上使用

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

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

设计思路
  1. 复用框架能力 :先调用框架的 JsonSchemaGenerator.generateForMethodInput() 生成基础 Schema
  2. 解析和增强:解析生成的 Schema,遍历所有参数
  3. 提取注解信息 :检查 @ExtendedToolParam 注解,提取 example 和 defaultValue
  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 注解,也无法生成 examples 和 default。

问题代码:

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

技术亮点

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

实际效果

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

未来展望

  1. 更多注解属性 :可以考虑添加更多属性,如 min、max、pattern 等
  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            # 配置类

相关资源

相关推荐
sell-cloud24 分钟前
Prompt Engineering要凉了?Fable 5.5能“猜“意图,但银行正在封杀AI Agent——企业AI的真正护城河是什么
人工智能·prompt
Navicat中国2 小时前
Navicat 18 内置 AI 助手让数据库开发从繁琐操作迈向智能协作
大数据·人工智能·数据库开发
4SAPI2 小时前
多模型请求路由怎么设计?成本、速度和能力如何平衡
人工智能·python·gateway·php
魔乐社区2 小时前
翼芯・智算测试与适配优化专区,让国产算力“选得准、跑得通、用得好“
人工智能·开源·大模型
知了学历孙老师4 小时前
小自考的校考和统考,出题方和难度差在哪
java·大数据·前端
Java爱好狂.4 小时前
Java初学者如何一周速通SpringBoot!
java·spring boot·后端·spring·高并发·架构师·java面试
XCDoris4 小时前
郴州logo设计机构
大数据·人工智能·python
Goboy5 小时前
一个人带三个Agent,我用全新TRAE从0到1搭了个资讯周刊站
人工智能·trae
俊哥V5 小时前
每日 AI 研究简报 · 2026-10-19
人工智能·ai
AI_AGENT_DEV_AI5 小时前
AI英语教培智能体的开发
人工智能