☰
Function Calling/MCP/Skill三者关系及Java落地实践
2026/11/3 10:55:53 网站建设 项目流程

前几天有位读者一口气给我发了三份文档链接,标题分别是《Function Calling 实战》《MCP 协议入门》和《Skill 使用指南》。他问我:这三个东西到底什么关系?我怎么感觉它们都在干同一件事——让 AI 调用工具?我回他:你的感觉没错,但这三样东西确实不是同一层面的概念,把它们混在一起理解,是当前 Agent 开发里最常见的认知障碍。

这篇文章我就用一套话说清楚 Function Calling、MCP、Skill 三者的本质区别,然后给出 Java 视角的完整落地流程。不管你是用 Spring AI 做企业级应用,还是想从底层手写一个最小可用链路,这篇都能直接拿来当参照。

1. 先拆清楚:三者根本不在同一个维度上

很多人默认 Function Calling、MCP、Skill 是三个并列的技术方案,于是产生"我该选哪个"的困惑。这个提问方式本身就是问题。如果硬要打个比方,你可以把这三者想象成打电话这个场景里完全不同的三样东西:

  • Function Calling是“说清楚我想拨哪个号码”的约定。它解决的是大模型如何结构化地表达“我要调用哪个函数、传什么参数”这件事,是模型输出侧的一种格式协议。
  • MCP是“电话网络本身”。它解决的是 AI 应用和外部工具之间如何建连、如何认证、如何传输请求和返回结果,是接入侧的通信协议。
  • Skill更像“客服话术手册”。它封装的是“遇到某类问题应该按什么步骤做、查什么数据、调用什么工具、如何判断结果是否正确”的方法论,是可被模型按需加载的经验包。

三者的典型特征对比如下:

维度Function CallingMCPSkill
解决的核心问题模型如何表达调用意图应用如何接入外部工具模型如何按套路完成任务
作用层面模型输出层系统接入层任务编排层
典型产物tool schema、tool_calls 指令Client/Server、stdio/HTTP 传输SKILL.md、脚本、资源模板
类比拨号动作电话网络客服话术手册
依赖关系MCP 和 Skill 都要借助它触发工具依赖 Function Calling 让模型发起调用依赖 MCP 提供工具方法

所以正确的理解方式不是“三选一”,而是“三层叠加”。Function Calling 在最底下,MCP 架在中间把工具接入统一起来,Skill 放在最上层把使用工具的方法沉淀成可复用资产。下面我逐层拆开讲。

2. Function Calling:模型端最底层的那份“约定”

2.1 它到底解决什么问题

大模型本质上是一个文本生成器,你给它一段 prompt,它给你一段续写。但 Agent 场景里,模型不能只输出文字,它必须能输出“调用指令”,比如:我需要查一下杭州的天气,请执行 get_weather(city=杭州),然后把结果返回给我。

如果没有 Function Calling,这个需求只能靠模型把意图“写”在自然语言里,再由应用层做语义解析,稳定性和准确率都指望不上。而 Function Calling 做的事情非常务实:它在模型能力中内建了“结构化工具调用”的输出格式约定。你给模型一份函数定义清单(tool schema),模型根据用户问题决定是否调用、怎么调用,并以严格的 JSON 结构输出 tool_calls,而不是自由文本。

2.2 一次 Function Calling 的完整链路

我在给团队内部培训时喜欢把这个流程画成一条直线,文字描述下来非常清晰:

  1. 应用构建请求,把系统提示词、用户问题和工具定义清单一起发给模型接口。
  2. 模型判断当前问题需要某个工具,于是返回 assistant 消息,消息里带 tool_calls 字段,内容是函数名和 JSON 格式的参数。
  3. 应用解析 tool_calls,在自己的代码里执行对应函数,拿到真实结果。
  4. 应用把函数执行结果作为 tool 角色的消息回传给模型。
  5. 模型基于工具结果继续生成最终答案。

关键就在于消息格式。一个典型的工具定义长这样:

{ "type": "function", "function": { "name": "get_weather", "description": "查询指定城市的实时天气", "parameters": { "type": "object", "properties": { "city": { "type": "string", "description": "城市名,如:北京" } }, "required": ["city"] } } }

模型返回的调用指令则长这样:

{ "choices": [{ "message": { "role": "assistant", "content": null, "tool_calls": [{ "id": "call_abc123", "type": "function", "function": { "name": "get_weather", "arguments": "{\"city\":\"北京\"}" } }] } }] }

