最近做了一个农业政策推荐的项目,里边涉及到了一些业务需要使用到大模型,之前一直都是用AI去写代码,一直有个想法是实战一下在项目中接入大模型,增加系统的价值和用户体验,也是自己的AI时代往前迈出的重要一步,一路遇到了很多坑,记录一下,以给大家避个坑。
我采用的是SSE(服务器发送事件)模式

首先我的项目是使用的是Jeecg boot框架,底层spring boot的版本如下

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-dependencies</artifactId>
    <version>3.5.5</version>
</parent>

接下来就是要做的一些配置可开发工作了
第一步:在pom.xml中加入spring ai的依赖

<!-- Spring AI MCP Server (SSE) -->
<dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
            <version>1.1.2</version>
</dependency>

第二步:实现mcp的核心业务代码
核心用到了如下两个注解

@McpTool     注释标记方法为MCP工具实现,并自动生成JSON模式。
@McpToolParam   标记为工具实现的方法入参,大模型会根据参数会自动通过用户问题进行参数抽取
package org.jeecg.modules.policy.mcp;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.modules.policy.entity.PolPolicy;
import org.jeecg.modules.policy.service.IPolPolicyService;
import org.springaicommunity.mcp.annotation.McpTool;
import org.springaicommunity.mcp.annotation.McpToolParam;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import java.text.SimpleDateFormat;
import java.util.*;

/**
 * 政策MCP服务
 * 提供政策查询、问答等功能,供AI Agent调用
 *
 * @author Spring
 */
@Component
@Slf4j
public class PolicyMcpService {

    @Autowired
    private IPolPolicyService polPolicyService;

