大模型开发 - 深入掌握模型上下文协议_通过 MCP 暴露提示词(SyncPromptRegistration )

在这里插入图片描述

概述

本篇博文将深入探讨如何在 Spring Boot 的服务端和客户端应用中,利用 Spring AI 对 MCP (模型上下文协议) 的支持。我们将梳理如何在服务端提供 AI 工具 (Tools) 和提示词 (Prompts),并在客户端的 Spring AI 应用中自动发现和使用它们。

模型上下文协议(Model Context Protocol, MCP)是管理与 AI 模型进行上下文交互的一项标准。它提供了一种标准化的方式,将 AI 模型与外部数据源和工具连接起来,有助于在大型语言模型(LLM)之上构建复杂的工作流。Spring AI MCP 是对 MCP Java SDK 的扩展,并提供了相应的 Spring Boot Starter 用于客户端和服务端。其中,MCP 客户端负责建立和管理与 MCP 服务器的连接。

  1. Spring AI 与聊天模型入门
  2. Spring AI 函数调用入门
  3. 在 Spring AI 中使用 RAG 和向量存储
  4. Spring AI 的多模态与图像生成
  5. Spring AI 整合 Ollama 本地模型
  6. Spring AI 的工具调用 (Tool Calling) 详解

为何选择 Spring AI 与 MCP?

MCP 为与 AI 模型交互的应用引入了一个非常有趣的概念。借助 MCP,一个应用可以为其他需要使用其数据的服务提供特定的工具/函数。此外,它还可以暴露提示词模板和资源。这样一来,我们就不需要在每个客户端服务内部重复实现 AI 工具/函数,而是可以直接与通过 MCP 暴露这些工具的应用进行集成。

理解 MCP 概念的最佳方式是通过一个例子。假设有一个应用连接到数据库,并通过 REST 端点暴露数据。如果我们想在 AI 应用中使用这些数据,就需要实现并注册能够通过调用这些 REST 端点来检索数据的 AI 工具。这意味着,每个需要源服务数据的客户端应用都必须在本地实现自己的一套 AI 工具。

MCP 的出现解决了这个痛点。源服务可以以标准化的形式定义并暴露 AI 工具/函数。所有其他需要向 AI 模型提供数据的应用,都可以加载并使用这套预定义的工具。

场景:两个 Spring Boot 应用作为 MCP 服务器。它们连接到数据库,并使用 Spring AI MCP Server 支持将 @Tool 方法暴露给 MCP 客户端应用。客户端应用与 OpenAI 模型通信,并在发送给 AI 模型的用户查询中包含了由 MCP 服务器暴露的工具。person-mcp-service 应用提供了用于在数据库表中查询人员信息的 @Tool 方法,而 account-mcp-service 则为查询人员账户信息提供相应的工具。

在这里插入图片描述

构建 MCP 服务端应用

我们首先来实现作为 MCP 服务器的应用。这两个应用都将使用内存中的 H2 数据库。为了与数据库交互,我们引入了 Spring Data JPA 模块。Spring AI 允许我们在三种传输类型之间切换:STDIO、Spring MVC 和 Spring WebFlux。使用 Spring WebFlux 的 MCP 服务器支持服务器发送事件 (SSE) 和可选的 STDIO 传输。

<dependencies>
  <dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-mcp-server-webflux-spring-boot-starter</artifactId>
  </dependency>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
  </dependency>
  <dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>runtime</scope>
  </dependency>
</dependencies>

1. 创建 Person MCP 服务

首先是用于与 person 表交互的 @Entity 类:

@Entity
public class Person {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String firstName;
    private String lastName;
    private int age;
    private String nationality;
    @Enumerated(EnumType.STRING)
    private Gender gender;
    
    // ... getters and setters
}

Spring Data Repository 接口包含一个按国籍查询人员的方法:

public interface PersonRepository extends CrudRepository<Person, Long> {
    List<Person> findByNationality(String nationality);
}

PersonTools 这个 @Service Bean 包含了两个 Spring AI 的 @Tool 方法。它注入 PersonRepository 来与 H2 数据库交互。getPersonById 方法返回具有特定 ID 的单个人员信息,而 getPersonsByNationality 返回具有给定国籍的所有人员列表。

@Service
public class PersonTools {

    private PersonRepository personRepository;

    public PersonTools(PersonRepository personRepository) {
        this.personRepository = personRepository;
    }

    @Tool(description = "通过 ID 查找人员")
    public Person getPersonById(
            @ToolParam(description = "人员 ID") Long id) {
        return personRepository.findById(id).orElse(null);
    }

