跳转至

多模态、工具调用Tool Calling与MCP

约 5475 个字 896 行代码 预计阅读时间 29 分钟

多模态

多模态介绍

多模态性指模型同时理解和处理文本、图像、音频及其他数据格式等多源信息的能力。

人类通过多模态数据输入并融合处理知识。我们的学习方式和体验都是多模态的 — 不只有视觉、听觉或文本的单一感知。

机器学习往往专注于处理单一模态的专业模型。例如,我们开发音频模型用于文本转语音或语音转文本任务,开发计算机视觉模型用于目标检测和分类等任务。

然而,新一代多模态大语言模型正在兴起。这些模型能接受文本、图像、音频和视频等多种输入,并通过整合这些输出生成文本响应

想象一下人类是怎么认识世界的?

  • 看:你能看到风景、人脸、文字、颜色(视觉)
  • 听:你能听到说话声、音乐、鸟叫、汽车轰鸣(听觉)
  • 闻:你能闻到花香、饭菜(嗅觉)
  • 尝:你能尝出酸甜苦辣咸(味觉)
  • 摸:你能感觉到冷热、软硬、粗糙光滑(触觉)
  • 说/写:你能用语言文字描述你的想法(语言)

这些不同的方式(看、听、闻、尝、摸、说),每一种就是一种"模态"(Modality)

"多模态"(Multimodal)的意思就是:同时使用多种不同的"模态"来理解和表达信息

从人类到人工智能:

  1. 传统AI:器官"单一"的机器,例如:

    • 聊天机器人:只处理文字,看不懂图,听不懂语音
    • 语音助手:只能听你说话,回答或执行命令,看不懂图片什么意思
  2. 多模态AI:同时具备"看"、"读"、"听"、"说"(甚至更多)两种及以上的能力,并把这些信息融合起来理解,例如:

    • 看图说话:给他一张照片,他可以理解图片内容,并使用文字/语音描述出来
    • 图片结合问答:给他一张照片和一些关于图片的问题,他能够结合图片和问题进行回答

图像模型介绍

基本使用

图像模型(Image Model)是专注于处理与理解视觉数据的人工智能模型,是计算机视觉与多模态学习的核心。主要分为两类:

  1. 图像生成模型:根据文本、图像等条件输入,合成新的图像
  2. 图像理解模型:对输入图像进行分析,完成分类、检测、分割等认知任务

在Spring AI中也有对图像模型的支持,具体参考官方文档,下面针对百度的千帆模型给出一个示例和介绍

为了可以使用到百度千帆模型,需要在百度千帆申请API Key。接着创建项目引入依赖,此处依旧是参考Spring AI介绍与基础使用中的依赖,接着编写配置文件如下:

YAML
1
2
3
4
5
6
7
8
9
spring:
  ai:
    openai:
      api-key: xxx
      base-url: https://qianfan.baidubce.com
      image:
        options:
          model: "qwen-image"
        imagesPath: /v2/images/generations

Note

需要注意的是,imagesPath比较特殊,在官方文档上并没有具体说明,但是因为百度更新过模型请求地址的版本,导致OpenAI默认的/v1/images/generations地址是不能用的

接着编写请求接口:

Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
@RestController
@RequestMapping("/qianfan")
public class ChatController {
    @Autowired
    private OpenAiImageModel openAiImageModel;

    @RequestMapping("/image")
    public void image() {
        // 输入提示词以及设置图片参数
        ImageResponse imageResponse = openAiImageModel.call(
                new ImagePrompt("帮我生成一张小猫站着的图片",
                OpenAiImageOptions.builder()
                    .quality("hd")
                    .N(1)
                    .height(1024)
                    .width(1024).build()));
        // 获取图片URL
        System.out.println(imageResponse.getResult().getOutput().getUrl());
    }
}

运行程序请求上面的地址即可获取到生成成功后的图片URL,点击访问即可在浏览器中看到图片

API介绍

在上面的示例代码中,使用到了OpenAiImageModel类,这个类实际上是OpenAI公司根据Spring的接口自行实现的类,其实现的接口为ImageModel,这个接口的定义如下:

Java
1
2
3
4
@FunctionalInterface
public interface ImageModel extends Model<ImagePrompt, ImageResponse> {
    ImageResponse call(ImagePrompt request);
}

call()方法中,需要传递一个ImagePrompt对象,这个对象可以理解为给模型的请求参数,该类实现如下:

Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
public class ImagePrompt implements ModelRequest<List<ImageMessage>> {
    private final List<ImageMessage> messages;
    private ImageOptions imageModelOptions;

    public ImagePrompt(List<ImageMessage> messages) {
        this.messages = messages;
    }

    public ImagePrompt(List<ImageMessage> messages, ImageOptions imageModelOptions) {
        this.messages = messages;
        this.imageModelOptions = imageModelOptions;
    }

    public ImagePrompt(ImageMessage imageMessage, ImageOptions imageOptions) {
        this(Collections.singletonList(imageMessage), imageOptions);
    }

    public ImagePrompt(String instructions, ImageOptions imageOptions) {
        this(new ImageMessage(instructions), imageOptions);
    }

    public ImagePrompt(String instructions) {
        this(new ImageMessage(instructions), ImageOptionsBuilder.builder().build());
    }

    public List<ImageMessage> getInstructions() {
        return this.messages;
    }

    public ImageOptions getOptions() {
        return this.imageModelOptions;
    }

    public String toString() {
        String var10000 = String.valueOf(this.messages);
        return "NewImagePrompt{messages=" + var10000 + ", imageModelOptions=" + String.valueOf(this.imageModelOptions) + "}";
    }

    public boolean equals(Object o) {
        if (this == o) {
            return true;
        } else if (!(o instanceof ImagePrompt)) {
            return false;
        } else {
            ImagePrompt that = (ImagePrompt)o;
            return Objects.equals(this.messages, that.messages) && Objects.equals(this.imageModelOptions, that.imageModelOptions);
        }
    }

    public int hashCode() {
        return Objects.hash(new Object[]{this.messages, this.imageModelOptions});
    }
}

其中ImageMessage代表图片消息,里面包含了给图片设置的相关信息,在请求时会通过这个类告诉模型图片需要的参数,具体实现如下:

Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class ImageMessage {
    private String text;
    private Float weight;

    public ImageMessage(String text) {
        this.text = text;
    }

    public ImageMessage(String text, Float weight) {
        this.text = text;
        this.weight = weight;
    }

    public String getText() {
        return this.text;
    }

    public Float getWeight() {
        return this.weight;
    }

    public String toString() {
        return "ImageMessage{text='" + this.text + "', weight=" + this.weight + "}";
    }

    public boolean equals(Object o) {
        if (this == o) {
            return true;
        } else if (!(o instanceof ImageMessage)) {
            return false;
        } else {
            ImageMessage that = (ImageMessage)o;
            return Objects.equals(this.text, that.text) && Objects.equals(this.weight, that.weight);
        }
    }

    public int hashCode() {
        return Objects.hash(new Object[]{this.text, this.weight});
    }
}