    /**
     * 搜索政策
     * 根据关键词、政策级别、政策类型等条件搜索政策
     */
    @McpTool(description = "搜索政策列表,支持按关键词、政策级别、政策类型、地区等条件筛选。返回符合条件的政策列表,包括政策标题、编号、发布机构、发布时间等信息。")
    public Map<String, Object> searchPolicy(
            @McpToolParam(description = "搜索关键词,用于在政策标题和内容中搜索", required = false) String keyword,
            @McpToolParam(description = "政策级别,如:国家级、省级、市级、县级", required = false) String policyLevel,
            @McpToolParam(description = "政策类型,如:补贴政策、贷款政策、技术支持等", required = false) String policyType,
            @McpToolParam(description = "省份名称", required = false) String province,
            @McpToolParam(description = "城市名称", required = false) String city,
            @McpToolParam(description = "区县名称", required = false) String district,
            @McpToolParam(description = "页码,从1开始", required = false) Integer pageNum,
            @McpToolParam(description = "每页数量,默认10条", required = false) Integer pageSize) {

        log.info("========== MCP方法调用: searchPolicy ==========");
        log.info(
                "参数: keyword={}, policyLevel={}, policyType={}, province={}, city={}, district={}, pageNum={}, pageSize={}",
                keyword, policyLevel, policyType, province, city, district, pageNum, pageSize);

        try {
            // 设置默认值
            if (pageNum == null || pageNum < 1) {
                pageNum = 1;
            }
            if (pageSize == null || pageSize < 1) {
                pageSize = 10;
            }
            if (pageSize > 50) {
                pageSize = 50; // 限制最大每页数量
            }

            // 构建查询条件
            PolPolicy queryPolicy = new PolPolicy();
            queryPolicy.setAuditStatus("APPROVED"); // 只查询已审核通过
            queryPolicy.setPolicyStatus("PUBLISHED"); // 只查询已发布

            if (StringUtils.hasText(policyLevel)) {
                queryPolicy.setPolicyLevel(policyLevel);
            }
            if (StringUtils.hasText(policyType)) {
                queryPolicy.setPolicyType(policyType);
            }
            if (StringUtils.hasText(province)) {
                queryPolicy.setProvince(province);
            }
            if (StringUtils.hasText(city)) {
                queryPolicy.setCity(city);
            }
            if (StringUtils.hasText(district)) {
                queryPolicy.setDistrict(district);
            }

            QueryWrapper<PolPolicy> queryWrapper = new QueryWrapper<>(queryPolicy);

            // 关键词搜索
            if (StringUtils.hasText(keyword)) {
                queryWrapper.and(wrapper -> wrapper
                        .like("title", keyword)
                        .or()
                        .like("summary", keyword)
                        .or()
                        .like("content", keyword));
            }

            // 按发布时间倒序
            queryWrapper.orderByDesc("publish_date");
            queryWrapper.orderByDesc("create_time");

            // 分页查询
            Page<PolPolicy> page = new Page<>(pageNum, pageSize);
            IPage<PolPolicy> pageResult = polPolicyService.page(page, queryWrapper);

            // 构建返回结果
            Map<String, Object> result = new HashMap<>();
            result.put("total", pageResult.getTotal());
            result.put("pageNum", pageNum);
            result.put("pageSize", pageSize);
            result.put("totalPages", pageResult.getPages());

            List<Map<String, Object>> policies = new ArrayList<>();
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");

            for (PolPolicy policy : pageResult.getRecords()) {
                Map<String, Object> policyMap = new HashMap<>();
                policyMap.put("id", policy.getId());
                policyMap.put("title", policy.getTitle());
                policyMap.put("policyCode", policy.getPolicyCode());
                policyMap.put("policyLevel", policy.getPolicyLevel());
                policyMap.put("policyType", policy.getPolicyType());
                policyMap.put("publishOrg", policy.getPublishOrg());
                policyMap.put("publishDate",
                        policy.getPublishDate() != null ? sdf.format(policy.getPublishDate()) : null);
                policyMap.put("summary", policy.getSummary());
                policyMap.put("province", policy.getProvince());
                policyMap.put("city", policy.getCity());
                policyMap.put("district", policy.getDistrict());
                policies.add(policyMap);
            }

            result.put("policies", policies);
            result.put("message", String.format("找到 %d 条政策", pageResult.getTotal()));

            log.info("MCP搜索政策成功: keyword={}, level={}, type={}, 结果数={}, 返回{}条数据",
                    keyword, policyLevel, policyType, pageResult.getTotal(), policies.size());
            log.info("========== MCP方法调用结束: searchPolicy ==========");

            return result;
        } catch (Exception e) {
            log.error("MCP搜索政策失败: keyword={}, level={}, type={}", keyword, policyLevel, policyType, e);
            Map<String, Object> errorResult = new HashMap<>();
            errorResult.put("error", "搜索政策失败: " + e.getMessage());
            errorResult.put("policies", Collections.emptyList());
            log.info("========== MCP方法调用结束: searchPolicy (异常) ==========");
            return errorResult;
        }
    }

