Spring AI Alibaba 实战:从maven依赖治理到声明式工具调用

目录

[一、先别急着写代码:BOM 是 Spring AI Alibaba 的"版本锚点"](#一、先别急着写代码:BOM 是 Spring AI Alibaba 的“版本锚点”)

[二、声明式工具:@Tool 把方法签名变成模型可读的 schema](#二、声明式工具:@Tool 把方法签名变成模型可读的 schema)

[三、把工具交给 ChatClient:defaultTools 与 tools 的取舍](#三、把工具交给 ChatClient:defaultTools 与 tools 的取舍)

四、几个容易踩的坑


在 Java 生态中落地大模型应用,Spring AI Alibaba 提供了一条相对平滑的路径:它把模型接入、对话管理和工具调用封装成 Spring 开发者熟悉的 Bean 与注解范式。本文从工程地基出发,先解决一个容易被轻视但影响深远的问题------依赖版本对齐,再完整走通"定义工具 → 注册工具 → 由模型自主调用"的工具调用链路。

一、先别急着写代码:BOM 是 Spring AI Alibaba 的"版本锚点"

任何一个涉及模型接入的 Spring Boot 项目,依赖清单都不会只有一两项。Spring AI Alibaba 的运行时依赖至少涉及三层:Spring AI Alibaba 自身的 starter 与模块、Spring AI 的核心抽象(ChatModel、ChatClient、ToolCallback 等),以及底层的 Spring Boot 与 Spring Framework。如果每一项都在 <dependencies> 里手动写 <version>,传递性依赖的版本仲裁机制(如"最近者优先")很容易把同一个库的不同传递路径解析成不一致的版本,编译期或运行期的问题往往以难以定位的形式暴露出来。

spring-ai-alibaba-bom 解决的正是这个问题。它是一个只做版本声明的特殊 POM,通过 <scope>import</scope> 导入到 dependencyManagement 之后,你在 <dependencies> 中声明 spring-ai-alibaba-starter-dashscope 等具体依赖时,就无需再写版本号,Maven 会自动从 BOM 中取用经过官方验证、彼此兼容的版本。官方文档也明确推荐:新项目应当通过 BOM 统一管理 Spring AI Alibaba、Spring AI 和 Spring Boot 的版本,避免三者在版本演进中相互冲突。

一个最小可用的依赖配置如下:

XML 复制代码
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>com.alibaba.cloud.ai</groupId>
            <artifactId>spring-ai-alibaba-bom</artifactId>
            <version>1.0.0.2</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

<dependencies>
    <dependency>
        <groupId>com.alibaba.cloud.ai</groupId>
        <artifactId>spring-ai-alibaba-starter-dashscope</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>

spring-ai-alibaba-starter-dashscope 负责自动装配 DashScope 的 ChatModel 实现。你还需要在 application.yml 中提供 API Key,推荐通过环境变量注入,避免密钥硬编码:

XML 复制代码
spring:
  ai:
    dashscope:
      api-key: ${AI_DASHSCOPE_API_KEY}
      chat:
        options:
          model: qwen-plus

spring.ai.dashscope.api-key 是访问 DashScope 的凭证属性,spring.ai.dashscope.chat.options.model 则用于指定默认模型。

二、声明式工具:@Tool 把方法签名变成模型可读的 schema

工具调用的核心机制并不复杂:框架把带有 @Tool 注解的方法签名转换成一份 JSON Schema,连同工具描述一起发送给模型;模型在需要时返回 tool_calls,框架再将参数反序列化,通过反射调用你的方法,并把返回值送回对话上下文。模型只做"选工具、填参数"的决策,方法的实际执行始终在客户端应用中完成,这是一个关键的安全边界。

@Tool 注解的源码

java 复制代码
@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Tool {

	/**
	 * The name of the tool. If not provided, the method name will be used.
	 */
	String name() default "";

	/**
	 * The description of the tool. If not provided, the method name will be used.
	 */
	String description() default "";

	/**
	 * Whether the tool result should be returned directly or passed back to the model.
	 */
	boolean returnDirect() default false;

	/**
	 * The class to use to convert the tool call result to a String.
	 */
	Class<? extends ToolCallResultConverter> resultConverter() default DefaultToolCallResultConverter.class;

}

@Tool 注解的四个属性中,description 是最需要认真对待的:

属性 作用
name 工具名称;未提供时默认使用方法名,同一模型上下文中必须唯一
description 工具用途说明;模型据此判断何时 以及如何调用该工具
returnDirect 工具结果直接返回客户端,还是回传给模型继续推理
resultConverter 将工具返回值转换为字符串的回传转换器,默认使用 Jackson 序列化为 JSON

description 写不好,模型要么在应该调用工具时错过,要么在参数上"自由发挥"。一个可用的天气工具类如下:

java 复制代码
@Component
public class WeatherTools {

    @Tool(description = "根据城市名称查询实时天气,返回天气状况和温度")
    public String getCurrentWeather(
            @ToolParam(description = "城市名称,例如:北京、上海、深圳") String city) {
        return switch (city) {
            case "上海" -> "上海:暴雨,26℃,湿度 88%";
            case "北京" -> "北京:大雪,-3℃,北风 4 级";
            case "深圳" -> "深圳:晴,30℃,湿度 65%";
            default -> "暂不支持该城市的天气查询";
        };
    }
}

这里使用了 @ToolParam 为参数补充描述。Spring AI 默认将所有参数视为必需,@ToolParam(required = false) 或 @Nullable 可以将其标记为可选。对于参数含义不够直观的工具,参数级描述同样会进入 JSON Schema,直接影响模型填参的准确率。

需要留意的是,方法工具对参数类型有一定限制:Optional、异步类型、响应式类型(Mono、Flux)以及函数类型均不在支持范围内。保持参数为简单类型或可序列化的 POJO,是最稳妥的选择。

三、把工具交给 ChatClient:defaultTools 与 tools 的取舍

工具定义好了,模型并不会"自动发现"它。需要显式地将工具对象注册到 ChatClient。Spring AI 提供了两个层面的注册入口:

  • ChatClient.Builder.defaultTools(...) :构建期注册,对由该 Builder 创建的所有请求生效;

  • ChatClient.prompt().tools(...) :运行期注册,只对当次请求生效,且会完全覆盖默认工具。

如果工具是应用的固定能力集,defaultTools 更简洁;如果工具需要根据用户权限或场景动态变化,运行期 tools 更合适。

ChatClient 本身有两种初始化方式。第一种是构造器注入,利用 Spring 自动装配的 ChatModel(DashScope starter 生成的是 DashScopeChatModel,它实现了 ChatModel 接口,注入时会自然向上转型):

java 复制代码
@RestController
@RequestMapping("/chat")
public class ChatController {

    private final ChatClient chatClient;

    public ChatController(ChatModel chatModel, WeatherTools weatherTools) {
        this.chatClient = ChatClient.builder(chatModel)
                .defaultTools(weatherTools)
                .defaultSystem("你是一个乐于助人的助手。需要天气信息时请调用工具。")
                .build();
    }

    @GetMapping("/weather")
    public String weather(@RequestParam String message) {
        return chatClient.prompt()
                .user(message)
                .call()
                .content();
    }
}

第二种是声明 @Bean,将 ChatClient 作为 Spring 容器中的单例统一管理,适合在多处注入复用的场景:

java 复制代码
@Configuration
public class ChatClientConfig {

    @Bean
    public ChatClient chatClient(ChatModel chatModel, WeatherTools weatherTools) {
        return ChatClient.builder(chatModel)
                .defaultTools(weatherTools)
                .build();
    }
}

两种方式在行为上等价,选择取决于你希望将装配逻辑放在 Controller 内部还是独立配置类中。完成注册后,当用户发送"北京天气怎么样",模型会自行判断需要调用 getCurrentWeather,填入 city = "北京",框架执行方法并将结果回传,最终由模型整合成自然语言回复。整个过程中,Controller 代码不需要任何 if (message.contains("天气")) 式的硬编码分发。

四、几个容易踩的坑

第一,工具返回值尽量结构化。 默认转换器会用 Jackson 把对象序列化为 JSON。如果返回一段随意拼接的中文长句,模型仍能处理,但结构化 JSON 在多次工具调用的场景下更稳定,也便于后续扩展为 RAG 或真实数据源时保持接口一致。

第二,工具调用会增加 Token 消耗。 每一次工具交互都意味着额外的"模型决策 → 工具执行 → 结果回传"轮次,复杂任务下成本会叠加。工具描述应精确但不过度冗长,工具粒度也不宜过细。

第三,涉及写操作的工具必须做权限围栏。 查询类工具(读天气、读订单状态)风险可控;但一旦工具方法包含写库、发邮件、修改配置等副作用,模型的一个错误决策就可能产生真实影响。建议在工具方法内部或切面层统一实现确认机制、幂等控制和审计日志,而不是把安全性寄托在 System Prompt 的约束上。

第四,工具与 RAG 不是互斥的。 本文天气工具返回的是虚构数据,是为了演示链路的最小化。在真实系统中,工具方法内部完全可以接入向量检索或业务数据库------工具负责"何时调用",检索负责"拿什么数据",二者组合才是生产级 AI 应用的常态。

相关推荐
北京中科新远科技3 小时前
高速光链路健康检查实战:DOM、FEC、误码与压力回归
人工智能·数据挖掘·回归
火山引擎和TA的超级拍档们3 小时前
超级拍档・光影新生|火山引擎 AIGC 主题峰会回顾
人工智能
冬奇Lab3 小时前
一天一个开源项目(第245篇):Raven —— ‘Harness 的 Harness‘,把 Claude Code、Codex 等智能体编排起来并让它递归自我改进
人工智能·开源·资讯
工业视觉老A3 小时前
攻克牙膏朝向人工分拣痛点,AI 视觉检测保障包装工序稳定
人工智能·计算机视觉·视觉检测
smartpi_ai3 小时前
离线语音芯片怎么选:SU、CI、JX、JL 系列语音模块选型指南
人工智能·ci/cd·语音识别
Elastic 中国社区官方博客3 小时前
我们如何通过 70 多个拉取请求,将 OpenTelemetry Java agent 迁移到 invokedynamic
java·大数据·开发语言·elasticsearch·搜索引擎·全文检索·es
tyn1883 小时前
记录本地安装nacos服务的方法及异常
java·开发语言
火山引擎开发者社区3 小时前
DeepSeek-V4.1-Flash 新版本登陆火山方舟,效果无损“经济版”八折上线
人工智能
HA-ming3 小时前
PCSwitch 智能呼叫系统技术拆解:AI 机器人、意图识别与模块化流程如何串起业务自动化
人工智能·机器人·自动化·智能呼叫系统·智能呼叫中心·智能外呼·可视化模块化