模型不会真的去执行这个函数,它只是表达“我建议你去调用 get_weather,参数是这个”。真正执行的人是你的代码。这就是为什么我一直强调:Function Calling 是协议,不是能力——工具本身跑在哪里、怎么跑,模型完全不关心。

2.3 为什么单独有它还不够

Function Calling 标准化了“模型如何表达意图”,但它没有解决“工具如何被应用发现和接入”的问题。

实操过的同学应该深有体会:我在 A 项目里给模型注册了查天气、查库存、查订单三个函数,到了 B 项目,同样的一套东西又得重新写一遍注册逻辑。每个工具服务商都有自己的接入 SDK,参数格式、鉴权方式、调用方式五花八门。如果只有 Function Calling,Agent 生态就会退化成“每个工具都造一套轮子”的局面。这正是 MCP 出现的原因。

3. MCP:把“工具接入”这件事标准化

3.1 它到底长什么样

MCP(Model Context Protocol)是 Anthropic 提出的开放协议,目标是统一 AI 应用接入外部工具和数据源的方式。它借鉴了 USB-C 的思想:以前每个设备有各自的充电口,现在一个接口通吃;MCP 就是 AI 世界的 USB-C。

它的架构里有四个角色:

  • Host:承载 AI 应用的进程,比如你的 Spring Boot 服务、Claude Desktop 这类客户端。
  • Client:Host 内部负责和 MCP Server 建立连接的组件,负责处理请求路由、响应映射。
  • Server:对外暴露工具、资源和提示词的能力提供方,比如一个文件系统 MCP Server、一个数据库 MCP Server。
  • Transport:Client 与 Server 之间的传输通道,常见的有 stdio、HTTP/SSE、WebSocket 等。

一个 MCP Server 可以暴露三类能力:

  • Tools:可执行的函数,调用后返回结构化结果,这是 Agent 用得最多的能力。
  • Resources:可读取的上下文数据,比如配置文件、文档片段,模型可以按需读取。
  • Prompts:服务器预设的提示词模板,供客户端复用来引导模型完成特定任务。

这种设计带来的直接好处是:工具提供方写一次 MCP Server,所有支持 MCP 的 AI 应用都能用;AI 应用接入一次 MCP Client,就能访问所有 MCP Server 暴露的能力。

3.2 MCP 并没有替代 Function Calling

我在很多技术群里看到有人争论“MCP 出来之后 Function Calling 是不是要没了”。这个说法完全站错了方向。

MCP 负责的是传输和接入,Function Calling 负责的是模型输出约定。实际调用链路里,两者是协作关系:Agent 从 MCP Client 拿到的工具列表,统一转换成模型能理解的 tool schema;模型发起 tool_calls 之后,Agent 把调用意图路由给 MCP Server;Server 执行完返回结果,再回填给模型。

换句话说,Function Calling 是“大脑的意志表达”,MCP 是“手脚的神经网络”。少任何一层,工具调用的闭环都走不通。

3.3 Java 生态里 MCP 已经很活跃

这两年 Java 侧对 MCP 的拥抱速度比我预想的快得多。Spring AI 已经内置 MCP Client 和 Server 的 starter,开源中后台框架开始把 MCP 能力内建到权限体系和菜单管理里,金融行情、数据库运维、浏览器自动化这些工具服务商也陆续提供了 Java 可调用的 MCP Server。如果你还在观望,现在入场并不晚。

4. Skill:比 MCP 更“软”的一层能力封装

4.1 Skill 封装的是“怎么做”

MCP 解决了“有什么工具可以用”,Skill 解决的是“遇到这类任务应该怎么干”。这是一层完全不同的东西。

我举一个具体的例子。假设你要做一个“代码审查 Agent”。MCP 可以给它提供代码仓库读取工具、Git 操作工具、静态扫描工具。但把工具堆给它,它不一定知道审查的正确姿势。这时候 Skill 就派上用场了。

一个代码审查 Skill 通常包含一个说明文档(例如 SKILL.md),里面会写清楚:

  • 什么时候启用这个技能:比如用户要求“帮我 review PR”时。
  • 操作顺序:先读取 diff,再逐个文件检查安全隐患、边界条件、命名规范。
  • 对 MCP 工具的使用方式:明确调用哪些工具,参数怎么传。
  • 输出格式:问题按严重程度分级,每条建议带具体代码位置和修改方向。

