springai完成mcp+知识库实现智能助手

本文将介绍如何使用spring-ai构建一个集成市面各大mcp服务与自有知识库的mcp客户端外加一个自己提供的mcp服务,快速把一个自有的系统变成一个可以调用自有能力,利用自有知识库,联网搜索,位置定位等能力的智能助手

Spring AI 项目由 Spring 官方开源并维护的 AI 应用开发框架,该项目目标是简化包含人工智能(AI)功能的应用程序的开发,避免不必要的复杂性。该项目从著名的 Python 项目(例如 LangChain 和 LlamaIndex)中汲取灵感,但 Spring AI 并非这些项目的直接移植,该项目的成立基于这样的信念:下一波生成式 AI 应用将不仅面向 Python 开发人员,还将遍及多种编程语言。从本质上讲,Spring AI 解决了 AI 集成的基本挑战:Connecting your enterprise Data and APIs with the AI Models。

MCP客户端

实现智能助手我们需要引入以下客户端,依赖版本我们使用截止目前最新的1.0.0-M6

xml 复制代码
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-openai-spring-boot-starter</artifactId>
    <version>${springai.version}</version>
</dependency>
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-mcp-client-webflux-spring-boot-starter</artifactId>
    <version>${springai.version}</version>
</dependency>
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-qianfan-spring-boot-starter</artifactId>
    <version>${springai.version}</version>
</dependency>
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-milvus-store-spring-boot-starter</artifactId>
    <version>${springai.version}</version>
</dependency>
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-tika-document-reader</artifactId>
    <version>${springai.version}</version>
</dependency>
  • 因为我们使用的是deepseek作为底座的大模型,所以我们这里引入的是spring-ai-openai-spring-boot-starter
  • 百度千帆作为嵌入模型(Embedding Model)
  • 向量库使用milvus
  • 文档切分使用spring-ai-tika-document-reader
客户端的配置文件部分
yaml 复制代码
ai:
  openai:
    api-key: ********************
    base-url: https://api.deepseek.com
    chat:
      options:
        model: deepseek-chat
        temperature: 0.8
    embedding:
      enabled: false
  chat:
    observations:
      include-completion: true
      include-error-logging: true
    client:
      observations:
        include-input: true
  mcp:
    client:
      enabled: true
      name: ysj-mcp-client
      type: ASYNC
      stdio:
        servers-configuration: classpath:/mcp-servers-config.json
      sse:
        connections:
          server1:
            url: http://127.0.0.1:8420
  qianfan:
    chat:
      enabled: false
    api-key: ***************
    secret-key: **************
    embedding:
      options:
        model: tao_8k
  vectorstore:
    milvus:
      initialize-schema: true
      client:
        host: ***.***.***.**
        port: *****
        username: *****
        password: *****
      databaseName: "default"
      collectionName: "vector_store"
      embeddingDimension: 1536
      indexType: IVF_FLAT
      metricType: COSINE

需要特别注意禁用qianfan的chatclient qianfan: chat: enabled: false 这个配置方式在下个版本中会有变更,以下是官方文档原文