其中text表示提示词文字,weight表示权重

接着是除图片基本消息以外的图片选项接口ImageOptions,具体实现如下:

Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
public interface ImageOptions extends ModelOptions {
    @Nullable
    Integer getN();

    @Nullable
    String getModel();

    @Nullable
    Integer getWidth();

    @Nullable
    Integer getHeight();

    @Nullable
    String getResponseFormat();

    @Nullable
    String getStyle();
}

对于OpenAI的模型来说,其有一个实现了ImageModel的类OpenAiImageOptions,其部分实现如下:

Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@JsonInclude(Include.NON_NULL)
public class OpenAiImageOptions implements ImageOptions {
    @JsonProperty("n")
    private Integer n;
    @JsonProperty("model")
    private String model;
    @JsonProperty("size_width")
    private Integer width;
    @JsonProperty("size_height")
    private Integer height;
    @JsonProperty("quality")
    private String quality;
    @JsonProperty("response_format")
    private String responseFormat;
    @JsonProperty("size")
    private String size;
    @JsonProperty("style")
    private String style;
    @JsonProperty("user")
    private String user;

    public static Builder builder() {
        return new Builder();
    }

    // ...
}

对于其中的参数,在官方文档中也有具体的介绍,此处给出中文翻译的参考版本:

属性 说明 默认值
spring.ai.openai.image.enabled(已移除且不再有效) 启用OpenAI图像模型 true
spring.ai.model.image 启用OpenAI图像模型 openai
spring.ai.openai.image.base-url 可选覆盖spring.ai.openai.base-url,用于提供图像专用URL -
spring.ai.openai.image.api-key 可选覆盖spring.ai.openai.api-key,用于提供图像专用API Key -
spring.ai.openai.image.organization-id 可选指定某次API请求使用的组织(organization) -
spring.ai.openai.image.project-id 可选指定某次API请求使用的项目(project) -
spring.ai.openai.image.options.n 要生成的图片数量,必须在1到10之间;对于dall-e-3仅支持n=1 -
spring.ai.openai.image.options.model 用于图像生成的模型 OpenAiImageApi.DEFAULT_IMAGE_MODEL
spring.ai.openai.image.options.quality 生成图像质量,hd会产生更精细细节和更高一致性,该参数仅dall-e-3支持 -
spring.ai.openai.image.options.response_format 返回生成图像的格式,必须是URLb64_json -
spring.ai.openai.image.options.size 生成图像尺寸:dall-e-2支持256x256512x5121024x1024dall-e-3支持1024x10241792x10241024x1792 -
spring.ai.openai.image.options.size_width 生成图像宽度,dall-e-2必须为2565121024 -
spring.ai.openai.image.options.size_height 生成图像高度,dall-e-2必须为2565121024 -
spring.ai.openai.image.options.style 生成图像风格,必须是vividnaturalvivid偏超写实和戏剧化,natural更自然不过度写实,该参数仅dall-e-3支持 -
spring.ai.openai.image.options.user 代表终端用户的唯一标识,可帮助OpenAI监控并检测滥用 -

其中,前缀可以在OpenAiImageAutoConfiguration类中找到:

Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
@AutoConfiguration(
    after = {RestClientAutoConfiguration.class, WebClientAutoConfiguration.class, SpringAiRetryAutoConfiguration.class}
)
@ConditionalOnClass({OpenAiApi.class})
@ConditionalOnProperty(
    name = {"spring.ai.model.image"},
    havingValue = "openai",
    matchIfMissing = true
)
@EnableConfigurationProperties({OpenAiConnectionProperties.class, OpenAiImageProperties.class}) 
public class OpenAiImageAutoConfiguration {
    @Bean
    @ConditionalOnMissingBean
    public OpenAiImageModel openAiImageModel(OpenAiConnectionProperties commonProperties, OpenAiImageProperties imageProperties, ObjectProvider<RestClient.Builder> restClientBuilderProvider, RetryTemplate retryTemplate, ResponseErrorHandler responseErrorHandler, ObjectProvider<ObservationRegistry> observationRegistry, ObjectProvider<ImageModelObservationConvention> observationConvention) {
        OpenAIAutoConfigurationUtil.ResolvedConnectionProperties resolved = OpenAIAutoConfigurationUtil.resolveConnectionProperties(commonProperties, imageProperties, "image");
        OpenAiImageApi openAiImageApi = OpenAiImageApi.builder().baseUrl(resolved.baseUrl()).apiKey(new SimpleApiKey(resolved.apiKey())).headers(resolved.headers()).imagesPath(imageProperties.getImagesPath()).restClientBuilder((RestClient.Builder)restClientBuilderProvider.getIfAvailable(RestClient::builder)).responseErrorHandler(responseErrorHandler).build();
        OpenAiImageModel imageModel = new OpenAiImageModel(openAiImageApi, imageProperties.getOptions(), retryTemplate, (ObservationRegistry)observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP));
        Objects.requireNonNull(imageModel);
        observationConvention.ifAvailable(imageModel::setObservationConvention);
        return imageModel;
    }
}

OpenAiImageProperties就可以找到CONFIG_PREFIX字段,这个字段就代表前缀:

Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
@ConfigurationProperties("spring.ai.openai.image")
public class OpenAiImageProperties extends OpenAiParentProperties {
    public static final String CONFIG_PREFIX = "spring.ai.openai.image";
    public static final String DEFAULT_IMAGES_PATH = "v1/images/generations";
    private String imagesPath = "v1/images/generations";
    public static final String DEFAULT_IMAGE_MODEL;
    @NestedConfigurationProperty
    private final OpenAiImageOptions options;

    // ...
}

然后进入OpenAiImageOptions找到需要配置的属性即可

