Appearance
Spring Boot + Spring AI:Function Call、工具链调用与 MCP
当最小聊天接口和流式输出已经跑通后,下一步最自然的问题通常就是:
模型怎么和外部世界发生真实交互?
这一步通常会遇到 3 个词:
Function Call- 工具链调用
MCP
它们经常一起出现,但不是一层东西。
1. 先分清这 3 层
1.1 Function Call
它更偏模型侧能力,回答的是:
模型如何表达“我想调用哪个工具,参数是什么”。
1.2 工具链调用
它更偏应用侧落地,回答的是:
Spring Boot 应用拿到工具调用意图后,具体怎么去调用本地 Bean、第三方 SDK、REST API 或内部系统。
1.3 MCP
它更偏协议层,回答的是:
宿主应用如何用更标准化的方式去发现和接入外部工具 / 资源 / Prompt。
🌟 简单记就是:Function Call 是模型怎么表达意图,MCP 是宿主怎么对接能力,中间真正执行工具的还是你的 Spring 应用。
2. Function Call 在 Spring Boot 项目里怎么落
最常见的思路是:
- 用 Spring Bean 承接业务工具
- 给方法补工具描述
- 让
ChatClient在调用时感知这些工具 - 由模型决定是否调用
真正落地时,最值得先分清的不是“注解怎么写”,而是:
- 哪些能力应该暴露成 Tool
- Tool 的参数应该收多粗
- Tool 的返回值应该返回什么粒度
- 有副作用的动作该加哪些保护
2.1 一个最小工具类
java
package com.example.ai.tool;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.stereotype.Component;
/**
* 天气工具示例。
*/
@Component
public class WeatherTools {
/**
* 根据城市名称查询天气。
*
* @param city 城市名
* @return 天气摘要
*/
@Tool(description = "根据城市名称查询天气")
public String getWeather(String city) {
return city + "今天多云,最高温 31 度。";
}
}2.2 一个带工具调用的 Service
java
package com.example.ai.service;
import com.example.ai.tool.WeatherTools;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Service;
/**
* 带工具调用能力的 AI 服务。
*/
@Service
public class AiToolService {
private final ChatClient chatClient;
private final WeatherTools weatherTools;
public AiToolService(ChatClient.Builder chatClientBuilder, WeatherTools weatherTools) {
this.chatClient = chatClientBuilder.build();
this.weatherTools = weatherTools;
}
/**
* 允许模型按需调用本地天气工具。
*
* @param question 用户问题
* @return 模型最终回答
*/
public String askWithTool(String question) {
return chatClient.prompt()
.user(question)
.tools(weatherTools)
.call()
.content();
}
}2.3 Tool 设计时最容易忽略的 3 个点
2.3.1 参数要业务化,不要底层化
模型更适合看到这种参数:
cityorderNostartDatedocType
而不是这种底层细节:
- 原始 SQL
- 任意 URL
- 任意脚本片段
- Redis Key 模式
如果参数太底层,模型虽然“能调”,但很难稳定。
2.3.2 返回值要够用,但不要把无关细节全吐给模型
例如查询订单时,更适合返回:
- 订单号
- 当前状态
- 金额
- 关键时间点
而不是直接把整份数据库对象、几十个字段、调试日志全塞回去。
2.3.3 有副作用的工具一定要先分级
可以粗分成 3 类:
| 等级 | 例子 | 处理建议 |
|---|---|---|
| 只读 | 查天气、查订单、查知识库 | 可直接开放给模型调用 |
| 低风险写操作 | 创建草稿、生成报表任务 | 建议补幂等和审计 |
| 高风险写操作 | 删除数据、发通知、下单、转账 | 建议加权限、确认、限流和审计 |
3. 一次工具调用完整链路怎么走
mermaid
sequenceDiagram
participant U as 用户
participant C as Controller
participant S as AiToolService
participant M as 模型
participant T as 本地 Tool Bean
U->>C: 帮我查北京天气
C->>S: askWithTool(question)
S->>M: 用户问题 + tools 描述
M-->>S: 调用 getWeather(city=北京)
S->>T: 执行本地工具
T-->>S: 返回天气结果
S->>M: 回传工具结果
M-->>S: 生成最终答案
S-->>C: 返回最终答案4. 工具链调用不一定都是本地 Bean
真实项目里,工具链往往不只一种来源。
更常见的 4 类来源是:
- 本地
Spring Bean - 第三方 SDK
- 内部 REST / RPC 接口
- 数据库、缓存、搜索系统
4.1 一个更贴近工程的包装方式
不要让模型直接“接触一堆基础设施细节”。更自然的做法是:
- 把业务语义包成 Tool
- Tool 再去调用真正的 Repository / SDK / API Client
例如:
java
package com.example.ai.tool;
import com.example.ai.client.OrderClient;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.stereotype.Component;
/**
* 订单工具示例。
*/
@Component
public class OrderTools {
private final OrderClient orderClient;
public OrderTools(OrderClient orderClient) {
this.orderClient = orderClient;
}
/**
* 根据订单号查询订单摘要。
*
* @param orderNo 订单号
* @return 订单摘要
*/
@Tool(description = "根据订单号查询订单摘要")
public String getOrderSummary(String orderNo) {
return orderClient.queryOrderSummary(orderNo);
}
}这样写的好处是:
- 模型看到的是业务语义化工具
- 具体调用 REST / RPC / DB 的细节被收在应用内部
4.2 在项目里更稳的分层方式
如果工具能力准备长期维护,通常建议至少拆成下面几层:
Tool Bean:直接暴露给模型的业务语义入口Application Service:组织业务主流程Client / Repository:访问外部系统或数据源Audit / Permission:负责审计和权限控制
mermaid
flowchart TD
A[模型输出工具调用意图] --> B[Tool Bean]
B --> C[Application Service]
C --> D[Client / Repository / SDK]
C --> E[Permission Guard]
C --> F[Audit Log]🌟 这样拆的关键价值是:模型看到的是稳定业务语义,基础设施细节和风控逻辑仍然留在应用内部。
4.3 一个更贴近工程的工具骨架
java
package com.example.ai.tool;
import com.example.ai.service.OrderQueryService;
import com.example.ai.service.ToolAuditService;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.stereotype.Component;
/**
* 暴露给模型的订单查询工具。
*/
@Component
public class OrderQueryTools {
private final OrderQueryService orderQueryService;
private final ToolAuditService toolAuditService;
public OrderQueryTools(
OrderQueryService orderQueryService,
ToolAuditService toolAuditService) {
this.orderQueryService = orderQueryService;
this.toolAuditService = toolAuditService;
}
/**
* 根据订单号查询订单摘要。
*
* @param orderNo 订单号
* @return 适合返回给模型的订单摘要
*/
@Tool(description = "根据订单号查询订单状态、金额和关键时间点")
public String getOrderSummary(String orderNo) {
String result = orderQueryService.querySummary(orderNo);
toolAuditService.recordReadTool("getOrderSummary", orderNo);
return result;
}
}这段代码最想表达的不是“写法花样”,而是:
- Tool Bean 自己不要承担太多业务逻辑
- 审计不要等到后面出问题才补
- 工具返回值要围绕模型真正需要的上下文来组织
5. MCP 在 Spring Boot 项目里落在哪一层
这一步最容易混。
更自然的理解是:
Spring AI是应用内 AI 编程模型MCP是外部能力接入协议
5.1 方案一:Spring Boot 应用是 AI 宿主
这时更常见的是:
- 应用自己接模型
- 同时通过
MCP Client去发现和调用外部MCP Server
5.2 方案二:Spring Boot 应用本身暴露 MCP 能力
这时更常见的是:
- 应用内部已有一批业务能力 Bean
- 再把这些能力封成
MCP Server对外给别的 AI 宿主使用
5.2.1 这时最常见的注解和 API 是什么
如果这一层准备写成真正可运行的 MCP Server,光知道“有 Host / Client / Server”还不够,通常还要认识下面这些 API:
| 名称 | 更常见的用途 |
|---|---|
@McpTool | 把一个方法暴露成 MCP Tool |
@McpToolParam | 给 Tool 参数补名称、描述、必填信息 |
McpSyncRequestContext | 在 Tool 执行时拿请求上下文、记录日志或进度 |
@McpResource | 暴露只读资源 |
@McpPrompt | 暴露可复用的 Prompt 模板 |
SyncMcpToolCallbackProvider / AsyncMcpToolCallbackProvider | 把远程 MCP 工具转成 Spring AI 可用的 ToolCallback |
SyncMcpToolCallback / AsyncMcpToolCallback | 把单个 MCP Tool 适配成 Spring AI 的工具接口 |
McpToolUtils | 在 ToolCallback 和 MCP Tool 规格之间做转换 |
🌟 这里最容易混的点是:
@McpTool、@McpToolParam更偏 把能力暴露成 MCP ServerSyncMcpToolCallbackProvider、AsyncMcpToolCallbackProvider更偏 消费远程 MCP Server 的工具
也就是说,这不是一组“并列注解”,而是两侧不同角色会用到的 API。
5.2.2 @McpTool 和 @McpToolParam 怎么用
如果你准备把 Spring Boot 应用本身暴露成 MCP Server,最常见的写法会长这样:
java
package com.example.ai.mcp;
import org.springframework.ai.mcp.annotation.McpTool;
import org.springframework.ai.mcp.annotation.McpToolParam;
import org.springframework.ai.mcp.McpSyncRequestContext;
import org.springframework.stereotype.Component;
/**
* 暴露给 MCP Client 的工具服务。
*/
@Component
public class DocumentTools {
/**
* 根据关键字搜索文档。
*
* @param keyword 查询关键字
* @param limit 返回条数
* @param context MCP 请求上下文
* @return 搜索结果摘要
*/
@McpTool(name = "search-documents", description = "根据关键字搜索知识库文档")
public String searchDocuments(
@McpToolParam(description = "用户输入的搜索关键字", required = true) String keyword,
@McpToolParam(description = "最多返回多少条结果") Integer limit,
McpSyncRequestContext context) {
context.info("search-documents called with keyword=" + keyword);
int size = limit == null ? 5 : Math.min(limit, 10);
return "共找到 " + size + " 条与 `" + keyword + "` 相关的文档结果";
}
}这段代码里最值得注意的不是注解本身,而是 3 个边界:
@McpTool标的是“这个方法可以被当成 MCP Tool 调用”@McpToolParam负责把参数信息补进 Tool 的描述和 SchemaMcpSyncRequestContext这种上下文参数是框架注入的,不是给模型看的业务参数
把这一层摊开看:
- 模型或宿主真正会看到的是 Tool 名、描述和参数 Schema
McpSyncRequestContext这类参数更像服务端执行时的辅助上下文- 如果参数没有描述,模型往往更难稳定地用对工具
5.2.3 McpSyncRequestContext 这类上下文参数是干什么的
这类参数更适合用来做:
- 记录工具执行日志
- 报告执行进度
- 读取请求上下文信息
- 在长耗时任务里给客户端回传状态
它不适合承载业务入参本身,因为业务入参应该仍然写在 @McpToolParam 上。
5.2.4 除了 Tool,还会用到哪些注解
如果场景继续变复杂,常见还会遇到:
@McpResource:暴露只读资源,更适合“把内容拿给模型看”@McpPrompt:暴露 Prompt 模板,更适合“给宿主复用提示结构”
把分工拉开:
@McpTool偏执行动作@McpResource偏读取内容@McpPrompt偏复用提示模板
5.2.5 消费远程 MCP 工具时,为什么更常见的是 Provider/Callback API
如果你的 Spring Boot 应用是宿主,要去消费别人的 MCP Server,更常见的入口反而不是 @McpTool,而是:
SyncMcpToolCallbackProviderAsyncMcpToolCallbackProviderSyncMcpToolCallbackAsyncMcpToolCallback
它们解决的问题是:
怎么把远程 MCP Server 上发现到的工具,转成 Spring AI 的 ToolCallback,让 ChatClient 可以继续使用。
例如:
java
package com.example.ai.mcp;
import io.modelcontextprotocol.client.McpSyncClient;
import org.springframework.ai.mcp.SyncMcpToolCallbackProvider;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 把远程 MCP Server 的工具适配成 Spring AI 可用的 ToolCallback。
*/
@Configuration
public class McpClientToolConfig {
@Bean
public ToolCallbackProvider remoteToolProvider(McpSyncClient mcpSyncClient) {
return SyncMcpToolCallbackProvider.builder()
.mcpClients(mcpSyncClient)
.build();
}
@Bean
public ToolCallback[] remoteToolCallbacks(ToolCallbackProvider remoteToolProvider) {
return remoteToolProvider.getToolCallbacks();
}
}这段代码最想说明的是:
- 远程 MCP 工具要先适配成
ToolCallback - 适配完之后,它才能进入 Spring AI 的工具调用体系
- 所以
MCP Client这一侧,重点通常不是注解,而是 Provider / Callback
5.2.6 这些远程 MCP Tool 最后怎么挂到 ChatClient
继续沿着上面的 ToolCallbackProvider 往下走,常见会像这样:
java
package com.example.ai.service;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.stereotype.Service;
/**
* 使用远程 MCP Tool 的 AI 服务。
*/
@Service
public class RemoteMcpToolService {
private final ChatClient chatClient;
private final ToolCallbackProvider remoteToolProvider;
public RemoteMcpToolService(
ChatClient.Builder chatClientBuilder,
ToolCallbackProvider remoteToolProvider) {
this.chatClient = chatClientBuilder.build();
this.remoteToolProvider = remoteToolProvider;
}
public String ask(String question) {
return chatClient.prompt()
.user(question)
.tools(remoteToolProvider)
.call()
.content();
}
}这时可以把整条链路压成一句话:
- Server 侧用
@McpTool、@McpToolParam把能力暴露出去 - Client 侧用
SyncMcpToolCallbackProvider/AsyncMcpToolCallbackProvider把远程工具接进来 - 最后再把这些 ToolCallback 挂给
ChatClient
5.3 什么时候更适合直接用本地 Tool,什么时候适合上 MCP
这个判断很重要,因为很多团队会过早把问题做重。
| 场景 | 更合适的做法 | 原因 |
|---|---|---|
| 只有当前 Spring Boot 应用自己要用 | 本地 Tool / Service | 接入最直接,排障也最简单 |
| 能力已经沉淀成共享服务,未来多个宿主要复用 | MCP | 协议统一后更容易复用 |
| 只是简单调用一个内部 REST API | 先包成 Tool | 没必要为了标准化过早引入额外协议层 |
| 需要同时暴露工具、资源、Prompt | MCP 更合适 | 这类场景更接近 MCP 的原生定位 |
落到选型上,通常就是这 3 句:
- 本地能力优先本地 Tool
- 开始跨宿主复用时再考虑
MCP MCP适合做“共享能力层”,不适合当所有 AI 项目的必选起点
5.4 Spring Boot 作为 MCP Client 时,通常怎么组织
当你的 Spring Boot 应用是 AI 宿主时,更常见的组织方式是:
- 应用内仍然保留
ChatClient - 再加一层
McpCapabilityService - 由这层统一负责连接外部
MCP Server - 对模型来说,它拿到的仍然是“可调用工具”
java
package com.example.ai.mcp;
import java.util.List;
import org.springframework.stereotype.Service;
/**
* 负责统一管理外部 MCP 能力。
*
* 这里用语义化占位接口表达职责边界,
* 重点是说明 Spring Boot 项目里应该把 MCP 连接管理单独收一层。
*/
@Service
public class McpCapabilityService {
private final McpSessionRegistry mcpSessionRegistry;
public McpCapabilityService(McpSessionRegistry mcpSessionRegistry) {
this.mcpSessionRegistry = mcpSessionRegistry;
}
/**
* 列出当前可用的远程工具。
*
* @return 工具描述列表
*/
public List<String> listRemoteTools() {
return mcpSessionRegistry.listTools();
}
/**
* 调用指定 MCP 工具。
*
* @param toolName 工具名
* @param arguments 参数
* @return 工具结果
*/
public String callRemoteTool(String toolName, String arguments) {
return mcpSessionRegistry.callTool(toolName, arguments);
}
}这层最重要的意义是:
MCP连接和会话管理不要散落在业务 Controller 里- 业务方拿到的是“能力服务”,不是协议细节
- 后面无论切换
stdio还是HTTP,影响面都会更小
5.5 Spring Boot 作为 MCP Server 时,更适合暴露什么
如果你的 Spring Boot 应用准备把能力对外开放成 MCP Server,更适合暴露的是:
- 业务语义明确的工具
- 只读或低风险的资源
- 可复用的 Prompt 模板
不太适合直接暴露的是:
- 原始数据库查询能力
- 任意 HTTP 代理能力
- 无边界的文件系统写入能力
- 高风险且没有审计的写操作
5.6 一个更务实的 MCP Server 暴露思路
可以按下面顺序做:
- 把应用内已有 Tool 整理成稳定业务能力
- 再挑出跨宿主复用价值高的那一批
- 把它们重新包装成
MCP Server暴露能力 - 最后再补鉴权、审计、调用额度和可观测性
🌟 要注意的是:MCP Server 不是“把系统里的所有能力都开放出去”,而是挑选那些适合标准化复用的能力。
6. Function Call 和 MCP 到底是什么关系
mermaid
flowchart TD
A[用户问题] --> B[Spring Boot 宿主把上下文发给模型]
B --> C{模型是否需要外部能力}
C -- 否 --> D[直接生成答案]
C -- 是 --> E[模型输出 Function Call 意图]
E --> F[宿主解析工具名和参数]
F --> G{工具是否通过 MCP 暴露}
G -- 是 --> H[通过 MCP Client 调 MCP Server]
G -- 否 --> I[直接调 Spring Bean / SDK / REST API]
H --> J[拿到工具结果]
I --> J
J --> K[把结果回传模型]
K --> L[模型生成最终答案]这张图最想说明的是:
Function Call不等于MCP- 模型先表达“要调什么”
- Spring Boot 宿主再决定“具体怎么调”
MCP只是其中一种更标准化的接入方式
6.1 在 Spring 项目里,一次“模型调 MCP 工具”通常会多出哪一层
如果换成项目视角,这条链路通常更像这样:
- Controller 收到用户请求
- AI Service 组织 Prompt
- 模型表达工具调用意图
- 宿主应用判断这个工具是本地 Tool 还是远程 MCP Tool
- 如果是远程 MCP Tool,就交给
McpCapabilityService - 拿到结果后,再继续回传模型生成最终答案
这也正是为什么:
MCP更像“能力接入层”- 业务 Service 不应该直接堆协议处理代码
- 模型、工具、协议三层最好保持边界清楚
7. 一个更完整的应用骨架
java
package com.example.ai.web;
import com.example.ai.service.AiToolService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* 工具调用示例接口。
*/
@RestController
public class AiToolController {
private final AiToolService aiToolService;
public AiToolController(AiToolService aiToolService) {
this.aiToolService = aiToolService;
}
@GetMapping("/demo/ai/tool")
public String tool(@RequestParam String q) {
return aiToolService.askWithTool(q);
}
}7.1 一个把本地 Tool 和 MCP Tool 收在一起的 Service 骨架
java
package com.example.ai.service;
import com.example.ai.mcp.McpCapabilityService;
import com.example.ai.tool.OrderQueryTools;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Service;
/**
* 统一组织本地工具和远程 MCP 能力的服务。
*/
@Service
public class AiCapabilityService {
private final ChatClient chatClient;
private final OrderQueryTools orderQueryTools;
private final McpCapabilityService mcpCapabilityService;
public AiCapabilityService(
ChatClient.Builder chatClientBuilder,
OrderQueryTools orderQueryTools,
McpCapabilityService mcpCapabilityService) {
this.chatClient = chatClientBuilder.build();
this.orderQueryTools = orderQueryTools;
this.mcpCapabilityService = mcpCapabilityService;
}
/**
* 统一处理需要外部能力的问题。
*
* @param question 用户问题
* @return 最终答案
*/
public String ask(String question) {
// 演示里只保留主线:本地工具直接挂到 ChatClient;
// 远程 MCP 能力则由独立服务统一收口。
String remoteToolSummary = String.join(", ", mcpCapabilityService.listRemoteTools());
return chatClient.prompt()
.system("""
你是一名企业助手。
本地可用工具包括订单查询;
远程 MCP 工具包括:%s。
只有在确实需要时才调用工具。
""".formatted(remoteToolSummary))
.user(question)
.tools(orderQueryTools)
.call()
.content();
}
}这段代码故意没有把所有远程 MCP 调用细节都展开,因为它最想说明的是:
- 本地 Tool 和远程
MCP能力可以共存 - 它们最好收在同一层能力编排服务里
- Controller 依旧保持轻,只负责暴露接口
8. 如果要落到真实项目,第二阶段最该补什么
比起继续堆更多 demo,第二阶段更值得先补的是下面这些能力:
- 工具权限控制
- 工具调用审计
- 超时与重试
- 幂等与确认
- 工具返回值裁剪
MCP连接稳定性和降级策略
8.1 一张“能跑”到“能稳”的补齐清单
| 能力 | 为什么需要 | 更常见的落点 |
|---|---|---|
| 权限控制 | 防止模型越权调用敏感工具 | Tool 前置校验 / Spring Security |
| 审计日志 | 方便追查谁调了什么工具 | Tool Service / Audit Service |
| 超时控制 | 防止远程工具卡住整条链路 | Client / MCP 调用层 |
| 幂等 | 防止重试导致重复写入 | 写操作服务层 |
| 二次确认 | 控制高风险动作 | Agent / Tool 编排层 |
| 降级 | 外部能力不可用时保持主流程可用 | Capability Service |
9. 第二阶段最容易踩的坑
9.1 把 Tool 直接写成“底层能力暴露”
例如直接把:
- SQL 执行器
- Redis 通用操作器
- HTTP 通用客户端
裸暴露给模型,这通常并不稳。
更自然的做法是:
- 对模型暴露“业务语义工具”
- 把底层细节藏在工具内部
9.2 有副作用的工具没有幂等和确认
像发消息、下单、删数据这类工具,一定要继续补:
- 权限控制
- 幂等设计
- 二次确认
- 审计日志
9.3 把 MCP 当成“必须先上”的能力
大多数项目并不是第一天就要上 MCP。
更常见的节奏是:
- 先用本地 Tool 或服务内调用跑通
- 工具能力开始跨宿主复用时,再考虑
MCP
9.4 把 MCP Server 当成“万能适配层”
这样很容易出现两个问题:
- 不该开放的底层能力也被顺手暴露出去
- 真正需要权限控制和审计的动作没有被单独建边界
所以更稳的做法通常是:
- 先收敛业务能力
- 再决定哪些值得开放成
MCP - 最后才谈协议化和共享复用
9.5 远程工具失败时没有降级策略
如果 MCP Server 临时不可用,而宿主完全没有降级,就会出现:
- 单个工具故障拖垮整条问答链路
- 模型反复重试导致成本上涨
- 用户只看到“系统坏了”,却不知道是哪个外部能力出了问题
至少可以补下面几件事:
- 超时
- 熔断或失败快速返回
- 清晰的错误信息
- 无工具时的降级回答
10. 推荐怎么读这一阶段
如果你是按项目落地顺序来看,读这一篇时可以重点盯住下面 4 件事:
- 先分清
Function Call、工具链调用和MCP不是一层 - 把本地 Tool 的业务语义收稳
- 再决定哪些能力值得升级成
MCP - 最后再补权限、审计、幂等和降级
11. 这一篇最重要的结论
第二阶段最重要的不是把所有外部能力都标准化,而是把“模型意图 -> Spring 应用执行 -> 结果回传模型”这条工具调用主线跑顺。Function Call 负责表达意图,工具链负责真正执行,MCP 则是在工具开始跨宿主复用时更有价值的一层协议化能力。