    @Tool(description = "通过国籍查找所有人员")
    public List<Person> getPersonsByNationality(
            @ToolParam(description = "国籍") String nationality) {
        return personRepository.findByNationality(nationality);
    }
}

定义了 @Tool 方法后,我们必须在 Spring AI MCP 服务器中注册它们。可以使用 ToolCallbackProvider Bean 来完成。具体来说,MethodToolCallbackProvider 类提供了一个构建器,用于创建一个包含 @Tool 方法所在对象引用的 ToolCallbackProvider 实例。

@SpringBootApplication
public class PersonMCPServer {

    public static void main(String[] args) {
        SpringApplication.run(PersonMCPServer.class, args);
    }

    @Bean
    public ToolCallbackProvider tools(PersonTools personTools) {
        return MethodToolCallbackProvider.builder()
                .toolObjects(personTools)
                .build();
    }
}

最后,我们需要提供配置属性。person-mcp-server 应用将监听 8060 端口。我们还应设置嵌入应用中的 MCP 服务器的名称和版本。

spring:
  ai:
    mcp:
      server:
        name: person-mcp-server
        version: 1.0.0
  jpa:
    database-platform: H2
    generate-ddl: true
    hibernate:
      ddl-auto: create-drop



server.port: 8060

logging:
  level:
    org.springframework.ai: DEBUG

好了!现在可以启动应用了。

$ cd spring-ai-mcp/person-mcp-service
$ mvn spring-boot:run

2. 创建 Account MCP 服务

接下来,我们在第二个作为 MCP 服务器的应用中执行非常相似的操作。这是用于与 account 表交互的 @Entity 类:

@Entity
public class Account {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String number;
    private int balance;
    private Long personId;
    
    // ... getters and setters
}

Repository 接口包含一个查询指定人员所有账户的方法:

public interface AccountRepository extends CrudRepository<Account, Long> {
    List<Account> findByPersonId(Long personId);
}

AccountTools 这个 @Service Bean 包含一个 @Tool 方法,用于返回指定 ID 的人员所拥有的账户列表。

@Service
public class AccountTools {

    private AccountRepository accountRepository;

    public AccountTools(AccountRepository accountRepository) {
        this.accountRepository = accountRepository;
    }

    @Tool(description = "通过人员 ID 查找所有账户")
    public List<Account> getAccountsByPersonId(
            @ToolParam(description = "人员 ID") Long personId) {
        return accountRepository.findByPersonId(personId);
    }
}

当然,account-mcp-server 应用也会使用 ToolCallbackProvider 来注册定义在 AccountTools 类中的 @Tool 方法。

@SpringBootApplication
public class AccountMCPService {

    public static void main(String[] args) {
        SpringApplication.run(AccountMCPService.class, args);
    }

    @Bean
    public ToolCallbackProvider tools(AccountTools accountTools) {
        return MethodToolCallbackProvider.builder()
                .toolObjects(accountTools)
                .build();
    }
}

这是 account-mcp-server 的配置,它将监听 8040 端口。

spring:
  ai:
    mcp:
      server:
        name: account-mcp-server
        version: 1.0.0
  jpa:
    database-platform: H2
    generate-ddl: true
    hibernate:
      ddl-auto: create-drop

server.port: 8040

logging:
  level:
    org.springframework.ai: DEBUG

启动第二个服务端应用:

$ cd spring-ai-mcp/account-mcp-service
$ mvn spring-boot:run

应用启动后,你会在日志中看到 MCP 服务器注册了多少个工具的信息。

构建 MCP 客户端应用

实现细节

我们将创建一个客户端应用。当然,你也可以想象一个架构,其中有许多应用消费由一个 MCP 服务器暴露的工具。我们的客户端应用与 OpenAI 聊天模型交互,因此需要引入 Spring AI OpenAI 启动器。对于 MCP 客户端启动器,我们可以选择 spring-ai-mcp-client-webflux-spring-boot-starter,Spring 团队推荐使用基于 WebFlux 的 SSE 连接。最后,我们引入 Spring Web 启动器来暴露 REST 端点。

<dependencies>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
  </dependency>
  <dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-mcp-client-webflux-spring-boot-starter</artifactId>
  </dependency>
  <dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-openai-spring-boot-starter</artifactId>
  </dependency>
</dependencies>

我们的 MCP 客户端需要连接两个 MCP 服务器。我们必须在 application.yml 文件中提供以下连接设置:

spring.ai.mcp.client.sse.connections:
  person-mcp-server:
    url: http://localhost:8060
  account-mcp-server:
    url: http://localhost:8040

示例 Spring Boot 应用包含两个 @RestControllerPersonController 定义了两个端点,用于按国籍搜索和统计人员。

