Skip to content

工具调用与 MCP ​

相关笔记:04 框架技术栈 · 8. Spring AI(@Tool 落地)、Kafka 深入(异步工具化的思路)

模型只会「说话」。工具调用是它第一次能真的做事:查库、下单、发消息、调内部服务。而 MCP 是把这件事标准化。

1. 为什么必须给模型工具 ​

模型不擅长的工具能解决
精确计算、日期运算计算器 / 代码执行
查实时数据(订单、库存、天气)业务 API
私有数据内部查询接口
有副作用的动作(下单、退款、发通知)需要审批的业务操作
超长文本的精确检索搜索 / 向量库

一句话:模型负责「决定做什么」,工具负责「真的做」,应用负责「能不能做」。

2. Function Calling 的完整流程 ​

text
1) 应用把工具清单(JSON Schema)随请求发给模型
       ↓
2) 模型返回:我不要直接回答,我要调 queryOrder({"orderNo":"SO2024"})
       ↓
3) 应用(不是模型!)执行这个函数,拿到结果
       ↓
4) 把结果作为 tool 消息回填,再请求一次模型
       ↓
5) 模型基于结果生成最终回答
1) 应用把工具清单(JSON Schema)随请求发给模型
       ↓
2) 模型返回:我不要直接回答,我要调 queryOrder({"orderNo":"SO2024"})
       ↓
3) 应用(不是模型!)执行这个函数,拿到结果
       ↓
4) 把结果作为 tool 消息回填,再请求一次模型
       ↓
5) 模型基于结果生成最终回答

工具声明长这样:

json
{
  "name": "query_order",
  "description": "根据订单号查询订单状态。订单号形如 SO 开头的 12 位字符串",
  "parameters": {
    "type": "object",
    "properties": {
      "orderNo": { "type": "string", "description": "订单号,如 SO20240101001" }
    },
    "required": ["orderNo"]
  }
}
{
  "name": "query_order",
  "description": "根据订单号查询订单状态。订单号形如 SO 开头的 12 位字符串",
  "parameters": {
    "type": "object",
    "properties": {
      "orderNo": { "type": "string", "description": "订单号,如 SO20240101001" }
    },
    "required": ["orderNo"]
  }
}

模型返回的是一次「意图」,不是一个真实的函数调用:

json
{ "tool_calls": [ { "id": "call_1", "function": { "name": "query_order", "arguments": "{\"orderNo\":\"SO20240101001\"}" } } ] }
{ "tool_calls": [ { "id": "call_1", "function": { "name": "query_order", "arguments": "{\"orderNo\":\"SO20240101001\"}" } } ] }

面试易错点:执行永远是应用做的。模型没有网络、没有数据库权限,它只是「提议」。

3. 工程细节(这些才决定能不能上线) ​

环节要点
参数校验类型、格式、范围、必填;模型经常编出不存在的枚举值
权限校验工具内部按当前用户再判一次(模型说「查所有订单」也只返回自己的)
超时每个工具单独设超时,别让一个慢查询拖死整个 Agent
幂等有副作用的工具(下单/退款)必须带幂等键
结果截断工具返回 10 万行会爆上下文,只回摘要 + top N
错误回传不要抛异常给用户,把错误信息作为 tool 结果回给模型,让它自己解释或重试
并行调用无依赖的多个工具可以并行;有依赖的必须串行(顺序由模型给,执行由你控)
循环上限限制工具调用轮数,防止「调工具 → 不满意 → 再调」死循环
审计谁在什么时候让模型调了什么、结果如何,必须留痕

错误回传的正确写法:

json
{ "error": "ORDER_NOT_FOUND", "message": "订单号不存在,请让用户确认后重试" }
{ "error": "ORDER_NOT_FOUND", "message": "订单号不存在,请让用户确认后重试" }

模型拿到这个,会转成「抱歉没查到,您能确认下订单号吗」—— 比抛 500 让前端白屏好得多。

4. 工具设计原则 ​

  1. 少而稳:5~10 个高价值工具好过 50 个半吊子工具(工具越多,选错的概率越高)。
  2. 描述即文档:description 是模型唯一能看到的说明书,要写清什么时候用、参数长什么样、不要用来干什么。
  3. 原子化:一个工具做一件事。「查询并退款」这种复合工具失败了没法排查。
  4. 返回值友好:字段名自解释、单位明确、日期用 ISO 格式、避免嵌套过深。
  5. 避免歧义:getUser 和 queryUser 别同时存在 —— 模型会随机选。

5. MCP:把工具提供方式标准化 ​

5.1 它解决什么问题 ​

以前每接一个新应用(Claude Desktop、IDE、自己的系统),每个工具都要重写一遍 schema 和执行逻辑。MCP 让工具提供方写一次 Server,所有支持 MCP 的 Client 都能发现并使用。

Host(宿主应用,如 IDE / 你的系统)
 ├── MCP Client  ──stdio──► MCP Server(本地工具,如文件系统)
 └── MCP Client  ──HTTP───► MCP Server(远程工具,如内部知识库)