    /**
     * 获取政策详情
     * 根据政策ID获取政策的详细信息
     */
    @McpTool(description = "根据政策ID获取政策的详细信息,包括标题、内容、发布机构、发布时间、适用范围等完整信息。")
    public Map<String, Object> getPolicyDetail(
            @McpToolParam(description = "政策ID", required = true) Long policyId) {

        log.info("========== MCP方法调用: getPolicyDetail ==========");
        log.info("参数: policyId={}", policyId);

        try {
            PolPolicy policy = polPolicyService.getById(policyId);

            if (policy == null) {
                log.warn("MCP获取政策详情失败: 政策不存在, policyId={}", policyId);
                Map<String, Object> errorResult = new HashMap<>();
                errorResult.put("error", "政策不存在,ID: " + policyId);
                log.info("========== MCP方法调用结束: getPolicyDetail (失败) ==========");
                return errorResult;
            }

            // 检查政策状态
            if (!"APPROVED".equals(policy.getAuditStatus()) ||
                    !"PUBLISHED".equals(policy.getPolicyStatus())) {
                log.warn("MCP获取政策详情失败: 政策未发布或未审核通过, policyId={}, auditStatus={}, policyStatus={}",
                        policyId, policy.getAuditStatus(), policy.getPolicyStatus());
                Map<String, Object> errorResult = new HashMap<>();
                errorResult.put("error", "政策未发布或未审核通过");
                log.info("========== MCP方法调用结束: getPolicyDetail (失败) ==========");
                return errorResult;
            }

            // 构建返回结果
            Map<String, Object> result = new HashMap<>();
            result.put("id", policy.getId());
            result.put("title", policy.getTitle());
            result.put("policyCode", policy.getPolicyCode());
            result.put("policyLevel", policy.getPolicyLevel());
            result.put("policyType", policy.getPolicyType());
            result.put("publishOrg", policy.getPublishOrg());
            result.put("publishDate", policy.getPublishDate());
            result.put("summary", policy.getSummary());
            result.put("content", policy.getContent());
            result.put("province", policy.getProvince());
            result.put("city", policy.getCity());
            result.put("district", policy.getDistrict());
            result.put("tags", policy.getTags());
            result.put("message", "获取政策详情成功");

            log.info("MCP获取政策详情成功: policyId={}, title={}", policyId, policy.getTitle());
            log.info("========== MCP方法调用结束: getPolicyDetail ==========");

            return result;
        } catch (Exception e) {
            log.error("MCP获取政策详情失败: policyId={}", policyId, e);
            Map<String, Object> errorResult = new HashMap<>();
            errorResult.put("error", "获取政策详情失败: " + e.getMessage());
            log.info("========== MCP方法调用结束: getPolicyDetail (异常) ==========");
            return errorResult;
        }
    }

    /**
     * 根据政策编号查询政策
     */
    @McpTool(description = "根据政策编号(政策代码)查询政策信息。政策编号是政策的唯一标识符。")
    public Map<String, Object> getPolicyByCode(
            @McpToolParam(description = "政策编号(政策代码)", required = true) String policyCode) {

        log.info("========== MCP方法调用: getPolicyByCode ==========");
        log.info("参数: policyCode={}", policyCode);

        try {
            QueryWrapper<PolPolicy> queryWrapper = new QueryWrapper<>();
            queryWrapper.eq("policy_code", policyCode);
            queryWrapper.eq("audit_status", "APPROVED");
            queryWrapper.eq("policy_status", "PUBLISHED");

            PolPolicy policy = polPolicyService.getOne(queryWrapper);

            if (policy == null) {
                log.warn("MCP根据编号查询政策失败: 未找到政策, policyCode={}", policyCode);
                Map<String, Object> errorResult = new HashMap<>();
                errorResult.put("error", "未找到编号为 " + policyCode + " 的政策");
                log.info("========== MCP方法调用结束: getPolicyByCode (失败) ==========");
                return errorResult;
            }

            // 构建返回结果
            Map<String, Object> result = new HashMap<>();
            result.put("id", policy.getId());
            result.put("title", policy.getTitle());
            result.put("policyCode", policy.getPolicyCode());
            result.put("policyLevel", policy.getPolicyLevel());
            result.put("policyType", policy.getPolicyType());
            result.put("publishOrg", policy.getPublishOrg());
            result.put("publishDate", policy.getPublishDate());
            result.put("summary", policy.getSummary());
            result.put("content", policy.getContent());
            result.put("message", "查询政策成功");

            log.info("MCP根据编号查询政策成功: policyCode={}, title={}", policyCode, policy.getTitle());
            log.info("========== MCP方法调用结束: getPolicyByCode ==========");

            return result;
        } catch (Exception e) {
            log.error("MCP根据编号查询政策失败: policyCode={}", policyCode, e);
            Map<String, Object> errorResult = new HashMap<>();
            errorResult.put("error", "查询政策失败: " + e.getMessage());
            log.info("========== MCP方法调用结束: getPolicyByCode (异常) ==========");
            return errorResult;
        }
    }