关键点在于:MCP Client Boot Starter 会自动配置与 Spring AI 工具执行框架集成的工具回调。因此,我们可以直接注入 ToolCallbackProvider 实例,并将其作为默认工具提供给 ChatClient。之后,我们就可以像平常一样使用 ChatClient 与 AI 模型交互,而客户端将自动使用由两个 MCP 服务器暴露的工具。

@RestController
@RequestMapping("/persons")
public class PersonController {

    private final ChatClient chatClient;

    public PersonController(ChatClient.Builder chatClientBuilder,
                            ToolCallbackProvider tools) {
        this.chatClient = chatClientBuilder
                .defaultTools(tools)
                .build();
    }

    @GetMapping("/nationality/{nationality}")
    String findByNationality(@PathVariable String nationality) {
        PromptTemplate pt = new PromptTemplate("查找国籍是 {nationality} 的人员。");
        Prompt p = pt.create(Map.of("nationality", nationality));
        return this.chatClient.prompt(p).call().content();
    }

    @GetMapping("/count-by-nationality/{nationality}")
    String countByNationality(@PathVariable String nationality) {
        PromptTemplate pt = new PromptTemplate("有多少人员来自 {nationality}?");
        Prompt p = pt.create(Map.of("nationality", nationality));
        return this.chatClient.prompt(p).call().content();
    }
}

AccountController 定义了两个端点。GET /accounts/balance-by-person-id/{personId} 稍微复杂一些。它需要计算某人所有账户的总余额,并且还必须返回该人员的姓名和国籍。这意味着,在调用用于按人员 ID 搜索账户的工具后,它还必须调用由 person-mcp-server 暴露的 getPersonById 工具

@RestController
@RequestMapping("/accounts")
public class AccountController {

    private final ChatClient chatClient;

    public AccountController(ChatClient.Builder chatClientBuilder,
                            ToolCallbackProvider tools) {
        this.chatClient = chatClientBuilder
                .defaultTools(tools)
                .build();
    }

    @GetMapping("/count-by-person-id/{personId}")
    String countByPersonId(@PathVariable String personId) {
        PromptTemplate pt = new PromptTemplate("ID 为 {personId} 的人员有多少个账户?");
        Prompt p = pt.create(Map.of("personId", personId));
        return this.chatClient.prompt(p).call().content();
    }

    @GetMapping("/balance-by-person-id/{personId}")
    String balanceByPersonId(@PathVariable String personId) {
        PromptTemplate pt = new PromptTemplate("""
                ID 为 {personId} 的人员有多少个账户?
                请返回该人员的姓名、国籍以及他/她账户的总余额。
                """);
        Prompt p = pt.create(Map.of("personId", personId));
        return this.chatClient.prompt(p).call().content();
    }
}

运行与测试

spring:
  ai:
    openai:
      api-key: sk-xxxxxxxxxxxxxxxxxxxx
      base-url: https://api.deepseek.com
      chat:
        options:
          model: deepseek-chat
    mcp:
      client:
        sse:
          connections:
            person-mcp-server:
              url: http://localhost:8060
            account-mcp-server:
              url: http://localhost:8040
        request-timeout: 60s

然后进入 sample-client 目录并运行应用:

$ cd spring-ai-mcp/sample-client
$ mvn spring-boot:run

启动应用后,查看日志。你会看到 sample-client 应用收到了来自 person-mcp-serveraccount-mcp-server 的工具响应。

我们的 MCP 客户端应用监听 8080 端口。让我们调用第一个端点来获取来自德国的人员列表:

curl http://localhost:8080/persons/nationality/Germany

以下是来自 DeepSeek模型的响应,它已经调用了远端工具并获得了数据:

I found 4 persons with German nationality: 1. **Hans Müller** (ID: 5) - 35 years old, Male 2. **Daniel Schmidt** (ID: 19) - 41 years old, Male 3. **Sophia Jones** (ID: 45) - 25 years old, Female 4. **James Müller** (ID: 46) - 40 years old, Male All these individuals have Germany listed as their nationality.

我们也可以调用统计数量的端点:

curl http://localhost:8080/persons/count-by-nationality/Germany

响应会是:"Based on the data, there are **4 persons** who come from Germany: 1. Hans Müller (35 years old, Male) 2. Daniel Schmidt (41 years old, Male) 3. Sophia Jones (25 years old, Female) 4. James Müller (40 years old, Male)"


作为最终测试,我们可以调用 GET /accounts/balance-by-person-id/{personId} 端点,

curl http://localhost:8080/accounts/balance-by-person-id/1