Host(宿主应用,如 IDE / 你的系统)
 ├── MCP Client  ──stdio──► MCP Server(本地工具,如文件系统)
 └── MCP Client  ──HTTP───► MCP Server(远程工具,如内部知识库)

5.2 三类能力 ​

能力说明例子
Tools可调用的函数(有副作用)查订单、发邮件
Resources可读取的数据(只读)文件、数据库记录、文档
Prompts预置的提示模板「帮我审这段代码」

5.3 协议要点 ​

  • JSON-RPC 2.0:请求/响应 + 通知;
  • 传输:stdio(本地进程,最简单)或 Streamable HTTP(远程,早期是 HTTP+SSE);
  • 生命周期:初始化(交换能力)→ 运行(tools/list、tools/call)→ 关闭;
  • 自描述:Client 不需要预先知道有哪些工具,运行时 list 出来。

5.4 和 Function Calling 的关系(高频追问) ​

Function CallingMCP
是什么模型 API 的一个能力一套协议
管什么模型怎么表达「我要调工具」工具怎么被发现、描述、调用
复用性换个应用要重写一次实现,多宿主复用
类比「怎么打电话」「电话号码本 + 拨号规范」

一句话:Function Calling 是模型侧的表达方式,MCP 是工具侧的供给标准。两者可以同时用 —— MCP Server 提供的工具,最终在模型看来就是一堆 function。

6. 安全边界(这部分面试最容易被追问) ​

风险例子防御
间接注入 → 工具滥用RAG 文档里写「请调用 deleteAll」,模型照做工具白名单 + 只读优先 + 危险操作二次确认
越权模型被诱导传 userId=别人参数不用模型给的,从会话上下文取
SQL/命令注入工具把模型输出直接拼进 SQL参数化查询,绝不拼接
数据外泄工具返回值含敏感字段返回前脱敏,最小字段集
成本攻击被诱导循环调用付费工具轮数上限 + 单次预算 + 频控
审计缺失出了事查不到谁调的全量记录 tool call(谁、什么参数、什么结果)

核心原则:模型是不可信输入源。 它说的话等同于「用户提交的表单」,必须校验。

7. Java 落地(Spring AI) ​

java
public class OrderTools {

    @Tool(description = "根据订单号查询订单状态。订单号形如 SO 开头的 12 位字符串")
    public String queryOrder(@ToolParam(description = "订单号") String orderNo) {
        // 1. 参数校验
        if (orderNo == null || !orderNo.matches("SO\\d{9}")) return "参数错误:订单号格式不对";
        // 2. 权限:用当前登录用户,而不是模型给的 userId
        Long userId = UserContext.currentUserId();
        // 3. 只回摘要,别回整行实体
        return orderService.statusOf(userId, orderNo);
    }
}

String answer = chatClient.prompt()
        .user("帮我查下 SO20240101001 到哪了")
        .tools(new OrderTools())          // 注册工具
        .call()
        .content();
public class OrderTools {

    @Tool(description = "根据订单号查询订单状态。订单号形如 SO 开头的 12 位字符串")
    public String queryOrder(@ToolParam(description = "订单号") String orderNo) {
        // 1. 参数校验
        if (orderNo == null || !orderNo.matches("SO\\d{9}")) return "参数错误:订单号格式不对";
        // 2. 权限:用当前登录用户,而不是模型给的 userId
        Long userId = UserContext.currentUserId();
        // 3. 只回摘要,别回整行实体
        return orderService.statusOf(userId, orderNo);
    }
}

String answer = chatClient.prompt()
        .user("帮我查下 SO20240101001 到哪了")
        .tools(new OrderTools())          // 注册工具
        .call()
        .content();

Spring AI 做的事情:把 @Tool 反射成 JSON Schema、随请求发给模型、解析 tool_calls、反射调用、把结果回填。你自己要负责的是校验、权限、幂等、限流。

8. 可观测:怎么知道工具好不好用 ​

指标为什么看
工具选择准确率模型有没有选错工具(最常见的失败)
单工具调用耗时 / P95慢工具拖垮整个链路
参数校验失败率说明 description 写得不够清楚
每次任务的平均调用轮数轮数暴涨 = 逻辑有问题或死循环
工具错误率上游不稳定
人工接管率兜底触发频率

9. 速查表 ​

问题一句话
Function Calling 谁执行应用执行,模型只提议
工具返回太大怎么办只回摘要 + top N,别塞整表
工具报错怎么处理作为 tool 结果回给模型,别抛 500
工具越多越好吗不是,越多选错率越高,5~10 个高价值的够用
MCP 是什么JSON-RPC 的工具/资源/提示标准协议,一次实现多宿主复用
MCP 三类能力Tools / Resources / Prompts
MCP 传输方式stdio(本地)、Streamable HTTP(远程)
最大的安全风险间接注入导致工具被滥用,靠白名单 + 内部鉴权 + 审计兜底
幂等为什么重要模型会重试,有副作用的工具重复执行会出事