模型拿到这份说明后,再结合 MCP 提供的工具,才能真正稳定地完成“审查”这件事。可以说,Skill 是把工程师的经验转译成模型可执行的操作规程,是知识资产化的关键载体。

4.2 不同平台上的 Skill 形态

Skill 并非一个严格统一的协议,更像是一套机制在不同产品里的具象化表现。Claude 的 Agent 体系里有 Skill 目录,Cursor 里有规则文件,Codex 里也有可插拔的 skill 插件机制,一些开源 Agent 框架则用“角色卡片+工具集”的方式实现类似能力。

在 Java 场景里,如果你是自己搭 Agent,最简单也最稳妥的 Skill 实现方式,就是把它当作一套结构化 prompt 模板引擎:把 Skill 说明渲染成系统提示词的一部分,随每次请求发给模型,或者在特定任务被触发时动态注入。这样不需要额外依赖,也能达到类似效果。

4.3 Skill 和 MCP 的边界最容易混

我见过最多的误用,是把 Skill 当 MCP 去用,或者反过来。请记住这条判断准则:

MCP 回答“能调什么”,Skill 回答“该怎么调”。

如果一个能力只是单纯提供数据查询,比如“查天气”“查股票行情”,那它是工具,适合做 MCP。如果一个能力包含多步骤流程和判断规则,比如“排查订单卡单”“分析接口性能瓶颈”,那它是方法,适合做成 Skill。一个完整的 Agent 往往是 MCP 提供弹药、Skill 提供打法,缺一不可。

5. 三者协作的真实时序与选型判断表

5.1 从一次真实请求看协作

为了让关系足够直观,我描述一个完整请求在“MCP + Function Calling + Skill”三层体系下的流转过程,全程不用一张图也能看明白:

  1. 用户给 Agent 发送问题:“帮我查一下最近三天订单量有没有异常。”
  2. Agent 框架先做任务识别,匹配到“订单异常排查 Skill”,于是加载该 Skill 的说明内容,注入当前上下文。
  3. Skill 要求在第一步查询订单趋势数据,指令指向 MCP 的订单统计 Server。
  4. Agent 把订单统计 Server 暴露的工具转换成 tool schema,连同 Skill 说明一起发给大模型。
  5. 模型根据用户意图和 Skill 指引,通过 Function Calling 输出“调用 query_order_trend,参数为近三天”。
  6. Agent 将调用指令交给 MCP Client,MCP Client 转发给订单统计 Server,执行查询。
  7. 结果返回后,Agent 把工具结果作为 tool 消息回传给模型。
  8. 模型结合 Skill 中“异常判定规则”分析结果,如果触发阈值,继续调用另一个异常明细查询工具。
  9. 循环上面过程若干次,直到模型认为信息足够,生成最终结论。

这个流程里,Skill 提供流程指引,MCP 提供执行通道,Function Calling 提供模型与执行层之间的通信语言。三层缺一不可,又彼此独立。

5.2 选型时先问自己处于哪一层

我整理了四类最容易遇到的场景,方便你做技术决策:

场景你真正需要的东西优先级判断
应用要调自有业务方法,模型需要根据用户输入决定调哪个Function Calling 落地先做,这是底座
应用要接入大量异构外部工具,不想每个工具一套 SDKMCP Client 接入,推动工具方提供 Server有痛点就引入
工具已经能调通,但模型输出质量不稳定,经常用错工具、漏步骤Skill 封装 + 流程约束 + 工具描述优化优先做这个
团队要沉淀领域经验,让不同项目复用同一套 Agent 行为Skill 库 + 共享 MCP Server从已有最佳实践提炼

这套判断表我用了几个月,团队从“争论技术选型”变成了“先分层再补位”,效率提升很明显。

6. Java 落地全流程:从零搭一个能跑通的链路

下面进入正题。我给出两条路径:第一条是“纯 JDK 手写 Function Calling 轮询循环”,适合把原理吃透;第二条是“Spring AI + MCP Client/Server”,适合接入生产项目。两条我都踩过坑,细节都写出来。

6.1 路径一:不依赖框架,手写最小 Function Calling 循环

这一步我不引入任何 AI 框架,只用 Java 17 内置的 HttpClient 和 Jackson。目标是把“模型表达意图 → 本地执行函数 → 回传结果 → 生成最终答案”这个闭环跑通。