了解完方法的参数,接下来介绍方法的返回值ImageResponseImageResponse封装AI模型的生成结果,具体实现如下:

Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public class ImageResponse implements ModelResponse<ImageGeneration> {
    private final ImageResponseMetadata imageResponseMetadata;
    private final List<ImageGeneration> imageGenerations;

    public ImageResponse(List<ImageGeneration> generations) {
        this(generations, new ImageResponseMetadata());
    }

    public ImageResponse(List<ImageGeneration> generations, ImageResponseMetadata imageResponseMetadata) {
        this.imageResponseMetadata = imageResponseMetadata;
        this.imageGenerations = List.copyOf(generations);
    }

    public List<ImageGeneration> getResults() {
        return this.imageGenerations;
    }

    public ImageGeneration getResult() {
        return CollectionUtils.isEmpty(this.imageGenerations) ? null : (ImageGeneration)this.imageGenerations.get(0);
    }

    public ImageResponseMetadata getMetadata() {
        return this.imageResponseMetadata;
    }

    public String toString() {
        String var10000 = String.valueOf(this.imageResponseMetadata);
        return "ImageResponse [imageResponseMetadata=" + var10000 + ", imageGenerations=" + String.valueOf(this.imageGenerations) + "]";
    }

    public boolean equals(Object o) {
        if (this == o) {
            return true;
        } else if (!(o instanceof ImageResponse)) {
            return false;
        } else {
            ImageResponse that = (ImageResponse)o;
            return Objects.equals(this.imageResponseMetadata, that.imageResponseMetadata) && Objects.equals(this.imageGenerations, that.imageGenerations);
        }
    }

    public int hashCode() {
        return Objects.hash(new Object[]{this.imageResponseMetadata, this.imageGenerations});
    }
}

其中的ImageGeneration类存储图片结果,具体实现如下:

Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class ImageGeneration implements ModelResult<Image> {
    private ImageGenerationMetadata imageGenerationMetadata;
    private Image image;

    public ImageGeneration(Image image) {
        this.image = image;
    }

    public ImageGeneration(Image image, ImageGenerationMetadata imageGenerationMetadata) {
        this.image = image;
        this.imageGenerationMetadata = imageGenerationMetadata;
    }

    public Image getOutput() {
        return this.image;
    }

    public ImageGenerationMetadata getMetadata() {
        return this.imageGenerationMetadata;
    }

    public String toString() {
        String var10000 = String.valueOf(this.imageGenerationMetadata);
        return "ImageGeneration{imageGenerationMetadata=" + var10000 + ", image=" + String.valueOf(this.image) + "}";
    }
}

应用启动时OpenAiImageAutoConfiguration先按spring.ai.openai.*spring.ai.openai.image.*装配出OpenAiImageModel和默认参数,接口被调用后业务代码构造ImagePrompt,其中ImageMessage承载提示词、OpenAiImageOptions承载如modelnqualitysizewidth/height等选项,随后执行openAiImageModel.call(...),框架把默认配置与本次请求参数合并并转换为供应商图片生成HTTP请求发送到images/generations端点(如OpenAI常见/v1/images/generations,示例里的千帆是/v2/images/generations),模型生成后返回包含urlb64_json的结果JSON,Spring AI再反序列化为统一的ImageResponse对象,你在业务层通过imageResponse.getResult().getOutput()取到首张图的输出(如URL)并返回给前端或调用方,同时错误处理与重试由底层的ResponseErrorHandlerRetryTemplate负责贯穿整个调用过程

Note

后续的其他多模态模型的API也可以按照类似上面的思路进行理解

Spring AI Alibaba介绍与基础使用

Spring AI Alibaba概述

随着生成式AI的快速发展,基于AI开发框架构建AI应用的诉求迅速增长,涌现出了包括LangChain、LlamaIndex等开发框架,它们为Python开发者提供了方便的API抽象。但这些开发框架对于国内习惯了Spring开发范式的Java开发者来说,并不十分友好和丝滑。因此,我们基于Spring AI发布并快速演进Spring AI Alibaba,通过提供一种方便的API抽象,帮助Java开发者简化AI应用的开发,一步迈入AI原生时代

Spring AI Alibaba开源项目基于Spring AI构建,是阿里云通义系列模型及服务在Java AI应用开发领域的API抽象与云原生基础设施集成方案,帮助开发者快速构建AI应用

Spring AI Alibaba作为开发AI应用程序的基础框架,定义了以下抽象概念与API,并提供了API与通义系列模型的适配:

  • 开发复杂AI应用的高阶抽象Fluent API — ChatClient
  • 提供多种大模型服务对接能力,包括主流开源与阿里云通义大模型服务(百炼)等
  • 支持的模型类型包括聊天、文生图、音频转录、语音合成等
  • 支持同步和流式API,在保持应用层API不变的情况下支持灵活切换底层模型服务,支持特定模型的定制化能力(参数传递)
  • 支持Structured Output,即将AI模型输出映射到POJOs
  • 支持向量数据库存储与检索
  • 支持函数调用Function Calling
  • 支持构建AI Agent所需要的工具调用和对话内存记忆能力
  • 支持RAG开发模式,包括离线文档处理如DocumentReader、Splitter、Embedding、VectorStore等,支持Retrieve检索

快速上手

Spring AI Alibaba实现了与阿里云通义模型的完整适配,接下来学习如何使用Spring AI Alibaba基于通义模型服务进行智能聊天

因为Spring AI Alibaba基于Spring Boot 3.x开发,因此本地JDK版本要求为17及以上

阿里云的模型服务平台百炼是一站式的大模型开发及应用构建平台。我们可以借助百炼平台,调用大模型,与大模型对话,实现内容创作、摘要生成等。当需要通过API或SDK方式调用大模型及应用时,需要获取一个合法的API-KEY并设置AI_DASHSCOPE_API_KEY环境变量

可参考:如何获取API Key_模型服务平台百炼(Model Studio)-阿里云帮助中心

接着引入Spring AI Alibaba的依赖:

XML
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>com.alibaba.cloud.ai</groupId>
            <artifactId>spring-ai-alibaba-bom</artifactId>
            <version>1.1.2.0</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-bom</artifactId>
            <version>1.1.2</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud.ai</groupId>
            <artifactId>spring-ai-alibaba-extensions-bom</artifactId>
            <version>1.1.2.1</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

<dependencies>
    <dependency>
        <groupId>com.alibaba.cloud.ai</groupId>
        <artifactId>spring-ai-alibaba-agent-framework</artifactId>
    </dependency>
    <dependency>
        <groupId>com.alibaba.cloud.ai</groupId>
        <artifactId>spring-ai-alibaba-starter-dashscope</artifactId>
    </dependency>
</dependencies>

需要注意,根据Spring AI Alibaba官方文档的版本说明,1.1.2.0版本要求Spring Boot版本最低为3.5.x

下面编写配置文件application.yml

YAML
1
2
3
4
spring:
  ai:
    dashscope:
      api-key: sk-xxx

编写基本的聊天示例:

Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
@RequestMapping("/ali")
@RestController
public class AliController {
    private static final String DEFAULT_PROMPT = "你是一个博学的智能聊天助手,请根据用户提问回答!";
    private final ChatClient dashScopeChatClient;