    /**
     * 政策问答
     * 根据用户问题,从政策库中查找相关信息并回答
     */
    @McpTool(description = "回答关于政策的问题。根据用户的问题,从政策库中搜索相关政策和信息,提供准确的答案。支持询问政策内容、申请条件、适用范围等问题。")
    public Map<String, Object> answerPolicyQuestion(
            @McpToolParam(description = "用户的问题,例如:'如何申请农业补贴?'、'有哪些农业贷款政策?'等", required = true) String question) {

        log.info("========== MCP方法调用: answerPolicyQuestion ==========");
        log.info("参数: question={}", question);

        try {
            if (!StringUtils.hasText(question)) {
                log.warn("MCP政策问答失败: 问题为空");
                Map<String, Object> errorResult = new HashMap<>();
                errorResult.put("error", "问题不能为空");
                log.info("========== MCP方法调用结束: answerPolicyQuestion (失败) ==========");
                return errorResult;
            }

            // 从问题中提取关键词
            String keyword = extractKeywords(question);
            log.debug("提取的关键词: {}", keyword);

            // 搜索相关政策
            QueryWrapper<PolPolicy> queryWrapper = new QueryWrapper<>();
            queryWrapper.eq("audit_status", "APPROVED");
            queryWrapper.eq("policy_status", "PUBLISHED");
            queryWrapper.and(wrapper -> wrapper
                    .like("title", keyword)
                    .or()
                    .like("summary", keyword)
                    .or()
                    .like("content", keyword));
            queryWrapper.orderByDesc("publish_date");
            queryWrapper.last("LIMIT 5"); // 最多返回5条相关政策

            List<PolPolicy> relatedPolicies = polPolicyService.list(queryWrapper);

            // 构建回答
            Map<String, Object> result = new HashMap<>();
            result.put("question", question);

            if (relatedPolicies.isEmpty()) {
                result.put("answer", "抱歉,未找到与您的问题相关的政策信息。建议您尝试使用其他关键词搜索,或联系相关部门咨询。");
                result.put("relatedPolicies", Collections.emptyList());
            } else {
                StringBuilder answerBuilder = new StringBuilder();
                answerBuilder.append("根据您的问题,我找到了以下相关政策信息:\n\n");

                List<Map<String, Object>> policyList = new ArrayList<>();
                SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");

                for (int i = 0; i < relatedPolicies.size(); i++) {
                    PolPolicy policy = relatedPolicies.get(i);
                    answerBuilder.append(String.format("%d. %s\n", i + 1, policy.getTitle()));
                    if (StringUtils.hasText(policy.getSummary())) {
                        answerBuilder.append(String.format("   摘要:%s\n", policy.getSummary()));
                    }
                    answerBuilder.append(String.format("   发布机构:%s\n", policy.getPublishOrg()));
                    if (policy.getPublishDate() != null) {
                        answerBuilder.append(String.format("   发布时间:%s\n", sdf.format(policy.getPublishDate())));
                    }
                    answerBuilder.append("\n");

                    Map<String, Object> policyMap = new HashMap<>();
                    policyMap.put("id", policy.getId());
                    policyMap.put("title", policy.getTitle());
                    policyMap.put("summary", policy.getSummary());
                    policyMap.put("policyCode", policy.getPolicyCode());
                    policyList.add(policyMap);
                }

                answerBuilder.append("如需了解更多详情,请查看具体政策内容。");

                result.put("answer", answerBuilder.toString());
                result.put("relatedPolicies", policyList);
            }

            result.put("message", "问题已回答");

            log.info("MCP政策问答成功: question={}, 找到{}条相关政策", question, relatedPolicies.size());
            log.info("========== MCP方法调用结束: answerPolicyQuestion ==========");

            return result;
        } catch (Exception e) {
            log.error("MCP政策问答失败: question={}", question, e);
            Map<String, Object> errorResult = new HashMap<>();
            errorResult.put("error", "回答问题失败: " + e.getMessage());
            errorResult.put("answer", "抱歉,处理您的问题时出现错误,请稍后重试。");
            log.info("========== MCP方法调用结束: answerPolicyQuestion (异常) ==========");
            return errorResult;
        }
    }