先定义工具 schema,注意这里的结构要和模型接口要求一致:

String getWeatherTool = """ { "type": "function", "function": { "name": "get_weather", "description": "查询指定城市的实时天气温度", "parameters": { "type": "object", "properties": { "city": { "type": "string", "description": "城市名,如:北京" } }, "required": ["city"] } } } """;

构建请求并把工具清单传给模型接口:

ObjectMapper mapper = new ObjectMapper(); Map<String, Object> message = new HashMap<>(); message.put("role", "user"); message.put("content", "北京今天多少度?"); Map<String, Object> requestBody = new HashMap<>(); requestBody.put("model", "qwen-plus"); // 这里替换为你实际使用的模型 requestBody.put("messages", List.of(message)); requestBody.put("tools", List.of(mapper.readTree(getWeatherTool)));

发送完请求后,重点解析响应中的 tool_calls 字段。我用一个工具注册表来分发执行:

Map<String, Function<String, String>> toolHandlers = new HashMap<>(); toolHandlers.put("get_weather", arguments -> { JsonNode args = mapper.readTree(arguments); String city = args.get("city").asText(); // 这里接你自己的业务逻辑,比如调用天气服务 return "{\"city\":\"" + city + "\",\"temperature\":26,\"unit\":\"C\"}"; });

拿到模型返回的 tool_calls 之后,按这个顺序处理:

  1. 把 assistant 消息(含 tool_calls)追加到 messages 列表。
  2. 对每个 tool_call,从注册表找到 handler,执行并拿到结果。
  3. 构造 tool 角色消息,必须带 tool_call_id,与模型返回的 id 保持一致:
Map<String, Object> toolResult = new HashMap<>(); toolResult.put("role", "tool"); toolResult.put("tool_call_id", toolCallId); toolResult.put("content", resultJson); messages.add(toolResult);
  1. 把完整的 messages 列表再次发给模型,模型就能基于工具结果生成最终回复。

这条手写链路价值极大。你一旦亲手实现过一遍,就会理解为什么 MCP Client 要做那些事:它本质上就是替你把工具列表的注册、传输协议的封装、调用结果的路由全部标准化了。

6.2 路径二:Spring AI 集成 MCP,Server 端暴露工具

项目落到生产环境,我推荐直接用 Spring AI 的 MCP starter,省去大量样板代码。环境要求:JDK 17 以上、Spring Boot 3.2 以上。

pom.xml 里引入关键依赖(版本号以 Spring 官方里程碑版本为准):

<dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-model-openai</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-mcp-server-webmvc</artifactId> </dependency>

先定义一个业务工具类,用@Tool注解暴露方法:

@Service public class MarketTools { @Tool(name = "query_stock_price", description = "查询指定股票代码的最新价格") public Map<String, Object> queryStockPrice( @ToolParam(description = "股票代码,如 AAPL、0700.HK") String code) { // 实际项目中这里调用行情服务,这里返回模拟值 return Map.of( "code", code, "price", 186.42, "currency", "USD", "quoteTime", LocalDateTime.now().toString() ); } }

再把它注册为 MCP Server 上的工具回调:

@Configuration public class McpServerConfig { @Bean public ToolCallbackProvider marketTools(MarketTools marketTools) { return MethodToolCallbackProvider.builder() .toolObjects(marketTools) .build(); } }

这样你的服务本身就是一个 MCP Server,其他支持 MCP 的客户端可以通过 HTTP 或 stdio 接入并调用query_stock_price。

6.3 路径二:Client 端配置与调用

如果你要连接另一个 MCP Server(比如团队的统一数据服务),在 application.yml 里配置 stdio 连接即可:

spring: ai: mcp: client: enabled: true stdio: connections: - name: market-server command: java args: ["-jar", "/opt/mcp-server/market-server.jar", "--server.port=0"]

然后通过 Spring AI 的 ChatClient 让模型自动选择工具:

@Service public class AgentService { private final ChatClient chatClient; public AgentService(ChatClient.Builder builder) { this.chatClient = builder.build(); } public String ask(String question) { return chatClient.prompt(question) .options(ChatOptions.builder() .model("qwen-plus") .build()) .call() .content(); } }

模型在收到“查询 AAPL 最新股价”这类问题时,会自动触发 Function Calling,由框架路由到已经注册的 MCP 工具并回传结果,最终返回给用户。你甚至可以在调用链路上叠加多个 MCP Server,模型根据用户问题自己选择该用哪个工具。

6.4 两种路径怎么选

我的实践结论是:教学和排查问题用手写版,业务交付用 Spring AI 版。手写版让你清楚每一步在做什么,遇到异常时能迅速定位是模型输出问题、工具执行问题还是消息格式问题;Spring AI 版则把样板代码收敛掉,适合快速交付。

7. 落地过程中我踩过的坑

工具链路从“能跑通”到“稳定可靠”,中间隔着大量细节。下面这几个坑是我在 Java 落地过程中真实踩过的,每个都值得记下来。

7.1 工具返回纯字符串,模型容易二次理解错误

一开始我把工具结果返回成一个普通 String,比如“温度26度”。模型拿到这串文字后,有时会在最终回复里把它转述得走样,甚至自行加工出不存在的数据。后来我改成返回结构化 JSON,模型直接引用字段,准确率明显提升。所以工具方法的返回类型尽量用Map或自定义 DTO,不要用裸字符串。

7.2 工具描述写得太宽泛,模型频繁误调

有段时间我为了省事,把几个查询工具的描述都写成“查询业务数据”。结果模型面对任何含糊的问题,先把这个工具调一遍,浪费大量 token。后来我把每个工具的描述写清楚:适用场景、输入参数规则、什么时候不该用。比如这个—— “仅当用户明确提供了股票代码时使用;如果用户只提到公司名,需要先用 translate_company_name 转换,不要直接猜测代码”。描述约束越具体,模型决策越稳定。

7.3 stdio Server 启动路径和 Java 版本问题

MCP 的 stdio 模式是通过命令行拉起子进程的。我遇到最典型的故障是:本地开发环境 Java 17,服务器部署环境 JDK 11,连接时直接拉起失败。排查思路是先用命令行手动执行java -jar server.jar --server.port=0确认能否正常启动,再看 application.yml 里配置的 command 和 args 是否完整。建议在 Server 端显式指定 Java 路径,避免依赖系统 PATH。

7.4 工具结果太大导致上下文膨胀

有些查询工具会把整表数据返回给模型,一次回复几万 token。这会让请求超时,还会挤占上下文窗口。我的处理方式是在工具内部做字段裁剪和分页,只返回摘要字段,必要时把明细数据异步落库,再告诉模型“明细已生成,文件编号见结果中的 reportId”。这属于“工具侧优化”而不是模型侧优化,很多人会忽略这个思路。

7.5 并发场景下 MCP Client 的连接管理

高并发场景里,多个请求同时触发相同的 MCP 工具,如果每个请求都建立一个全新连接,很快会把 Server 打挂。Spring AI 的 MCP Client 本身有连接池能力,但需要你确认配置开启。我的经验是对使用 stdio 的本地轻量工具可以按进程复用连接,对远程 Server 配置合理的超时时间和最大连接数。

7.6 本地小模型的 Function Calling 效果不稳定

很多团队在本地用 Ollama 这类方案跑开源模型,发现工具调用经常不按 schema 输出。说实话,这是正常的——Function Calling 对模型训练质量的要求相当高,7B 以下参数的模型能稳定输出正确 JSON 就已经不错了。所以在生产环境,要么选对工具调用支持良好的模型服务,要么在手写链路里加一层“解析容错”,把模型输出的 JSON 做修复和校验,而不是直接把原始输出硬塞给工具分发器。

7.7 版本升级导致 API 变化

Spring AI 的 MCP 相关 API 在里程碑阶段变化比较频繁,我升级版本后遇到过ToolCallbackProvider的包路径变化、配置项 key 改变这类问题。处理办法是把版本锁死在一个你验证过的版本,不要随意跟随最新快照,同时关注官方的迁移说明。

最后分享一点我的实际体会

绕完这一圈,我的结论很简单:不要试图用某一个概念替代另外两个。Function Calling 是地基,MCP 是管线,Skill 是方法论。团队里如果有人还在争执“MCP 和 Skill 用哪个”,你就把这篇里那张选型判断表发给他,让他先定位自己在做哪一层的活。先手写一遍最小函数调用链路,理解模型输出和工具执行的协作关系;再接入 Spring AI 的 MCP,让工具接入规模化;最后挑两个高频业务场景沉淀成 Skill,让模型从“会调工具”进化到“按正确步骤把事情办完”。

这套三层的落地路径,至少能帮你少走我当初走过的那段弯路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询