    public AliController(ChatClient.Builder chatClientBuilder) {
        this.dashScopeChatClient = chatClientBuilder
            .defaultSystem(DEFAULT_PROMPT)
            .defaultAdvisors(new SimpleLoggerAdvisor()) // 实现Logger的Advisor
            .build();
    }

    @GetMapping("/chat")
    public String chat(String message) {
        return dashScopeChatClient.prompt(message).call().content();
    }
}

ChatClient

Spring AI Alibaba是基于Spring AI进行构建的。所以Spring AI ChatClient具备的功能,Spring AI Alibaba大多也具备,如流式响应、返回实体类等

具体参考:Chat Client-阿里云Spring AI Alibaba官方官网

流式响应

Java
1
2
3
4
Flux<String> output = chatClient.prompt()
    .user("Tell me a joke")
    .stream()
    .content();

返回实体类

Java
1
2
3
4
5
6
7
record ActorFilms(String actor, List<String> movies) {
}

ActorFilms actorFilms = chatClient.prompt()
    .user("Generate the filmography for a random actor.")
    .call()
    .entity(ActorFilms.class);

角色预设

Java
1
2
3
4
5
6
7
8
@Configuration
class Config {
    @Bean
    ChatClient chatClient(ChatClient.Builder builder) {
        return builder.defaultSystem("You are a friendly chat bot that answers question in the voice of a Pirate")
            .build();
    }
}

在上面builder.defaultSystem()创建ChatClient的时候,还可以选择使用模板({参数名称}),有机会在每次调用前修改请求参数

Java
1
2
3
4
5
6
7
8
@Configuration
public class ChatClientConfiguration {
    @Bean
    ChatClient chatClient(ChatClient.Builder builder) {
        return builder.defaultSystem("请你给我回答问题时,前面带一个{word}")
            .build();
    }
}

测试代码如下:

Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@Autowired
private ChatClient dashScopeChatClient;

@GetMapping("/chat")
public String chat(String message, String words) {
    return dashScopeChatClient.prompt(message)
        .system(sp -> sp.param("word", words)) // 传递参数
        .call()
        .content();
}

其他默认设置

除了defaultSystem之外,还可以在ChatClient.Builder上指定其他默认提示。

  • defaultOptions(ChatOptions chatOptions):传入ChatOptions类中定义的可移植选项或特定于模型实现的如DashScopeChatOptions选项。有关特定于模型的ChatOptions实现的更多信息,请参阅JavaDocs
  • defaultFunction(String name, String description, java.util.function.Function<I, O> function)name用于在文本中引用该函数,description解释该函数的用途并帮助AI模型选择正确的函数以获得准确的响应,参数function是模型将在必要时执行的Java函数实例
  • defaultFunctions(String... functionNames):应用程序上下文中定义的java.util.Function的Bean名称
  • defaultUser(String text)defaultUser(Resource text)defaultUser(Consumer<UserSpec> userSpecConsumer)这些方法允许您定义用户消息输入,Consumer<UserSpec>允许您使用lambda指定用户消息输入和任何默认参数
  • defaultAdvisors(RequestResponseAdvisor... advisor):Advisors允许修改用于创建Prompt的数据,QuestionAnswerAdvisor实现通过在Prompt中附加与文本相关的上下文信息来实现Retrieval Augmented Generation模式
  • defaultAdvisors(Consumer<AdvisorSpec> advisorSpecConsumer):此方法允许您定义一个Consumer并使用AdvisorSpec配置多个Advisor,Advisor可以修改用于创建Prompt的最终数据,Consumer<AdvisorSpec>允许您指定lambda来添加Advisor例如QuestionAnswerAdvisor

可以在运行时使用ChatClient提供的不带default前缀的相应方法覆盖这些默认值:

  • options(ChatOptions chatOptions)
  • function(String name, String description, java.util.function.Function<I, O> function)
  • functions(String... functionNames)
  • user(String text)user(Resource text)user(Consumer<UserSpec> userSpecConsumer)
  • advisors(RequestResponseAdvisor... advisor)
  • advisors(Consumer<AdvisorSpec> advisorSpecConsumer)

多模态支持

下使用阿里云百炼平台中提供的模型进行演示,也可以参考Spring AI Alibaba的官方示例

图像理解和图像生成

Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
@GetMapping("/image")
public String image(String prompt) throws Exception {
    String url = "https://dashscope.oss-cn-beijing.aliyuncs.com/images/dog_and_girl.jpeg";
    List<Media> mediaList = List.of(new Media(MimeTypeUtils.IMAGE_PNG, new URI(url).toURL().toURI()));
    UserMessage message = UserMessage.builder().text(prompt).media(mediaList).build();
    ChatResponse response = client
        .prompt(new Prompt(message))
        .call()
        .chatResponse();
    return response.getResult().getOutput().getText();
}
Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
@Autowired
private DashScopeImageModel imageModel;

@RequestMapping("/gen1")
public void gen1() {
    // 使用默认模型:wanx-v1
    ImageResponse imageResponse = imageModel.call(new ImagePrompt("帮我生成一张小猫的图片"));
    System.out.println(imageResponse.getResult().getOutput().getUrl());
}

@RequestMapping("/gen2")
public void gen2() {
    // 切换模型为wan2.2-t2i-flash
    DashScopeImageOptions imageOptions = DashScopeImageOptions.builder()
            .model("wan2.2-t2i-flash")
            .build();
    ImageResponse imageResponse = imageModel.call(new ImagePrompt("帮我生成一张小猫的图片", imageOptions));
    System.out.println(imageResponse.getResult().getOutput().getUrl());
}