    /**
     * 从问题中提取关键词
     */
    private String extractKeywords(String question) {
        // 简单的关键词提取逻辑
        // 可以移除常见的疑问词和助词
        String cleaned = question
                .replaceAll("(如何|怎样|怎么|什么|哪些|哪里|哪个|为什么|是否|有没有)", "")
                .replaceAll("(的|了|吗|呢|啊|呀)", "")
                .trim();

        // 如果清理后为空,返回原问题
        if (!StringUtils.hasText(cleaned)) {
            return question;
        }

        return cleaned;
    }
}

第三步:在appclication.yml中配置参数

# 参考文档:https://docs.spring.io/spring-ai/reference/guides/getting-started-mcp.html
spring:
  ai:
    mcp:
      server:
        annotation-scanner:
          enabled: true
        enabled: true  # 启用MCP Server
        protocol: SSE  # 使用SSE协议
        name: Policy MCP Server
        version: 1.0.0
        stdio: false  # 禁用stdio模式,使用SSE
        #  SSE端点配置
        sse-endpoint: /mcp/sse  # SSE连接端点
        sse-message-endpoint: /mcp/message  # 消息端点

到目前来说该写的代码,改配置的都已经可以了,接下来就是启动服务了
启动我的服务AgriSystemApplication
 

接下来就是测试了
先用apifox测一把,测试没有问题

接下来就是接入到大模型中进行测试了

我使用的是Cherro Studio进行测试
这是我的配置

但实际是报错了

Error activating server:Error POSTing to endpoint (HTTP 404): <!doctype html><htmllang="en"><head><title>HTTP Status 404-Not Found</title><styletype="text/css">body {Font-family:Tahoma,Arial,sans-serif;} h1,h2, h3,b{color:white;background-color:#525D76:} h1 {Font-size:22px;} h2 {font-size:16px;}h3{Font-size:14px;} p {font-size:12px;} a {color:black;}.line {height:1px;background-color:#525D76;border:none;}</style></head><body><h1>HTTP Status 404-NotFound</h1><hr class="line"/><p><b>Type</b> Status Report</p><p><b>Description</b> The origin server did not find a current representation For thetarget resource or is not willing to disclose that one exists.</p><hr class="line"/><h3>Apache Tomcat/10.1.44</h3></body></html>


接下来就是折腾了我快2天时间,一直以为是什么地方写的不对,用把Cursro也用上了,还是没有解决,吐槽一下Cursor,也是经验吧,Cursor干一些常规并且普通的代码还是非常好的,但是对一些比较新,但是很少人遇到的问题他解决不了,还得上人工,所以高级工程师还是有饭吃的。
官方文档也完整读了至少2遍,还是没有发现问题
Model Context Protocol (MCP) :: Spring AI Reference
直到我发现一篇文章,忽然恍然大悟,就是在浏览器和apifox为啥都行,cherro studio上就不行了
SSE MCP server fails when URL contains subpaths
https://github.com/modelcontextprotocol/python-sdk/issues/490
是不支持子路径
然后我又把官方文档翻了一遍,终于被我发现了,
解决方案就在这里
https://docs.spring.io/spring-ai/reference/api/mcp/mcp-stdio-sse-server-boot-starter-docs.html
base-url这个参数的配置加上就可以了,因为之前项目配置有前缀

context-path: /agri

好吧那就是试试,修改我的配置

spring:
  ai:
    mcp:
      server:
        annotation-scanner:
          enabled: true
        enabled: true  # 启用MCP Server
        protocol: SSE  # 使用SSE协议
        name: Policy MCP Server
        version: 1.0.0
        stdio: false  # 禁用stdio模式,使用SSE
        #  SSE端点配置
        base-url: /agri   #解决前缀问题
        sse-endpoint: /mcp/sse  # SSE连接端点
        sse-message-endpoint: /mcp/message  # 消息端点

终于搞定继续测试,选择我自己本地的MCP

结果出来了,搞定。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