Enabling and disabling of the chat auto-configurations are now configured via top level properties with the prefix spring.ai.model.chat. To enable, spring.ai.model.chat=openai (It is enabled by default) To disable, spring.ai.model.chat=none (or any value which doesn't match openai) This change is done to allow configuration of multiple models.

mcp客户端集成配置mcp-servers-config.json

swift 复制代码
{
  "mcpServers": {
    "tavily": {
      "command": "C:\Program Files\nodejs\npx.cmd",
      "args": ["-y", "@mcptools/mcp-tavily"],
      "env": {
        "TAVILY_API_KEY": "**************"
      }
    },
    "baidu-map": {
      "command": "C:\Program Files\nodejs\npx.cmd",
      "args": [
        "-y",
        "@baidumap/mcp-server-baidu-map"
      ],
      "env": {
        "BAIDU_MAP_API_KEY": "****************"
      }
    }
  }
}

这里集成的是一个AI搜索的第三方服务和一个百度地图的mcp服务,正在尝试接入一个text2Sql可本地私有化部署的mcp服务实现自然语言的数据库查询,还未完整实现先不放出,实现后再补充

mcp客户端代码部分

1.定义chatclient

scss 复制代码
@Configuration
public class ChatClientConfig {

  @Resource
  private VectorStore vectorStore;

  /**
   * 配置ChatClient,注册系统指令和工具函数
   */
  @Bean
  public ChatClient chatClient(ChatClient.Builder builder,
                               List<McpSyncClient> mcpSyncClients,
                               ToolCallbackProvider tools) {
    //添加顾问
    Advisor retrievalAugmentationAdvisor = RetrievalAugmentationAdvisor.builder()
            .documentRetriever(VectorStoreDocumentRetriever.builder()
                    .similarityThreshold(0.5)
                    .vectorStore(vectorStore)
                    .build())
            .queryAugmenter(ContextualQueryAugmenter.builder()
                    .allowEmptyContext(true)
                    .build())
            .build();

    return builder
            .defaultSystem("""
              你是企业微信的智能助理------闪闪!
              你性格活泼轻松,擅长以亲切友好的方式为用户提供帮助。
              你的主要职责包括办理日常业务、与企业微信交互等实用功能,让用户的工作更高效、更便捷。
              请以专业但不失幽默的语气与用户交流,确保沟通愉快且高效!
              请说中文
              返回的所有内容使用MarkDown格式展示
              返回的MarkDown数据要适应前端流式展示的需求
            """)
            // 注册工具方法
            .defaultTools(tools)
            .defaultAdvisors(retrievalAugmentationAdvisor)
            .build();
  }
}

Advisor是springai封装的用来拦截、修改和增强 Spring 应用程序中的 AI 驱动交互,我们可以很便捷的设置各种知识库的特性,spring-ai把复杂的逻辑都帮我们封装好了使得我们可以很简单就实现一个复杂的知识库助手

超时配置

针对超时配置的部分官方文档的配置中有提供超时参数,但是我尝试下来没有生效,这里使用另一种方式实现超时参数的配置

typescript 复制代码
@Configuration
public class McpClientConfig implements McpAsyncClientCustomizer {

    @Override
    public void customize(String name, McpClient.AsyncSpec spec) {
        spec.requestTimeout(Duration.ofSeconds(30));
    }
}
对话接口
less 复制代码
@RestController
@RequestMapping("/chat-client")
public class McpController {

    @Resource
    private ChatClient chatClient;

    private final ChatMemory chatMemory = new InMemoryChatMemory();

    @GetMapping("/generate_stream")
    public Flux<ChatResponse> generateStream(HttpServletResponse response, @RequestParam("id") String id,
                                             @RequestParam("prompt") String prompt) {
        response.setCharacterEncoding("UTF-8");
        var messageChatMemoryAdvisor = new MessageChatMemoryAdvisor(chatMemory, id, 10);
        return this.chatClient.prompt(prompt)
                .advisors(messageChatMemoryAdvisor)
                .stream()
                .chatResponse();
    }
}

使用webflux实现流式数据传输,配合前端实现打字机效果

文档分隔向量化存储到向量库中
less 复制代码
@RestController
@RequestMapping("/vectorization")
public class VectorizationController {
    @Resource
    private VectorStore vectorStore;

    @GetMapping("/txt")
    private void txt() {
        File documentation = new File("D:\rag_knowledge_base_test_data.txt");
        TikaDocumentReader tikaDocumentReader = new TikaDocumentReader(new FileSystemResource(documentation));
        TokenTextSplitter splitter = new TokenTextSplitter(300, 200, 10, 400, true);
        List<Document> documents = splitter.apply(tikaDocumentReader.get());
        documents.forEach(document -> {
            vectorStore.add(List.of(new Document(document.getFormattedContent())));
        });
    }
}
前段部分

前端部分使用AI生成一个聊天界面,现在的代码生成能力已经很强了,经过多轮对话对于简单需求已经能达到很好的效果了

MCP服务端

需要引入的依赖
xml 复制代码
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-openai-spring-boot-starter</artifactId>
    <version>${springai.version}</version>
</dependency>
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-mcp-server-webflux-spring-boot-starter</artifactId>
    <version>${springai.version}</version>
</dependency>
服务端配置
yaml 复制代码
ai:
  openai:
    api-key: **********
    base-url: https://api.deepseek.com
    chat:
      options:
        model: deepseek-chat
        temperature: 0.8
    embedding:
      enabled: false
  chat:
    observations:
      include-completion: true
      include-error-logging: true
    client:
      observations:
        include-input: true
  mcp:
    server:
      name: ysj-mcp-server
      version: 1.0.0
      type: ASYNC
      sse-message-endpoint: /mcp/message

应该是不需要spring-ai-openai-spring-boot-starter和大模型的相关配置的,请求大模型相关的在客户端,这里我暂时没有去掉先一起列出来

注册工具
kotlin 复制代码
@Configuration
public class McpServerConfig {

  /**
   * 注册工具
   *
   * @param chatService 工具列表
   * @return 工具回调提供者
   */
  @Bean
  public ToolCallbackProvider chatToolCallbackProvider(ChatService chatService) {
    return MethodToolCallbackProvider.builder()
            .toolObjects(chatService)
            .build();
  }

}
自定义MCP服务的工具
typescript 复制代码
@Service
@Slf4j
public class ChatServiceImpl implements ChatService {
    String districtUrl = "https://restapi.amap.com/v3/config/district?subdistrict=0&key=***********=";
    String weatherUrl = "https://api.map.baidu.com/weather/v1/";

    @Override
    @Tool(name = "weatherQueries", description = "查询某个地方最近的天气情况")
    public WeatherResponse weatherQueries(@ToolParam(description = "当前地点") String location) {
        log.debug("调用天气工具成功,获取到的入参:{}", location);
        DistrictResponse districtResponse = getDistrict(location);
        if ("1".equals(districtResponse.getStatus())) {
            String adCode = districtResponse.getDistricts().getFirst().getAdcode();
            return getWeather(adCode);
        } else {
            return new WeatherResponse().setStatus(-1).setMessage("获取天气异常,天气工具执行失败!");
        }
    }

    /**
     * 发送消息到企业微信
     * @date:  2025/3/31 下午3:59
     * @author  ysj
     * @param employeeName:
     * @return com.ysj.ai.assistant.mcp.entity.response.QwResponse
     */
    @Override
    @Tool(name = "sendMessageToUser", description = "根据名称发送消息到某个员工的企微")
    public QwResponse sendMessageToUser(@ToolParam(description = "员工名称") String employeeName,
                                        @ToolParam(description = "发送消息的内容") String message) {
        Map<String,Object> params = Map.of("employeeName", employeeName, "message", message);
        HttpResponse httpResponse = HttpUtil.createPost(CommonConstant.MESSAGE_TO_USER_URL)
                .addHeaders(CommonUtils.getQwHeaders().toSingleValueMap())
                .body(JSONUtil.toJsonStr(params))
                .execute();
        return JSONUtil.toBean(JSONUtil.parse(httpResponse.body(), JSONConfig.create().setIgnoreCase(true)), QwResponse.class, true);
    }

    private DistrictResponse getDistrict(String location) {
        HttpResponse httpResponse = HttpUtil.createGet(districtUrl+location).execute();
        return JSONUtil.toBean(httpResponse.body(), DistrictResponse.class);
    }

    private WeatherResponse getWeather(String adCode) {
        weatherUrl += "?data_type=all&district_id="+adCode+"&ak=*********";
        HttpResponse httpResponse = HttpUtil.createGet(weatherUrl).execute();
        return JSONUtil.toBean(httpResponse.body(), WeatherResponse.class);
    }
}

这里我们实现了2个工具,一个天气查询的工具和一个给企微发送消息的工具

这样mcp客户端就可以连上我们的mcp服务端自动的调度需要调用哪些服务,我们把一个陈旧的系统加上智能化的助手只需要简单几步,大模型会使用Function Calling帮我们设置好接口的请求参数

这里有一点需要注意,spring ai的mcp客户端在使用webflux接入了mcp服务同时又接入了知识库可能会出现 block()/blockFirst()/blockLast() are blocking, which is not supported in thread 这个错误,issues中已经有相关问题并且在M7版本应该解决了,公共仓库中暂时还没有M7版本,应该在不久后就可以下载到,到时候我会再验证一遍

相关推荐
秋野酱10 分钟前
基于SpringBoot酒店管理系统设计和实现(源码+文档+部署讲解)
java·spring boot·后端
在下木子生23 分钟前
mac如何将jar包上传到maven中央仓库中
java·macos·maven·jar
写代码的小王吧42 分钟前
【网络安全】 防火墙技术
java·python·安全·web安全·网络安全·docker
XiaoLeisj42 分钟前
【MyBatis】深入解析 MyBatis:关于注解和 XML 的 MyBatis 开发方案下字段名不一致的的查询映射解决方案
xml·java·spring boot·spring·java-ee·tomcat·mybatis
信徒_1 小时前
Spring 中有哪些设计模式?
java·spring·设计模式
爱的叹息1 小时前
关于 Spring自定义缓存管理器 的详细说明,包含两种实现方式的对比和代码示例,并附表格总结
java·spring·缓存
刘龙超1 小时前
如何应对 Android 面试官 -> 网络如何优化?
android·java
IT瘾君1 小时前
Java基础:Logback日志框架
java·开发语言·logback
suimeng61 小时前
Java的Selenium的特殊元素操作与定位之select下拉框
java·自动化测试·selenium
ChinaRainbowSea2 小时前
8. RabbitMQ 消息队列 + 结合配合 Spring Boot 框架实现 “发布确认” 的功能
java·spring boot·分布式·后端·rabbitmq·java-rabbitmq