除了在代码中通过选项类设置模型以外,还可以在配置文件中进行配置,先在DashScopeImageAutoConfiguration找到配置类DashScopeImageProperties

Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@AutoConfiguration(
    after = {RestClientAutoConfiguration.class, WebClientAutoConfiguration.class, SpringAiRetryAutoConfiguration.class}
)
@ConditionalOnClass({DashScopeImageApi.class})
@ConditionalOnDashScopeEnabled
@ConditionalOnProperty(
    name = {"spring.ai.model.image"},
    havingValue = "dashscope",
    matchIfMissing = true
)
@EnableConfigurationProperties({DashScopeConnectionProperties.class, DashScopeImageProperties.class})
@ImportAutoConfiguration(
    classes = {SpringAiRetryAutoConfiguration.class, RestClientAutoConfiguration.class, WebClientAutoConfiguration.class}
)
public class DashScopeImageAutoConfiguration {
    @Bean
    @ConditionalOnMissingBean
    public DashScopeImageModel dashScopeImageModel(DashScopeConnectionProperties commonProperties, DashScopeImageProperties imageProperties, ObjectProvider<RestClient.Builder> restClientBuilderProvider, RetryTemplate retryTemplate, ResponseErrorHandler responseErrorHandler, ObjectProvider<ObservationRegistry> observationRegistry, ObjectProvider<ImageModelObservationConvention> observationConvention) {
        ResolvedConnectionProperties resolved = DashScopeConnectionUtils.resolveConnectionProperties(commonProperties, imageProperties, "image");
        DashScopeImageApi dashScopeImageApi = DashScopeImageApi.builder().apiKey(resolved.apiKey()).baseUrl(resolved.baseUrl()).imagesPath(imageProperties.getImagesPath()).queryTaskPath(imageProperties.getQueryTaskPath()).workSpaceId(resolved.workspaceId()).restClientBuilder((RestClient.Builder)restClientBuilderProvider.getIfAvailable(RestClient::builder)).responseErrorHandler(responseErrorHandler).build();
        DashScopeImageModel dashScopeImageModel = DashScopeImageModel.builder().dashScopeApi(dashScopeImageApi).defaultOptions(imageProperties.getOptions()).retryTemplate(retryTemplate).observationRegistry((ObservationRegistry)observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP)).build();
        Objects.requireNonNull(dashScopeImageModel);
        observationConvention.ifAvailable(dashScopeImageModel::setObservationConvention);
        return dashScopeImageModel;
    }
}

DashScopeImageProperties就可以找到CONFIG_PREFIX字段,这个字段就代表前缀,然后进入DashScopeImageOptions找到需要配置的属性即可:

Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
@JsonInclude(Include.NON_NULL)
public class DashScopeImageOptions implements ImageOptions {
    @JsonProperty("model")
    private String model;
    @JsonProperty("n")
    private Integer n;
    @JsonProperty("width")
    private Integer width;
    @JsonProperty("height")
    private Integer height;
    @JsonProperty("size")
    private String size;
    @JsonProperty("style")
    private String style;
    @JsonProperty("seed")
    private Integer seed;
    @JsonProperty("ref_img")
    private String refImg;
    @JsonProperty("ref_strength")
    private Float refStrength;
    @JsonProperty("response_format")
    private String responseFormat;
    @JsonProperty("ref_mode")
    private String refMode;
    @JsonProperty("negative_prompt")
    private String negativePrompt;
    @JsonProperty("prompt_extend")
    private Boolean promptExtend;
    @JsonProperty("watermark")
    private Boolean watermark;
    @JsonProperty("function")
    private String function;
    @JsonProperty("base_image_url")
    private String baseImageUrl;
    @JsonProperty("mask_image_url")
    private String maskImageUrl;
    @JsonProperty("sketch_image_url")
    private String sketchImageUrl;
    @JsonProperty("sketch_weight")
    private Integer sketchWeight;
    @JsonProperty("sketch_extraction")
    private Boolean sketchExtraction;
    @JsonProperty("sketch_color")
    private Integer[][] sketchColor;
    @JsonProperty("mask_color")
    private Integer[][] maskColor;
    @JsonProperty("max_images")
    private Integer maxImages;
    @JsonProperty("enable_interleave")
    private Boolean enableInterleave;

    // ...
}

其中@JsonProperty中的值即为对应官方文档的配置

需要注意的是,阿里云文生图的模型有很多,模型对应的请求参数也不同,需要在请求体中具体查看

语音合成和语音识别

Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@Autowired
private DashScopeAudioSpeechModel audioSpeechModel;

private final String TEXT = """
        今天是一个适合安静下来的下午,窗外的光线不算刺眼,房间里也没有太多杂音。我坐在桌前,打开电脑,准备测试一段中文 AI 语音朗读效果。
        """;

@RequestMapping("/gen1")
public void gen1() throws IOException {
    // 使用默认模型
    TextToSpeechResponse speechResponse = audioSpeechModel.call(new TextToSpeechPrompt(TEXT));
    File file = new File(System.getProperty("user.dir") + "/output.mp3");
    try (FileOutputStream fos = new FileOutputStream(file)) {
        byte[] output = speechResponse.getResult().getOutput();
        fos.write(output);
    }
    catch (IOException e) {
        throw new IOException(e.getMessage());
    }
}

@RequestMapping("/gen2")
public void gen2() throws IOException {
    // 使用模型cosyvoice-v3-flash,音色为猴哥longhouge_v3,文件格式为wav
    DashScopeAudioSpeechOptions speechOptions = DashScopeAudioSpeechOptions.builder()
            .model("cosyvoice-v3-flash")
            .responseFormat(DashScopeAudioSpeechApi.ResponseFormat.WAV)
            .voice("longhouge_v3")
            .build();
    TextToSpeechResponse speechResponse = audioSpeechModel.call(new TextToSpeechPrompt(TEXT, speechOptions));
    File file = new File(System.getProperty("user.dir") + "/output.wav");
    try (FileOutputStream fos = new FileOutputStream(file)) {
        byte[] output = speechResponse.getResult().getOutput();
        fos.write(output);
    }
    catch (IOException e) {
        throw new IOException(e.getMessage());
    }
}

除了在代码中通过选项类设置模型以外,还可以在配置文件中进行配置,先在DashScopeAutoAutoConfiguration找到配置类DashScopeAudioSpeechSynthesisProperties

Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@AutoConfiguration(
    after = {RestClientAutoConfiguration.class, WebClientAutoConfiguration.class, SpringAiRetryAutoConfiguration.class}
)
@ConditionalOnClass({DashScopeAudioSpeechApi.class})
@ConditionalOnDashScopeEnabled
@ConditionalOnProperty(
    name = {"spring.ai.model.audio.speech"},
    havingValue = "dashscope",
    matchIfMissing = true
)
@EnableConfigurationProperties({DashScopeConnectionProperties.class, DashScopeAudioSpeechSynthesisProperties.class})
@ImportAutoConfiguration(
    classes = {SpringAiRetryAutoConfiguration.class, RestClientAutoConfiguration.class, WebClientAutoConfiguration.class}
)
public class DashScopeAudioSpeechAutoConfiguration {
    @Bean
    @ConditionalOnMissingBean
    public DashScopeAudioSpeechModel dashScopeSpeechSynthesisModel(DashScopeConnectionProperties commonProperties, DashScopeAudioSpeechSynthesisProperties audioSpeechProperties, RetryTemplate retryTemplate) {
        DashScopeAudioSpeechApi dashScopeSpeechSynthesisApi = this.audioSpeechApi(commonProperties, audioSpeechProperties);
        return DashScopeAudioSpeechModel.builder().audioSpeechApi(dashScopeSpeechSynthesisApi).defaultOptions(audioSpeechProperties.getOptions()).retryTemplate(retryTemplate).build();
    }

