SpringBoot+AI 大模型 MCP实战:农业政策智能推荐系统开发
在项目中通过Spring AI 实现自己的第一个MCP Server
最近做了一个农业政策推荐的项目,里边涉及到了一些业务需要使用到大模型,之前一直都是用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
结果出来了,搞定。

更多推荐



所有评论(0)