它会与两个 MCP 服务器暴露的工具进行交互,要求 AI 模型结合来自人员和账户的数据源。

根据查询结果,ID1的人员信息如下: **人员信息:** - 姓名:John Smith - 国籍:USA **账户情况:** 该人员共有3个账户,账户总余额为:1,000 + 500 + 2,000 = **3,500** **详细账户信息:** 1. 账户号:1234567890,余额:1,000 2. 账户号:2345678901,余额:500 3. 账户号:3456789012,余额:2,000

进阶:通过 MCP 暴露提示词

我们还可以通过 Spring AI MCP 服务器支持来暴露提示词和资源。要注册和暴露提示词,我们需要定义一个 SyncPromptRegistration 对象列表。它包含提示词的名称、输入参数列表和文本内容。

@SpringBootApplication
public class PersonMCPServer {

    // ... main 和 tools bean ...

    @Bean
    public List<McpServerFeatures.SyncPromptRegistration> prompts() {
        var prompt = new McpSchema.Prompt("persons-by-nationality", "按国籍获取人员",
                List.of(new McpSchema.PromptArgument("nationality", "人员国籍", true)));

        var promptRegistration = new McpServerFeatures.SyncPromptRegistration(prompt, getPromptRequest -> {
            String argument = (String) getPromptRequest.arguments().get("nationality");
            var userMessage = new McpSchema.PromptMessage(McpSchema.Role.USER,
                    new McpSchema.TextContent("有多少人员来自 " + argument + " ?"));
            return new McpSchema.GetPromptResult("按国籍统计人员", List.of(userMessage));
        });

        return List.of(promptRegistration);
    }
}

定义了一个Spring Bean,用于注册一个同步Prompt处理逻辑。它创建了一个名为“persons-by-nationality”的Prompt,要求用户提供国籍参数,然后根据该参数生成一条查询该国籍人员数量的提示信息。

启动后,应用会在日志中打印出已注册的提示词列表信息。

在这里插入图片描述

目前,Spring AI 客户端还没有内置的高级支持来加载这些通过 MCP 暴露的提示词。但是,Spring AI MCP 支持正在积极开发中,我们期待很快会有新功能。就目前而言,Spring AI 提供了自动配置的 McpSyncClient 实例。我们可以用它在从服务器接收到的提示词列表中搜索指定的提示词,然后使用注册的内容准备 PromptTemplate 实例,并通过填充模板来创建 Prompt

@RestController
@RequestMapping("/persons")
public class PersonController {

    private final ChatClient chatClient;
    private final List<McpSyncClient> mcpSyncClients;

    public PersonController(ChatClient.Builder chatClientBuilder,
                            ToolCallbackProvider tools,
                            List<McpSyncClient> mcpSyncClients) {
        this.chatClient = chatClientBuilder
                .defaultTools(tools)
                .build();
        this.mcpSyncClients = mcpSyncClients;
    }

    // ... 其他端点 ...
    
     @GetMapping("/count-by-nationality-from-client/{nationality}")
    String countByNationalityFromClient(@PathVariable String nationality) {
        return this.chatClient.prompt(loadPromptByName("persons-by-nationality", nationality))
                .call()
                .content();
    }

    Prompt loadPromptByName(String name, String nationality) {
        McpSchema.GetPromptRequest r = new McpSchema.GetPromptRequest(name, Map.of("nationality", nationality));
        var client = mcpSyncClients.stream()
                .filter(c -> c.getServerInfo().name().equals("person-mcp-server"))
                .findFirst();
        if (client.isPresent()) {
            var messages = client.get().getPrompt(r).messages();
            if (!messages.isEmpty()) {
                var message = messages.get(0);
                if (message.content() instanceof McpSchema.TextContent) {
                    var content = (McpSchema.TextContent) message.content();
                    PromptTemplate pt = new PromptTemplate(content.text());
                    Prompt p = pt.create(Map.of("nationality", nationality));
                    LOG.info("Prompt: {}", p);
                    return p;
                }
            }
        }
        return null;
    }
}

总结

模型上下文协议(MCP)是 AI 领域的一项重要举措。它使我们能够避免为每个新数据源重复“造轮子”。统一的协议简化了集成,最大限度地减少了开发时间和复杂性。随着企业扩展其 AI 工具集,MCP 实现了跨多个系统的无缝连接,而无需承担过多的自定义代码负担。

借助 Spring AI 的客户端和服务端启动器,我们可以实现一种分布式架构,其中多个不同的应用可以使用由单个服务暴露的 AI 工具。

源码

https://gitee.com/yangshangwei/spring-ai-apps

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小小工匠

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值