    private DashScopeAudioSpeechApi audioSpeechApi(DashScopeConnectionProperties commonProperties, DashScopeAudioSpeechSynthesisProperties audioSpeechProperties) {
        ResolvedConnectionProperties resolved = DashScopeConnectionUtils.resolveConnectionProperties(commonProperties, audioSpeechProperties, "audio.synthesis");
        return new DashScopeAudioSpeechApi(resolved.apiKey(), resolved.workspaceId());
    }
}

接着在DashScopeAudioSpeechSynthesisProperties找到CONFIG_PREFIX即为配置前缀,在DashScopeAudioSpeechOptions中可以查看支持配置的字段:

Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@JsonInclude(Include.NON_NULL)
public class DashScopeAudioSpeechOptions implements TextToSpeechOptions {
    @JsonProperty("model")
    private String model;
    @JsonProperty("text")
    private String text;
    @JsonProperty("voice")
    private String voice;
    @JsonProperty("request_text_type")
    private DashScopeAudioSpeechApi.RequestTextType requestTextType;
    @JsonProperty("sample_rate")
    private Integer sampleRate;
    @JsonProperty("volume")
    private Integer volume;
    @JsonProperty("speed")
    private Double speed;
    @JsonProperty("pitch")
    private Double pitch;
    @JsonProperty("enable_word_timestamp")
    private Boolean enableWordTimestamp;
    @JsonProperty("enable_phoneme_timestamp")
    private Boolean enablePhonemeTimestamp;
    @JsonProperty("enable_ssml")
    private Boolean enableSsml;
    @JsonProperty("bit_rate")
    private Integer bitRate;
    @JsonProperty("seed")
    private Integer seed;
    @JsonProperty("language_hints")
    private List<String> languageHints;
    @JsonProperty("instruction")
    private String instruction;
    @JsonProperty("response_format")
    private DashScopeAudioSpeechApi.ResponseFormat responseFormat;

    // ...
}

其中@JsonProperty中的值即为对应官方文档的配置

Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Autowired
private DashScopeAudioTranscriptionModel transcriptionModel;
private static final String PARAFORMER_TEST_AUDIO_URL = "https://dashscope.oss-cn-beijing.aliyuncs.com/samples/audio/paraformer/hello_world_female2.wav";

@RequestMapping("/gen3")
public void gen3() throws MalformedURLException {
    // 使用URL资源
    Resource audioFile = new UrlResource(PARAFORMER_TEST_AUDIO_URL);
    AudioTranscriptionPrompt prompt = new AudioTranscriptionPrompt(audioFile);
    AudioTranscriptionResponse response = transcriptionModel.call(prompt);
    System.out.println(response.getResult().getOutput());
}

@RequestMapping("/gen4")
public void gen4() throws MalformedURLException {
    DashScopeAudioTranscriptionOptions options = DashScopeAudioTranscriptionOptions.builder()
            .model("paraformer-v1")
            .format(DashScopeAudioTranscriptionApi.AudioFormat.WAV)
            .build();

    Resource audioFile = new UrlResource(PARAFORMER_TEST_AUDIO_URL);
    AudioTranscriptionPrompt prompt = new AudioTranscriptionPrompt(audioFile, options);
    AudioTranscriptionResponse response = transcriptionModel.call(prompt);
    System.out.println(response.getResult().getOutput());
}

关于选项等的设置,参考上面介绍的思路,此处不再介绍,除此之外还可以参考官方文档

视频生成

Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@Autowired
private DashScopeVideoModel dashScopeVideoModel;

private final String PROMPT_TEXT = "一只橘猫坐在窗台上晒太阳,微风吹动窗帘,镜头缓慢推进,电影感";

@RequestMapping("/gen1")
public void gen1() {
    DashScopeVideoOptions videoOptions = DashScopeVideoOptions.builder()
            .model("wan2.6-t2v")
            .input(
                    DashScopeVideoOptions.InputOptions.builder()
                            .prompt(PROMPT_TEXT)
                            .build()
            )
            .parameters(
                    DashScopeVideoOptions.ParametersOptions.builder()
                            .size("1280*720")
                            .duration(5)
                            .promptExtend(true)
                            .seed(12345L)
                            .build()
            )
            .build();

    VideoPrompt videoPrompt = new VideoPrompt(
            List.of(new VideoMessage(PROMPT_TEXT)),
            videoOptions
    );

    VideoResponse response = dashScopeVideoModel.call(videoPrompt);

    System.out.println(response.getResult().getOutput().videoUrl());;
}

关于选项等的设置,参考上面介绍的思路,此处不再介绍,除此之外还可以参考官方文档

工具调用Tool Calling

介绍

随着大语言模型(LLM)能力的飞速发展,我们不再满足于仅仅让它们生成文本或回答问题。我们期望它们能成为真正的智能助手,能够与外部世界交互,执行具体任务,比如查询数据库、发送邮件或分析数据。为了解决这一挑战,工具调用(Tool Calling)应运而生

Tool Calling(工具调用),是AI应用程序中的一种常见模式,允许大语言模型(LLM)根据用户请求,智能地选择、调用外部工具(如函数、API、服务)并获取结果的技术流程,从而增强其功能

在早期更流行叫Function Calling(函数调用),其是指LLM请求调用一个开发者预定义的函数 (Function),这里的“函数”就是代码中的一个方法。而Tool Calling是一个更通用、更广泛的概念,不仅包含了Function Calling,还涵盖了调用其他类型的工具,所以后面发生了更名

文档与基础使用

工具调用可以参考官方文档以及官方文档(翻译版)。下面给出工具调用的示例

工具调用主要分为声明式和编程式,声明式就是通过注解修饰工具方法和对应的参数,编程式就是编写代码提供工具方法

声明式工具调用

声明式工具调用主要用到两个注解:

  1. @Tool修饰方法,表示指定方法为一个工具方法
  2. @ToolParam修饰方法的参数

具体介绍参考文档

使用@Tool时,需要注意,建议显式指定description。例如下面的根据地区获取该地区的日期和时间工具方法示例:

Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
@Component
public class DateTimeTool {
    @Tool(description = "根据地区获取该地区的日期和时间")
    public String getCurrentDateAndTime(@ToolParam(description = "地区名称") String place) {
        return switch (place) {
            case "北京" -> "2026-04-02 19:00";
            case "纽约" -> "2026-04-02 7:00";
            default -> "错误地区";
        };
    }
}

