项目代码: Github


1. 本文概述

上篇文章中(Spring Boot 集成 Spring AI:实现可被大模型调用的 MCP Server),我们构建了一个MCP Server,提供了查询个人信息、查询订单信息、下单等操作。

但是,上篇文章我们使用的是Cherry Studio作为客户端来与后端进行交互。实际生产中,让客户去下载Cherry Studio,然后配置MCP Server显然不显示。

因此,本篇文章将会教会你如何使用 Spring AI 构建一个简单的聊天界面,并支持MCP Client.

通过本文学习,你将会学到如下内容:

  1. Spring AI 接入DeepSeek
  2. 如何使用Spring AI构建一个多轮对话(非流式)。
  3. 多轮对话如何实现MCP Server的调用
  4. 调用MCP Server时,如何通过传递token,来让MCP Server区分当前用户,以返回正确的信息。

效果展示(本文重点不在前端,因此使用Postman做交互):

在这里插入图片描述

2. 项目初始化

2.1 使用Spring Initializr初始化

如果你是新项目可以,使用 Spring Initializr 生成一个项目。

在这里插入图片描述

在构建项目时,需要增加3个主要依赖:

  • Spring Web: 用于支持HTTP接口
  • DeepSeek:用于接入DeepSeek
  • Model Context Protocol Client:支持MCP Client.

2.2 增加项目依赖

如果你是现有项目,那么就需要手动增加上节提到的3个依赖。代码如下:

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
	<groupId>org.springframework.ai</groupId>
	<artifactId>spring-ai-starter-mcp-client</artifactId>
</dependency>

<dependency>
	<groupId>org.springframework.ai</groupId>
	<artifactId>spring-ai-starter-model-deepseek</artifactId>
</dependency>

如果无法正常导入,请配置具体版本。本文使用的是1.1.0

3. 使用Spring AI实现多轮对话

3.1 多轮对话原理简介

大模型是没有记忆的,所谓的多轮对话就是每次调用都将前面所有的对话一并给到大模型。

例如,第一次调用大模型时,输入输出为:

输入:
User: 你好,简单介绍一下你自己

输出:
Assistant: 您好,我是DeepSeek.

若用户继续输入问题,则你需要将之前的聊天记录都作为输入传给大模型。输入输出为:

输入:
User: 你好,简单介绍一下你自己
Assistant: 您好,我是DeepSeek.
User: 重复下我的问题

输出:
Assistant:您的问题是“你好,简单介绍下你自己”

在与大模型交互时,有三个重要的角色:

  • User:用户。对应用户输入的信息
  • Assistant:大模型助手。其输出的内容就是用户在聊天界面上能看到的内容。
  • System:系统。这部分通常是作为应用的全局提示词,不展示给用户。例如:“你是XXX公司的AI客服,公司的简介是XXX。在与用户聊天的过程中,不要透露你是AI”。

3.2 配置DeepSeek API Key

首先,你需要到 DeepSeek Platform 冲些钱,获取自己的API Key。不用太多,10块就能用好久。

在这里插入图片描述
当然,你也可以使用其他的大模型,Spring AI基本支持大部分主流大模型。

3.3 配置application.yaml

获取API Key后,需要在application.yaml中配置,配置如下:

spring:
  ai:
    deepseek:
      api-key: "sk-**********1123"

3.4 实现单轮对话

Spring AI中,要实现与大模型对话,需要如下几步操作:

  1. 获取ChatClient.Builder实例,Spring已经帮忙初始化了,只需要注入即可。
  2. 使用ChatClient.Builder实例构造ChatClient对象。该过程可以设置全局的系统提示词,即给到system角色的内容。
  3. ChatClient传入对话内容,调用call方法即可获取大模型回复。

示例代码如下:

import org.springframework.ai.chat.client.ChatClient;

@RestController
@RequestMapping(value = "/chat")
public class ChatController {

    @Autowired
    private ChatClient.Builder builder;

    @PostMapping("/ask")
    public String ask(@RequestParam("question") String question) {
        ChatClient chatClient = builder
                .defaultSystem("你是蜗牛公司的一名AI助手,你的名字叫小蜗。你可以帮助用户解答关于AI相关的知识").build();

        return chatClient.prompt().user(question).call().content();
    }

}

注:这里我是在方法内实例化的ChatClient对象,但实践中通常需要将其注册为Bean,即使用单例。

使用效果:

在这里插入图片描述

3.5 实现多轮对话

实现多轮其实很简单,只需要将单轮对话中的user(...)方法替换为messages(...)方法即可,里面传入历史对话内容。

样例代码:

import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.UserMessage;

@RestController
@RequestMapping(value = "/chat")
public class ChatController {

    @Autowired
    private ChatClient.Builder builder;

    @PostMapping("/ask2")
    public String ask2(@RequestParam("question") String question) {
        ChatClient chatClient = builder
                .defaultSystem("你是蜗牛公司的一名AI助手,你的名字叫小蜗。你可以帮助用户解答关于AI相关的知识").build();

        List<Message> messageList = new ArrayList<>();
        messageList.add(new UserMessage("你好,我是小明。你是谁?"));
        messageList.add(new AssistantMessage("你好!我是小蜗,蜗牛公司的AI助手。很高兴为你服务!有什么我可以帮你的吗?"));  // AI的回答
        // ... 其他对话历史
        messageList.add(new UserMessage(question));  // 用户的新问题

        return chatClient.prompt()
                .messages(messageList)  // 传入对话历史
                .call().content();
    }

}