接着,需要为AI添加工具,使用tools()方法:

Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@Autowired
private DateTimeTool dateTimeTool;

@RequestMapping("/gen2")
public String gen2(String message) {
    return chatClient.prompt()
            .user(message)
            .tools(dateTimeTool)
            .call().content();
}

对比有无工具时,AI的回复:

Text Only
1
当前北京时间为 **2024-07-24 16:30(星期三)**。 中国使用东八区时间(UTC+8),没有夏令时。如果你在其他时区,可以据此推算时差。
Text Only
1
现在北京的时间是 **2026年4月2日 晚上7点**。

除了上面在运行时指定工具以外,还可以指定默认工具,使用defaultTools()方法即可,例如:

Java
1
2
3
4
5
6
7
8
9
@Configuration
public class AiConfig {
    @Bean
    public ChatClient chatClient(ChatClient.Builder builder) {
        return builder
                .defaultTools(new DateTimeTool())
                .build();
    }
}

但是需要注意的是,相同的工具方法只能定义一次,否则在调用时会报错,例如:

Text Only
1
2026-04-02T19:20:46.506+08:00 ERROR 48972 --- [spring-ai-tool-calling] [nio-8080-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: java.lang.IllegalStateException: Multiple tools with the same name (getCurrentDateAndTime) found in ToolCallingChatOptions] with root cause

编程式工具调用

对于编程式来说,需要做的工作会比声明式多一小部分,就是通过反射获取到工具方法并设置到模型的工具回调中,还是以上面的工具方法为例,去掉其@Tool@ToolParam注解:

Java
1
2
3
4
5
6
7
8
// 编程式
public String getCurrentDateAndTime(String place) {
    return switch (place) {
        case "北京" -> "2026-04-02 19:00";
        case "纽约" -> "2026-04-02 7:00";
        default -> "错误地区";
    };
}

接着回到AI调用的方法中,为了能拿到方法,首先需要通过反射获取到方法对象:

Java
1
2
3
4
5
6
import org.springframework.util.ReflectionUtils;

Method getCurrentDateAndTimeMethod =
        ReflectionUtils.findMethod(DateTimeTool.class, "getCurrentDateAndTime");
// 断言方法对象不能为空
Assert.notNull(getCurrentDateAndTimeMethod, "指定方法不存在");

然后该方法为工具方法:

Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 定义工具回调对象
ToolCallback toolCallback = MethodToolCallback.builder()
    .toolDefinition(ToolDefinition.builder() // 定义工具信息
            .name("getCurrentDateAndTime") // 工具方法名称
            .description("根据地区获取该地区的日期和时间") // 工具方法描述
            // 工具参数约束
            .inputSchema("""
                {
                "type": "object",
                "properties": {
                    "place": {
                    "type": "string",
                    "description": "地区名称"
                    }
                },
                "required": ["place"]
                }
            """)
            .build())
    .toolMethod(getCurrentDateAndTimeMethod) // 注册为工具方法
    .toolObject(dateTimeTool) // 非静态方法时需要工具类对象
    .build();

Note

上面的代码中,使用到了JSON Schema进行参数约束

最后,同样在模型中添加工具方法回调:

Java
1
2
3
4
chatClient.prompt()
    .user(message)
    .toolCallbacks(toolCallback) // 工具方法回调
    .call().content();

需要注意的是,在官方文档的描述中,使用的是ToolDefinition.Builder,此时必须要工具方法指定name,否则会报错,如果是官方文档的代码示例ToolDefinitions.BuilderToolDefinitions),则因为builder()中可以携带方法对象,所以可以省略name,即:

Java
1
2
3
4
5
ToolCallback toolCallback = MethodToolCallback.builder()
        .toolDefinition(ToolDefinitions.builder(getCurrentDateAndTimeMethod)
        // ...
        .build())
        // ...

如果使用的是ToolDefinitions,可以不手写Json Schema,默认会生成JSON Schema,此时只要方法有参数就是必传,如果想变成选传,可以在定义方法的参数中使用@Nullable,表示可以为空,例如:

Java
1
2
3
public String getCurrentDateAndTime(@Nullable String place) {
    // ...
}

工具上下文ToolContext

上面工具方法的参数都是由AI从用户的提示词中获取,但是有的时候部分数据从提示词无法获取,例如用户ID,此时可以在调用工具之前,将用户ID通过上下文传递给AI。例如下面的工具方法代码:

Java
1
2
3
4
@Tool(description = "获取当前用户信息")
public String getCurrentUserData(ToolContext toolContext) {
    return "当前用户的ID信息为:" + toolContext.getContext().get("userId");
}

在添加工具方法给AI时还需要给上下文设置信息:

Java
1
2
3
4
5
6
7
8
@RequestMapping("/gen5")
public String gen5(String message) {
    return chatClient.prompt()
            .user(message)
            .tools(dateTimeTool)
            .toolContext(Map.of("userId", "1")) // 传递参数给上下文
            .call().content();
}

可以得到类似下面的回复:

Text Only
1
根据系统返回的信息,当前用户的ID是 **1**。 这是系统识别到的用户标识信息。如果您需要了解更多关于该用户的详细信息,可能需要联系系统管理员或查看其他相关系统功能。

结果直接返回

有的时候,只需要AI自动调用工具并将结果直接返回给用户,而不需要将结果经过AI二次加工,例如一些结构化数据。在上面的示例中,默认情况下都是会将数据给AI进行二次加工

对于声明式来说,可以在@Tool注解中添加returnDirect,将其值设置为true即可将工具方法执行的结果直接返回给用户

对于编程式来说,需要创建ToolMetaData对象,在其中设置returnDirect

例如下面的代码:

Java
1
2
3
4
@Tool(description = "根据地区获取该地区的日期和时间", returnDirect = true)
public String getCurrentDateAndTime(String place) {
    // ...
}
Java
1
2
3
4
5
6
7
ToolCallback toolCallback = MethodToolCallback.builder()
.toolDefinition(ToolDefinitions.builder(getCurrentDateAndTimeMethod)
        // ...
.toolMetadata(ToolMetadata.builder()
        .returnDirect(true)
        .build())
// ...

工具调用流程介绍

模型上下文协议(MCP)

介绍

MCP是“让AI应用接入外部世界的标准协议”,工具调用是其中一种非常重要的能力,但两者不是一回事

MCP全称Model Context Protocol。官方把它定义为一种开放标准,用来把AI应用连接到外部系统,比如本地文件、数据库、搜索、业务API、工作流等。它的核心不是某一个模型能力,而是一套“应用和外部能力如何对接”的统一协议。可以把它理解成:MCP = AI世界里的USB-C,不是某个具体设备,而是“统一插口标准”。

从架构上看,MCP是host -> client -> server模式:

  • host是ChatGPT、Claude、IDE这类AI应用
  • client负责和某个MCP server建立连接
  • server暴露能力,比如toolsresourcesprompts

Java MCP架构

参考官方文档

Spring AI应用使用MCP

Spring AI应用使用MCP,本质就是Spring AI应用作为MCP Client,首先引入下面的依赖:

XML
1
2
3
4
5
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-mcp-client</artifactId>
    <version>1.1.2</version>
</dependency>

接着,编写配置文件内容:

YAML
1
2
3
4
5
6
spring:
  ai:
    mcp:
      client:
        stdio:
          servers-configuration: classpath:/mcp/mcp-servers.json # 存放MCP Json配置的文件

接着,在项目的resources目录下创建一个mcp目录,并在该目录中新增mcp-servers.json文件,以chrome dev tools mcp为例:

JSON
1
2
3
4
5
6
7
8
{
    "mcpServers": {
        "chrome-devtools": {
        "command": "npx",
        "args": ["-y", "chrome-devtools-mcp@latest"]
        }
    }
}

Note

除了上面的方式配置MCP以外,还可以在配置文件中直接编写MCP需要使用到的命令、配置等,具体参考官方文档

需要注意的是,在Windows下,像npxnpm这些命令本质都需要在cmd中执行,而不是原生的可执行文件,所以Java的ProcessBuilder无法直接执行,所以上面的MCP是无法被Java程序正常执行的,有两种执行方式:

JSON
1
2
3
4
5
6
7
8
{
    "mcpServers": {
        "chrome-devtools": {
        "command": "npx.cmd", // 写全命令,包括后缀,以确保是一个可执行程序而不是命令
        "args": ["-y", "chrome-devtools-mcp@latest"]
        }
    }
}
JSON
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
{
    "mcpServers": {
        "filesystem": {
        "command": "cmd.exe", // 执行命令行
        // 执行命令
        "args": [
            "/c",
            "npx",
            "-y",
            "chrome-devtools-mcp@latest",
        ]
        }
    }
}

接着,为AI添加工具:

Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@Configuration
public class AiConfig {
    @Bean
    public ChatClient chatClient(ChatClient.Builder builder,
                                 ToolCallbackProvider toolCallbackProvider) {
        return builder
                .defaultToolCallbacks(toolCallbackProvider)
                .build();
    }
}

使用Java创建MCP工具

MCP客户端和服务端通信方式有两种:

  1. stdio:适用于本地进程通信。客户端启动MCP服务端子进程,并通过stdin/stdout交换JSON-RPC消息,优点是简单、高效、无网络开销
  2. sse:适用于远程通信。客户端通过HTTP与MCP服务端交互,支持流式返回,也可结合SSE实现服务端消息推送,是当前MCP官方规范推荐的远程传输方式

下面根据这两种通信方式进行MCP工具创建,首先引入MCP Server的依赖

对于stdio引入下面的依赖:

XML
1
2
3
4
5
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-mcp-server</artifactId>
    <version>1.1.2</version>
</dependency>

对于sse引入下面的依赖:

XML
1
2
3
4
5
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
    <version>1.1.2</version>
</dependency>
sse其他依赖

sse除了上面的依赖,还可以使用下面的依赖:

XML
1
2
3
4
5
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-mcp-server-webflux</artifactId>
    <version>1.1.2</version>
</dependency>

但是就会导致MCP端点(例如/sse)无法正常工作,这是因为项目中同时存在org.springframework.web.servlet.DispatcherServletorg.springframework.web.reactive.DispatcherHandler,Spring Boot默认优先考虑前者

可以通过设置spring.main.web-application-type=reactive来解决:

YAML
1
2
3
4
5
spring:
  ai:
    # ...
  main:
    web-application-type: reactive

接着编写配置

对于stdio来说,配置如下:

YAML
1
2
3
4
5
6
7
8
9
spring:
  ai:
    mcp:
      server:
        name: user-info
        version: 0.0.1
    main:
      web-application-type: none
      banner-mode: off

对于sse来说,配置如下:

YAML
1
2
3
4
5
6
7
8
server:
  port: 8088 # 任意端口
spring:
  ai:
    mcp:
      server:
        name: user-info
        version: 0.0.1

定义一个工具,为了演示,本次定义一个获取用户信息的工具,数据自行Mock:

Java
1
2
3
4
5
6
7
8
@Data
@AllArgsConstructor
public class UserInfo {
    private String name;
    private int age;
    private String sex;
    private String address;
}
Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
@Service
public class UserService {
    static Map<String, UserInfo> map = new HashMap<String, UserInfo>();

    static {
        map.put("zhangsan",new UserInfo("zhangsan", 18, "男", "北京"));
        map.put("lisi",new UserInfo("lisi", 19, "男", "上海"));
        map.put("wangwu",new UserInfo("wangwu", 20, "女", "广州"));
        map.put("zhaoliu",new UserInfo("zhaoliu", 21, "女", "深圳"));
        map.put("sunqi",new UserInfo("sunqi", 22, "男", "西安"));
        map.put("zhouba",new UserInfo("zhouba", 23, "女", "天津"));
    }

    @Tool(description = "根据用户姓名查找用户信息")
    public String getUserInfo(String username) {
        if (map.containsKey(username)) {
            return map.get(username).toString();
        }
        return "未获取到用户信息";
    }
}

最后,对于stdio的来说,暴露工具并打包:

Java
1
2
3
4
5
6
7
8
9
@Configuration
public class ToolConfig {
    @Bean
    public ToolCallbackProvider getUserInfo(UserService userService){
        return MethodToolCallbackProvider.builder()
                .toolObjects(userService)
                .build();
    }
}

打包完成后,编写MCP JSON供其他编程工具或者AI应用使用:

JSON
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
{
    "mcpServers": {
        "user-info": {
            "command": "java",
            "args": [
                "-Dspring.ai.mcp.server.stdio=true",
                "-Dlogging.pattern.console=",
                "-jar",
                "D:\\xxx" // jar包所在路径
            ]
        }
    }
}

对于sse,首先访问http://127.0.0.1:8088/sse,确保sse端点可用,接着编写MCP JSON配置:

JSON
1
2
3
4
5
6
7
{
    "mcpServers": {
        "user-info": {
            "url": "http://127.0.0.1:8088/sse"
        }
    }
}

对于Spring AI应用,配置如下:

YAML
1
2
3
4
5
6
7
8
spring:
  ai:
    mcp: 
      client: 
        sse:
          connections:
            user-info:
              url: http://127.0.0.1:8088/sse

MCP推荐

参考常见MCP工具配置