使用样例:

在这里插入图片描述

在实际使用中,通常有两种方法来构造List<Message>

  1. 前端将所有的历史对话全部一并传过来。
  2. 将历史对话存入服务端,每次前端只传新的问题。Spring AI提供了Chat Memory 来实习这个功能。

这里我们使用第一种方式进行演示,要求前端传参格式如下:

[
    {
        "role": "user",  // 角色; system, user 或 assistant
        "content": "你好,你是谁"  // 对话内容
    },
    ...
]

后端接口代码如下:

@PostMapping("/multiTurnChat")
@ResponseBody
public List<Map<String, String>> multiTurnChat(@RequestBody List<Map<String, String>> messages) {
    List<Message> messageList = new ArrayList<>();
    messages.forEach(itemMap -> {
        String role = itemMap.get("role");
        String content = itemMap.get("content");

        if ("user".equals(role)) {
            messageList.add(new UserMessage(content));
        } else if ("assistant".equals(role)) {
            messageList.add(new AssistantMessage(content));
        }
    });

    ChatClient.CallResponseSpec responseSpec = builder.build()
            .prompt()
            .messages(messageList)
            .call();

		// 将大模型生成的内容重新加入到messages中,作为响应
    messages.add(Map.of(
            "role", "assistant",
            "content", responseSpec.content()
    ));

    return messages;
}

使用样例:

在这里插入图片描述

4. 配置MCP实现工具调用

Spring AI 实现了MCP Client功能,可以很方便的接入MCP Server,实现工具调用。

4.1 配置MCP Server

首先我们需要在application.yaml中配置好我们的MCP Server。代码如下:

spring:
  ai:
    mcp:
      client:
        streamable-http:
          connections:
            MyMcpServer1:  # 随便什么名字
              url: http://localhost:8080
              endpoint: /api/mcp-endpoint

该MCP Server是我们上一篇中实现的那个。url和endpoint可以在上一篇中的配置中找到。

4.2 多轮对话中使用MCP Tools

在大模型对话中使用MCP Tools非常简单,Spring AI已经帮我们封装的特别好了。只需要2步:

  1. 通过注入方式获取SyncMcpToolCallbackProvider provider实例。
  2. 在调用大模型之间,也就是.call()之间,加上.toolCallbacks(provider.getToolCallbacks())方法,告诉ChatClient有哪些工具可以用。其中provider.getToolCallbacks()会返回所有可用的MCP Tools.

代码片段如下:

import org.springframework.ai.mcp.SyncMcpToolCallbackProvider;

@Autowired
private SyncMcpToolCallbackProvider provider;

ChatClient.CallResponseSpec responseSpec = builder.build()
        .prompt()
        .messages(messageList)
        .toolCallbacks(provider.getToolCallbacks())  // 加上这行
        .call();

使用效果:

在这里插入图片描述
在本次示例中可以看到,第一轮对话系统调用了MCP Server,告知了其可以实现的功能。但是,在第二轮对话中,系统拒绝了用户的请求,这是因为目前为止,我们在调用MCP Server的时候并没有传token,这样MCP Server端是不知道我们当前用户是谁。这要是我们上一篇实现的功能之一。

4.3 MCP Client增加token请求头(Headers)

目前Spring AI的MCP Client实现中并不直接支持动态headers头,但在查阅Issues(#4305)后,官方给出了一种解决方案:构造McpSyncHttpClientRequestCustomizer的bean,在其增加自定义Header

我的代码如下:

import io.modelcontextprotocol.client.transport.customizer.McpSyncHttpClientRequestCustomizer;
import io.modelcontextprotocol.common.McpTransportContext;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import java.net.URI;
import java.net.http.HttpRequest;

@Configuration
public class McpConfig {

    @Bean
    McpSyncHttpClientRequestCustomizer requestCustomizer() {
        McpSyncHttpClientRequestCustomizer mcpSyncHttpClientRequestCustomizer = new McpSyncHttpClientRequestCustomizer() {
            @Override
            public void customize(HttpRequest.Builder builder, String method, URI endpoint, String body, McpTransportContext context) {
                if (RequestContextHolder.getRequestAttributes() == null) {
                    return;
                }

                ServletRequestAttributes attributes =
                        (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();
                HttpServletRequest request = attributes.getRequest();
                // 从HTTP请求的header中获取token
                String token = request.getHeader("token");  

                if (token != null) {
                    builder.header("token", token);
                }
            }
        };

        return mcpSyncHttpClientRequestCustomizer;
    }

}

在增加了该配置类后,再次尝试刚刚的样例。这次需要在http的请求头中也需要加入对应的header:

在这里插入图片描述

至此,一个简单的MCP Client已经实现完毕。如果有更多想要了解的,可以进一步查阅官方文档。



参考资料

Logo

更多推荐